You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

171 lines
6.4 KiB

package console
import (
"fmt"
"strings"
"time"
"yunyan/comm"
"yunyan/lego/sys/mysql"
"yunyan/lego/sys/postgres"
"yunyan/pb"
"gorm.io/gorm/clause"
)
// 月结算数据模型。结算单存 console 主库(与品牌商/渠道商同库),
// 结算基数来自【各应用业务库】的订单表 payorder——订单分散在各部署,生成时逐库聚合再汇总。
//
// 分成算法(渠道商从品牌商那份里再切,比例在生成时快照冻结进结算行):
//
// brand_gross = base * brand_rate / 10000 品牌商总提成
// channel_amount = brand_gross * channel_rate / 10000 渠道商从品牌那份里切
// brand_net = brand_gross - channel_amount 品牌商实得
// platform_amount = base - brand_gross 平台留存
//
// 退款口径:结算只统计【当前状态为已支付】的订单。同月内发生的退款会让订单状态离开
// 已支付,重算时自然从基数里消失;跨月退款不回改已确认的历史月份(历史账不可变),
// 需要冲正时由管理员在当月结算单的备注里登记处理。
// 结算单状态取值(与 DBSettlementMonth.status 及前端一致)。
const (
settlementStatusDraft int32 = 0 // 草稿:可被重算覆盖
settlementStatusConfirmed int32 = 1 // 已确认:锁定,重算跳过
settlementStatusPaid int32 = 2 // 已支付
)
// rateBase 分成比例的分母(万分比:2000 = 20%)。
const rateBase int64 = 10000
// ensureSettlementTable 在 console 主库建好结算单表(幂等)。
func ensureSettlementTable() error {
return postgres.CreateTable(comm.TableSettlementMonth, &pb.DBSettlementMonth{})
}
// settleAgg 一个 (品牌商, 渠道商) 归属在某月的订单聚合结果。
type settleAgg struct {
Brandid uint32 `gorm:"column:brandid"`
Channelid string `gorm:"column:channelid"`
OrderCount int64 `gorm:"column:order_count"`
BaseAmount int64 `gorm:"column:base_amount"`
}
// periodRange 把结算月 YYYYMM 换算成 [起, 止) 的 unix 秒区间(按统计时区,与统计口径一致)。
func periodRange(period uint32) (start, end int64, err error) {
if period < 200001 || period > 999912 {
return 0, 0, fmt.Errorf("结算月格式非法(应为 YYYYMM): %d", period)
}
y := int(period / 100)
m := int(period % 100)
if m < 1 || m > 12 {
return 0, 0, fmt.Errorf("结算月月份非法: %d", period)
}
loc := statLocation()
from := time.Date(y, time.Month(m), 1, 0, 0, 0, 0, loc)
return from.Unix(), from.AddDate(0, 1, 0).Unix(), nil
}
// periodOf 把时间转成结算月 YYYYMM(按统计时区)。
func periodOf(t time.Time) uint32 {
tt := t.In(statLocation())
return uint32(tt.Year()*100 + int(tt.Month()))
}
// aggOrdersOnDB 在一个应用业务库上,按 (来源品牌商, 来源渠道商) 聚合该月已支付订单。
// 归属取订单自己的来源设备快照(src_*):下单时冻结,事后换绑/换设备不影响历史账。
// 未迁移出 src_* 列的老库会查询报错,由调用方按「跳过该库并记录」处理。
func aggOrdersOnDB(db mysql.ISys, start, end int64) ([]*settleAgg, error) {
rows := make([]*settleAgg, 0)
err := db.Table(comm.TablePayOrder).
Select("src_brandid AS brandid, src_channelid AS channelid, COUNT(*) AS order_count, COALESCE(SUM(amount),0) AS base_amount").
Where("status = ? AND pay_time >= ? AND pay_time < ?", int32(pb.PayOrderStatus_PAY_ORDER_PAID), start, end).
Group("src_brandid, src_channelid").
Scan(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
// computeSettlement 按分成比例算出一行结算的各项金额(分,整数运算,末位截断)。
func computeSettlement(base int64, brandRate, channelRate int32) (brandGross, channelAmount, brandNet, platform int64) {
if brandRate < 0 {
brandRate = 0
}
if channelRate < 0 {
channelRate = 0
}
brandGross = base * int64(brandRate) / rateBase
channelAmount = brandGross * int64(channelRate) / rateBase
brandNet = brandGross - channelAmount
platform = base - brandGross
return
}
// upsertSettlements 批量覆盖写入结算行(按复合主键 period+brandid+channelid)。
func upsertSettlements(rows []*pb.DBSettlementMonth) error {
if len(rows) == 0 {
return nil
}
return postgres.Table(comm.TableSettlementMonth).
Clauses(clause.OnConflict{UpdateAll: true}).
CreateInBatches(rows, 200).Error
}
// listSettlements 按条件列结算单。brandid=0 不限;channelid 用指针区分「不限」与「空串(无渠道归属)」;
// status<0 不限。
func listSettlements(period uint32, brandid uint32, channelid *string, status int32, brandScope []uint32) (models []*pb.DBSettlementMonth, err error) {
models = make([]*pb.DBSettlementMonth, 0)
conds := make([]string, 0, 5)
args := make([]interface{}, 0, 5)
if period > 0 {
conds = append(conds, "period=?")
args = append(args, period)
}
if brandid != 0 {
conds = append(conds, "brandid=?")
args = append(args, brandid)
}
if channelid != nil {
conds = append(conds, "channelid=?")
args = append(args, *channelid)
}
if status >= 0 {
conds = append(conds, "status=?")
args = append(args, status)
}
// brandScope 非 nil 时限定到这些品牌商(品牌商账号收敛用);空切片代表「无任何可见品牌商」。
if brandScope != nil {
if len(brandScope) == 0 {
return models, nil
}
conds = append(conds, "brandid IN ?")
args = append(args, brandScope)
}
err = postgres.Table(comm.TableSettlementMonth).
Where(strings.Join(conds, " AND "), args...).
Order("period DESC, brandid ASC, channelid ASC").
Find(&models).Error
return
}
// latestSettlementPeriod 取库里最近一个有结算数据的月份(前端不传月份时的默认值)。
func latestSettlementPeriod() (period uint32) {
postgres.Table(comm.TableSettlementMonth).
Select("COALESCE(MAX(period),0)").Scan(&period)
return
}
// getSettlement 按复合主键取一行结算单。
func getSettlement(period uint32, brandid uint32, channelid string) (model *pb.DBSettlementMonth, err error) {
model = &pb.DBSettlementMonth{}
err = postgres.FindOne(comm.TableSettlementMonth, model, "period=? AND brandid=? AND channelid=?", period, brandid, channelid)
return
}
// saveSettlement 覆盖保存一行结算单。
func saveSettlement(model *pb.DBSettlementMonth) error {
return postgres.Table(comm.TableSettlementMonth).
Clauses(clause.OnConflict{UpdateAll: true}).
Create(model).Error
}