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