package employeecollection import ( "context" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // ApplicationQuery 查询员工代收款核销申请列表与详情。 // Query 只做权限范围、筛选、分页与 DTO 投影,不修改任何状态。 type ApplicationQuery struct { db *gorm.DB } // NewApplicationQuery 创建核销申请查询。 func NewApplicationQuery(db *gorm.DB) *ApplicationQuery { return &ApplicationQuery{db: db} } // List 分页查询当前可见范围内的核销申请。 func (q *ApplicationQuery) List( ctx context.Context, request dto.EmployeeCollectionApplicationListRequest, ) (*dto.EmployeeCollectionApplicationListResponse, error) { query, err := q.applyApplicationScope(ctx, request) if err != nil { return nil, err } page, pageSize := normalizePage(request.Page, request.PageSize) var total int64 if err := query.Count(&total).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询核销申请总数失败") } var applications []model.EmployeeCollectionApplication if err := query.Order("created_at DESC, id DESC"). Offset((page - 1) * pageSize).Limit(pageSize). Find(&applications).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询核销申请列表失败") } list := make([]*dto.EmployeeCollectionApplicationResponse, 0, len(applications)) for index := range applications { list = append(list, ProjectApplication(&applications[index])) } return &dto.EmployeeCollectionApplicationListResponse{ List: list, Total: total, Page: page, PageSize: pageSize, }, nil } // Detail 返回当前可见范围内的核销申请详情,包含全部分摊与审批尝试历史。 // 不可见与不存在返回同一错误,不产生可枚举差异。 func (q *ApplicationQuery) Detail(ctx context.Context, id uint) (*dto.EmployeeCollectionApplicationDetailResponse, error) { if q == nil || q.db == nil || id == 0 { return nil, errors.New(errors.CodeEmployeeCollectionApplicationNotFound) } scoped, err := applyApplicationVisibility(ctx, q.db.WithContext(ctx).Model(&model.EmployeeCollectionApplication{})) if err != nil { return nil, err } var application model.EmployeeCollectionApplication if err := scoped.Where("id = ?", id).First(&application).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeEmployeeCollectionApplicationNotFound) } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询核销申请详情失败") } allocations, err := q.applicationAllocations(ctx, application.ID) if err != nil { return nil, err } attempts, err := q.applicationAttemptList(ctx, application.ID) if err != nil { return nil, err } return &dto.EmployeeCollectionApplicationDetailResponse{ Application: ProjectApplication(&application), Allocations: allocations, Attempts: attempts, }, nil } // applyApplicationScope 生成同时应用可见性与筛选条件的核销申请查询。 func (q *ApplicationQuery) applyApplicationScope( ctx context.Context, request dto.EmployeeCollectionApplicationListRequest, ) (*gorm.DB, error) { if q == nil || q.db == nil { return nil, errors.New(errors.CodeServiceUnavailable, "核销申请查询尚未配置") } query, err := applyApplicationVisibility(ctx, q.db.WithContext(ctx).Model(&model.EmployeeCollectionApplication{})) if err != nil { return nil, err } if request.ApplicantAccountID != nil && *request.ApplicantAccountID > 0 { query = query.Where("applicant_account_id = ?", *request.ApplicantAccountID) } if request.PaymentMethodID != nil && *request.PaymentMethodID > 0 { query = query.Where("payment_method_id = ?", *request.PaymentMethodID) } if request.Status != nil { switch *request.Status { case constants.EmployeeCollectionApplicationStatusPending, constants.EmployeeCollectionApplicationStatusApproved, constants.EmployeeCollectionApplicationStatusRejected, constants.EmployeeCollectionApplicationStatusRevoked: query = query.Where("status = ?", *request.Status) default: return nil, errors.New(errors.CodeInvalidParam, "不支持的核销申请状态") } } if request.CreatedFrom != nil { from, err := parseBillDate(*request.CreatedFrom) if err != nil { return nil, err } if from != nil { query = query.Where("created_at >= ?", *from) } } if request.CreatedTo != nil { to, err := parseBillDate(*request.CreatedTo) if err != nil { return nil, err } if to != nil { query = query.Where("created_at < ?", to.AddDate(0, 0, 1)) } } return query, nil } // applyApplicationVisibility 应用核销申请可见性:超级管理员见全部,其他账号仅见本人申请。 func applyApplicationVisibility(ctx context.Context, query *gorm.DB) (*gorm.DB, error) { if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeSuperAdmin { return query, nil } accountID := middleware.GetUserIDFromContext(ctx) if accountID == 0 { return nil, errors.New(errors.CodeUnauthorized) } return query.Where("applicant_account_id = ?", accountID), nil } // applicationAllocations 读取申请的全部分摊并附带目标账单只读摘要。 func (q *ApplicationQuery) applicationAllocations( ctx context.Context, applicationID uint, ) ([]*dto.EmployeeCollectionApplicationAllocationResponse, error) { var allocations []model.EmployeeCollectionApplicationAllocation if err := q.db.WithContext(ctx).Where("application_id = ?", applicationID). Order("bill_id ASC, id ASC").Find(&allocations).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询核销申请分摊失败") } billIDs := make([]uint, 0, len(allocations)) for index := range allocations { billIDs = append(billIDs, allocations[index].BillID) } bills := make(map[uint]model.EmployeeCollectionBill, len(billIDs)) if len(billIDs) > 0 { var loaded []model.EmployeeCollectionBill if err := q.db.WithContext(ctx).Where("id IN ?", billIDs).Find(&loaded).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询核销分摊目标账单失败") } for index := range loaded { bills[loaded[index].ID] = loaded[index] } } result := make([]*dto.EmployeeCollectionApplicationAllocationResponse, 0, len(allocations)) for index := range allocations { allocation := allocations[index] bill := bills[allocation.BillID] result = append(result, &dto.EmployeeCollectionApplicationAllocationResponse{ ID: allocation.ID, BillID: allocation.BillID, Amount: allocation.Amount, Status: allocation.Status, StatusName: constants.GetEmployeeCollectionAllocationStatusName(allocation.Status), ReleasedAt: allocation.ReleasedAt, BillSourceType: bill.SourceType, BillSourceNo: bill.SourceNo, BillReceivableAmount: bill.ReceivableAmount, BillReceivedAmount: bill.ReceivedAmount, BillReservedAmount: bill.ReservedAmount, BillStatus: bill.Status, BillStatusName: constants.GetEmployeeCollectionBillStatusName(bill.Status), CreatedAt: allocation.CreatedAt, }) } return result, nil } // applicationAttemptList 读取申请的全部审批尝试记录与审批实例状态。 func (q *ApplicationQuery) applicationAttemptList( ctx context.Context, applicationID uint, ) ([]*dto.EmployeeCollectionBillAttemptResponse, error) { var attempts []model.EmployeeCollectionApplicationAttempt if err := q.db.WithContext(ctx).Where("application_id = ?", applicationID). Order("attempt_no ASC, id ASC").Find(&attempts).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询核销审批尝试记录失败") } approvalStatus, approvalOpinions, err := approvalStatusOfAttempts(ctx, q.db, attempts) if err != nil { return nil, err } return attemptResponses(applicationID, attempts, approvalStatus, approvalOpinions) } // ProjectApplication 将核销申请事实投影为对外响应,附件只返回对象键引用。 func ProjectApplication(application *model.EmployeeCollectionApplication) *dto.EmployeeCollectionApplicationResponse { if application == nil { return nil } vouchers := []string(application.PaymentVoucherKeys) if vouchers == nil { vouchers = []string{} } return &dto.EmployeeCollectionApplicationResponse{ ID: application.ID, ApplicantAccountID: application.ApplicantAccountID, ActingOperatorID: application.ActingOperatorID, ActingReason: application.ActingReason, PaymentMethodID: application.PaymentMethodID, PaymentMethodCode: application.PaymentMethodCode, PaymentMethodName: application.PaymentMethodName, PaidAmount: application.PaidAmount, PayerName: application.PayerName, PaidAt: application.PaidAt, ExternalTransactionNo: application.ExternalTransactionNo, PaymentVoucherKeys: vouchers, Remark: application.Remark, Status: application.Status, StatusName: constants.GetEmployeeCollectionApplicationStatusName(application.Status), LatestAttemptID: application.LatestAttemptID, LatestApprovalInstanceID: application.LatestApprovalInstanceID, DecidedAt: application.DecidedAt, TerminalReason: application.TerminalReason, CreatedAt: application.CreatedAt, UpdatedAt: application.UpdatedAt, } } // ApplicationSubmitProjection 是创建或重提核销申请返回的投影输入。 type ApplicationSubmitProjection struct { Application *model.EmployeeCollectionApplication Attempt *model.EmployeeCollectionApplicationAttempt Allocations []*model.EmployeeCollectionApplicationAllocation Bills []*model.EmployeeCollectionBill InstanceID uint InstanceStatus int } // ProjectApplicationSubmit 组装创建或重提核销申请的响应。 // 附件只返回对象键引用,审批状态取本次创建的通用审批实例状态,不返回对象存储内容。 func ProjectApplicationSubmit(projection ApplicationSubmitProjection) (*dto.EmployeeCollectionApplicationSubmitResponse, error) { allocations, err := projectSubmitAllocations(projection.Allocations, projection.Bills) if err != nil { return nil, err } var attempts []*dto.EmployeeCollectionBillAttemptResponse if projection.Attempt != nil { approvalStatus := map[uint]int{} if projection.InstanceID > 0 { approvalStatus[projection.InstanceID] = projection.InstanceStatus } attempts, err = attemptResponses( projection.Attempt.ApplicationID, []model.EmployeeCollectionApplicationAttempt{*projection.Attempt}, approvalStatus, map[uint]string{}, ) if err != nil { return nil, err } } response := &dto.EmployeeCollectionApplicationSubmitResponse{ Application: ProjectApplication(projection.Application), Allocations: allocations, ApprovalInstanceID: projection.InstanceID, ApprovalStatus: projection.InstanceStatus, ApprovalStatusName: constants.GetApprovalStatusName(projection.InstanceStatus), } if len(attempts) > 0 { response.Attempt = attempts[0] } return response, nil } // projectSubmitAllocations 将分摊与目标账单摘要投影为响应。 func projectSubmitAllocations( allocations []*model.EmployeeCollectionApplicationAllocation, bills []*model.EmployeeCollectionBill, ) ([]*dto.EmployeeCollectionApplicationAllocationResponse, error) { billByID := make(map[uint]*model.EmployeeCollectionBill, len(bills)) for _, bill := range bills { if bill != nil { billByID[bill.ID] = bill } } result := make([]*dto.EmployeeCollectionApplicationAllocationResponse, 0, len(allocations)) for _, allocation := range allocations { if allocation == nil { continue } bill := billByID[allocation.BillID] if bill == nil { return nil, errors.New(errors.CodeInternalError, "核销分摊缺少目标账单事实") } result = append(result, &dto.EmployeeCollectionApplicationAllocationResponse{ ID: allocation.ID, BillID: allocation.BillID, Amount: allocation.Amount, Status: allocation.Status, StatusName: constants.GetEmployeeCollectionAllocationStatusName(allocation.Status), ReleasedAt: allocation.ReleasedAt, BillSourceType: bill.SourceType, BillSourceNo: bill.SourceNo, BillReceivableAmount: bill.ReceivableAmount, BillReceivedAmount: bill.ReceivedAmount, BillReservedAmount: bill.ReservedAmount, BillStatus: bill.Status, BillStatusName: constants.GetEmployeeCollectionBillStatusName(bill.Status), CreatedAt: allocation.CreatedAt, }) } return result, nil }