package my_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/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" "gorm.io/gorm" ) type Service struct { db *gorm.DB shopStore *postgres.ShopStore walletStore *postgres.WalletStore commissionWithdrawalRequestStore *postgres.CommissionWithdrawalRequestStore commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStore commissionRecordStore *postgres.CommissionRecordStore walletTransactionStore *postgres.WalletTransactionStore } func New( db *gorm.DB, shopStore *postgres.ShopStore, walletStore *postgres.WalletStore, commissionWithdrawalRequestStore *postgres.CommissionWithdrawalRequestStore, commissionWithdrawalSettingStore *postgres.CommissionWithdrawalSettingStore, commissionRecordStore *postgres.CommissionRecordStore, walletTransactionStore *postgres.WalletTransactionStore, ) *Service { return &Service{ db: db, shopStore: shopStore, walletStore: walletStore, commissionWithdrawalRequestStore: commissionWithdrawalRequestStore, commissionWithdrawalSettingStore: commissionWithdrawalSettingStore, commissionRecordStore: commissionRecordStore, walletTransactionStore: walletTransactionStore, } } // GetCommissionSummary 获取我的佣金概览 func (s *Service) GetCommissionSummary(ctx context.Context) (*dto.MyCommissionSummaryResp, error) { userType := middleware.GetUserTypeFromContext(ctx) if userType != constants.UserTypeAgent { return nil, errors.New(errors.CodeForbidden, "仅代理商用户可访问") } shopID := middleware.GetShopIDFromContext(ctx) if shopID == 0 { return nil, errors.New(errors.CodeForbidden, "无法获取店铺信息") } shop, err := s.shopStore.GetByID(ctx, shopID) if err != nil { return nil, errors.New(errors.CodeShopNotFound, "店铺不存在") } // 使用 GetShopCommissionWallet 获取店铺佣金钱包 wallet, err := s.walletStore.GetShopCommissionWallet(ctx, shopID) if err != nil { // 钱包不存在时返回空数据 return &dto.MyCommissionSummaryResp{ ShopID: shopID, ShopName: shop.ShopName, }, nil } // 计算累计佣金(当前余额 + 冻结余额 + 已提现金额) // 由于 Wallet 模型没有 TotalIncome、TotalWithdrawn 字段, // 我们需要从 WalletTransaction 表计算或简化处理 var totalWithdrawn int64 s.db.WithContext(ctx).Model(&model.CommissionWithdrawalRequest{}). Where("shop_id = ? AND status IN ?", shopID, []int{2, 4}). // 已通过或已到账 Select("COALESCE(SUM(actual_amount), 0)").Scan(&totalWithdrawn) totalCommission := wallet.Balance + wallet.FrozenBalance + totalWithdrawn return &dto.MyCommissionSummaryResp{ ShopID: shopID, ShopName: shop.ShopName, TotalCommission: totalCommission, WithdrawnCommission: totalWithdrawn, UnwithdrawCommission: wallet.Balance + wallet.FrozenBalance, FrozenCommission: wallet.FrozenBalance, WithdrawingCommission: wallet.FrozenBalance, // 提现中的金额等于冻结金额 AvailableCommission: wallet.Balance, }, nil } // CreateWithdrawalRequest 发起提现申请 func (s *Service) CreateWithdrawalRequest(ctx context.Context, req *dto.CreateMyWithdrawalReq) (*dto.CreateMyWithdrawalResp, error) { userType := middleware.GetUserTypeFromContext(ctx) if userType != constants.UserTypeAgent { return nil, errors.New(errors.CodeForbidden, "仅代理商用户可访问") } shopID := middleware.GetShopIDFromContext(ctx) currentUserID := middleware.GetUserIDFromContext(ctx) if shopID == 0 || 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.walletStore.GetShopCommissionWallet(ctx, shopID) if err != nil { return nil, errors.New(errors.CodeInsufficientBalance, "钱包不存在") } // 验证余额 if req.Amount > wallet.Balance { return nil, errors.New(errors.CodeInsufficientBalance, "可提现余额不足") } // 验证今日提现次数 today := time.Now().Format("2006-01-02") todayStart := today + " 00:00:00" todayEnd := today + " 23:59:59" var todayCount int64 s.db.WithContext(ctx).Model(&model.CommissionWithdrawalRequest{}). Where("shop_id = ? AND created_at >= ? AND created_at <= ?", shopID, todayStart, todayEnd). 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() // 构建账户信息 JSON 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 { // 冻结余额 if err := tx.WithContext(ctx).Model(&model.Wallet{}). Where("id = ? AND balance >= ?", wallet.ID, req.Amount). Updates(map[string]interface{}{ "balance": gorm.Expr("balance - ?", req.Amount), "frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount), }).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "冻结余额失败") } // 创建提现申请 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.WalletTransaction{ WalletID: wallet.ID, 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, } 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 } // ListMyWithdrawalRequests 查询我的提现记录 func (s *Service) ListMyWithdrawalRequests(ctx context.Context, req *dto.MyWithdrawalListReq) (*dto.WithdrawalRequestPageResult, error) { userType := middleware.GetUserTypeFromContext(ctx) if userType != constants.UserTypeAgent { return nil, errors.New(errors.CodeForbidden, "仅代理商用户可访问") } shopID := middleware.GetShopIDFromContext(ctx) if shopID == 0 { return nil, errors.New(errors.CodeForbidden, "无法获取店铺信息") } page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } query := s.db.WithContext(ctx).Model(&model.CommissionWithdrawalRequest{}). Where("shop_id = ?", shopID) if req.Status != nil { query = query.Where("status = ?", *req.Status) } if req.StartTime != "" { query = query.Where("created_at >= ?", req.StartTime) } if req.EndTime != "" { query = query.Where("created_at <= ?", req.EndTime) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "统计提现记录失败") } var requests []model.CommissionWithdrawalRequest offset := (page - 1) * pageSize if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&requests).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询提现记录失败") } items := make([]dto.WithdrawalRequestItem, 0, len(requests)) for _, r := range requests { // 解析账户信息 accountName, accountNumber := parseAccountInfo(r.AccountInfo) items = append(items, dto.WithdrawalRequestItem{ ID: r.ID, WithdrawalNo: r.WithdrawalNo, ShopID: r.ShopID, Amount: r.Amount, FeeRate: r.FeeRate, Fee: r.Fee, ActualAmount: r.ActualAmount, WithdrawalMethod: r.WithdrawalMethod, AccountName: accountName, AccountNumber: accountNumber, Status: r.Status, StatusName: getWithdrawalStatusName(r.Status), CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"), }) } return &dto.WithdrawalRequestPageResult{ Items: items, Total: total, Page: page, Size: pageSize, }, nil } // ListMyCommissionRecords 查询我的佣金明细 func (s *Service) ListMyCommissionRecords(ctx context.Context, req *dto.MyCommissionRecordListReq) (*dto.MyCommissionRecordPageResult, error) { userType := middleware.GetUserTypeFromContext(ctx) if userType != constants.UserTypeAgent { return nil, errors.New(errors.CodeForbidden, "仅代理商用户可访问") } shopID := middleware.GetShopIDFromContext(ctx) if shopID == 0 { return nil, errors.New(errors.CodeForbidden, "无法获取店铺信息") } page := req.Page pageSize := req.PageSize if page == 0 { page = 1 } if pageSize == 0 { pageSize = constants.DefaultPageSize } query := s.db.WithContext(ctx).Model(&model.CommissionRecord{}). Where("shop_id = ?", shopID) if req.CommissionSource != nil { query = query.Where("commission_source = ?", *req.CommissionSource) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "统计佣金记录失败") } var records []model.CommissionRecord offset := (page - 1) * pageSize if err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&records).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询佣金记录失败") } items := make([]dto.MyCommissionRecordItem, 0, len(records)) for _, r := range records { items = append(items, dto.MyCommissionRecordItem{ ID: r.ID, ShopID: r.ShopID, OrderID: r.OrderID, CommissionSource: r.CommissionSource, Amount: r.Amount, Status: r.Status, StatusName: getCommissionStatusName(r.Status), CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"), }) } return &dto.MyCommissionRecordPageResult{ Items: items, Total: total, Page: page, Size: pageSize, }, nil } func (s *Service) GetStats(ctx context.Context, req *dto.CommissionStatsRequest) (*dto.CommissionStatsResponse, error) { shopID, err := s.getShopIDFromContext(ctx) if err != nil { return nil, err } 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, tierBonusPercent int64 if stats.TotalAmount > 0 { costDiffPercent = stats.CostDiffAmount * 1000 / stats.TotalAmount oneTimePercent = stats.OneTimeAmount * 1000 / stats.TotalAmount tierBonusPercent = stats.TierBonusAmount * 1000 / stats.TotalAmount } return &dto.CommissionStatsResponse{ TotalAmount: stats.TotalAmount, CostDiffAmount: stats.CostDiffAmount, OneTimeAmount: stats.OneTimeAmount, TierBonusAmount: stats.TierBonusAmount, CostDiffPercent: costDiffPercent, OneTimePercent: oneTimePercent, TierBonusPercent: tierBonusPercent, TotalCount: stats.TotalCount, CostDiffCount: stats.CostDiffCount, OneTimeCount: stats.OneTimeCount, TierBonusCount: stats.TierBonusCount, }, nil } func (s *Service) GetDailyStats(ctx context.Context, req *dto.DailyCommissionStatsRequest) ([]*dto.DailyCommissionStatsResponse, error) { shopID, err := s.getShopIDFromContext(ctx) if err != nil { return nil, err } 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 } func (s *Service) getShopIDFromContext(ctx context.Context) (uint, error) { userType := middleware.GetUserTypeFromContext(ctx) if userType != constants.UserTypeAgent { return 0, errors.New(errors.CodeForbidden, "仅代理商用户可访问") } shopID := middleware.GetShopIDFromContext(ctx) if shopID == 0 { return 0, errors.New(errors.CodeForbidden, "无法获取店铺信息") } return shopID, nil } // generateWithdrawalNo 生成提现单号 func generateWithdrawalNo() string { now := time.Now() return fmt.Sprintf("W%s%04d", now.Format("20060102150405"), rand.Intn(10000)) } // getWithdrawalStatusName 获取提现状态名称 func getWithdrawalStatusName(status int) string { switch status { case 1: return "待审核" case 2: return "已通过" case 3: return "已拒绝" case 4: return "已到账" default: return "未知" } } // getCommissionStatusName 获取佣金状态名称 func getCommissionStatusName(status int) string { switch status { case 1: return "已冻结" case 2: return "解冻中" case 3: return "已发放" case 4: return "已失效" default: return "未知" } } // parseAccountInfo 解析账户信息 JSON func parseAccountInfo(data []byte) (accountName, accountNumber string) { if len(data) == 0 { return "", "" } var info map[string]string if err := json.Unmarshal(data, &info); err != nil { return "", "" } return info["account_name"], info["account_number"] }