refactor(distribution): 重写分销迁移并落地利润百分比分成与货架比例

将 000064-000078 合并为 064/065/066,默认规则改为收号 35%、卖出 40%、服务费 25%。
货架调价改为增加分站 1 元兑换哈夫币比例。sql-guard 仅保护 main 已发布迁移。
This commit is contained in:
yml2213
2026-09-08 19:02:58 +08:00
parent 48a2961245
commit d762b33f54
48 changed files with 745 additions and 1073 deletions
@@ -13,6 +13,7 @@ import (
"hfb_sys/backend/internal/auditlog"
"hfb_sys/backend/internal/model"
listingpkg "hfb_sys/backend/internal/modules/listing"
"hfb_sys/backend/internal/modules/wallet"
"github.com/redis/go-redis/v9"
@@ -319,12 +320,19 @@ func (r *Repository) CreateRule(ctx context.Context, adminID uint64, req CreateR
if effectiveFrom.IsZero() {
effectiveFrom = time.Now()
}
row := model.DistributionRuleVersion{
SiteID: req.SiteID, Mode: strings.TrimSpace(req.Mode), Version: version, Status: "draft",
parsed := Rule{
PlatformReceiveRatio: req.PlatformReceiveRatio, PlatformSellRatio: req.PlatformSellRatio,
DistributorReceiveRatio: req.DistributorReceiveRatio, DistributorSellRatio: req.DistributorSellRatio,
DistributorARatio: req.DistributorARatio, DistributorBRatio: req.DistributorBRatio,
ServiceFeeValue: req.ServiceFeeValue, ServiceFeeUnit: strings.TrimSpace(req.ServiceFeeUnit),
}
normalizeRulePercents(&parsed)
row := model.DistributionRuleVersion{
SiteID: req.SiteID, Mode: strings.TrimSpace(req.Mode), Version: version, Status: "draft",
PlatformReceiveRatio: parsed.PlatformReceiveRatio, PlatformSellRatio: parsed.PlatformSellRatio,
DistributorReceiveRatio: parsed.DistributorReceiveRatio, DistributorSellRatio: parsed.DistributorSellRatio,
DistributorARatio: parsed.DistributorARatio, DistributorBRatio: parsed.DistributorBRatio,
ServiceFeeValue: parsed.ServiceFeeValue, ServiceFeeUnit: parsed.ServiceFeeUnit,
PaymentFeeBPS: req.PaymentFeeBPS, TaxEnabled: req.TaxEnabled, TaxRateBPS: req.TaxRateBPS,
EffectiveFrom: effectiveFrom, CreatedBy: &adminID,
}
@@ -443,23 +451,24 @@ func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*Distri
func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, status string) ([]AccountDisplayDTO, error) {
type accountDisplayRow struct {
ID uint64 `gorm:"column:id"`
SiteID uint64 `gorm:"column:site_id"`
AccountID uint64 `gorm:"column:account_id"`
ListingID uint64 `gorm:"column:listing_id"`
DisplayStatus string `gorm:"column:display_status"`
MarkupAmountCent int64 `gorm:"column:markup_amount_cent"`
ListingNo string `gorm:"column:listing_no"`
Title string `gorm:"column:title"`
GameName string `gorm:"column:game_name"`
ServerRegion string `gorm:"column:server_region"`
LoginPlatform string `gorm:"column:login_platform"`
RankLevel string `gorm:"column:rank_level"`
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
PriceCent int64 `gorm:"column:price_cent"`
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
ListingStatus string `gorm:"column:listing_status"`
InTransaction bool `gorm:"column:in_transaction"`
ID uint64 `gorm:"column:id"`
SiteID uint64 `gorm:"column:site_id"`
AccountID uint64 `gorm:"column:account_id"`
ListingID uint64 `gorm:"column:listing_id"`
DisplayStatus string `gorm:"column:display_status"`
MarkupAmountCent int64 `gorm:"column:markup_amount_cent"`
ShelfRatioDelta float64 `gorm:"column:shelf_ratio_delta"`
ListingNo string `gorm:"column:listing_no"`
Title string `gorm:"column:title"`
GameName string `gorm:"column:game_name"`
ServerRegion string `gorm:"column:server_region"`
LoginPlatform string `gorm:"column:login_platform"`
RankLevel string `gorm:"column:rank_level"`
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
PriceCent int64 `gorm:"column:price_cent"`
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
ListingStatus string `gorm:"column:listing_status"`
InTransaction bool `gorm:"column:in_transaction"`
}
var rows []accountDisplayRow
@@ -472,6 +481,7 @@ func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, sta
dsa.listing_id,
dsa.display_status,
COALESCE(dsa.markup_amount_cent, 0) AS markup_amount_cent,
COALESCE(dsa.shelf_ratio_delta, 0) AS shelf_ratio_delta,
COALESCE(l.listing_no, '') AS listing_no,
COALESCE(ga.title, '') AS title,
COALESCE(ga.game_name, '') AS game_name,
@@ -496,13 +506,17 @@ func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, sta
}
items := make([]AccountDisplayDTO, 0, len(rows))
for _, row := range rows {
sitePrice, baseRatio, siteRatio := listingpkg.ApplyShelfRatio(row.HafCoinAmount, row.PriceCent, row.ShelfRatioDelta)
items = append(items, AccountDisplayDTO{
ID: row.ID,
SiteID: row.SiteID,
AccountID: row.AccountID,
ListingID: row.ListingID,
DisplayStatus: row.DisplayStatus,
MarkupAmountCent: row.MarkupAmountCent,
MarkupAmountCent: sitePrice - row.PriceCent,
ShelfRatioDelta: row.ShelfRatioDelta,
BaseRatio: baseRatio,
SiteRatio: siteRatio,
ListingNo: row.ListingNo,
Title: row.Title,
GameName: row.GameName,
@@ -511,6 +525,7 @@ func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, sta
RankLevel: row.RankLevel,
HafCoinAmount: row.HafCoinAmount,
PriceCent: row.PriceCent,
SitePriceCent: sitePrice,
DepositAmountCent: row.DepositAmountCent,
ListingStatus: row.ListingStatus,
InTransaction: row.InTransaction,
@@ -913,12 +928,14 @@ func (r *Repository) CreateFinancialSnapshot(ctx context.Context, adminID, order
for _, party := range req.Parties {
partyInputs = append(partyInputs, PartyProfitInput{PartyRole: party.PartyRole, IncomeAmountCent: party.IncomeAmountCent, CostAmountCent: party.CostAmountCent})
}
result, err := CalculateSettlement(Rule{Mode: rule.Mode, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, PaymentFeeBPS: rule.PaymentFeeBPS, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS}, SettlementInput{GrossPaidAmountCent: req.GrossPaidAmountCent, RefundAmountCent: req.RefundAmountCent, MarkupAmountCent: req.MarkupAmountCent, GrossProfitByParty: partyInputs})
parsed := ruleFromVersion(rule)
result, err := CalculateSettlement(parsed, SettlementInput{GrossPaidAmountCent: req.GrossPaidAmountCent, RefundAmountCent: req.RefundAmountCent, MarkupAmountCent: req.MarkupAmountCent, GrossProfitByParty: partyInputs})
if err != nil {
return nil, err
}
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
financial := model.DistributionOrderFinancial{OrderID: orderID, SiteID: req.SiteID, RuleVersionID: req.RuleVersionID, GrossPaidAmountCent: result.GrossPaidAmountCent, RefundAmountCent: result.RefundAmountCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: result.MarkupAmountCent, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRemainderCent(result.SplitBaseAmountCent, SplitSettlement(Rule{Mode: rule.Mode}, result.SplitBaseAmountCent, result.MarkupAmountCent)), SettlementStatus: "pending"}
splits := SplitSettlement(parsed, result.SplitBaseAmountCent)
financial := model.DistributionOrderFinancial{OrderID: orderID, SiteID: req.SiteID, RuleVersionID: req.RuleVersionID, GrossPaidAmountCent: result.GrossPaidAmountCent, RefundAmountCent: result.RefundAmountCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: parsed.ServiceFeeValue, ServiceFeeUnit: parsed.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: result.MarkupAmountCent, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, splits), SettlementStatus: "pending"}
if err := tx.Create(&financial).Error; err != nil {
return err
}
@@ -1184,16 +1201,23 @@ func (r *Repository) OnboardSite(ctx context.Context, req OnboardSiteRequest) (*
return err
}
// 一键开通只落草稿,禁止把文档占位 3.5/4 当成已生效分成。
// 一键开通只落草稿。字段存百分数;口头 3.5/4/2.5 写入时换成 35/40/25。
parsed := Rule{
DistributorReceiveRatio: req.DistributorReceiveRatio,
PlatformSellRatio: req.PlatformSellRatio,
ServiceFeeValue: req.ServiceFeeValue,
ServiceFeeUnit: ServiceFeeUnitPercent,
}
normalizeRulePercents(&parsed)
rule := model.DistributionRuleVersion{
SiteID: &site.ID,
Mode: ModeTwoPartyDistributorReceive,
Version: 1,
Status: "draft",
DistributorReceiveRatio: req.DistributorReceiveRatio,
PlatformSellRatio: req.PlatformSellRatio,
ServiceFeeValue: req.ServiceFeeValue,
ServiceFeeUnit: ServiceFeeUnitPercent,
DistributorReceiveRatio: parsed.DistributorReceiveRatio,
PlatformSellRatio: parsed.PlatformSellRatio,
ServiceFeeValue: parsed.ServiceFeeValue,
ServiceFeeUnit: parsed.ServiceFeeUnit,
PaymentFeeBPS: 50,
TaxEnabled: taxEnabled,
TaxRateBPS: taxRate,
@@ -1264,19 +1288,19 @@ func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string
}
type publicListingRow struct {
ListingID uint64 `gorm:"column:listing_id"`
ListingNo string `gorm:"column:listing_no"`
AccountID uint64 `gorm:"column:account_id"`
Title string `gorm:"column:title"`
GameName string `gorm:"column:game_name"`
ServerRegion string `gorm:"column:server_region"`
LoginPlatform string `gorm:"column:login_platform"`
RankLevel string `gorm:"column:rank_level"`
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
PriceCent int64 `gorm:"column:price_cent"`
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
MarkupAmountCent int64 `gorm:"column:markup_amount_cent"`
InTransaction bool `gorm:"column:in_transaction"`
ListingID uint64 `gorm:"column:listing_id"`
ListingNo string `gorm:"column:listing_no"`
AccountID uint64 `gorm:"column:account_id"`
Title string `gorm:"column:title"`
GameName string `gorm:"column:game_name"`
ServerRegion string `gorm:"column:server_region"`
LoginPlatform string `gorm:"column:login_platform"`
RankLevel string `gorm:"column:rank_level"`
HafCoinAmount int64 `gorm:"column:haf_coin_amount"`
PriceCent int64 `gorm:"column:price_cent"`
DepositAmountCent int64 `gorm:"column:deposit_amount_cent"`
ShelfRatioDelta float64 `gorm:"column:shelf_ratio_delta"`
InTransaction bool `gorm:"column:in_transaction"`
}
baseQuery := r.db.WithContext(ctx).
@@ -1302,9 +1326,9 @@ func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string
COALESCE(ga.login_platform, '') AS login_platform,
COALESCE(ga.rank_level, '') AS rank_level,
COALESCE(ga.haf_coin_amount, 0) AS haf_coin_amount,
COALESCE(l.price_cent, 0) + COALESCE(dsa.markup_amount_cent, 0) AS price_cent,
COALESCE(l.price_cent, 0) AS price_cent,
COALESCE(l.deposit_amount_cent, 0) AS deposit_amount_cent,
COALESCE(dsa.markup_amount_cent, 0) AS markup_amount_cent,
COALESCE(dsa.shelf_ratio_delta, 0) AS shelf_ratio_delta,
COALESCE(l.in_transaction, 0) AS in_transaction
`).
Order("dsa.id DESC").
@@ -1317,6 +1341,7 @@ func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string
items := make([]PublicDistributionListingDTO, 0, len(rows))
for _, row := range rows {
sitePrice, _, _ := listingpkg.ApplyShelfRatio(row.HafCoinAmount, row.PriceCent, row.ShelfRatioDelta)
items = append(items, PublicDistributionListingDTO{
ListingID: row.ListingID,
ListingNo: row.ListingNo,
@@ -1327,9 +1352,10 @@ func (r *Repository) ListPublicSiteListings(ctx context.Context, siteCode string
LoginPlatform: row.LoginPlatform,
RankLevel: row.RankLevel,
HafCoinAmount: row.HafCoinAmount,
PriceCent: row.PriceCent,
PriceCent: sitePrice,
DepositAmountCent: row.DepositAmountCent,
MarkupAmountCent: row.MarkupAmountCent,
MarkupAmountCent: sitePrice - row.PriceCent,
ShelfRatioDelta: row.ShelfRatioDelta,
InTransaction: row.InTransaction,
SiteCode: site.SiteCode,
SiteName: site.SiteName,
@@ -1456,8 +1482,8 @@ func (r *Repository) UpdateSiteBranding(ctx context.Context, adminID, siteID uin
return &dto, nil
}
func (r *Repository) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, markupCent int64) (*AccountDisplayDTO, error) {
if markupCent < 0 {
func (r *Repository) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, ratioDelta float64) (*AccountDisplayDTO, error) {
if ratioDelta < 0 {
return nil, ErrInvalidAmount
}
var row model.DistributionSiteAccount
@@ -1470,7 +1496,7 @@ func (r *Repository) UpdateAccountMarkup(ctx context.Context, adminID, id uint64
if err := r.ensureSiteAccess(ctx, adminID, row.SiteID); err != nil {
return nil, err
}
if err := r.db.WithContext(ctx).Model(&row).Update("markup_amount_cent", markupCent).Error; err != nil {
if err := r.db.WithContext(ctx).Model(&row).Update("shelf_ratio_delta", ratioDelta).Error; err != nil {
return nil, err
}
displays, err := r.ListAccountDisplays(ctx, row.SiteID, "")
@@ -1492,7 +1518,7 @@ func (r *Repository) BatchUpdateAccountMarkup(ctx context.Context, adminID, site
if err := r.ensureSiteAccess(ctx, adminID, siteID); err != nil {
return err
}
if req.MarkupAmountCent < 0 {
if req.ShelfRatioDelta < 0 {
return ErrInvalidAmount
}
if len(req.ListingIDs) == 0 {
@@ -1500,7 +1526,7 @@ func (r *Repository) BatchUpdateAccountMarkup(ctx context.Context, adminID, site
}
return r.db.WithContext(ctx).Model(&model.DistributionSiteAccount{}).
Where("site_id = ? AND listing_id IN ?", siteID, req.ListingIDs).
Update("markup_amount_cent", req.MarkupAmountCent).Error
Update("shelf_ratio_delta", req.ShelfRatioDelta).Error
}
func (r *Repository) ListReceivedListings(ctx context.Context, adminID, siteID uint64, q ReceivedListingQuery) (*ReceivedListingPage, error) {