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
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
|
|
}
|
|
|