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.
 
 
 
 
 
 

214 lines
7.2 KiB

package console
import (
"strings"
"time"
"yunyan/lego/sys/log"
"yunyan/pb"
"github.com/gin-gonic/gin"
)
// ============================ 月结算域 (settlement) ============================
//
// 平台 → 品牌商 → 渠道商 的两级分成月结算。
//
// base = 该月归属该 (品牌商, 渠道商) 的已支付订单实付金额合计
// brand_gross = base × 品牌商比例
// channel_amount = brand_gross × 渠道商比例 ← 渠道商从品牌商那份里再切
// brand_net = brand_gross - channel_amount
// platform_amount = base - brand_gross
//
// 订单散在各应用业务库,生成时逐库聚合再按 (brandid, channelid) 汇总;比例在生成那一刻
// 快照进结算行,之后调比例不影响已生成的月份。已确认(status>=1)的行重算时跳过,历史账不可变。
// getSettlements 结算单列表。品牌商/渠道商账号由后端按归属强制收敛。
func (this *serverComp) getSettlements(c *gin.Context) {
var req pb.ApiGetSettlementsReq
_ = c.ShouldBindJSON(&req)
s := scopeOf(c)
period := req.Period
if period == 0 {
period = latestSettlementPeriod() // 不传月份 → 最近一个有数据的月
}
brandid := effectiveBrandId(c, req.Brandid)
// 渠道商账号锁死到自己那一个渠道商;其余按前端传值(空=不限)。
var channelPtr *string
if cid := effectiveChannelId(c, req.Channelid); cid != "" {
channelPtr = &cid
}
// 品牌商账号即便不传 brandid 也只能看自己那一个(effectiveBrandId 已强制),
// 这里再给一层品牌商白名单,防止将来新增入口漏改。
var brandScope []uint32
if !s.unlimited && s.brandId != 0 {
brandScope = []uint32{s.brandId}
}
items, err := listSettlements(period, brandid, channelPtr, req.Status, brandScope)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
resp := &pb.ApiGetSettlementsResp{Items: items}
for _, it := range items {
resp.TotalBase += it.BaseAmount
resp.TotalBrandNet += it.BrandNet
resp.TotalChannel += it.ChannelAmount
resp.TotalPlatform += it.PlatformAmount
}
writeOK(c, resp)
}
// genSettlement 生成/重算某月结算单(仅超管/管理员)。
// 已确认的行原样保留并计入 skipped;其余行按当前比例重算覆盖。
func (this *serverComp) genSettlement(c *gin.Context) {
if !canAuditChannel(c) { // 与渠道审批同权限:超管 + 管理员
writeErr(c, pb.ErrorCode_InsufficientPermissions, "仅超管/管理员可生成结算单")
return
}
var req pb.ApiGenSettlementReq
_ = c.ShouldBindJSON(&req)
period := req.Period
if period == 0 {
// 不传月份 → 结算上一个自然月(月初跑当月生成上月账的常见用法)。
period = periodOf(time.Now().AddDate(0, -1, 0))
}
start, end, err := periodRange(period)
if err != nil {
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
return
}
// 1) 逐个应用业务库聚合该月已支付订单,按 (品牌商, 渠道商) 汇总。
type key struct {
brandid uint32
channelid string
}
sums := make(map[key]*settleAgg)
apps, err := this.module.model.listApps()
if err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
var scanned, failed int
for _, app := range apps {
if !app.Enabled {
continue
}
db, derr := this.module.registry.getServiceDB(app.Id)
if derr != nil {
failed++
log.Warn("console.settlement: 应用业务库不可用,已跳过",
log.Field{Key: "app", Value: app.Name}, log.Field{Key: "err", Value: derr.Error()})
continue
}
rows, aerr := aggOrdersOnDB(db, start, end)
if aerr != nil {
failed++
// 常见原因:该库还没跑渠道维度迁移,payorder 缺 src_* 列。
log.Warn("console.settlement: 订单聚合失败,已跳过该应用",
log.Field{Key: "app", Value: app.Name}, log.Field{Key: "err", Value: aerr.Error()})
continue
}
scanned++
for _, r := range rows {
k := key{brandid: r.Brandid, channelid: r.Channelid}
if cur, ok := sums[k]; ok {
cur.OrderCount += r.OrderCount
cur.BaseAmount += r.BaseAmount
} else {
sums[k] = &settleAgg{Brandid: r.Brandid, Channelid: r.Channelid, OrderCount: r.OrderCount, BaseAmount: r.BaseAmount}
}
}
}
// 2) 取品牌商/渠道商当前比例,算钱,落库(已确认的跳过)。
sys := consoleDeviceConn()
now := time.Now().Unix()
out := make([]*pb.DBSettlementMonth, 0, len(sums))
var skipped int64
for k, agg := range sums {
if old, oerr := getSettlement(period, k.brandid, k.channelid); oerr == nil && old != nil && old.Status >= settlementStatusConfirmed {
skipped++ // 已确认:锁定,不覆盖
continue
}
var brandRate, channelRate int32
if k.brandid != 0 {
if b, berr := dvBrand(sys, k.brandid); berr == nil && b != nil {
brandRate = b.Sharerate
}
}
if k.channelid != "" {
if ch, cerr := dvChannel(sys, k.channelid); cerr == nil && ch != nil {
channelRate = ch.Sharerate
}
}
brandGross, channelAmount, brandNet, platform := computeSettlement(agg.BaseAmount, brandRate, channelRate)
out = append(out, &pb.DBSettlementMonth{
Period: period,
Brandid: k.brandid,
Channelid: k.channelid,
OrderCount: agg.OrderCount,
BaseAmount: agg.BaseAmount,
BrandRate: brandRate,
BrandGross: brandGross,
ChannelRate: channelRate,
ChannelAmount: channelAmount,
BrandNet: brandNet,
PlatformAmount: platform,
Status: settlementStatusDraft,
GenTime: now,
})
}
if err = upsertSettlements(out); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
log.Infof("console.settlement: 生成结算单 period=%d 行数=%d 跳过(已确认)=%d 应用库 成功=%d 跳过=%d",
period, len(out), skipped, scanned, failed)
writeOK(c, &pb.ApiGenSettlementResp{Period: period, Rows: int64(len(out)), Skipped: skipped})
}
// confirmSettlement 确认 / 标记已支付 / 退回草稿(仅超管/管理员)。
// 确认后该行被锁定,重算不再覆盖。
func (this *serverComp) confirmSettlement(c *gin.Context) {
if !canAuditChannel(c) {
writeErr(c, pb.ErrorCode_InsufficientPermissions, "仅超管/管理员可确认结算单")
return
}
var req pb.ApiConfirmSettlementReq
_ = c.ShouldBindJSON(&req)
if req.Period == 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "period 必填")
return
}
switch req.Status {
case settlementStatusDraft, settlementStatusConfirmed, settlementStatusPaid:
default:
writeErr(c, pb.ErrorCode_ReqParameterError, "status 仅支持 0(退回草稿) / 1(已确认) / 2(已支付)")
return
}
channelid := strings.TrimSpace(req.Channelid)
model, err := getSettlement(req.Period, req.Brandid, channelid)
if err != nil || model.Period == 0 {
writeErr(c, pb.ErrorCode_DBError, "结算单不存在")
return
}
model.Status = req.Status
model.Remark = req.Remark
if req.Status == settlementStatusDraft {
// 退回草稿:清掉确认痕迹,下次重算会重新覆盖这一行。
model.ConfirmAccount = ""
model.ConfirmTime = 0
} else {
model.ConfirmAccount = ctxStr(c, "username")
model.ConfirmTime = time.Now().Unix()
}
if err = saveSettlement(model); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
writeOK(c, &pb.ApiConfirmSettlementResp{Item: model})
}