package payment import ( "context" "encoding/json" "errors" "fmt" "strings" "time" "server/models" "github.com/beego/beego/v2/client/orm" beelog "github.com/beego/beego/v2/core/logs" ) /* ============================================================= * 推广佣金:规则匹配 -> 金额计算 -> 佣金单生成(幂等)-> 发放登记 * 佣金是平台自有资金支出,与租户收款资金流解耦。 * ============================================================= */ // OnOrderPaid 支付成功后生成佣金单(幂等:同一支付单 + 同一推广方只生成一张) // 无推广方 / 无匹配规则 / 佣金金额为 0 时返回 (nil, nil)。 func OnOrderPaid(ctx context.Context, order *models.PlatformPaymentOrder) (*models.PlatformCommissionOrder, error) { if order == nil || order.PromoterID == "" { return nil, nil } // 幂等查重 exist := &models.PlatformCommissionOrder{} err := models.Orm.QueryTable(new(models.PlatformCommissionOrder)). Filter("pay_no", order.PayNo). Filter("promoter_id", order.PromoterID). Filter("delete_time__isnull", true). One(exist) if err == nil { return exist, nil } else if err != orm.ErrNoRows { return nil, fmt.Errorf("查询佣金单失败: %w", err) } rule, err := matchCommissionRule(order.OrderType) if err != nil { return nil, err } if rule == nil { return nil, nil } amount := calcCommissionAmount(rule, order.Amount) if amount <= 0 { beelog.Warn("佣金规则 %s 计算金额为 0,跳过(订单 %s)", rule.RuleName, order.PayNo) return nil, nil } row := &models.PlatformCommissionOrder{ CommissionNo: NextCommissionNo(), PayNo: order.PayNo, OutTradeNo: order.OutTradeNo, RuleID: rule.ID, RuleName: rule.RuleName, CalcType: rule.CalcType, PromoterID: order.PromoterID, PromoterName: order.PromoterName, PromoterType: order.PromoterType, BaseAmount: order.Amount, Amount: amount, Status: models.CommissionStatusPayable, Remark: fmt.Sprintf("支付单 %s 支付成功自动生成", order.PayNo), } if _, err := models.Orm.Insert(row); err != nil { if strings.Contains(err.Error(), "Duplicate") || strings.Contains(err.Error(), "1062") { // 并发生成命中唯一键:查回已有记录 return exist, nil } return nil, fmt.Errorf("创建佣金单失败: %w", err) } beelog.Info("佣金单已生成: %s 支付单 %s 推广方 %s 金额 %d 分", row.CommissionNo, order.PayNo, order.PromoterName, amount) return row, nil } // matchCommissionRule 匹配启用中的佣金规则(同类型规则取最新一条) func matchCommissionRule(orderType string) (*models.PlatformCommissionRule, error) { var rules []models.PlatformCommissionRule _, err := models.Orm.QueryTable(new(models.PlatformCommissionRule)). Filter("status", 1). Filter("delete_time__isnull", true). OrderBy("-id"). All(&rules) if err != nil { return nil, err } for i := range rules { if rules[i].OrderType == models.CommissionOrderTypeAll || rules[i].OrderType == orderType { return &rules[i], nil } } return nil, nil } // calcCommissionAmount 按规则计算佣金(分),向下取整 func calcCommissionAmount(rule *models.PlatformCommissionRule, baseAmount int64) int64 { if rule == nil || baseAmount <= 0 { return 0 } switch rule.CalcType { case models.CommissionCalcPercent: return baseAmount * rule.Value / 10000 // value = 百分比 × 100 case models.CommissionCalcFixed: return rule.Value case models.CommissionCalcTiered: for _, tier := range parseTiers(rule.TierJSON) { if baseAmount < tier.Min { continue } if tier.Max != nil && baseAmount >= *tier.Max { continue } return baseAmount * tier.Rate / 10000 } return 0 default: return 0 } } func parseTiers(raw *string) []models.CommissionTier { tiers := []models.CommissionTier{} if raw == nil || *raw == "" { return tiers } _ = json.Unmarshal([]byte(*raw), &tiers) return tiers } /* ---------------- 佣金台账查询与发放 ---------------- */ // CommissionListInput 台账查询条件 type CommissionListInput struct { Promoter string Status string StartTime string // YYYY-MM-DD EndTime string Page int PageSize int } // ListCommissions 佣金台账分页 func ListCommissions(in CommissionListInput) ([]models.PlatformCommissionOrder, int64, error) { qs := models.Orm.QueryTable(new(models.PlatformCommissionOrder)).Filter("delete_time__isnull", true) if in.Promoter != "" { qs = qs.Filter("promoter_name__icontains", in.Promoter) } if in.Status != "" { qs = qs.Filter("status", in.Status) } if in.StartTime != "" { if t, err := time.ParseInLocation("2006-01-02", in.StartTime, time.Local); err == nil { qs = qs.Filter("create_time__gte", t) } } if in.EndTime != "" { if t, err := time.ParseInLocation("2006-01-02", in.EndTime, time.Local); err == nil { qs = qs.Filter("create_time__lt", t.AddDate(0, 0, 1)) } } total, err := qs.Count() if err != nil { return nil, 0, err } if in.Page <= 0 { in.Page = 1 } if in.PageSize <= 0 || in.PageSize > 200 { in.PageSize = 10 } var rows []models.PlatformCommissionOrder _, err = qs.OrderBy("-id").Limit(in.PageSize, (in.Page-1)*in.PageSize).All(&rows) return rows, total, err } // CommissionStats 台账统计(应付 / 已付 / 待处理笔数) type CommissionStats struct { PayableAmount int64 `json:"payable_amount"` PaidAmount int64 `json:"paid_amount"` PendingCount int64 `json:"pending_count"` } // GetCommissionStats 统计口径: // // 应付总额 = status=payable 的金额合计;已付总额 = status=settled;待处理笔数 = payable + tax_processing。 func GetCommissionStats() (*CommissionStats, error) { type row struct { Total int64 `orm:"column(total)"` Cnt int64 `orm:"column(cnt)"` } stats := &CommissionStats{} pay := row{} if err := models.Orm.Raw( "SELECT COALESCE(SUM(amount),0) AS total, COUNT(*) AS cnt FROM yz_platform_commission_order WHERE delete_time IS NULL AND status = ?", models.CommissionStatusPayable).QueryRow(&pay); err != nil { return nil, err } stats.PayableAmount = pay.Total stats.PendingCount = pay.Cnt paid := row{} if err := models.Orm.Raw( "SELECT COALESCE(SUM(amount),0) AS total, COUNT(*) AS cnt FROM yz_platform_commission_order WHERE delete_time IS NULL AND status = ?", models.CommissionStatusSettled).QueryRow(&paid); err != nil { return nil, err } stats.PaidAmount = paid.Total tax := row{} if err := models.Orm.Raw( "SELECT COALESCE(SUM(amount),0) AS total, COUNT(*) AS cnt FROM yz_platform_commission_order WHERE delete_time IS NULL AND status = ?", models.CommissionStatusTaxProcessing).QueryRow(&tax); err != nil { return nil, err } stats.PendingCount += tax.Cnt return stats, nil } // MarkCommissionPaidInput 标记已付入参 type MarkCommissionPaidInput struct { ID uint64 PayMethod string PayVoucher string Remark string OperatorID string OperatorName string } // MarkCommissionPaid 发放登记:应付/税务处理中 -> 已付 func MarkCommissionPaid(ctx context.Context, in MarkCommissionPaidInput) (*models.PlatformCommissionOrder, error) { row := &models.PlatformCommissionOrder{} if err := models.Orm.QueryTable(new(models.PlatformCommissionOrder)). Filter("id", in.ID). Filter("delete_time__isnull", true). One(row); err != nil { return nil, fmt.Errorf("佣金单不存在") } if row.Status == models.CommissionStatusSettled { return nil, errors.New("该佣金单已是已付状态") } now := time.Now() _, err := models.Orm.QueryTable(new(models.PlatformCommissionOrder)). Filter("id", row.ID). Update(orm.Params{ "status": models.CommissionStatusSettled, "pay_method": in.PayMethod, "pay_voucher": in.PayVoucher, "paid_at": now, "paid_user_id": in.OperatorID, "paid_user_name": in.OperatorName, "remark": in.Remark, "update_time": now, }) if err != nil { return nil, err } return GetCommissionByID(in.ID) } // GetCommissionByID 按 ID 查询佣金单 func GetCommissionByID(id uint64) (*models.PlatformCommissionOrder, error) { row := &models.PlatformCommissionOrder{} err := models.Orm.QueryTable(new(models.PlatformCommissionOrder)). Filter("id", id). Filter("delete_time__isnull", true). One(row) return row, err }