// Package distributionwithdrawal 提供分销注册、提现资格与提现审批的只读投影查询。 // 查询不得修改任何状态;可见性由调用方在业务边界先行校验。 package distributionwithdrawal import ( "context" "encoding/json" "gorm.io/gorm" distributiondomain "github.com/break/junhong_cmp_fiber/internal/domain/distribution" "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" ) // Query 提供提现资料资格与提现申请详情的只读投影。 type Query struct { db *gorm.DB } // NewQuery 创建分销与提现审批只读查询。 func NewQuery(db *gorm.DB) *Query { return &Query{db: db} } // ListQualifications 按数据范围分页查询提现资料资格版本,证件号按脱敏值返回。 func (q *Query) ListQualifications( ctx context.Context, shopIDs []uint, req *dto.WithdrawalQualificationListReq, ) (*dto.WithdrawalQualificationPageResult, error) { if q == nil || q.db == nil { return nil, errors.New(errors.CodeInternalError, "提现资料资格查询能力未配置") } page := req.Page if page <= 0 { page = constants.DefaultPage } pageSize := req.PageSize if pageSize <= 0 { pageSize = constants.DefaultPageSize } if pageSize > constants.MaxPageSize { pageSize = constants.MaxPageSize } query := q.db.WithContext(ctx).Model(&model.WithdrawalQualification{}) if len(shopIDs) == 1 { query = query.Where("shop_id = ?", shopIDs[0]) } else if len(shopIDs) > 1 { query = query.Where("shop_id IN ?", shopIDs) } if req.Status != nil { query = query.Where("status = ?", *req.Status) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计提现资料资格失败") } var versions []model.WithdrawalQualification if err := query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize). Find(&versions).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询提现资料资格失败") } shopNames, err := q.shopNames(ctx, versions) if err != nil { return nil, err } instanceStatuses, err := q.approvalStatuses(ctx, constants.ApprovalBusinessTypeWithdrawalQualification, instanceIDsOfQualifications(versions)) if err != nil { return nil, err } items := make([]dto.WithdrawalQualificationItem, 0, len(versions)) for index := range versions { version := &versions[index] instanceID := uint(0) if version.ApprovalInstanceID != nil { instanceID = *version.ApprovalInstanceID } item := dto.WithdrawalQualificationItem{ ID: version.ID, ShopID: version.ShopID, ShopName: shopNames[version.ShopID], SubjectType: version.SubjectType, SubjectTypeName: constants.GetWithdrawalQualificationSubjectTypeName(version.SubjectType), SubjectCodeMasked: distributiondomain.MaskSubjectCode(version.SubjectCode), LegalPersonIDCardMask: distributiondomain.MaskSubjectCode(version.LegalPersonIDCard), ContractFileKey: version.ContractFileKey, IDCardFrontFileKey: version.IDCardFrontFileKey, IDCardBackFileKey: version.IDCardBackFileKey, BusinessLicenseFileKey: version.BusinessLicenseFileKey, ShopFrontFileKey: version.ShopFrontFileKey, InvoiceFileKey: version.InvoiceFileKey, InvoiceTitle: version.InvoiceTitle, Status: version.Status, StatusName: constants.GetWithdrawalQualificationStatusName(version.Status), InvalidReason: version.InvalidReason, ApprovalInstanceID: instanceID, CreatedAt: version.CreatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: version.UpdatedAt.Format("2006-01-02 15:04:05"), } if version.InvalidatedAt != nil { item.InvalidatedAt = version.InvalidatedAt.Format("2006-01-02 15:04:05") } if status, ok := instanceStatuses[instanceID]; ok { item.ApprovalStatus = status item.ApprovalStatusName = constants.GetApprovalStatusName(status) } else { item.ApprovalStatusName = constants.GetApprovalStatusName(-1) } items = append(items, item) } return &dto.WithdrawalQualificationPageResult{ Items: items, Total: total, Page: page, Size: pageSize, }, nil } // WithdrawalDetail 查询单笔提现申请详情与其全部审批尝试记录。 func (q *Query) WithdrawalDetail( ctx context.Context, requestID uint, ) (*dto.ShopWithdrawalRequestDetailResp, error) { if q == nil || q.db == nil { return nil, errors.New(errors.CodeInternalError, "提现详情查询能力未配置") } var request model.CommissionWithdrawalRequest if err := q.db.WithContext(ctx).First(&request, requestID).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "提现申请不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询提现申请失败") } var attempts []model.CommissionWithdrawalRequestAttempt if err := q.db.WithContext(ctx). Where("request_id = ?", request.ID).Order("attempt_no DESC").Find(&attempts).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询提现审批尝试记录失败") } instanceStatuses, err := q.approvalStatuses(ctx, constants.ApprovalBusinessTypeCommissionWithdrawal, instanceIDsOfAttempts(attempts)) if err != nil { return nil, err } shopNames, err := q.shopNames(ctx, []model.WithdrawalQualification{{ShopID: request.ShopID}}) if err != nil { return nil, err } accountName, accountNumber := decodeAttemptAccount(request.AccountInfo) processedAt := "" if request.ProcessedAt != nil { processedAt = request.ProcessedAt.Format("2006-01-02 15:04:05") } paidAt := "" if request.PaidAt != nil { paidAt = request.PaidAt.Format("2006-01-02 15:04:05") } detail := &dto.ShopWithdrawalRequestDetailResp{ ShopWithdrawalRequestItem: dto.ShopWithdrawalRequestItem{ ID: request.ID, WithdrawalNo: request.WithdrawalNo, Amount: request.Amount, FeeRate: request.FeeRate, Fee: request.Fee, ActualAmount: request.ActualAmount, Status: request.Status, StatusName: constants.GetWithdrawalStatusName(request.Status), ShopID: request.ShopID, ShopName: shopNames[request.ShopID], ApplicantID: request.ApplicantID, WithdrawalMethod: request.WithdrawalMethod, PaymentType: request.PaymentType, AccountName: accountName, AccountNumber: accountNumber, RejectReason: request.RejectReason, Remark: request.Remark, CreatedAt: request.CreatedAt.Format("2006-01-02 15:04:05"), ProcessedAt: processedAt, PaidAt: paidAt, }, LatestAttemptID: request.LatestAttemptID, LatestApprovalInstanceID: request.LatestApprovalInstanceID, AnomalyFlag: request.AnomalyFlag, AnomalyName: constants.GetWithdrawalAnomalyName(request.AnomalyFlag), AnomalyReason: request.AnomalyReason, Attempts: make([]dto.WithdrawalRequestAttemptItem, 0, len(attempts)), } if request.ApprovalInstanceID != nil { detail.ApprovalInstanceID = *request.ApprovalInstanceID } for index := range attempts { attempt := &attempts[index] instanceID := uint(0) if attempt.ApprovalInstanceID != nil { instanceID = *attempt.ApprovalInstanceID } item := dto.WithdrawalRequestAttemptItem{ ID: attempt.ID, AttemptNo: attempt.AttemptNo, Amount: attempt.Amount, Fee: attempt.Fee, FeeRate: attempt.FeeRate, ActualAmount: attempt.ActualAmount, WithdrawalMethod: attempt.WithdrawalMethod, SubmittedByID: attempt.SubmittedByAccountID, ApprovalInstanceID: instanceID, CreatedAt: attempt.CreatedAt.Format("2006-01-02 15:04:05"), } if status, ok := instanceStatuses[instanceID]; ok { item.ApprovalStatus = status item.ApprovalStatusName = constants.GetApprovalStatusName(status) } else { item.ApprovalStatusName = constants.GetApprovalStatusName(-1) } if attempt.ReleasedAt != nil { item.ReleasedAt = attempt.ReleasedAt.Format("2006-01-02 15:04:05") } detail.Attempts = append(detail.Attempts, item) } return detail, nil } // shopNames 批量读取店铺名称,缺失店铺留空。 func (q *Query) shopNames(ctx context.Context, versions []model.WithdrawalQualification) (map[uint]string, error) { shopIDs := make([]uint, 0, len(versions)) seen := make(map[uint]struct{}, len(versions)) for _, version := range versions { if version.ShopID == 0 { continue } if _, exists := seen[version.ShopID]; exists { continue } seen[version.ShopID] = struct{}{} shopIDs = append(shopIDs, version.ShopID) } names := make(map[uint]string, len(shopIDs)) if len(shopIDs) == 0 { return names, nil } var shops []model.Shop if err := q.db.WithContext(ctx).Select("id", "shop_name").Where("id IN ?", shopIDs).Find(&shops).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺名称失败") } for _, shop := range shops { names[shop.ID] = shop.ShopName } return names, nil } // approvalStatuses 批量读取通用审批实例状态,缺失实例不进入结果。 func (q *Query) approvalStatuses( ctx context.Context, businessType string, instanceIDs []uint, ) (map[uint]int, error) { statuses := make(map[uint]int, len(instanceIDs)) if len(instanceIDs) == 0 { return statuses, nil } var instances []model.ApprovalInstance if err := q.db.WithContext(ctx).Select("id", "status"). Where("business_type = ? AND id IN ?", businessType, instanceIDs). Find(&instances).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询通用审批实例状态失败") } for _, instance := range instances { statuses[instance.ID] = instance.Status } return statuses, nil } // instanceIDsOfQualifications 提取资料版本关联的审批实例 ID。 func instanceIDsOfQualifications(versions []model.WithdrawalQualification) []uint { ids := make([]uint, 0, len(versions)) for index := range versions { if versions[index].ApprovalInstanceID != nil { ids = append(ids, *versions[index].ApprovalInstanceID) } } return ids } // instanceIDsOfAttempts 提取提现审批尝试关联的审批实例 ID。 func instanceIDsOfAttempts(attempts []model.CommissionWithdrawalRequestAttempt) []uint { ids := make([]uint, 0, len(attempts)) for index := range attempts { if attempts[index].ApprovalInstanceID != nil { ids = append(ids, *attempts[index].ApprovalInstanceID) } } return ids } // decodeAttemptAccount 解析收款账户快照,解析失败时留空。 func decodeAttemptAccount(payload []byte) (string, string) { var info map[string]string if err := json.Unmarshal(payload, &info); err != nil { return "", "" } return info["account_name"], info["account_number"] }