fix(distribution): 修正分销模式判定与结算入账,冻结未确认分成
按买卖站点纯函数判定五种模式,分站卖主站货命中 two_party_platform_receive。 结算批次只收本站 site party,平台分成不再进入分销钱包;生产环境冻结结算入账与站点提现。
This commit is contained in:
@@ -5,61 +5,83 @@ import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestResolveDistributionMode(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
buySiteID uint64
|
||||
listingSiteID uint64
|
||||
want string
|
||||
}{
|
||||
{name: "主站买+主站货", buySiteID: 0, listingSiteID: 0, want: ""},
|
||||
{name: "主站买+分站货", buySiteID: 0, listingSiteID: 11, want: ModeTwoPartyDistributorReceive},
|
||||
{name: "分站卖+主站货", buySiteID: 22, listingSiteID: 0, want: ModeTwoPartyPlatformReceive},
|
||||
{name: "分站B买+分站A货", buySiteID: 22, listingSiteID: 11, want: ModeThreeParty},
|
||||
{name: "本站自卖", buySiteID: 11, listingSiteID: 11, want: ModeSiteSelfSell},
|
||||
}
|
||||
for _, item := range cases {
|
||||
t.Run(item.name, func(t *testing.T) {
|
||||
got := resolveDistributionMode(item.buySiteID, item.listingSiteID)
|
||||
if got != item.want {
|
||||
t.Fatalf("resolveDistributionMode(%d, %d) = %q, want %q", item.buySiteID, item.listingSiteID, got, item.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOrderSourceTx(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
|
||||
// 空 siteCode 正常返回 nil
|
||||
site, rule, err := ResolveOrderSourceTx(db, "", 1, 1, time.Now())
|
||||
if err != nil || site != nil || rule != nil {
|
||||
t.Fatalf("expected nil for empty siteCode, got site=%v, rule=%v, err=%v", site, rule, err)
|
||||
}
|
||||
|
||||
// 站点不存在
|
||||
_, _, err = ResolveOrderSourceTx(db, "unknown_site", 1, 1, time.Now())
|
||||
if err != ErrSiteNotFound {
|
||||
t.Fatalf("expected ErrSiteNotFound, got %v", err)
|
||||
}
|
||||
|
||||
// 创建正常站点
|
||||
now := time.Now()
|
||||
activeSite := model.DistributionSite{
|
||||
SiteCode: "site_test",
|
||||
SiteName: "测试分销站",
|
||||
Domain: "test.dist.com",
|
||||
Status: "active",
|
||||
SiteCode: "site_test",
|
||||
SiteName: "测试分销站",
|
||||
Domain: "test.dist.com",
|
||||
Status: "active",
|
||||
DisplayAccountEnabled: true,
|
||||
}
|
||||
if err := db.Create(&activeSite).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 账号展示关系不存在
|
||||
_, _, err = ResolveOrderSourceTx(db, "site_test", 10, 20, now)
|
||||
if err != ErrAccountDisplayNotFound {
|
||||
t.Fatalf("expected ErrAccountDisplayNotFound, got %v", err)
|
||||
}
|
||||
|
||||
// 创建展示关系
|
||||
listing := model.RentalListing{ListingNo: "L-MAIN-10", AccountID: 20, OwnerID: 1, Status: "published"}
|
||||
if err := db.Create(&listing).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
accountDisplay := model.DistributionSiteAccount{
|
||||
SiteID: activeSite.ID,
|
||||
AccountID: 20,
|
||||
ListingID: 10,
|
||||
ListingID: listing.ID,
|
||||
DisplayStatus: "enabled",
|
||||
}
|
||||
if err := db.Create(&accountDisplay).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 规则不存在
|
||||
_, _, err = ResolveOrderSourceTx(db, "site_test", 10, 20, now)
|
||||
_, _, err = ResolveOrderSourceTx(db, "site_test", listing.ID, 20, now)
|
||||
if err != ErrRuleNotFound {
|
||||
t.Fatalf("expected ErrRuleNotFound, got %v", err)
|
||||
}
|
||||
|
||||
// 创建有效规则
|
||||
ruleVersion := model.DistributionRuleVersion{
|
||||
wrongMode := model.DistributionRuleVersion{
|
||||
SiteID: &activeSite.ID,
|
||||
Mode: ModeTwoPartyDistributorReceive,
|
||||
Version: 1,
|
||||
@@ -71,32 +93,162 @@ func TestResolveOrderSourceTx(t *testing.T) {
|
||||
PaymentFeeBPS: 50,
|
||||
EffectiveFrom: now.Add(-1 * time.Hour),
|
||||
}
|
||||
if err := db.Create(&wrongMode).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, err = ResolveOrderSourceTx(db, "site_test", listing.ID, 20, now)
|
||||
if err != ErrRuleNotFound {
|
||||
t.Fatalf("分站卖主站货不得命中 two_party_distributor_receive, got %v", err)
|
||||
}
|
||||
|
||||
ruleVersion := model.DistributionRuleVersion{
|
||||
SiteID: &activeSite.ID,
|
||||
Mode: ModeTwoPartyPlatformReceive,
|
||||
Version: 1,
|
||||
Status: "active",
|
||||
PlatformReceiveRatio: 3.5,
|
||||
DistributorSellRatio: 4.0,
|
||||
ServiceFeeValue: 2.5,
|
||||
ServiceFeeUnit: ServiceFeeUnitFixedAmount,
|
||||
PaymentFeeBPS: 50,
|
||||
EffectiveFrom: now.Add(-1 * time.Hour),
|
||||
}
|
||||
if err := db.Create(&ruleVersion).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 正常解析
|
||||
foundSite, foundRule, err := ResolveOrderSourceTx(db, "site_test", 10, 20, now)
|
||||
foundSite, foundRule, err := ResolveOrderSourceTx(db, "site_test", listing.ID, 20, now)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if foundSite.ID != activeSite.ID || foundRule.ID != ruleVersion.ID {
|
||||
if foundSite.ID != activeSite.ID || foundRule.ID != ruleVersion.ID || foundRule.Mode != ModeTwoPartyPlatformReceive {
|
||||
t.Fatalf("unexpected result: site=%v, rule=%v", foundSite, foundRule)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOrderSourceTxModes(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
now := time.Now()
|
||||
|
||||
siteA := mustCreateDistSite(t, db, "site_a", "收号站A", "a.dist.com")
|
||||
siteB := mustCreateDistSite(t, db, "site_b", "卖出站B", "b.dist.com")
|
||||
|
||||
listingA := model.RentalListing{ListingNo: "L-A-1", AccountID: 101, OwnerID: 1, Status: "published", DistributionSiteID: &siteA.ID}
|
||||
if err := db.Create(&listingA).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listingMain := model.RentalListing{ListingNo: "L-MAIN-1", AccountID: 201, OwnerID: 1, Status: "published"}
|
||||
if err := db.Create(&listingMain).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, rel := range []model.DistributionSiteAccount{
|
||||
{SiteID: siteA.ID, AccountID: listingA.AccountID, ListingID: listingA.ID, DisplayStatus: "enabled"},
|
||||
{SiteID: siteB.ID, AccountID: listingA.AccountID, ListingID: listingA.ID, DisplayStatus: "enabled"},
|
||||
{SiteID: siteB.ID, AccountID: listingMain.AccountID, ListingID: listingMain.ID, DisplayStatus: "enabled"},
|
||||
} {
|
||||
if err := db.Create(&rel).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
recvRule := mustCreateActiveRule(t, db, siteA.ID, ModeTwoPartyDistributorReceive, now)
|
||||
sellRule := mustCreateActiveRule(t, db, siteB.ID, ModeTwoPartyPlatformReceive, now)
|
||||
threeRule := mustCreateActiveRule(t, db, siteB.ID, ModeThreeParty, now)
|
||||
selfRule := mustCreateActiveRule(t, db, siteA.ID, ModeSiteSelfSell, now)
|
||||
|
||||
t.Run("主站买+分站货", func(t *testing.T) {
|
||||
site, rule, err := ResolveOrderSourceTx(db, "", listingA.ID, listingA.AccountID, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if site.ID != siteA.ID || rule.ID != recvRule.ID || rule.Mode != ModeTwoPartyDistributorReceive {
|
||||
t.Fatalf("got site=%d mode=%s", site.ID, rule.Mode)
|
||||
}
|
||||
})
|
||||
t.Run("分站卖+主站货", func(t *testing.T) {
|
||||
site, rule, err := ResolveOrderSourceTx(db, "site_b", listingMain.ID, listingMain.AccountID, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if site.ID != siteB.ID || rule.ID != sellRule.ID || rule.Mode != ModeTwoPartyPlatformReceive {
|
||||
t.Fatalf("got site=%d mode=%s", site.ID, rule.Mode)
|
||||
}
|
||||
})
|
||||
t.Run("分站B买+分站A货", func(t *testing.T) {
|
||||
site, rule, err := ResolveOrderSourceTx(db, "site_b", listingA.ID, listingA.AccountID, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if site.ID != siteB.ID || rule.ID != threeRule.ID || rule.Mode != ModeThreeParty {
|
||||
t.Fatalf("got site=%d mode=%s", site.ID, rule.Mode)
|
||||
}
|
||||
})
|
||||
t.Run("本站自卖", func(t *testing.T) {
|
||||
site, rule, err := ResolveOrderSourceTx(db, "site_a", listingA.ID, listingA.AccountID, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if site.ID != siteA.ID || rule.ID != selfRule.ID || rule.Mode != ModeSiteSelfSell {
|
||||
t.Fatalf("got site=%d mode=%s", site.ID, rule.Mode)
|
||||
}
|
||||
})
|
||||
t.Run("本站自卖不回落两方规则", func(t *testing.T) {
|
||||
if err := db.Model(&model.DistributionRuleVersion{}).Where("id = ?", selfRule.ID).Update("status", "draft").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, err := ResolveOrderSourceTx(db, "site_a", listingA.ID, listingA.AccountID, now)
|
||||
if err != ErrRuleNotFound {
|
||||
t.Fatalf("expected ErrRuleNotFound without self-sell rule, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mustCreateDistSite(t *testing.T, db *gorm.DB, code, name, domain string) model.DistributionSite {
|
||||
t.Helper()
|
||||
site := model.DistributionSite{SiteCode: code, SiteName: name, Domain: domain, Status: "active", DisplayAccountEnabled: true}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return site
|
||||
}
|
||||
|
||||
func mustCreateActiveRule(t *testing.T, db *gorm.DB, siteID uint64, mode string, now time.Time) model.DistributionRuleVersion {
|
||||
t.Helper()
|
||||
rule := model.DistributionRuleVersion{
|
||||
SiteID: &siteID,
|
||||
Mode: mode,
|
||||
Version: 1,
|
||||
Status: "active",
|
||||
DistributorReceiveRatio: 3.5,
|
||||
PlatformSellRatio: 4,
|
||||
PlatformReceiveRatio: 3.5,
|
||||
DistributorSellRatio: 4,
|
||||
DistributorARatio: 3.5,
|
||||
DistributorBRatio: 4,
|
||||
ServiceFeeValue: 0,
|
||||
ServiceFeeUnit: ServiceFeeUnitFixedAmount,
|
||||
PaymentFeeBPS: 0,
|
||||
EffectiveFrom: now.Add(-time.Hour),
|
||||
}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
|
||||
now := time.Now()
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "site_pay",
|
||||
SiteName: "支付测试站",
|
||||
Domain: "pay.dist.com",
|
||||
Status: "active",
|
||||
SiteCode: "site_pay",
|
||||
SiteName: "支付测试站",
|
||||
Domain: "pay.dist.com",
|
||||
Status: "active",
|
||||
DisplayAccountEnabled: true,
|
||||
TaxEnabled: true,
|
||||
TaxRateBPS: 100,
|
||||
TaxEnabled: true,
|
||||
TaxRateBPS: 100,
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -150,9 +302,15 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
||||
}
|
||||
|
||||
var parties []model.DistributionOrderParty
|
||||
if err := db.Where("order_id = ?", order.ID).Find(&parties).Error; err != nil || len(parties) != 2 {
|
||||
if err := db.Where("order_id = ?", order.ID).Order("id ASC").Find(&parties).Error; err != nil || len(parties) != 2 {
|
||||
t.Fatalf("unexpected parties count: len=%d, err=%v", len(parties), err)
|
||||
}
|
||||
if parties[0].PartyRole != "distributor_receive" || parties[0].PartyType != PartyTypeSite || parties[0].PartyID != site.ID || parties[0].IncomeAmountCent != 340 {
|
||||
t.Fatalf("distributor party = %+v", parties[0])
|
||||
}
|
||||
if parties[1].PartyRole != "platform_sell" || parties[1].PartyType != PartyTypePlatform || parties[1].PartyID != 0 || parties[1].IncomeAmountCent != 688 {
|
||||
t.Fatalf("platform party = %+v", parties[1])
|
||||
}
|
||||
|
||||
// 幂等写入测试:重复调用不报错且不重复追加 party
|
||||
if err := SnapshotPaidOrderTx(db, order.ID, grossPaid); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user