package repository import ( "errors" "strings" "hyapp-admin-server/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) var ErrFinanceApplicationAlreadyAudited = errors.New("finance application already audited") type FinanceApplicationListOptions struct { Page int PageSize int AppCode string Operation string Status string Keyword string ApplicantUserID uint } type FinanceApplicationAuditInput struct { Decision string AuditorUserID uint AuditorName string AuditRemark string AuditedAtMS int64 WalletCommandID string WalletTransactionID string WalletAssetType string WalletAmountDelta int64 WalletBalanceAfter int64 } func (s *Store) CreateFinanceApplication(application *model.FinanceApplication) error { if application == nil { return errors.New("finance application is required") } return s.db.Create(application).Error } func (s *Store) GetFinanceApplication(id uint) (*model.FinanceApplication, error) { var application model.FinanceApplication if err := s.db.First(&application, id).Error; err != nil { return nil, err } return &application, nil } func (s *Store) ListFinanceApplications(options FinanceApplicationListOptions) ([]model.FinanceApplication, int64, error) { page, pageSize := normalizePage(options.Page, options.PageSize) query := s.db.Model(&model.FinanceApplication{}) query = applyFinanceApplicationFilters(query, options) var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, err } var applications []model.FinanceApplication 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) AuditFinanceApplication(id uint, input FinanceApplicationAuditInput) (*model.FinanceApplication, error) { if id == 0 { return nil, errors.New("finance application id is required") } if input.Decision != model.FinanceApplicationStatusApproved && input.Decision != model.FinanceApplicationStatusRejected { return nil, errors.New("finance application decision is invalid") } var application model.FinanceApplication 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.FinanceApplicationStatusPending { return ErrFinanceApplicationAlreadyAudited } return tx.Model(&application).Updates(map[string]any{ "status": input.Decision, "auditor_user_id": input.AuditorUserID, "auditor_name": strings.TrimSpace(input.AuditorName), "audit_remark": strings.TrimSpace(input.AuditRemark), "audited_at_ms": input.AuditedAtMS, "wallet_command_id": strings.TrimSpace(input.WalletCommandID), "wallet_transaction_id": strings.TrimSpace(input.WalletTransactionID), "wallet_asset_type": strings.TrimSpace(input.WalletAssetType), "wallet_amount_delta": input.WalletAmountDelta, "wallet_balance_after": input.WalletBalanceAfter, "updated_at_ms": input.AuditedAtMS, }).Error }) if err != nil { return nil, err } return s.GetFinanceApplication(id) } func applyFinanceApplicationFilters(query *gorm.DB, options FinanceApplicationListOptions) *gorm.DB { if options.ApplicantUserID > 0 { // “我的申请”接口只允许服务端用登录态限定申请人,不能信任前端传申请人 ID,避免普通发起人越权查看他人财务申请。 query = query.Where("applicant_user_id = ?", options.ApplicantUserID) } if appCode := strings.TrimSpace(options.AppCode); appCode != "" { query = query.Where("app_code = ?", appCode) } if operation := strings.TrimSpace(options.Operation); operation != "" { query = query.Where("operation = ?", operation) } if status := strings.TrimSpace(options.Status); status != "" && status != "all" { query = query.Where("status = ?", status) } if keyword := strings.TrimSpace(options.Keyword); keyword != "" { like := "%" + keyword + "%" query = query.Where("target_user_id LIKE ? OR applicant_name LIKE ? OR auditor_name LIKE ? OR CAST(applicant_user_id AS CHAR) LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like) } return query }