package shop_commission import ( "context" "encoding/json" "fmt" "math/rand" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/store" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "go.uber.org/zap" "gorm.io/gorm" ) // Service 代理商资金管理服务 type Service struct { shopStore *postgres.ShopStore accountStore *postgres.AccountStore agentWalletStore *postgres.AgentWalletStore commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStore commissionRecordStore *postgres.CommissionRecordStore agentWalletTransactionStore *postgres.AgentWalletTransactionStore db *gorm.DB logger *zap.Logger } // New 创建代理商资金管理服务 func New( shopStore *postgres.ShopStore, accountStore *postgres.AccountStore, agentWalletStore *postgres.AgentWalletStore, commissionWithdrawalReqStore *postgres.CommissionWithdrawalRequestStore, commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStore, commissionRecordStore *postgres.CommissionRecordStore, agentWalletTransactionStore *postgres.AgentWalletTransactionStore, db *gorm.DB, logger *zap.Logger, ) *Service { return &Service{ shopStore: shopStore, accountStore: accountStore, agentWalletStore: agentWalletStore, commissionWithdrawalReqStore: commissionWithdrawalReqStore, commissionWithdrawalSettingStore: commissionWithdrawalSettingStore, commissionRecordStore: commissionRecordStore, agentWalletTransactionStore: agentWalletTransactionStore, db: db, logger: logger, } } // ListShopFundSummary 代理商资金概况列表(含预充值余额和佣金钱包) // GET /shops/fund-summary func (s *Service) ListShopFundSummary(ctx context.Context, req *dto.ShopFundSummaryListReq) (*dto.ShopFundSummaryPageResult, error) { opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "created_at DESC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := make(map[string]interface{}) if req.ShopName != "" { filters["shop_name"] = req.ShopName } shops, total, err := s.shopStore.List(ctx, opts, filters) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺列表失败") } if len(shops) == 0 { return &dto.ShopFundSummaryPageResult{ Items: []dto.ShopFundSummaryItem{}, Total: 0, Page: opts.Page, Size: opts.PageSize, }, nil } shopIDs := make([]uint, 0, len(shops)) for _, shop := range shops { shopIDs = append(shopIDs, shop.ID) } // 批量获取主钱包(预充值钱包)余额 mainWallets, err := s.agentWalletStore.GetShopMainWalletBatch(ctx, shopIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询主钱包余额失败") } walletSummaries, err := s.agentWalletStore.GetShopCommissionSummaryBatch(ctx, shopIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺钱包汇总失败") } withdrawnAmounts, err := s.commissionWithdrawalReqStore.SumAmountByShopIDsAndStatus(ctx, shopIDs, constants.WithdrawalStatusApproved) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询已提现金额失败") } withdrawingAmounts, err := s.commissionWithdrawalReqStore.SumAmountByShopIDsAndStatus(ctx, shopIDs, constants.WithdrawalStatusPending) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询提现中金额失败") } primaryAccounts, err := s.accountStore.GetPrimaryAccountsByShopIDs(ctx, shopIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询主账号失败") } accountMap := make(map[uint]*model.Account) for _, acc := range primaryAccounts { if acc.ShopID != nil { accountMap[*acc.ShopID] = acc } } items := make([]dto.ShopFundSummaryItem, 0, len(shops)) for _, shop := range shops { if req.Username != "" { acc := accountMap[shop.ID] if acc == nil || !containsSubstring(acc.Username, req.Username) { total-- continue } } item := s.buildFundSummaryItem(shop, mainWallets[shop.ID], walletSummaries[shop.ID], withdrawnAmounts[shop.ID], withdrawingAmounts[shop.ID], accountMap[shop.ID]) items = append(items, item) } return &dto.ShopFundSummaryPageResult{ Items: items, Total: total, Page: opts.Page, Size: opts.PageSize, }, nil } // buildFundSummaryItem 构造资金概况条目 func (s *Service) buildFundSummaryItem(shop *model.Shop, mainWallet, commissionWallet *model.AgentWallet, withdrawnAmount, withdrawingAmount int64, account *model.Account) dto.ShopFundSummaryItem { // 主钱包余额(若未充值则为0) var mainBalance, mainFrozenBalance int64 if mainWallet != nil { mainBalance = mainWallet.Balance mainFrozenBalance = mainWallet.FrozenBalance } // 佣金钱包 var balance, frozenBalance int64 if commissionWallet != nil { balance = commissionWallet.Balance frozenBalance = commissionWallet.FrozenBalance } totalCommission := balance + frozenBalance + withdrawnAmount unwithdrawCommission := totalCommission - withdrawnAmount availableCommission := balance - withdrawingAmount if availableCommission < 0 { availableCommission = 0 } var username, phone string if account != nil { username = account.Username phone = account.Phone } return dto.ShopFundSummaryItem{ ShopID: shop.ID, ShopName: shop.ShopName, ShopCode: shop.ShopCode, Username: username, Phone: phone, MainBalance: mainBalance, MainFrozenBalance: mainFrozenBalance, TotalCommission: totalCommission, WithdrawnCommission: withdrawnAmount, UnwithdrawCommission: unwithdrawCommission, FrozenCommission: frozenBalance, WithdrawingCommission: withdrawingAmount, AvailableCommission: availableCommission, CreatedAt: shop.CreatedAt.Format("2006-01-02 15:04:05"), } } // ListShopWithdrawalRequests 查询代理商提现记录 // GET /shops/:id/withdrawal-requests func (s *Service) ListShopWithdrawalRequests(ctx context.Context, shopID uint, req *dto.ShopWithdrawalRequestListReq) (*dto.ShopWithdrawalRequestPageResult, error) { // 越权校验:平台人员可查所有,代理只能查自己和下级 if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } _, err := s.shopStore.GetByID(ctx, shopID) if err != nil { return nil, errors.New(errors.CodeShopNotFound, "店铺不存在") } opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "created_at DESC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := &postgres.WithdrawalRequestListFilters{ ShopID: shopID, WithdrawalNo: req.WithdrawalNo, } if req.StartTime != "" { t, err := time.Parse("2006-01-02 15:04:05", req.StartTime) if err == nil { filters.StartTime = &t } } if req.EndTime != "" { t, err := time.Parse("2006-01-02 15:04:05", req.EndTime) if err == nil { filters.EndTime = &t } } requests, total, err := s.commissionWithdrawalReqStore.ListByShopID(ctx, opts, filters) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询提现记录失败") } shop, _ := s.shopStore.GetByID(ctx, shopID) shopHierarchy := s.buildShopHierarchyPath(ctx, shop) applicantIDs := make([]uint, 0) processorIDs := make([]uint, 0) for _, r := range requests { if r.ApplicantID > 0 { applicantIDs = append(applicantIDs, r.ApplicantID) } if r.ProcessorID > 0 { processorIDs = append(processorIDs, r.ProcessorID) } } applicantMap := make(map[uint]string) processorMap := make(map[uint]string) if len(applicantIDs) > 0 { accounts, _ := s.accountStore.GetByIDs(ctx, applicantIDs) for _, acc := range accounts { applicantMap[acc.ID] = acc.Username } } if len(processorIDs) > 0 { accounts, _ := s.accountStore.GetByIDs(ctx, processorIDs) for _, acc := range accounts { processorMap[acc.ID] = acc.Username } } items := make([]dto.ShopWithdrawalRequestItem, 0, len(requests)) for _, r := range requests { item := s.buildWithdrawalRequestItem(r, shop.ShopName, shopHierarchy, applicantMap, processorMap) items = append(items, item) } return &dto.ShopWithdrawalRequestPageResult{ Items: items, Total: total, Page: opts.Page, Size: opts.PageSize, }, nil } // buildWithdrawalRequestItem 构造提现记录条目 func (s *Service) buildWithdrawalRequestItem(r *model.CommissionWithdrawalRequest, shopName, shopHierarchy string, applicantMap, processorMap map[uint]string) dto.ShopWithdrawalRequestItem { var processorID *uint if r.ProcessorID > 0 { processorID = &r.ProcessorID } var accountName, accountNumber, bankName string if len(r.AccountInfo) > 0 { var info map[string]interface{} if err := json.Unmarshal(r.AccountInfo, &info); err == nil { if v, ok := info["account_name"].(string); ok { accountName = v } if v, ok := info["account_number"].(string); ok { accountNumber = v } if v, ok := info["bank_name"].(string); ok { bankName = v } } } var processedAt, paidAt string if r.ProcessedAt != nil { processedAt = r.ProcessedAt.Format("2006-01-02 15:04:05") } if r.PaidAt != nil { paidAt = r.PaidAt.Format("2006-01-02 15:04:05") } return dto.ShopWithdrawalRequestItem{ ID: r.ID, WithdrawalNo: r.WithdrawalNo, Amount: r.Amount, FeeRate: r.FeeRate, Fee: r.Fee, ActualAmount: r.ActualAmount, Status: r.Status, StatusName: getWithdrawalStatusName(r.Status), ShopID: r.ShopID, ShopName: shopName, ShopHierarchy: shopHierarchy, ApplicantID: r.ApplicantID, ApplicantName: applicantMap[r.ApplicantID], ProcessorID: processorID, ProcessorName: processorMap[r.ProcessorID], WithdrawalMethod: r.WithdrawalMethod, PaymentType: r.PaymentType, AccountName: accountName, AccountNumber: accountNumber, BankName: bankName, RejectReason: r.RejectReason, Remark: r.Remark, CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"), ProcessedAt: processedAt, PaidAt: paidAt, } } // buildShopHierarchyPath 构造店铺层级路径(最多两层上级) func (s *Service) buildShopHierarchyPath(ctx context.Context, shop *model.Shop) string { if shop == nil { return "" } path := shop.ShopName current := shop depth := 0 for current.ParentID != nil && depth < 2 { parent, err := s.shopStore.GetByID(ctx, *current.ParentID) if err != nil { break } path = parent.ShopName + "_" + path current = parent depth++ } return path } // ListShopCommissionRecords 查询代理商佣金明细 // GET /shops/:id/commission-records func (s *Service) ListShopCommissionRecords(ctx context.Context, shopID uint, req *dto.ShopCommissionRecordListReq) (*dto.ShopCommissionRecordPageResult, error) { // 越权校验:平台人员可查所有,代理只能查自己和下级 if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } _, err := s.shopStore.GetByID(ctx, shopID) if err != nil { return nil, errors.New(errors.CodeShopNotFound, "店铺不存在") } opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "created_at DESC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := &postgres.CommissionRecordListFilters{ ShopID: shopID, CommissionSource: req.CommissionSource, ICCID: req.ICCID, DeviceNo: req.VirtualNo, OrderNo: req.OrderNo, } records, total, err := s.commissionRecordStore.ListByShopID(ctx, opts, filters) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询佣金明细失败") } sellerShopIDs := make([]uint, 0) for _, r := range records { if r.SellerShopID != nil && *r.SellerShopID > 0 { sellerShopIDs = append(sellerShopIDs, *r.SellerShopID) } } shopNameMap := make(map[uint]string) if len(sellerShopIDs) > 0 { shops, err := s.shopStore.GetByIDs(ctx, sellerShopIDs) if err == nil { for _, sh := range shops { shopNameMap[sh.ID] = sh.ShopName } } } items := make([]dto.ShopCommissionRecordItem, 0, len(records)) for _, r := range records { var orderCreatedAt string if r.OrderCreatedAt != nil { orderCreatedAt = r.OrderCreatedAt.Format("2006-01-02 15:04:05") } var sellerShopID uint if r.SellerShopID != nil { sellerShopID = *r.SellerShopID } item := dto.ShopCommissionRecordItem{ ID: r.ID, Amount: r.Amount, BalanceAfter: r.BalanceAfter, CommissionSource: r.CommissionSource, Status: r.Status, StatusName: getCommissionStatusName(r.Status), OrderID: r.OrderID, OrderNo: r.OrderNo, VirtualNo: r.VirtualNo, ICCID: r.ICCID, OrderCreatedAt: orderCreatedAt, SellerShopID: sellerShopID, SellerShopName: shopNameMap[sellerShopID], CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"), } items = append(items, item) } return &dto.ShopCommissionRecordPageResult{ Items: items, Total: total, Page: opts.Page, Size: opts.PageSize, }, nil } // GetStats 获取佣金统计 // GET /shops/:id/commission-stats func (s *Service) GetStats(ctx context.Context, shopID uint, req *dto.CommissionStatsRequest) (*dto.CommissionStatsResponse, error) { // 越权校验:平台人员可查所有,代理只能查自己和下级 if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } filters := &postgres.CommissionRecordListFilters{ ShopID: shopID, StartTime: req.StartTime, EndTime: req.EndTime, } stats, err := s.commissionRecordStore.GetStats(ctx, filters) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "获取佣金统计失败") } if stats == nil { return &dto.CommissionStatsResponse{}, nil } var costDiffPercent, oneTimePercent int64 if stats.TotalAmount > 0 { costDiffPercent = stats.CostDiffAmount * 1000 / stats.TotalAmount oneTimePercent = stats.OneTimeAmount * 1000 / stats.TotalAmount } return &dto.CommissionStatsResponse{ TotalAmount: stats.TotalAmount, CostDiffAmount: stats.CostDiffAmount, OneTimeAmount: stats.OneTimeAmount, CostDiffPercent: costDiffPercent, OneTimePercent: oneTimePercent, TotalCount: stats.TotalCount, CostDiffCount: stats.CostDiffCount, OneTimeCount: stats.OneTimeCount, }, nil } // GetDailyStats 获取每日佣金统计 // GET /shops/:id/commission-daily-stats func (s *Service) GetDailyStats(ctx context.Context, shopID uint, req *dto.DailyCommissionStatsRequest) ([]*dto.DailyCommissionStatsResponse, error) { // 越权校验:平台人员可查所有,代理只能查自己和下级 if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } days := 30 if req.Days != nil && *req.Days > 0 { days = *req.Days } filters := &postgres.CommissionRecordListFilters{ ShopID: shopID, StartTime: req.StartDate, EndTime: req.EndDate, } dailyStats, err := s.commissionRecordStore.GetDailyStats(ctx, filters, days) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "获取每日佣金统计失败") } result := make([]*dto.DailyCommissionStatsResponse, 0, len(dailyStats)) for _, stat := range dailyStats { result = append(result, &dto.DailyCommissionStatsResponse{ Date: stat.Date, TotalAmount: stat.TotalAmount, TotalCount: stat.TotalCount, }) } return result, nil } // CreateWithdrawalRequest 代理发起提现申请 // POST /shops/:id/withdrawal-requests func (s *Service) CreateWithdrawalRequest(ctx context.Context, shopID uint, req *dto.CreateMyWithdrawalReq) (*dto.CreateMyWithdrawalResp, error) { // 提现权限比查询更严格:必须是代理账号本人操作,不允许平台人员或顶级代理替下级提现 userType := middleware.GetUserTypeFromContext(ctx) if userType != constants.UserTypeAgent { return nil, errors.New(errors.CodeForbidden, "仅代理商用户可发起提现") } if shopID != middleware.GetShopIDFromContext(ctx) { return nil, errors.New(errors.CodeForbidden, "仅可为本人店铺发起提现") } currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeForbidden, "无法获取用户信息") } // 获取提现配置 setting, err := s.commissionWithdrawalSettingStore.GetCurrent(ctx) if err != nil { return nil, errors.New(errors.CodeInvalidParam, "暂未开放提现功能") } // 验证最低提现金额 if req.Amount < setting.MinWithdrawalAmount { return nil, errors.New(errors.CodeInvalidParam, fmt.Sprintf("提现金额不能低于 %.2f 元", float64(setting.MinWithdrawalAmount)/100)) } // 获取佣金钱包 wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, shopID) if err != nil { return nil, errors.New(errors.CodeInsufficientBalance, "钱包不存在") } // 验证可用余额 if req.Amount > wallet.GetAvailableBalance() { return nil, errors.New(errors.CodeInsufficientBalance, "可提现余额不足") } // 验证今日提现次数 today := time.Now().Format("2006-01-02") var todayCount int64 s.db.WithContext(ctx).Model(&model.CommissionWithdrawalRequest{}). Where("shop_id = ? AND created_at >= ? AND created_at <= ?", shopID, today+" 00:00:00", today+" 23:59:59"). Count(&todayCount) if int(todayCount) >= setting.DailyWithdrawalLimit { return nil, errors.New(errors.CodeInvalidParam, "今日提现次数已达上限") } // 计算手续费 fee := req.Amount * setting.FeeRate / 10000 actualAmount := req.Amount - fee withdrawalNo := generateWithdrawalNo() accountInfo := map[string]string{ "account_name": req.AccountName, "account_number": req.AccountNumber, } accountInfoJSON, _ := json.Marshal(accountInfo) var withdrawalRequest *model.CommissionWithdrawalRequest err = s.db.Transaction(func(tx *gorm.DB) error { // 使用条件更新防并发 result := tx.WithContext(ctx).Model(&model.AgentWallet{}). Where("id = ? AND balance - frozen_balance >= ?", wallet.ID, req.Amount). Updates(map[string]interface{}{ "frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount), }) if result.Error != nil { return errors.Wrap(errors.CodeInternalError, result.Error, "冻结余额失败") } if result.RowsAffected == 0 { return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突,请稍后重试") } withdrawalRequest = &model.CommissionWithdrawalRequest{ WithdrawalNo: withdrawalNo, ShopID: shopID, ApplicantID: currentUserID, Amount: req.Amount, FeeRate: setting.FeeRate, Fee: fee, ActualAmount: actualAmount, WithdrawalMethod: req.WithdrawalMethod, AccountInfo: accountInfoJSON, Status: 1, // 待审核 } withdrawalRequest.Creator = currentUserID withdrawalRequest.Updater = currentUserID if err := tx.WithContext(ctx).Create(withdrawalRequest).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建提现申请失败") } remark := fmt.Sprintf("提现冻结,单号:%s", withdrawalNo) refType := constants.ReferenceTypeWithdrawal transaction := &model.AgentWalletTransaction{ AgentWalletID: wallet.ID, ShopID: shopID, UserID: currentUserID, TransactionType: constants.TransactionTypeWithdrawal, Amount: -req.Amount, BalanceBefore: wallet.Balance, BalanceAfter: wallet.Balance - req.Amount, Status: constants.TransactionStatusProcessing, ReferenceType: &refType, ReferenceID: &withdrawalRequest.ID, Remark: &remark, Creator: currentUserID, ShopIDTag: shopID, } if err := tx.WithContext(ctx).Create(transaction).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建钱包流水失败") } return nil }) if err != nil { return nil, err } return &dto.CreateMyWithdrawalResp{ ID: withdrawalRequest.ID, WithdrawalNo: withdrawalRequest.WithdrawalNo, Amount: withdrawalRequest.Amount, FeeRate: withdrawalRequest.FeeRate, Fee: withdrawalRequest.Fee, ActualAmount: withdrawalRequest.ActualAmount, Status: withdrawalRequest.Status, StatusName: getWithdrawalStatusName(withdrawalRequest.Status), CreatedAt: withdrawalRequest.CreatedAt.Format("2006-01-02 15:04:05"), }, nil } // ListMainWalletTransactions 查询代理主钱包(预充值钱包)流水 // GET /shops/:id/main-wallet/transactions func (s *Service) ListMainWalletTransactions(ctx context.Context, shopID uint, req *dto.MainWalletTransactionListRequest) (*dto.MainWalletTransactionListResponse, error) { // 越权校验 if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } // 获取主钱包,不存在则返回空列表 mainWallet, err := s.agentWalletStore.GetMainWallet(ctx, shopID) if err != nil { return &dto.MainWalletTransactionListResponse{ Items: []dto.MainWalletTransactionItem{}, Total: 0, Page: page, Size: pageSize, }, nil } filters := &postgres.AgentWalletTransactionListFilters{ TransactionType: req.TransactionType, StartDate: req.StartDate, EndDate: req.EndDate, } offset := (page - 1) * pageSize transactions, err := s.agentWalletTransactionStore.ListByWalletIDWithFilters(ctx, mainWallet.ID, offset, pageSize, filters) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询主钱包流水失败") } total, err := s.agentWalletTransactionStore.CountByWalletID(ctx, mainWallet.ID, filters) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "统计主钱包流水失败") } items := make([]dto.MainWalletTransactionItem, 0, len(transactions)) for _, t := range transactions { var remark string if t.Remark != nil { remark = *t.Remark } var subtype string if t.TransactionSubtype != nil { subtype = *t.TransactionSubtype } items = append(items, dto.MainWalletTransactionItem{ ID: t.ID, TransactionType: t.TransactionType, TransactionSubtype: subtype, Amount: t.Amount, BalanceBefore: t.BalanceBefore, BalanceAfter: t.BalanceAfter, Remark: remark, CreatedAt: t.CreatedAt.Format("2006-01-02 15:04:05"), }) } return &dto.MainWalletTransactionListResponse{ Items: items, Total: total, Page: page, Size: pageSize, }, nil } // ResolveCommissionRecord 修正待审佣金记录(status=99) // release: 填入金额并入账到代理佣金钱包 // invalidate: 标记为已失效 func (s *Service) ResolveCommissionRecord(ctx context.Context, recordID uint, req *dto.CommissionRecordResolveRequest) error { record, err := s.commissionRecordStore.GetByID(ctx, recordID) if err != nil { return errors.Wrap(errors.CodeNotFound, err, "佣金记录不存在") } if record.Status != constants.CommissionStatusPendingReview { return errors.New(errors.CodeInvalidParam, "该记录不是待修正状态") } now := time.Now() resolveRemark := record.Remark if req.Remark != "" { resolveRemark += " | 处理备注: " + req.Remark } if req.Action == "invalidate" { return s.commissionRecordStore.UpdateByID(ctx, nil, recordID, map[string]any{ "status": constants.CommissionStatusInvalid, "remark": resolveRemark, }) } // release 入账 if req.Amount == nil || *req.Amount <= 0 { return errors.New(errors.CodeInvalidParam, "入账操作必须指定金额") } amount := *req.Amount wallet, err := s.agentWalletStore.GetCommissionWallet(ctx, record.ShopID) if err != nil { return errors.Wrap(errors.CodeNotFound, err, "店铺佣金钱包不存在") } return s.db.Transaction(func(tx *gorm.DB) error { if err := s.commissionRecordStore.UpdateByID(ctx, tx, recordID, map[string]any{ "status": constants.CommissionStatusReleased, "amount": amount, "released_at": now, "remark": resolveRemark, }); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新佣金记录失败") } balanceBefore := wallet.Balance result := tx.Model(&model.AgentWallet{}). Where("id = ? AND version = ?", wallet.ID, wallet.Version). Updates(map[string]any{ "balance": gorm.Expr("balance + ?", amount), "version": gorm.Expr("version + 1"), }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新佣金钱包余额失败") } if result.RowsAffected == 0 { return errors.New(errors.CodeInternalError, "佣金钱包版本冲突,请重试") } if err := s.commissionRecordStore.UpdateByID(ctx, tx, recordID, map[string]any{ "balance_after": balanceBefore + amount, }); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新入账后余额失败") } return nil }) } // getWithdrawalStatusName 获取提现状态名称 func getWithdrawalStatusName(status int) string { switch status { case constants.WithdrawalStatusPending: return "待审核" case constants.WithdrawalStatusApproved: return "已通过" case constants.WithdrawalStatusRejected: return "已拒绝" case constants.WithdrawalStatusPaid: return "已到账" default: return "未知" } } // getCommissionStatusName 获取佣金状态名称 func getCommissionStatusName(status int) string { switch status { case constants.CommissionStatusFrozen: return "已冻结" case constants.CommissionStatusUnfreezing: return "解冻中" case constants.CommissionStatusReleased: return "已发放" case constants.CommissionStatusInvalid: return "已失效" default: return "未知" } } // generateWithdrawalNo 生成提现单号 func generateWithdrawalNo() string { now := time.Now() return fmt.Sprintf("W%s%04d", now.Format("20060102150405"), rand.Intn(10000)) } func containsSubstring(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && len(substr) > 0 && contains(s, substr))) } func contains(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }