增加支付功能

This commit is contained in:
2026-09-15 12:46:29 +08:00
parent 943c3708b0
commit a82b300b1a
46 changed files with 11675 additions and 21 deletions
+273
View File
@@ -0,0 +1,273 @@
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
}