Files
2026-09-15 12:46:29 +08:00

274 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}