274 lines
8.2 KiB
Go
274 lines
8.2 KiB
Go
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
|
||
}
|