package repository import ( "errors" "strings" "hyapp-admin-server/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) var ErrWithdrawalApplicationAlreadyAudited = errors.New("withdrawal application already audited") type WithdrawalApplicationListOptions struct { Page int PageSize int AppCode string Keyword string } type WithdrawalApplicationAuditInput struct { Decision string ApproverUserID uint ApproverName string AuditRemark string AuditImageURL string AuditCommandID string AuditTransactionID string ApprovedAtMS int64 } func (s *Store) GetWithdrawalApplication(id uint) (*model.UserWithdrawalApplication, error) { var application model.UserWithdrawalApplication if err := s.db.First(&application, id).Error; err != nil { return nil, err } return &application, nil } func (s *Store) ListWithdrawalApplications(options WithdrawalApplicationListOptions) ([]model.UserWithdrawalApplication, int64, error) { page, pageSize := normalizePage(options.Page, options.PageSize) query := s.db.Model(&model.UserWithdrawalApplication{}) query = applyWithdrawalApplicationFilters(query, options) var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, err } var applications []model.UserWithdrawalApplication // 提现申请是财务审核场景,默认按最新申请优先展示;id 作为同毫秒写入时的稳定次序,避免分页翻页时记录抖动。 err := query. Order("created_at_ms DESC, id DESC"). Limit(pageSize). Offset((page - 1) * pageSize). Find(&applications).Error return applications, total, err } func (s *Store) AuditWithdrawalApplication(id uint, input WithdrawalApplicationAuditInput) (*model.UserWithdrawalApplication, error) { if id == 0 { return nil, errors.New("withdrawal application id is required") } if input.Decision != model.WithdrawalApplicationStatusApproved && input.Decision != model.WithdrawalApplicationStatusRejected { return nil, errors.New("withdrawal application decision is invalid") } var application model.UserWithdrawalApplication err := s.db.Transaction(func(tx *gorm.DB) error { // 审核只能从 pending 进入终态;行锁阻止两个财务同时覆盖同一张提现单的执行结果。 if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&application, id).Error; err != nil { return err } if application.Status != model.WithdrawalApplicationStatusPending { return ErrWithdrawalApplicationAlreadyAudited } return tx.Model(&application).Updates(map[string]any{ "status": input.Decision, "approver_user_id": input.ApproverUserID, "approver_name": strings.TrimSpace(input.ApproverName), "audit_remark": strings.TrimSpace(input.AuditRemark), "audit_image_url": strings.TrimSpace(input.AuditImageURL), "audit_command_id": strings.TrimSpace(input.AuditCommandID), "audit_transaction_id": strings.TrimSpace(input.AuditTransactionID), "approved_at_ms": input.ApprovedAtMS, "updated_at_ms": input.ApprovedAtMS, }).Error }) if err != nil { return nil, err } return s.GetWithdrawalApplication(id) } func applyWithdrawalApplicationFilters(query *gorm.DB, options WithdrawalApplicationListOptions) *gorm.DB { if appCode := strings.TrimSpace(options.AppCode); appCode != "" { query = query.Where("app_code = ?", appCode) } if keyword := strings.TrimSpace(options.Keyword); keyword != "" { like := "%" + keyword + "%" // 关键词面向财务排查,覆盖用户、收款方式、收款地址和审批人;不做金额模糊,避免 decimal 隐式转换拖慢常规查询。 query = query.Where("user_id LIKE ? OR withdraw_method LIKE ? OR withdraw_address LIKE ? OR approver_name LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like) } return query }