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 }