refactor(distribution): 重写分销迁移并落地利润百分比分成与货架比例
将 000064-000078 合并为 064/065/066,默认规则改为收号 35%、卖出 40%、服务费 25%。 货架调价改为增加分站 1 元兑换哈夫币比例。sql-guard 仅保护 main 已发布迁移。
This commit is contained in:
@@ -46,6 +46,7 @@ type DistributionSiteAccount struct {
|
||||
ListingID uint64 `gorm:"not null;index" json:"listing_id"`
|
||||
DisplayStatus string `gorm:"size:16;not null;default:'enabled';index" json:"display_status"`
|
||||
MarkupAmountCent int64 `gorm:"not null;default:0" json:"markup_amount_cent"`
|
||||
ShelfRatioDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"shelf_ratio_delta"`
|
||||
DisplayStartedAt *time.Time `json:"display_started_at,omitempty"`
|
||||
DisplayEndedAt *time.Time `json:"display_ended_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -29,6 +29,8 @@ type RentalOrder struct {
|
||||
BasePriceCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
ShelfMarkupCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
MarkupSiteID uint64 `gorm:"not null;default:0;index" json:"markup_site_id,omitempty"`
|
||||
ShelfRatio float64 `gorm:"type:decimal(10,2);not null;default:0" json:"shelf_ratio,omitempty"`
|
||||
ShelfRatioDelta float64 `gorm:"type:decimal(10,2);not null;default:0" json:"shelf_ratio_delta,omitempty"`
|
||||
RentOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
RentDiscountAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
PureCoinOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
|
||||
@@ -48,8 +48,8 @@ func TestOnboardSiteAndLoginFlow(t *testing.T) {
|
||||
if resp.Member.SiteID == nil || *resp.Member.SiteID != resp.Site.ID {
|
||||
t.Fatalf("expected SiteID=%d, got %v", resp.Site.ID, resp.Member.SiteID)
|
||||
}
|
||||
if resp.Rule.DistributorReceiveRatio != 3.8 {
|
||||
t.Fatalf("expected rule ratio 3.8, got %f", resp.Rule.DistributorReceiveRatio)
|
||||
if resp.Rule.DistributorReceiveRatio != 38 {
|
||||
t.Fatalf("expected rule percent 38, got %f", resp.Rule.DistributorReceiveRatio)
|
||||
}
|
||||
if resp.Rule.Status != "draft" {
|
||||
t.Fatalf("onboard rule must stay draft until split is verified, got %s", resp.Rule.Status)
|
||||
|
||||
@@ -131,23 +131,27 @@ type DistributionOrderPage struct {
|
||||
}
|
||||
|
||||
type AccountDisplayDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
SiteID uint64 `json:"site_id"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
DisplayStatus string `json:"display_status"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
ListingNo string `json:"listing_no,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
GameName string `json:"game_name,omitempty"`
|
||||
ServerRegion string `json:"server_region,omitempty"`
|
||||
LoginPlatform string `json:"login_platform,omitempty"`
|
||||
RankLevel string `json:"rank_level,omitempty"`
|
||||
HafCoinAmount int64 `json:"haf_coin_amount,omitempty"`
|
||||
PriceCent int64 `json:"price_cent,omitempty"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent,omitempty"`
|
||||
ListingStatus string `json:"listing_status,omitempty"`
|
||||
InTransaction bool `json:"in_transaction,omitempty"`
|
||||
ID uint64 `json:"id"`
|
||||
SiteID uint64 `json:"site_id"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
DisplayStatus string `json:"display_status"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
ShelfRatioDelta float64 `json:"shelf_ratio_delta"`
|
||||
BaseRatio float64 `json:"base_ratio,omitempty"`
|
||||
SiteRatio float64 `json:"site_ratio,omitempty"`
|
||||
ListingNo string `json:"listing_no,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
GameName string `json:"game_name,omitempty"`
|
||||
ServerRegion string `json:"server_region,omitempty"`
|
||||
LoginPlatform string `json:"login_platform,omitempty"`
|
||||
RankLevel string `json:"rank_level,omitempty"`
|
||||
HafCoinAmount int64 `json:"haf_coin_amount,omitempty"`
|
||||
PriceCent int64 `json:"price_cent,omitempty"`
|
||||
SitePriceCent int64 `json:"site_price_cent,omitempty"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent,omitempty"`
|
||||
ListingStatus string `json:"listing_status,omitempty"`
|
||||
InTransaction bool `json:"in_transaction,omitempty"`
|
||||
}
|
||||
|
||||
type SyncSiteAccountsRequest struct {
|
||||
@@ -161,21 +165,22 @@ type SyncSiteAccountsResult struct {
|
||||
}
|
||||
|
||||
type PublicDistributionListingDTO struct {
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
ListingNo string `json:"listing_no"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
Title string `json:"title"`
|
||||
GameName string `json:"game_name"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
RankLevel string `json:"rank_level"`
|
||||
HafCoinAmount int64 `json:"haf_coin_amount"`
|
||||
PriceCent int64 `json:"price_cent"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
InTransaction bool `json:"in_transaction"`
|
||||
SiteCode string `json:"site_code"`
|
||||
SiteName string `json:"site_name"`
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
ListingNo string `json:"listing_no"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
Title string `json:"title"`
|
||||
GameName string `json:"game_name"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
RankLevel string `json:"rank_level"`
|
||||
HafCoinAmount int64 `json:"haf_coin_amount"`
|
||||
PriceCent int64 `json:"price_cent"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
ShelfRatioDelta float64 `json:"shelf_ratio_delta"`
|
||||
InTransaction bool `json:"in_transaction"`
|
||||
SiteCode string `json:"site_code"`
|
||||
SiteName string `json:"site_name"`
|
||||
}
|
||||
|
||||
type PublicDistributionListingsResult struct {
|
||||
@@ -199,7 +204,6 @@ type PublicSiteInfoDTO struct {
|
||||
DefaultSupportGroupID *uint64 `json:"default_support_group_id,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
type UpdateAccountDisplayRequest struct {
|
||||
DisplayStatus string `json:"display_status" binding:"required,oneof=enabled disabled sold offline"`
|
||||
}
|
||||
@@ -359,14 +363,14 @@ type UpdateSiteBrandingRequest struct {
|
||||
FooterInfoJSON *string `json:"footer_info_json"`
|
||||
}
|
||||
|
||||
// 自主加价 DTO
|
||||
// 自主加价 DTO:增加该站「1 元兑换哈夫币」比例,不是人民币。
|
||||
type UpdateAccountMarkupRequest struct {
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
ShelfRatioDelta float64 `json:"shelf_ratio_delta"`
|
||||
}
|
||||
|
||||
type BatchUpdateAccountMarkupRequest struct {
|
||||
ListingIDs []uint64 `json:"listing_ids" binding:"required"`
|
||||
MarkupAmountCent int64 `json:"markup_amount_cent"`
|
||||
ListingIDs []uint64 `json:"listing_ids" binding:"required"`
|
||||
ShelfRatioDelta float64 `json:"shelf_ratio_delta"`
|
||||
}
|
||||
|
||||
// 商户钱包与提现 DTO
|
||||
@@ -435,4 +439,3 @@ type DistributionTrendPoint struct {
|
||||
GMVCent int64 `json:"gmv_cent"`
|
||||
ProfitCent int64 `json:"profit_cent"`
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +264,9 @@ func (h *Handler) SyncSiteAccounts(c *gin.Context) {
|
||||
|
||||
func (h *Handler) ListPublicSiteListings(c *gin.Context) {
|
||||
siteCode := strings.TrimSpace(c.Param("site_code"))
|
||||
if siteCode == "" {
|
||||
siteCode = strings.TrimSpace(c.Param("id"))
|
||||
}
|
||||
if siteCode == "" {
|
||||
siteCode = strings.TrimSpace(c.Query("site_code"))
|
||||
}
|
||||
@@ -544,7 +547,7 @@ func (h *Handler) UpdateAccountMarkup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
adminID, _ := currentAdminID(c)
|
||||
dto, err := h.service.UpdateAccountMarkup(c.Request.Context(), adminID, id, req.MarkupAmountCent)
|
||||
dto, err := h.service.UpdateAccountMarkup(c.Request.Context(), adminID, id, req.ShelfRatioDelta)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
|
||||
@@ -148,7 +148,7 @@ func ResolveOrderSourceTx(tx *gorm.DB, siteCode string, listingID, accountID uin
|
||||
}
|
||||
|
||||
func ruleFromVersion(rule model.DistributionRuleVersion) Rule {
|
||||
return Rule{
|
||||
parsed := Rule{
|
||||
Mode: rule.Mode,
|
||||
PlatformReceiveRatio: rule.PlatformReceiveRatio,
|
||||
PlatformSellRatio: rule.PlatformSellRatio,
|
||||
@@ -162,6 +162,8 @@ func ruleFromVersion(rule model.DistributionRuleVersion) Rule {
|
||||
TaxEnabled: rule.TaxEnabled,
|
||||
TaxRateBPS: rule.TaxRateBPS,
|
||||
}
|
||||
normalizeRulePercents(&parsed)
|
||||
return parsed
|
||||
}
|
||||
|
||||
// calculateForRule 按规则快照计算手续费、服务费和分成基数。
|
||||
@@ -170,7 +172,7 @@ func calculateForRule(rule model.DistributionRuleVersion, input SettlementInput)
|
||||
}
|
||||
|
||||
// SnapshotPaidOrderTx 在支付确认事务内幂等写入财务和参与方快照。
|
||||
// 参与方金额按规则比例对分成基数拆分,剩余(含尾差)记入 RoundingAdjustmentCent 归平台。
|
||||
// 利润 = 租金 − 号主 − 通道手续费,再按 35%/40%/25% 切;尾差记入 RoundingAdjustmentCent。
|
||||
func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error {
|
||||
if !distributionTablesPresent(tx) {
|
||||
return nil
|
||||
@@ -186,16 +188,24 @@ func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error
|
||||
if err := tx.First(&rule, *order.DistributionRuleVersionID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
grossRent := order.RentAmountCent
|
||||
if grossRent <= 0 {
|
||||
grossRent = grossPaidCent
|
||||
}
|
||||
markup := order.ShelfMarkupCent
|
||||
if markup < 0 {
|
||||
markup = 0
|
||||
}
|
||||
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: grossPaidCent, MarkupAmountCent: markup})
|
||||
ownerRent := order.OwnerRentAmountCent
|
||||
if ownerRent < 0 {
|
||||
ownerRent = 0
|
||||
}
|
||||
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: grossRent, OwnerRentAmountCent: ownerRent, MarkupAmountCent: markup})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, markup)
|
||||
financial := model.DistributionOrderFinancial{OrderID: order.ID, SiteID: *order.DistributionSiteID, RuleVersionID: rule.ID, GrossPaidAmountCent: grossPaidCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: markup, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRemainderCent(result.SplitBaseAmountCent, splits), SettlementStatus: "pending"}
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent)
|
||||
financial := model.DistributionOrderFinancial{OrderID: order.ID, SiteID: *order.DistributionSiteID, RuleVersionID: rule.ID, GrossPaidAmountCent: grossRent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: markup, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, splits), SettlementStatus: "pending"}
|
||||
if err := tx.Where("order_id = ?", order.ID).Assign(financial).FirstOrCreate(&financial).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -242,19 +252,30 @@ func ApplyRefundTx(tx *gorm.DB, orderID uint64, refundAmountCent int64) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
if refundAmountCent < 0 || refundAmountCent > financial.GrossPaidAmountCent {
|
||||
if refundAmountCent < 0 {
|
||||
return ErrInvalidAmount
|
||||
}
|
||||
if refundAmountCent > financial.GrossPaidAmountCent {
|
||||
refundAmountCent = financial.GrossPaidAmountCent
|
||||
}
|
||||
var rule model.DistributionRuleVersion
|
||||
if err := tx.First(&rule, financial.RuleVersionID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: financial.GrossPaidAmountCent, RefundAmountCent: refundAmountCent, MarkupAmountCent: financial.MarkupAmountCent})
|
||||
var order model.RentalOrder
|
||||
ownerRent := int64(0)
|
||||
if err := tx.Select("owner_rent_amount_cent").First(&order, orderID).Error; err == nil {
|
||||
ownerRent = order.OwnerRentAmountCent
|
||||
if ownerRent < 0 {
|
||||
ownerRent = 0
|
||||
}
|
||||
}
|
||||
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: financial.GrossPaidAmountCent, RefundAmountCent: refundAmountCent, OwnerRentAmountCent: ownerRent, MarkupAmountCent: financial.MarkupAmountCent})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent, financial.MarkupAmountCent)
|
||||
if err := tx.Model(&financial).Updates(map[string]any{"refund_amount_cent": refundAmountCent, "net_paid_amount_cent": result.NetPaidAmountCent, "payment_fee_cent": result.PaymentFeeCent, "service_fee_cent": result.ServiceFeeCent, "split_base_amount_cent": result.SplitBaseAmountCent, "rounding_adjustment_cent": SplitRemainderCent(result.SplitBaseAmountCent, splits), "settlement_status": "pending"}).Error; err != nil {
|
||||
splits := SplitSettlement(ruleFromVersion(rule), result.SplitBaseAmountCent)
|
||||
if err := tx.Model(&financial).Updates(map[string]any{"refund_amount_cent": refundAmountCent, "net_paid_amount_cent": result.NetPaidAmountCent, "payment_fee_cent": result.PaymentFeeCent, "service_fee_cent": result.ServiceFeeCent, "split_base_amount_cent": result.SplitBaseAmountCent, "rounding_adjustment_cent": SplitRoundingCent(result.SplitBaseAmountCent, result.ServiceFeeCent, splits), "settlement_status": "pending"}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, split := range splits {
|
||||
|
||||
@@ -259,10 +259,10 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
||||
Mode: ModeTwoPartyDistributorReceive,
|
||||
Version: 1,
|
||||
Status: "active",
|
||||
DistributorReceiveRatio: 3.5,
|
||||
PlatformSellRatio: 4.0,
|
||||
ServiceFeeValue: 2.5,
|
||||
ServiceFeeUnit: ServiceFeeUnitFixedAmount,
|
||||
DistributorReceiveRatio: 35,
|
||||
PlatformSellRatio: 40,
|
||||
ServiceFeeValue: 25,
|
||||
ServiceFeeUnit: ServiceFeeUnitPercent,
|
||||
PaymentFeeBPS: 50,
|
||||
TaxEnabled: true,
|
||||
TaxRateBPS: 100,
|
||||
@@ -279,8 +279,9 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
||||
OwnerID: 1,
|
||||
RenterID: 2,
|
||||
Status: "renting",
|
||||
PlatformFeeCent: 999,
|
||||
ShelfMarkupCent: 300,
|
||||
RentAmountCent: 10000,
|
||||
OwnerRentAmountCent: 6000,
|
||||
PlatformFeeCent: 4000,
|
||||
DistributionSiteID: &site.ID,
|
||||
DistributionRuleVersionID: &rule.ID,
|
||||
}
|
||||
@@ -301,18 +302,18 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
||||
if financial.GrossPaidAmountCent != grossPaid || financial.SiteID != site.ID {
|
||||
t.Fatalf("unexpected financial snapshot: %+v", financial)
|
||||
}
|
||||
if financial.MarkupAmountCent != 300 {
|
||||
t.Fatalf("markup must come from shelf snapshot, got %d", financial.MarkupAmountCent)
|
||||
if financial.SplitBaseAmountCent != 3950 || financial.ServiceFeeCent != 988 {
|
||||
t.Fatalf("profit snapshot = %+v", financial)
|
||||
}
|
||||
|
||||
var parties []model.DistributionOrderParty
|
||||
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 {
|
||||
if parties[0].PartyRole != "distributor_receive" || parties[0].PartyType != PartyTypeSite || parties[0].PartyID != site.ID || parties[0].IncomeAmountCent != 1383 {
|
||||
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 {
|
||||
if parties[1].PartyRole != "platform_sell" || parties[1].PartyType != PartyTypePlatform || parties[1].PartyID != 0 || parties[1].IncomeAmountCent != 1580 {
|
||||
t.Fatalf("platform party = %+v", parties[1])
|
||||
}
|
||||
|
||||
@@ -330,10 +331,6 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
||||
if err := ApplyRefundTx(db, order.ID, -10); err != ErrInvalidAmount {
|
||||
t.Fatalf("expected ErrInvalidAmount for negative refund, got %v", err)
|
||||
}
|
||||
if err := ApplyRefundTx(db, order.ID, grossPaid+100); err != ErrInvalidAmount {
|
||||
t.Fatalf("expected ErrInvalidAmount for excessive refund, got %v", err)
|
||||
}
|
||||
|
||||
// 合法退款
|
||||
refundAmount := int64(2000)
|
||||
if err := ApplyRefundTx(db, order.ID, refundAmount); err != nil {
|
||||
@@ -352,7 +349,7 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapshotPaidOrderTxAddsShelfMarkupToSeller(t *testing.T) {
|
||||
func TestSnapshotPaidOrderTxSplitsPercentOfProfit(t *testing.T) {
|
||||
db := distributionTestDB(t)
|
||||
now := time.Now()
|
||||
site := model.DistributionSite{SiteCode: "sell_markup", SiteName: "卖出加价站", Domain: "sell-markup.dist.com", Status: "active", DisplayAccountEnabled: true}
|
||||
@@ -361,8 +358,8 @@ func TestSnapshotPaidOrderTxAddsShelfMarkupToSeller(t *testing.T) {
|
||||
}
|
||||
rule := model.DistributionRuleVersion{
|
||||
SiteID: &site.ID, Mode: ModeTwoPartyPlatformReceive, Version: 1, Status: "active",
|
||||
PlatformReceiveRatio: 3.5, DistributorSellRatio: 4,
|
||||
ServiceFeeValue: 0, ServiceFeeUnit: ServiceFeeUnitFixedAmount, PaymentFeeBPS: 50,
|
||||
PlatformReceiveRatio: 35, DistributorSellRatio: 40,
|
||||
ServiceFeeValue: 25, ServiceFeeUnit: ServiceFeeUnitPercent, PaymentFeeBPS: 50,
|
||||
EffectiveFrom: now.Add(-time.Hour),
|
||||
}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
@@ -370,7 +367,7 @@ func TestSnapshotPaidOrderTxAddsShelfMarkupToSeller(t *testing.T) {
|
||||
}
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-MARKUP-SELL", ListingID: 1, AccountID: 2, OwnerID: 3, RenterID: 4, Status: "renting",
|
||||
PlatformFeeCent: 2000, ShelfMarkupCent: 10000,
|
||||
RentAmountCent: 10000, OwnerRentAmountCent: 6000, PlatformFeeCent: 4000,
|
||||
DistributionSiteID: &site.ID, DistributionRuleVersionID: &rule.ID, DistributionMode: ModeTwoPartyPlatformReceive,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
@@ -395,20 +392,10 @@ func TestSnapshotPaidOrderTxAddsShelfMarkupToSeller(t *testing.T) {
|
||||
if seller == nil || platform == nil {
|
||||
t.Fatalf("missing parties: %+v", parties)
|
||||
}
|
||||
if seller.IncomeAmountCent < 10000 {
|
||||
t.Fatalf("seller split must include 10000 markup, got %d", seller.IncomeAmountCent)
|
||||
if seller.IncomeAmountCent != 1580 {
|
||||
t.Fatalf("seller 40%% of profit = %d, want 1580", seller.IncomeAmountCent)
|
||||
}
|
||||
if platform.PartyID != 0 {
|
||||
t.Fatalf("platform party_id = %d, want 0", platform.PartyID)
|
||||
}
|
||||
if err := db.Model(&order).Update("shelf_markup_cent", 0).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var financial model.DistributionOrderFinancial
|
||||
if err := db.Where("order_id = ?", order.ID).First(&financial).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if financial.MarkupAmountCent != 10000 {
|
||||
t.Fatalf("historical snapshot must keep markup, got %d", financial.MarkupAmountCent)
|
||||
if platform.IncomeAmountCent != 1383 || platform.PartyID != 0 {
|
||||
t.Fatalf("platform receive = %+v", platform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ const (
|
||||
ServiceFeeUnitPercent = "percent"
|
||||
ServiceFeeUnitFixedAmount = "fixed_amount"
|
||||
ServiceFeeUnitRatio = "ratio"
|
||||
|
||||
DefaultReceivePercent = 35.0
|
||||
DefaultSellPercent = 40.0
|
||||
DefaultServicePercent = 25.0
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,7 +31,7 @@ var (
|
||||
ErrUnsupportedMode = errors.New("unsupported distribution mode")
|
||||
)
|
||||
|
||||
// Rule 是后台可编辑的分账规则。比例字段按参与方角色命名,避免方向歧义。
|
||||
// Rule 是后台可编辑的分账规则。比例字段存百分数(35/40/25),按参与方角色命名。
|
||||
type Rule struct {
|
||||
Mode string
|
||||
PlatformReceiveRatio float64
|
||||
@@ -47,6 +51,7 @@ type Rule struct {
|
||||
type SettlementInput struct {
|
||||
GrossPaidAmountCent int64
|
||||
RefundAmountCent int64
|
||||
OwnerRentAmountCent int64
|
||||
MarkupAmountCent int64
|
||||
GrossProfitByParty []PartyProfitInput
|
||||
}
|
||||
@@ -75,23 +80,22 @@ type SettlementResult struct {
|
||||
TaxByParty []PartyTaxResult
|
||||
}
|
||||
|
||||
// DefaultRule 返回文档中的第一版占位值,仅供对照,禁止当作已手算确认的生效分成。
|
||||
// 本站自卖没有默认比例,必须单独配置。
|
||||
// DefaultRule 返回已确认的默认百分比:收号 35%、卖出 40%、服务费 25%。
|
||||
func DefaultRule(mode string) (Rule, error) {
|
||||
base := Rule{
|
||||
Mode: mode, ServiceFeeValue: 2.5, ServiceFeeUnit: ServiceFeeUnitRatio,
|
||||
Mode: mode, ServiceFeeValue: DefaultServicePercent, ServiceFeeUnit: ServiceFeeUnitPercent,
|
||||
PaymentFeeBPS: 50, TaxEnabled: false, TaxRateBPS: 100,
|
||||
}
|
||||
switch mode {
|
||||
case ModeTwoPartyDistributorReceive:
|
||||
base.DistributorReceiveRatio = 3.5
|
||||
base.PlatformSellRatio = 4
|
||||
base.DistributorReceiveRatio = DefaultReceivePercent
|
||||
base.PlatformSellRatio = DefaultSellPercent
|
||||
case ModeTwoPartyPlatformReceive:
|
||||
base.PlatformReceiveRatio = 3.5
|
||||
base.DistributorSellRatio = 4
|
||||
base.PlatformReceiveRatio = DefaultReceivePercent
|
||||
base.DistributorSellRatio = DefaultSellPercent
|
||||
case ModeThreeParty:
|
||||
base.DistributorARatio = 3.5
|
||||
base.DistributorBRatio = 4
|
||||
base.DistributorARatio = DefaultReceivePercent
|
||||
base.DistributorBRatio = DefaultSellPercent
|
||||
case ModeSiteSelfSell:
|
||||
return Rule{}, ErrUnsupportedMode
|
||||
default:
|
||||
@@ -100,9 +104,38 @@ func DefaultRule(mode string) (Rule, error) {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// CalculateSettlement 先扣支付手续费,再计算服务费和分成基数;退款金额不能超过支付金额。
|
||||
// NormalizeSplitPercent 把口头 3.5/4/2.5 换成 35/40/25;已经是百分数的原样保留。
|
||||
func NormalizeSplitPercent(value float64) float64 {
|
||||
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
if value <= 10 {
|
||||
return math.Round(value*10*10) / 10
|
||||
}
|
||||
return math.Round(value*10) / 10
|
||||
}
|
||||
|
||||
func normalizeRulePercents(row *Rule) {
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
row.PlatformReceiveRatio = NormalizeSplitPercent(row.PlatformReceiveRatio)
|
||||
row.PlatformSellRatio = NormalizeSplitPercent(row.PlatformSellRatio)
|
||||
row.DistributorReceiveRatio = NormalizeSplitPercent(row.DistributorReceiveRatio)
|
||||
row.DistributorSellRatio = NormalizeSplitPercent(row.DistributorSellRatio)
|
||||
row.DistributorARatio = NormalizeSplitPercent(row.DistributorARatio)
|
||||
row.DistributorBRatio = NormalizeSplitPercent(row.DistributorBRatio)
|
||||
if row.ServiceFeeUnit == ServiceFeeUnitRatio || row.ServiceFeeUnit == "" {
|
||||
row.ServiceFeeUnit = ServiceFeeUnitPercent
|
||||
}
|
||||
if row.ServiceFeeUnit == ServiceFeeUnitPercent {
|
||||
row.ServiceFeeValue = NormalizeSplitPercent(row.ServiceFeeValue)
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateSettlement 先扣通道手续费和号主应得,得到利润后再按 35/40/25 切。
|
||||
func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, error) {
|
||||
if input.GrossPaidAmountCent < 0 || input.RefundAmountCent < 0 || input.MarkupAmountCent < 0 || input.RefundAmountCent > input.GrossPaidAmountCent {
|
||||
if input.GrossPaidAmountCent < 0 || input.RefundAmountCent < 0 || input.OwnerRentAmountCent < 0 || input.MarkupAmountCent < 0 || input.RefundAmountCent > input.GrossPaidAmountCent {
|
||||
return SettlementResult{}, ErrInvalidAmount
|
||||
}
|
||||
if rule.PaymentFeeBPS < 0 || rule.PaymentFeeBPS > 10000 || rule.TaxRateBPS < 0 || rule.TaxRateBPS > 10000 || rule.ServiceFeeValue < 0 {
|
||||
@@ -113,13 +146,16 @@ func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, er
|
||||
}
|
||||
netPaid := input.GrossPaidAmountCent - input.RefundAmountCent
|
||||
paymentFee := roundHalfUp(float64(netPaid) * float64(rule.PaymentFeeBPS) / 10000)
|
||||
serviceFee, err := serviceFeeCent(rule, netPaid)
|
||||
profit := netPaid - input.OwnerRentAmountCent - paymentFee
|
||||
if profit < 0 {
|
||||
profit = 0
|
||||
}
|
||||
serviceFee, err := serviceFeeCent(rule, profit, netPaid)
|
||||
if err != nil {
|
||||
return SettlementResult{}, err
|
||||
}
|
||||
splitBase := netPaid - paymentFee - serviceFee
|
||||
if splitBase < 0 {
|
||||
splitBase = 0
|
||||
if serviceFee > profit {
|
||||
serviceFee = profit
|
||||
}
|
||||
result := SettlementResult{
|
||||
GrossPaidAmountCent: input.GrossPaidAmountCent,
|
||||
@@ -128,24 +164,24 @@ func CalculateSettlement(rule Rule, input SettlementInput) (SettlementResult, er
|
||||
PaymentFeeCent: paymentFee,
|
||||
ServiceFeeCent: serviceFee,
|
||||
MarkupAmountCent: input.MarkupAmountCent,
|
||||
SplitBaseAmountCent: splitBase,
|
||||
SplitBaseAmountCent: profit,
|
||||
}
|
||||
if rule.TaxEnabled {
|
||||
result.TaxByParty = make([]PartyTaxResult, 0, len(input.GrossProfitByParty))
|
||||
for _, party := range input.GrossProfitByParty {
|
||||
profit := party.IncomeAmountCent - party.CostAmountCent
|
||||
tax := roundHalfUp(float64(maxInt64(profit, 0)) * float64(rule.TaxRateBPS) / 10000)
|
||||
profitParty := party.IncomeAmountCent - party.CostAmountCent
|
||||
tax := roundHalfUp(float64(maxInt64(profitParty, 0)) * float64(rule.TaxRateBPS) / 10000)
|
||||
result.TaxAmountCent += tax
|
||||
result.TaxByParty = append(result.TaxByParty, PartyTaxResult{PartyRole: party.PartyRole, GrossProfitCent: profit, TaxAmountCent: tax})
|
||||
result.TaxByParty = append(result.TaxByParty, PartyTaxResult{PartyRole: party.PartyRole, GrossProfitCent: profitParty, TaxAmountCent: tax})
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func serviceFeeCent(rule Rule, netPaid int64) (int64, error) {
|
||||
func serviceFeeCent(rule Rule, profit, netPaid int64) (int64, error) {
|
||||
switch rule.ServiceFeeUnit {
|
||||
case ServiceFeeUnitPercent:
|
||||
return roundHalfUp(float64(netPaid) * rule.ServiceFeeValue / 100), nil
|
||||
return roundHalfUp(float64(profit) * rule.ServiceFeeValue / 100), nil
|
||||
case ServiceFeeUnitFixedAmount:
|
||||
return roundHalfUp(rule.ServiceFeeValue * 100), nil
|
||||
case ServiceFeeUnitRatio:
|
||||
@@ -176,11 +212,8 @@ type PartySplit struct {
|
||||
AmountCent int64
|
||||
}
|
||||
|
||||
// SplitSettlement 按规则模式把分成基数以百分比拆给各分销参与方。
|
||||
// 销售加价全部计入卖出方收入(设计文档 7.6);
|
||||
// 剩余金额(含四舍五入尾差)由调用方归入平台留存并记录 RoundingAdjustmentCent,
|
||||
// 最终业务口径以设计文档 7.2 待确认项的结论为准。
|
||||
func SplitSettlement(rule Rule, splitBase int64, markupCent int64) []PartySplit {
|
||||
// SplitSettlement 按百分数切利润:收号 35%、卖出 40%。服务费 25% 记在财务快照,不并进参与方。
|
||||
func SplitSettlement(rule Rule, profitCent int64) []PartySplit {
|
||||
roles := []struct {
|
||||
role string
|
||||
ratio float64
|
||||
@@ -206,28 +239,13 @@ func SplitSettlement(rule Rule, splitBase int64, markupCent int64) []PartySplit
|
||||
}
|
||||
splits := make([]PartySplit, 0, len(roles))
|
||||
for _, item := range roles {
|
||||
amount := roundHalfUp(float64(splitBase) * item.ratio / 100)
|
||||
amount := roundHalfUp(float64(profitCent) * item.ratio / 100)
|
||||
splits = append(splits, PartySplit{PartyRole: item.role, Ratio: item.ratio, AmountCent: maxInt64(amount, 0)})
|
||||
}
|
||||
if markupCent > 0 {
|
||||
// 加价归卖出方:两方-分销收号与三方模式的卖出方分别是 platform_sell / distributor_b。
|
||||
sellerRole := "distributor_sell"
|
||||
switch rule.Mode {
|
||||
case ModeTwoPartyDistributorReceive:
|
||||
sellerRole = "platform_sell"
|
||||
case ModeThreeParty:
|
||||
sellerRole = "distributor_b"
|
||||
}
|
||||
for i := range splits {
|
||||
if splits[i].PartyRole == sellerRole {
|
||||
splits[i].AmountCent += markupCent
|
||||
}
|
||||
}
|
||||
}
|
||||
return splits
|
||||
}
|
||||
|
||||
// SplitRemainderCent 返回分成基数扣除各方分成后的平台留存金额(含尾差)。
|
||||
// SplitRemainderCent 返回利润扣除各方分成后的剩余(未扣服务费)。
|
||||
func SplitRemainderCent(splitBase int64, splits []PartySplit) int64 {
|
||||
sum := int64(0)
|
||||
for _, item := range splits {
|
||||
@@ -235,3 +253,8 @@ func SplitRemainderCent(splitBase int64, splits []PartySplit) int64 {
|
||||
}
|
||||
return splitBase - sum
|
||||
}
|
||||
|
||||
// SplitRoundingCent 返回利润扣完收号、卖出和服务费后的四舍五入尾差。
|
||||
func SplitRoundingCent(profitCent, serviceFeeCent int64, splits []PartySplit) int64 {
|
||||
return SplitRemainderCent(profitCent, splits) - serviceFeeCent
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ func TestDefaultRules(t *testing.T) {
|
||||
mode string
|
||||
check func(Rule) bool
|
||||
}{
|
||||
{ModeTwoPartyDistributorReceive, func(r Rule) bool { return r.DistributorReceiveRatio == 3.5 && r.PlatformSellRatio == 4 }},
|
||||
{ModeTwoPartyPlatformReceive, func(r Rule) bool { return r.PlatformReceiveRatio == 3.5 && r.DistributorSellRatio == 4 }},
|
||||
{ModeThreeParty, func(r Rule) bool { return r.DistributorARatio == 3.5 && r.DistributorBRatio == 4 }},
|
||||
{ModeTwoPartyDistributorReceive, func(r Rule) bool { return r.DistributorReceiveRatio == 35 && r.PlatformSellRatio == 40 }},
|
||||
{ModeTwoPartyPlatformReceive, func(r Rule) bool { return r.PlatformReceiveRatio == 35 && r.DistributorSellRatio == 40 }},
|
||||
{ModeThreeParty, func(r Rule) bool { return r.DistributorARatio == 35 && r.DistributorBRatio == 40 }},
|
||||
}
|
||||
for _, item := range cases {
|
||||
rule, err := DefaultRule(item.mode)
|
||||
if err != nil || !item.check(rule) || rule.ServiceFeeValue != 2.5 || rule.PaymentFeeBPS != 50 {
|
||||
if err != nil || !item.check(rule) || rule.ServiceFeeValue != 25 || rule.ServiceFeeUnit != ServiceFeeUnitPercent || rule.PaymentFeeBPS != 50 {
|
||||
t.Fatalf("default rule %s = %#v, err=%v", item.mode, rule, err)
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,29 @@ func TestDefaultRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateSettlementDeductsFeeBeforeSplit(t *testing.T) {
|
||||
rule := Rule{ServiceFeeValue: 0, ServiceFeeUnit: ServiceFeeUnitFixedAmount, PaymentFeeBPS: 50}
|
||||
got, err := CalculateSettlement(rule, SettlementInput{GrossPaidAmountCent: 10001, RefundAmountCent: 1000, MarkupAmountCent: 300})
|
||||
func TestNormalizeSplitPercent(t *testing.T) {
|
||||
if got := NormalizeSplitPercent(3.5); got != 35 {
|
||||
t.Fatalf("3.5 -> %v, want 35", got)
|
||||
}
|
||||
if got := NormalizeSplitPercent(4); got != 40 {
|
||||
t.Fatalf("4 -> %v, want 40", got)
|
||||
}
|
||||
if got := NormalizeSplitPercent(2.5); got != 25 {
|
||||
t.Fatalf("2.5 -> %v, want 25", got)
|
||||
}
|
||||
if got := NormalizeSplitPercent(35); got != 35 {
|
||||
t.Fatalf("35 stays %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateSettlementSplitsProfitAfterOwnerAndFee(t *testing.T) {
|
||||
rule := Rule{ServiceFeeValue: 25, ServiceFeeUnit: ServiceFeeUnitPercent, PaymentFeeBPS: 50}
|
||||
got, err := CalculateSettlement(rule, SettlementInput{GrossPaidAmountCent: 10000, OwnerRentAmountCent: 6000})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.NetPaidAmountCent != 9001 || got.PaymentFeeCent != 45 || got.SplitBaseAmountCent != 8956 {
|
||||
// 通道费 50,利润 10000-6000-50=3950,服务费 25%=988
|
||||
if got.NetPaidAmountCent != 10000 || got.PaymentFeeCent != 50 || got.SplitBaseAmountCent != 3950 || got.ServiceFeeCent != 988 {
|
||||
t.Fatalf("unexpected settlement: %#v", got)
|
||||
}
|
||||
}
|
||||
@@ -58,48 +74,37 @@ func TestCalculateSettlementRejectsRefundOverPayment(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSplitSettlementTwoPartyDistributorReceive(t *testing.T) {
|
||||
rule := Rule{Mode: ModeTwoPartyDistributorReceive, DistributorReceiveRatio: 3.5, PlatformSellRatio: 4}
|
||||
splits := SplitSettlement(rule, 10000, 0)
|
||||
rule := Rule{Mode: ModeTwoPartyDistributorReceive, DistributorReceiveRatio: 35, PlatformSellRatio: 40}
|
||||
splits := SplitSettlement(rule, 3950)
|
||||
if len(splits) != 2 {
|
||||
t.Fatalf("splits = %#v", splits)
|
||||
}
|
||||
if splits[0].PartyRole != "distributor_receive" || splits[0].AmountCent != 350 {
|
||||
if splits[0].PartyRole != "distributor_receive" || splits[0].AmountCent != 1383 {
|
||||
t.Fatalf("distributor split = %#v", splits[0])
|
||||
}
|
||||
if splits[1].PartyRole != "platform_sell" || splits[1].AmountCent != 400 {
|
||||
if splits[1].PartyRole != "platform_sell" || splits[1].AmountCent != 1580 {
|
||||
t.Fatalf("platform split = %#v", splits[1])
|
||||
}
|
||||
if got := SplitRemainderCent(10000, splits); got != 9250 {
|
||||
t.Fatalf("remainder = %d, want 9250", got)
|
||||
if got := SplitRoundingCent(3950, 988, splits); got != -1 {
|
||||
t.Fatalf("rounding = %d, want -1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSettlementMarkupGoesToSeller(t *testing.T) {
|
||||
// 两方-分销收号模式:卖出方是主站(platform_sell)。
|
||||
rule := Rule{Mode: ModeTwoPartyDistributorReceive, DistributorReceiveRatio: 3.5, PlatformSellRatio: 4}
|
||||
splits := SplitSettlement(rule, 10000, 300)
|
||||
if splits[1].AmountCent != 700 {
|
||||
t.Fatalf("platform split with markup = %d, want 700", splits[1].AmountCent)
|
||||
func TestSplitSettlementDoesNotAddYuanMarkup(t *testing.T) {
|
||||
rule := Rule{Mode: ModeTwoPartyPlatformReceive, DistributorSellRatio: 40, PlatformReceiveRatio: 35}
|
||||
splits := SplitSettlement(rule, 10000)
|
||||
if splits[0].PartyRole != "distributor_sell" || splits[0].AmountCent != 4000 {
|
||||
t.Fatalf("distributor sell = %#v", splits[0])
|
||||
}
|
||||
// 两方-分销卖出模式:卖出方是分销网(distributor_sell)。
|
||||
ruleSell := Rule{Mode: ModeTwoPartyPlatformReceive, DistributorSellRatio: 4, PlatformReceiveRatio: 3.5}
|
||||
splitsSell := SplitSettlement(ruleSell, 10000, 300)
|
||||
if splitsSell[0].PartyRole != "distributor_sell" || splitsSell[0].AmountCent != 700 {
|
||||
t.Fatalf("distributor sell split with markup = %#v", splitsSell[0])
|
||||
}
|
||||
if got := SplitRemainderCent(10000, splitsSell); got != 8950 {
|
||||
t.Fatalf("remainder = %d, want 8950", got)
|
||||
if splits[1].AmountCent != 3500 {
|
||||
t.Fatalf("platform receive = %#v", splits[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSettlementThreeParty(t *testing.T) {
|
||||
rule := Rule{Mode: ModeThreeParty, DistributorARatio: 3.5, DistributorBRatio: 4}
|
||||
splits := SplitSettlement(rule, 10001, 0)
|
||||
if len(splits) != 2 || splits[0].AmountCent != 350 || splits[1].AmountCent != 400 {
|
||||
rule := Rule{Mode: ModeThreeParty, DistributorARatio: 35, DistributorBRatio: 40}
|
||||
splits := SplitSettlement(rule, 10000)
|
||||
if len(splits) != 2 || splits[0].AmountCent != 3500 || splits[1].AmountCent != 4000 {
|
||||
t.Fatalf("splits = %#v", splits)
|
||||
}
|
||||
// 尾差归平台:10001 - 750 = 9251。
|
||||
if got := SplitRemainderCent(10001, splits); got != 9251 {
|
||||
t.Fatalf("remainder = %d, want 9251", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -548,31 +548,31 @@ func TestDistributionBrandingAndMarkup(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
display, err := repo.UpdateAccountMarkup(t.Context(), 0, dsa.ID, 500)
|
||||
display, err := repo.UpdateAccountMarkup(t.Context(), 0, dsa.ID, 5)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateAccountMarkup failed: %v", err)
|
||||
}
|
||||
if display.MarkupAmountCent != 500 {
|
||||
t.Fatalf("expected markup 500, got %d", display.MarkupAmountCent)
|
||||
if display.ShelfRatioDelta != 5 {
|
||||
t.Fatalf("expected shelf ratio delta 5, got %v", display.ShelfRatioDelta)
|
||||
}
|
||||
|
||||
// Batch update markup
|
||||
if err := repo.BatchUpdateAccountMarkup(t.Context(), 0, site.ID, BatchUpdateAccountMarkupRequest{
|
||||
ListingIDs: nil,
|
||||
MarkupAmountCent: 800,
|
||||
ListingIDs: nil,
|
||||
ShelfRatioDelta: 8,
|
||||
}); !errors.Is(err, ErrEmptyListingIDs) {
|
||||
t.Fatalf("empty listing_ids err = %v", err)
|
||||
}
|
||||
if err := repo.BatchUpdateAccountMarkup(t.Context(), 0, site.ID, BatchUpdateAccountMarkupRequest{
|
||||
ListingIDs: []uint64{201},
|
||||
MarkupAmountCent: 800,
|
||||
ListingIDs: []uint64{201},
|
||||
ShelfRatioDelta: 8,
|
||||
}); err != nil {
|
||||
t.Fatalf("BatchUpdateAccountMarkup failed: %v", err)
|
||||
}
|
||||
|
||||
displays, err := repo.ListAccountDisplays(t.Context(), site.ID, "")
|
||||
if err != nil || len(displays) == 0 || displays[0].MarkupAmountCent != 800 {
|
||||
t.Fatalf("expected markup 800 after batch, got: %+v", displays)
|
||||
if err != nil || len(displays) == 0 || displays[0].ShelfRatioDelta != 8 {
|
||||
t.Fatalf("expected shelf ratio delta 8 after batch, got: %+v", displays)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,11 +249,11 @@ func (s *Service) UpdateSiteBranding(ctx context.Context, adminID, siteID uint64
|
||||
return s.repo.UpdateSiteBranding(ctx, adminID, siteID, req)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, markupCent int64) (*AccountDisplayDTO, error) {
|
||||
func (s *Service) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, ratioDelta float64) (*AccountDisplayDTO, error) {
|
||||
if err := s.available(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.UpdateAccountMarkup(ctx, adminID, id, markupCent)
|
||||
return s.repo.UpdateAccountMarkup(ctx, adminID, id, ratioDelta)
|
||||
}
|
||||
|
||||
func (s *Service) BatchUpdateAccountMarkup(ctx context.Context, adminID, siteID uint64, req BatchUpdateAccountMarkupRequest) error {
|
||||
|
||||
@@ -237,11 +237,11 @@ func (r *Repository) FindPublic(ctx context.Context, id uint64, siteCode string)
|
||||
return nil, err
|
||||
}
|
||||
applyPublicListingURLs(dto)
|
||||
markups, err := r.shelfMarkupByListingID(ctx, siteCode, []uint64{dto.ID})
|
||||
if err != nil {
|
||||
items := []ListingDTO{*dto}
|
||||
if err := r.applyShelfMarkup(ctx, siteCode, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto.PriceCent += markups[dto.ID]
|
||||
*dto = items[0]
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
@@ -253,36 +253,40 @@ func (r *Repository) applyShelfMarkup(ctx context.Context, siteCode string, item
|
||||
for _, item := range items {
|
||||
ids = append(ids, item.ID)
|
||||
}
|
||||
markups, err := r.shelfMarkupByListingID(ctx, siteCode, ids)
|
||||
deltas, err := r.shelfRatioDeltaByListingID(ctx, siteCode, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items {
|
||||
items[i].PriceCent += markups[items[i].ID]
|
||||
delta := deltas[items[i].ID]
|
||||
if delta == 0 {
|
||||
continue
|
||||
}
|
||||
items[i].PriceCent, _, _ = ApplyShelfRatio(items[i].HafCoinAmount, items[i].PriceCent, delta)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) shelfMarkupByListingID(ctx context.Context, siteCode string, listingIDs []uint64) (map[uint64]int64, error) {
|
||||
result := make(map[uint64]int64, len(listingIDs))
|
||||
func (r *Repository) shelfRatioDeltaByListingID(ctx context.Context, siteCode string, listingIDs []uint64) (map[uint64]float64, error) {
|
||||
result := make(map[uint64]float64, len(listingIDs))
|
||||
if siteCode == "" || len(listingIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
type markupRow struct {
|
||||
ListingID uint64 `gorm:"column:listing_id"`
|
||||
MarkupAmountCent int64 `gorm:"column:markup_amount_cent"`
|
||||
type ratioRow struct {
|
||||
ListingID uint64 `gorm:"column:listing_id"`
|
||||
ShelfRatioDelta float64 `gorm:"column:shelf_ratio_delta"`
|
||||
}
|
||||
var rows []markupRow
|
||||
var rows []ratioRow
|
||||
if err := r.db.WithContext(ctx).Table("distribution_site_accounts AS dsa").
|
||||
Select("dsa.listing_id, COALESCE(dsa.markup_amount_cent, 0) AS markup_amount_cent").
|
||||
Select("dsa.listing_id, COALESCE(dsa.shelf_ratio_delta, 0) AS shelf_ratio_delta").
|
||||
Joins("INNER JOIN distribution_sites AS ds ON ds.id = dsa.site_id AND ds.site_code = ? AND ds.status = 'active'", siteCode).
|
||||
Where("dsa.display_status = 'enabled' AND dsa.listing_id IN ?", listingIDs).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row.MarkupAmountCent > 0 {
|
||||
result[row.ListingID] = row.MarkupAmountCent
|
||||
if row.ShelfRatioDelta > 0 {
|
||||
result[row.ListingID] = row.ShelfRatioDelta
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
||||
@@ -688,12 +688,12 @@ func TestListPublicWithDistributionSiteCode(t *testing.T) {
|
||||
|
||||
repo := NewRepository(db, nil)
|
||||
|
||||
account1 := model.GameAccount{Title: "号1", GameName: "delta", Status: "active"}
|
||||
account1 := model.GameAccount{Title: "号1", GameName: "delta", Status: "active", HafCoinAmount: 100_000_000}
|
||||
account2 := model.GameAccount{Title: "号2", GameName: "delta", Status: "active"}
|
||||
db.Create(&account1)
|
||||
db.Create(&account2)
|
||||
|
||||
l1 := model.RentalListing{ListingNo: "L101", AccountID: account1.ID, Status: "published", ReviewStatus: "approved", PriceCent: 1000}
|
||||
l1 := model.RentalListing{ListingNo: "L101", AccountID: account1.ID, Status: "published", ReviewStatus: "approved", PriceCent: 10000}
|
||||
l2 := model.RentalListing{ListingNo: "L102", AccountID: account2.ID, Status: "published", ReviewStatus: "approved", PriceCent: 2000}
|
||||
db.Create(&l1)
|
||||
db.Create(&l2)
|
||||
@@ -701,7 +701,7 @@ func TestListPublicWithDistributionSiteCode(t *testing.T) {
|
||||
site := model.DistributionSite{SiteCode: "site_alpha", SiteName: "分销A", Domain: "a.com", Status: "active", DisplayAccountEnabled: true}
|
||||
db.Create(&site)
|
||||
|
||||
dsa := model.DistributionSiteAccount{SiteID: site.ID, AccountID: account1.ID, ListingID: l1.ID, DisplayStatus: "enabled", MarkupAmountCent: 10000}
|
||||
dsa := model.DistributionSiteAccount{SiteID: site.ID, AccountID: account1.ID, ListingID: l1.ID, DisplayStatus: "enabled", ShelfRatioDelta: 25}
|
||||
db.Create(&dsa)
|
||||
|
||||
res, err := repo.ListPublic(context.Background(), PublicListQuery{
|
||||
@@ -713,22 +713,22 @@ func TestListPublicWithDistributionSiteCode(t *testing.T) {
|
||||
if len(res.Items) != 1 || res.Items[0].ListingNo != "L101" {
|
||||
t.Fatalf("expected only L101, got %+v", res.Items)
|
||||
}
|
||||
if res.Items[0].PriceCent != 11000 {
|
||||
t.Fatalf("expected public price 11000 with markup, got %d", res.Items[0].PriceCent)
|
||||
if res.Items[0].PriceCent != 8000 {
|
||||
t.Fatalf("expected public price 8000 with ratio delta, got %d", res.Items[0].PriceCent)
|
||||
}
|
||||
|
||||
detail, err := repo.FindPublic(context.Background(), l1.ID, "site_alpha")
|
||||
if err != nil {
|
||||
t.Fatalf("FindPublic err: %v", err)
|
||||
}
|
||||
if detail.PriceCent != 11000 {
|
||||
t.Fatalf("expected detail price 11000 with markup, got %d", detail.PriceCent)
|
||||
if detail.PriceCent != 8000 {
|
||||
t.Fatalf("expected detail price 8000 with ratio delta, got %d", detail.PriceCent)
|
||||
}
|
||||
mainDetail, err := repo.FindPublic(context.Background(), l1.ID, "")
|
||||
if err != nil {
|
||||
t.Fatalf("FindPublic main err: %v", err)
|
||||
}
|
||||
if mainDetail.PriceCent != 1000 {
|
||||
if mainDetail.PriceCent != 10000 {
|
||||
t.Fatalf("main site must keep original price, got %d", mainDetail.PriceCent)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package listing
|
||||
|
||||
// CoinWan 将内部哈夫币数量转为万。
|
||||
func CoinWan(hafCoinAmount int64) float64 {
|
||||
return float64(hafCoinAmount) / 10000
|
||||
}
|
||||
|
||||
// BaseExchangeRatio 是主站挂牌的「1 元兑换哈夫币」比例。
|
||||
func BaseExchangeRatio(hafCoinAmount, priceCent int64) float64 {
|
||||
if hafCoinAmount <= 0 || priceCent <= 0 {
|
||||
return 0
|
||||
}
|
||||
return roundRatio(CoinWan(hafCoinAmount) / centToYuan(priceCent))
|
||||
}
|
||||
|
||||
// ApplyShelfRatio 按分站加价比例重算售价。delta=0 时保持原价,避免四舍五入漂移。
|
||||
func ApplyShelfRatio(hafCoinAmount, basePriceCent int64, ratioDelta float64) (priceCent int64, baseRatio, siteRatio float64) {
|
||||
baseRatio = BaseExchangeRatio(hafCoinAmount, basePriceCent)
|
||||
if ratioDelta < 0 {
|
||||
ratioDelta = 0
|
||||
}
|
||||
if baseRatio <= 0 || ratioDelta == 0 {
|
||||
return basePriceCent, baseRatio, baseRatio
|
||||
}
|
||||
siteRatio = roundRatio(baseRatio + ratioDelta)
|
||||
if siteRatio <= 0 {
|
||||
return basePriceCent, baseRatio, baseRatio
|
||||
}
|
||||
priceCent = yuanToCent(CoinWan(hafCoinAmount) / siteRatio)
|
||||
if priceCent < 0 {
|
||||
priceCent = 0
|
||||
}
|
||||
return priceCent, baseRatio, siteRatio
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package listing
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestApplyShelfRatioIncreasesExchangeRatio(t *testing.T) {
|
||||
// 100M = 10000 万,主站 100 元 → 1 元 = 100w
|
||||
price, base, site := ApplyShelfRatio(100_000_000, 10000, 25)
|
||||
if base != 100 {
|
||||
t.Fatalf("base ratio = %v, want 100", base)
|
||||
}
|
||||
if site != 125 {
|
||||
t.Fatalf("site ratio = %v, want 125", site)
|
||||
}
|
||||
if price != 8000 {
|
||||
t.Fatalf("site price = %d, want 8000", price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyShelfRatioZeroDeltaKeepsPrice(t *testing.T) {
|
||||
price, _, site := ApplyShelfRatio(100_000_000, 10000, 0)
|
||||
if price != 10000 || site != 100 {
|
||||
t.Fatalf("price=%d site=%v", price, site)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ type OrderDTO struct {
|
||||
BasePriceCent int64 `json:"base_price_cent,omitempty"`
|
||||
ShelfMarkupCent int64 `json:"shelf_markup_cent,omitempty"`
|
||||
MarkupSiteID uint64 `json:"markup_site_id,omitempty"`
|
||||
ShelfRatio float64 `json:"shelf_ratio,omitempty"`
|
||||
ShelfRatioDelta float64 `json:"shelf_ratio_delta,omitempty"`
|
||||
RentOriginalAmountCent int64 `json:"rent_original_amount_cent,omitempty"`
|
||||
RentDiscountAmountCent int64 `json:"rent_discount_amount_cent,omitempty"`
|
||||
PureCoinOriginalAmountCent int64 `json:"pure_coin_original_amount_cent,omitempty"`
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/chat"
|
||||
"hfb_sys/backend/internal/modules/distribution"
|
||||
listingpkg "hfb_sys/backend/internal/modules/listing"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/rentergrowth"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
@@ -58,14 +59,28 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
return err
|
||||
}
|
||||
rentHours := estimateOrderDurationHours(snapshot)
|
||||
shelfMarkupCent, markupSiteID, err := loadShelfMarkupTx(tx, req.DistributionSiteCode, listing.ID)
|
||||
shelfRatioDelta, markupSiteID, err := loadShelfRatioTx(tx, req.DistributionSiteCode, listing.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pricing := buildOrderPricing(listing, account)
|
||||
basePriceCent := listing.PriceCent
|
||||
if shelfMarkupCent > 0 {
|
||||
pricing.RentAmountCent += shelfMarkupCent
|
||||
sitePriceCent, _, siteRatio := listingpkg.ApplyShelfRatio(account.HafCoinAmount, listing.PriceCent, shelfRatioDelta)
|
||||
shelfMarkupCent := sitePriceCent - basePriceCent
|
||||
if sitePriceCent != listing.PriceCent {
|
||||
pricing.RentAmountCent = sitePriceCent
|
||||
if pricing.OwnerRentAmountCent > pricing.RentAmountCent {
|
||||
pricing.OwnerRentAmountCent = pricing.RentAmountCent
|
||||
}
|
||||
pricing.PlatformFeeCent = maxCent(pricing.RentAmountCent-pricing.OwnerRentAmountCent, 0)
|
||||
if pricing.ExtraItemAmountCent > pricing.RentAmountCent {
|
||||
pricing.ExtraItemAmountCent = pricing.RentAmountCent
|
||||
}
|
||||
pricing.PureCoinAmountCent = maxCent(pricing.RentAmountCent-pricing.ExtraItemAmountCent, 0)
|
||||
if pricing.OwnerPureCoinAmountCent > pricing.OwnerRentAmountCent {
|
||||
pricing.OwnerPureCoinAmountCent = pricing.OwnerRentAmountCent
|
||||
}
|
||||
pricing.PureCoinPlatformFeeCent = maxCent(pricing.PureCoinAmountCent-pricing.OwnerPureCoinAmountCent, 0)
|
||||
}
|
||||
distributionSite, distributionRule, err := distribution.ResolveOrderSourceTx(tx, req.DistributionSiteCode, listing.ID, account.ID, time.Now())
|
||||
if err != nil {
|
||||
@@ -116,6 +131,8 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
BasePriceCent: basePriceCent,
|
||||
ShelfMarkupCent: shelfMarkupCent,
|
||||
MarkupSiteID: markupSiteID,
|
||||
ShelfRatio: siteRatio,
|
||||
ShelfRatioDelta: shelfRatioDelta,
|
||||
RentOriginalAmountCent: rentOriginalAmountCent,
|
||||
RentDiscountAmountCent: rentDiscountAmountCent,
|
||||
PureCoinOriginalAmountCent: pricing.PureCoinAmountCent,
|
||||
@@ -168,17 +185,17 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
return r.FindForUser(ctx, renterID, createdID)
|
||||
}
|
||||
|
||||
func loadShelfMarkupTx(tx *gorm.DB, siteCode string, listingID uint64) (int64, uint64, error) {
|
||||
func loadShelfRatioTx(tx *gorm.DB, siteCode string, listingID uint64) (float64, uint64, error) {
|
||||
siteCode = strings.TrimSpace(siteCode)
|
||||
if siteCode == "" || listingID == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
var row struct {
|
||||
SiteID uint64 `gorm:"column:site_id"`
|
||||
Markup int64 `gorm:"column:markup"`
|
||||
SiteID uint64 `gorm:"column:site_id"`
|
||||
Delta float64 `gorm:"column:delta"`
|
||||
}
|
||||
err := tx.Table("distribution_site_accounts AS dsa").
|
||||
Select("dsa.site_id, COALESCE(dsa.markup_amount_cent, 0) AS markup").
|
||||
Select("dsa.site_id, COALESCE(dsa.shelf_ratio_delta, 0) AS delta").
|
||||
Joins("INNER JOIN distribution_sites AS ds ON ds.id = dsa.site_id AND ds.site_code = ? AND ds.status = 'active'", siteCode).
|
||||
Where("dsa.listing_id = ? AND dsa.display_status = 'enabled'", listingID).
|
||||
Take(&row).Error
|
||||
@@ -188,10 +205,10 @@ func loadShelfMarkupTx(tx *gorm.DB, siteCode string, listingID uint64) (int64, u
|
||||
}
|
||||
return 0, 0, err
|
||||
}
|
||||
if row.Markup < 0 {
|
||||
row.Markup = 0
|
||||
if row.Delta < 0 {
|
||||
row.Delta = 0
|
||||
}
|
||||
return row.Markup, row.SiteID, nil
|
||||
return row.Delta, row.SiteID, nil
|
||||
}
|
||||
|
||||
func orderAccountSourceSnapshot(tx *gorm.DB, listingID uint64) (string, string, error) {
|
||||
|
||||
@@ -95,6 +95,8 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
BasePriceCent: row.BasePriceCent,
|
||||
ShelfMarkupCent: row.ShelfMarkupCent,
|
||||
MarkupSiteID: row.MarkupSiteID,
|
||||
ShelfRatio: row.ShelfRatio,
|
||||
ShelfRatioDelta: row.ShelfRatioDelta,
|
||||
RentOriginalAmountCent: effectiveRentOriginalAmountCent(row.RentalOrder),
|
||||
RentDiscountAmountCent: row.RentDiscountAmountCent,
|
||||
PureCoinOriginalAmountCent: orderPureCoinOriginalAmountCent(row.RentalOrder),
|
||||
|
||||
@@ -291,14 +291,7 @@ func effectiveCheckoutDepositDeductCent(checkout model.OrderCheckout) int64 {
|
||||
}
|
||||
|
||||
func orderConsumptionPrepaidCent(order model.RentalOrder) int64 {
|
||||
prepaid := order.RentAmountCent
|
||||
if order.ShelfMarkupCent > 0 {
|
||||
prepaid -= order.ShelfMarkupCent
|
||||
if prepaid < 0 {
|
||||
prepaid = 0
|
||||
}
|
||||
}
|
||||
return prepaid
|
||||
return order.RentAmountCent
|
||||
}
|
||||
|
||||
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
|
||||
|
||||
@@ -298,6 +298,7 @@ func TestCreateOrderSnapshotsShelfMarkup(t *testing.T) {
|
||||
ServerRegion: "国服",
|
||||
LoginPlatform: "steam",
|
||||
Title: "加价账号",
|
||||
HafCoinAmount: 100_000_000,
|
||||
AssetSummary: datatypes.JSON([]byte(`{
|
||||
"price_breakdown": {
|
||||
"seller_total_price": 80,
|
||||
@@ -325,7 +326,7 @@ func TestCreateOrderSnapshotsShelfMarkup(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.DistributionSiteAccount{
|
||||
SiteID: site.ID, AccountID: account.ID, ListingID: listing.ID, DisplayStatus: "enabled", MarkupAmountCent: 10000,
|
||||
SiteID: site.ID, AccountID: account.ID, ListingID: listing.ID, DisplayStatus: "enabled", ShelfRatioDelta: 25,
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -343,30 +344,30 @@ func TestCreateOrderSnapshotsShelfMarkup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if dto.RentAmountCent == nil || *dto.RentAmountCent != 20000 {
|
||||
t.Fatalf("paid rent = %v, want 20000", dto.RentAmountCent)
|
||||
if dto.RentAmountCent == nil || *dto.RentAmountCent != 8000 {
|
||||
t.Fatalf("paid rent = %v, want 8000", dto.RentAmountCent)
|
||||
}
|
||||
if dto.BasePriceCent != 10000 || dto.ShelfMarkupCent != 10000 || dto.MarkupSiteID != site.ID {
|
||||
t.Fatalf("markup snapshot = base %d markup %d site %d", dto.BasePriceCent, dto.ShelfMarkupCent, dto.MarkupSiteID)
|
||||
if dto.BasePriceCent != 10000 || dto.ShelfMarkupCent != -2000 || dto.MarkupSiteID != site.ID || dto.ShelfRatioDelta != 25 {
|
||||
t.Fatalf("ratio snapshot = base %d deltaCent %d site %d ratioDelta %v", dto.BasePriceCent, dto.ShelfMarkupCent, dto.MarkupSiteID, dto.ShelfRatioDelta)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, dto.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.OwnerRentAmountCent != 8000 || saved.PlatformFeeCent != 2000 {
|
||||
t.Fatalf("owner/platform must stay on base price, got %d/%d", saved.OwnerRentAmountCent, saved.PlatformFeeCent)
|
||||
if saved.OwnerRentAmountCent != 8000 {
|
||||
t.Fatalf("owner rent = %d, want 8000", saved.OwnerRentAmountCent)
|
||||
}
|
||||
|
||||
if err := db.Model(&model.DistributionSiteAccount{}).Where("listing_id = ?", listing.ID).Update("markup_amount_cent", 0).Error; err != nil {
|
||||
if err := db.Model(&model.DistributionSiteAccount{}).Where("listing_id = ?", listing.ID).Update("shelf_ratio_delta", 0).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var after model.RentalOrder
|
||||
if err := db.First(&after, dto.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.ShelfMarkupCent != 10000 || after.RentAmountCent != 20000 {
|
||||
t.Fatalf("historical order must keep markup snapshot, got markup %d rent %d", after.ShelfMarkupCent, after.RentAmountCent)
|
||||
if after.ShelfRatioDelta != 25 || after.RentAmountCent != 8000 {
|
||||
t.Fatalf("historical order must keep ratio snapshot, got delta %v rent %d", after.ShelfRatioDelta, after.RentAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,19 +118,19 @@ func TestBuildOrderPricingDoesNotDiscountAllExtraItemOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutDoesNotRefundShelfMarkup(t *testing.T) {
|
||||
func TestCheckoutRefundsSiteRent(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
RentAmountCent: 20000,
|
||||
RentAmountCent: 8000,
|
||||
OwnerRentAmountCent: 8000,
|
||||
PlatformFeeCent: 2000,
|
||||
PlatformFeeCent: 0,
|
||||
BasePriceCent: 10000,
|
||||
ShelfMarkupCent: 10000,
|
||||
ShelfRatioDelta: 25,
|
||||
DepositAmountCent: 1000,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"asset_summary": {
|
||||
"price_breakdown": {
|
||||
"buyer_coin_base_price": 100,
|
||||
"buyer_coin_base_price": 80,
|
||||
"seller_coin_base_price": 80,
|
||||
"consumable_price": 0
|
||||
}
|
||||
@@ -139,11 +139,11 @@ func TestCheckoutDoesNotRefundShelfMarkup(t *testing.T) {
|
||||
}
|
||||
settlement := calculateCheckoutSettlement(order, 0, 100, 0)
|
||||
if settlement.RentRefundCent != 0 {
|
||||
t.Fatalf("full consume must not refund shelf markup, rent refund = %d", settlement.RentRefundCent)
|
||||
t.Fatalf("full consume must not refund rent, rent refund = %d", settlement.RentRefundCent)
|
||||
}
|
||||
zeroUse := calculateCheckoutSettlement(order, 0, 0, 0)
|
||||
if zeroUse.RentRefundCent != 10000 {
|
||||
t.Fatalf("unused listing refunds base price only, got %d want 10000", zeroUse.RentRefundCent)
|
||||
if zeroUse.RentRefundCent != 8000 {
|
||||
t.Fatalf("unused listing refunds site rent, got %d want 8000", zeroUse.RentRefundCent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +417,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
api.GET("/home-announcements", systemConfigHandler.HomeAnnouncements)
|
||||
api.GET("/mobile-home-config", systemConfigHandler.HomeConfig)
|
||||
api.GET("/public/files/object", fileHandler.PublicObject)
|
||||
api.GET("/distribution/sites/:site_code/listings", distributionHandler.ListPublicSiteListings)
|
||||
// Gin 同一前缀只能有一种通配符名;管理端已用 /sites/:id/*,公开列表必须同名。
|
||||
api.GET("/distribution/sites/:id/listings", distributionHandler.ListPublicSiteListings)
|
||||
api.GET("/distribution/site-info", distributionHandler.GetPublicSiteInfo)
|
||||
api.POST("/payments/leshua/notify", paymentHandler.LeshuaNotify)
|
||||
api.POST("/payments/lakala/notify", paymentHandler.LakalaNotify)
|
||||
|
||||
@@ -60,6 +60,8 @@ func TestExtractedAdminRoutesRemainRegisteredWithoutDuplicates(t *testing.T) {
|
||||
"PUT /api/admin/roles/:id/permissions",
|
||||
"PUT /api/admin/admin-users/:id/password",
|
||||
"POST /api/admin/announcements/:id/publish",
|
||||
"GET /api/distribution/sites/:id/listings",
|
||||
"GET /api/distribution/sites/:id/wallet",
|
||||
} {
|
||||
if _, exists := registered[key]; !exists {
|
||||
t.Errorf("missing route after extraction: %s", key)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 分销站点、规则、分账、结算,以及主站表上的租户隔离列。
|
||||
-- 分成字段存百分数:收号 35、卖出 40、服务费 25。
|
||||
-- 货架调价存 shelf_ratio_delta(分站 1 元兑换哈夫币比例相对主站的增量)。
|
||||
|
||||
CREATE TABLE distribution_sites (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
site_code VARCHAR(64) NOT NULL,
|
||||
@@ -7,6 +11,11 @@ CREATE TABLE distribution_sites (
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
display_account_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
logo_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
favicon_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
banners_json TEXT NULL,
|
||||
announcements_json TEXT NULL,
|
||||
footer_info_json TEXT NULL,
|
||||
default_support_group_id BIGINT UNSIGNED NULL,
|
||||
tax_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
tax_rate_bps BIGINT NOT NULL DEFAULT 100,
|
||||
@@ -40,6 +49,8 @@ CREATE TABLE distribution_site_accounts (
|
||||
account_id BIGINT UNSIGNED NOT NULL,
|
||||
listing_id BIGINT UNSIGNED NOT NULL,
|
||||
display_status VARCHAR(16) NOT NULL DEFAULT 'enabled',
|
||||
markup_amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||
shelf_ratio_delta DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
display_started_at DATETIME NULL,
|
||||
display_ended_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
@@ -64,7 +75,7 @@ CREATE TABLE distribution_rule_versions (
|
||||
distributor_a_ratio DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
distributor_b_ratio DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
service_fee_value DECIMAL(12,4) NOT NULL DEFAULT 0,
|
||||
service_fee_unit VARCHAR(24) NOT NULL DEFAULT 'ratio',
|
||||
service_fee_unit VARCHAR(24) NOT NULL DEFAULT 'percent',
|
||||
payment_fee_bps BIGINT NOT NULL DEFAULT 50,
|
||||
tax_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
tax_rate_bps BIGINT NOT NULL DEFAULT 100,
|
||||
@@ -90,7 +101,7 @@ CREATE TABLE distribution_order_financials (
|
||||
payment_fee_bps BIGINT NOT NULL DEFAULT 50,
|
||||
payment_fee_cent BIGINT NOT NULL DEFAULT 0,
|
||||
service_fee_value DECIMAL(12,4) NOT NULL DEFAULT 0,
|
||||
service_fee_unit VARCHAR(24) NOT NULL DEFAULT 'ratio',
|
||||
service_fee_unit VARCHAR(24) NOT NULL DEFAULT 'percent',
|
||||
service_fee_cent BIGINT NOT NULL DEFAULT 0,
|
||||
markup_amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||
tax_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
@@ -182,19 +193,54 @@ CREATE TABLE distribution_site_payment_channels (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分销网站收款通道绑定';
|
||||
|
||||
ALTER TABLE rental_orders
|
||||
ADD COLUMN base_price_cent BIGINT NOT NULL DEFAULT 0 AFTER platform_fee_cent,
|
||||
ADD COLUMN shelf_markup_cent BIGINT NOT NULL DEFAULT 0 AFTER base_price_cent,
|
||||
ADD COLUMN markup_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER shelf_markup_cent,
|
||||
ADD COLUMN shelf_ratio DECIMAL(10,2) NOT NULL DEFAULT 0 AFTER markup_site_id,
|
||||
ADD COLUMN shelf_ratio_delta DECIMAL(10,2) NOT NULL DEFAULT 0 AFTER shelf_ratio,
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER source_channel,
|
||||
ADD COLUMN distribution_mode VARCHAR(32) NOT NULL DEFAULT '' AFTER distribution_site_id,
|
||||
ADD COLUMN distribution_rule_version_id BIGINT UNSIGNED NULL AFTER distribution_mode,
|
||||
ADD KEY idx_rental_orders_markup_site (markup_site_id),
|
||||
ADD KEY idx_rental_orders_distribution_site (distribution_site_id, distribution_mode),
|
||||
ADD CONSTRAINT fk_rental_orders_distribution_site FOREIGN KEY (distribution_site_id) REFERENCES distribution_sites (id),
|
||||
ADD CONSTRAINT fk_rental_orders_distribution_rule FOREIGN KEY (distribution_rule_version_id) REFERENCES distribution_rule_versions (id);
|
||||
|
||||
ALTER TABLE rental_listings
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER managed_admin_id,
|
||||
ADD COLUMN source_channel VARCHAR(64) NOT NULL DEFAULT '' AFTER distribution_site_id,
|
||||
ADD KEY idx_rental_listings_distribution_site (distribution_site_id, status);
|
||||
|
||||
ALTER TABLE chat_qrcode_pool
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER group_name,
|
||||
ADD KEY idx_chat_qrcode_distribution_site (distribution_site_id, status),
|
||||
ADD CONSTRAINT fk_chat_qrcode_distribution_site FOREIGN KEY (distribution_site_id) REFERENCES distribution_sites (id);
|
||||
|
||||
ALTER TABLE chat_conversations
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER listing_id,
|
||||
ADD KEY idx_chat_conversations_distribution_site (distribution_site_id, status);
|
||||
|
||||
ALTER TABLE wallet_accounts
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER user_id,
|
||||
DROP KEY uk_wallet_accounts_user_id,
|
||||
ADD UNIQUE KEY uk_wallet_accounts_user_site (user_id, distribution_site_id);
|
||||
|
||||
ALTER TABLE wallet_ledger
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER user_id,
|
||||
ADD KEY idx_wallet_ledger_user_site (user_id, distribution_site_id);
|
||||
|
||||
ALTER TABLE withdrawal_requests
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER user_id,
|
||||
ADD KEY idx_withdrawal_user_site (user_id, distribution_site_id);
|
||||
|
||||
ALTER TABLE notifications
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER biz_id,
|
||||
ADD KEY idx_notifications_user_site (user_id, distribution_site_id, id);
|
||||
|
||||
INSERT INTO permissions (code, name, resource, action) VALUES
|
||||
('distribution:site_view', '查看分销网站', 'distribution', 'site_view'),
|
||||
('distribution:site_manage', '管理分销网站', 'distribution', 'site_manage'),
|
||||
('distribution:order_view', '查看分销订单', 'distribution', 'order_view'),
|
||||
('distribution:process_view', '查看分销订单过程', 'distribution', 'process_view'),
|
||||
('distribution:finance_view', '查看分销财务', 'distribution', 'finance_view'),
|
||||
('distribution:settlement_view', '查看分销结算', 'distribution', 'settlement_view'),
|
||||
('distribution:settlement_manage', '管理分销结算', 'distribution', 'settlement_manage'),
|
||||
('distribution:rule_view', '查看分销规则', 'distribution', 'rule_view'),
|
||||
@@ -203,14 +249,19 @@ INSERT INTO permissions (code, name, resource, action) VALUES
|
||||
('distribution:account_display', '管理分销账号展示', 'distribution', 'account_display'),
|
||||
('distribution:payment_view', '查看分销支付通道', 'distribution', 'payment_view'),
|
||||
('distribution:payment_manage', '管理分销支付通道', 'distribution', 'payment_manage'),
|
||||
('distribution:chat_view', '查看分销客服消息', 'distribution', 'chat_view'),
|
||||
('distribution:chat_send', '发送分销客服消息', 'distribution', 'chat_send')
|
||||
('distribution:all_view', '查看全部分销站点', 'distribution', 'all_view')
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name), resource = VALUES(resource), action = VALUES(action);
|
||||
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id FROM roles r CROSS JOIN permissions p
|
||||
WHERE r.code = 'super_admin' AND p.code LIKE 'distribution:%';
|
||||
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id FROM roles r CROSS JOIN permissions p
|
||||
WHERE r.code IN ('ops', 'finance')
|
||||
AND p.code LIKE 'distribution:%'
|
||||
AND p.code <> 'distribution:all_view';
|
||||
|
||||
INSERT INTO distribution_rule_versions (
|
||||
site_id, mode, version, status,
|
||||
platform_receive_ratio, platform_sell_ratio,
|
||||
@@ -219,29 +270,64 @@ INSERT INTO distribution_rule_versions (
|
||||
service_fee_value, service_fee_unit, payment_fee_bps,
|
||||
tax_enabled, tax_rate_bps, effective_from, created_at
|
||||
) VALUES
|
||||
(NULL, 'two_party_distributor_receive', 1, 'active', 0, 4, 3.5, 0, 0, 0, 2.5, 'ratio', 50, 0, 100, NOW(), NOW()),
|
||||
(NULL, 'two_party_platform_receive', 1, 'active', 3.5, 0, 0, 4, 0, 0, 2.5, 'ratio', 50, 0, 100, NOW(), NOW()),
|
||||
(NULL, 'three_party', 1, 'active', 0, 0, 0, 0, 3.5, 4, 2.5, 'ratio', 50, 0, 100, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
service_fee_value = VALUES(service_fee_value),
|
||||
service_fee_unit = VALUES(service_fee_unit),
|
||||
payment_fee_bps = VALUES(payment_fee_bps);
|
||||
(NULL, 'two_party_distributor_receive', 1, 'active', 0, 40, 35, 0, 0, 0, 25, 'percent', 50, 0, 100, NOW(), NOW()),
|
||||
(NULL, 'two_party_platform_receive', 1, 'active', 35, 0, 0, 40, 0, 0, 25, 'percent', 50, 0, 100, NOW(), NOW()),
|
||||
(NULL, 'three_party', 1, 'active', 0, 0, 0, 0, 35, 40, 25, 'percent', 50, 0, 100, NOW(), NOW());
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP TABLE IF EXISTS distribution_order_financials;
|
||||
DROP TABLE IF EXISTS distribution_site_payment_channels;
|
||||
DROP TABLE IF EXISTS distribution_settlement_items;
|
||||
DROP TABLE IF EXISTS distribution_settlement_batches;
|
||||
DROP TABLE IF EXISTS distribution_order_parties;
|
||||
ALTER TABLE notifications
|
||||
DROP KEY idx_notifications_user_site,
|
||||
DROP COLUMN distribution_site_id;
|
||||
|
||||
ALTER TABLE withdrawal_requests
|
||||
DROP KEY idx_withdrawal_user_site,
|
||||
DROP COLUMN distribution_site_id;
|
||||
|
||||
ALTER TABLE wallet_ledger
|
||||
DROP KEY idx_wallet_ledger_user_site,
|
||||
DROP COLUMN distribution_site_id;
|
||||
|
||||
DELETE FROM wallet_accounts WHERE distribution_site_id <> 0;
|
||||
|
||||
ALTER TABLE wallet_accounts
|
||||
DROP KEY uk_wallet_accounts_user_site,
|
||||
DROP COLUMN distribution_site_id,
|
||||
ADD UNIQUE KEY uk_wallet_accounts_user_id (user_id);
|
||||
|
||||
ALTER TABLE chat_conversations
|
||||
DROP KEY idx_chat_conversations_distribution_site,
|
||||
DROP COLUMN distribution_site_id;
|
||||
|
||||
ALTER TABLE chat_qrcode_pool
|
||||
DROP FOREIGN KEY fk_chat_qrcode_distribution_site,
|
||||
DROP KEY idx_chat_qrcode_distribution_site,
|
||||
DROP COLUMN distribution_site_id;
|
||||
|
||||
ALTER TABLE rental_listings
|
||||
DROP KEY idx_rental_listings_distribution_site,
|
||||
DROP COLUMN source_channel,
|
||||
DROP COLUMN distribution_site_id;
|
||||
|
||||
ALTER TABLE rental_orders
|
||||
DROP FOREIGN KEY fk_rental_orders_distribution_rule,
|
||||
DROP FOREIGN KEY fk_rental_orders_distribution_site,
|
||||
DROP KEY idx_rental_orders_distribution_site,
|
||||
DROP KEY idx_rental_orders_markup_site,
|
||||
DROP COLUMN distribution_rule_version_id,
|
||||
DROP COLUMN distribution_mode,
|
||||
DROP COLUMN distribution_site_id;
|
||||
DROP COLUMN distribution_site_id,
|
||||
DROP COLUMN shelf_ratio_delta,
|
||||
DROP COLUMN shelf_ratio,
|
||||
DROP COLUMN markup_site_id,
|
||||
DROP COLUMN shelf_markup_cent,
|
||||
DROP COLUMN base_price_cent;
|
||||
|
||||
DROP TABLE IF EXISTS distribution_settlement_items;
|
||||
DROP TABLE IF EXISTS distribution_settlement_batches;
|
||||
DROP TABLE IF EXISTS distribution_order_financials;
|
||||
DROP TABLE IF EXISTS distribution_order_parties;
|
||||
DROP TABLE IF EXISTS distribution_site_payment_channels;
|
||||
DROP TABLE IF EXISTS distribution_rule_versions;
|
||||
DROP TABLE IF EXISTS distribution_site_accounts;
|
||||
DROP TABLE IF EXISTS distribution_site_members;
|
||||
@@ -251,9 +337,4 @@ DELETE rp FROM role_permissions rp
|
||||
JOIN permissions p ON p.id = rp.permission_id
|
||||
WHERE p.code LIKE 'distribution:%';
|
||||
|
||||
DELETE FROM permissions WHERE code IN (
|
||||
'distribution:site_view', 'distribution:site_manage', 'distribution:order_view', 'distribution:process_view',
|
||||
'distribution:finance_view', 'distribution:settlement_view', 'distribution:settlement_manage',
|
||||
'distribution:rule_view', 'distribution:rule_manage', 'distribution:account_view', 'distribution:account_display',
|
||||
'distribution:payment_view', 'distribution:payment_manage', 'distribution:chat_view', 'distribution:chat_send'
|
||||
);
|
||||
DELETE FROM permissions WHERE code LIKE 'distribution:%';
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE chat_qrcode_pool
|
||||
ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER group_name,
|
||||
ADD KEY idx_chat_qrcode_distribution_site (distribution_site_id, status),
|
||||
ADD CONSTRAINT fk_chat_qrcode_distribution_site FOREIGN KEY (distribution_site_id) REFERENCES distribution_sites (id);
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE chat_qrcode_pool
|
||||
DROP FOREIGN KEY fk_chat_qrcode_distribution_site,
|
||||
DROP KEY idx_chat_qrcode_distribution_site,
|
||||
DROP COLUMN distribution_site_id;
|
||||
@@ -1,34 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 分销后台权限:站点、规则、订单、账号展示、支付通道、结算批次。
|
||||
INSERT INTO permissions (code, name, resource, action) VALUES
|
||||
('distribution:site_view', '查看分销网站', 'distribution_site', 'view'),
|
||||
('distribution:site_manage', '管理分销网站', 'distribution_site', 'manage'),
|
||||
('distribution:rule_view', '查看分销规则', 'distribution_rule', 'view'),
|
||||
('distribution:rule_manage', '管理分销规则', 'distribution_rule', 'manage'),
|
||||
('distribution:order_view', '查看分销订单', 'distribution_order', 'view'),
|
||||
('distribution:account_view', '查看分销账号展示', 'distribution_account', 'view'),
|
||||
('distribution:account_display', '管理分销账号展示', 'distribution_account', 'display'),
|
||||
('distribution:payment_view', '查看分销支付通道', 'distribution_payment', 'view'),
|
||||
('distribution:payment_manage', '管理分销支付通道', 'distribution_payment', 'manage'),
|
||||
('distribution:settlement_view', '查看分销结算', 'distribution_settlement', 'view'),
|
||||
('distribution:settlement_manage', '管理分销结算', 'distribution_settlement', 'manage')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
resource = VALUES(resource),
|
||||
action = VALUES(action);
|
||||
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.code LIKE 'distribution:%'
|
||||
WHERE r.code IN ('super_admin', 'ops', 'finance');
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DELETE rp
|
||||
FROM role_permissions rp
|
||||
JOIN permissions p ON p.id = rp.permission_id
|
||||
WHERE p.code LIKE 'distribution:%';
|
||||
|
||||
DELETE FROM permissions WHERE code LIKE 'distribution:%';
|
||||
+4
-3
@@ -1,14 +1,15 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 私有文件上传归属:在业务记录写入前,仅允许上传者预览自己的临时文件。
|
||||
CREATE TABLE IF NOT EXISTS file_upload_owners (
|
||||
-- 私有文件上传归属:业务记录写入前,仅允许上传者预览自己的临时文件。
|
||||
CREATE TABLE file_upload_owners (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
object_key VARCHAR(512) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_file_upload_owners_object_key (object_key),
|
||||
KEY idx_file_upload_owners_user_id (user_id)
|
||||
KEY idx_file_upload_owners_user_id (user_id),
|
||||
KEY idx_file_upload_owners_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私有文件上传归属';
|
||||
|
||||
-- +goose Down
|
||||
@@ -1,21 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 为长期未关联上传归属记录的定时清理提供索引。
|
||||
-- 兼容历史上已按旧编号执行过该索引的数据库,重复执行时跳过。
|
||||
SET @idx_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'file_upload_owners'
|
||||
AND index_name = 'idx_file_upload_owners_created_at'
|
||||
);
|
||||
SET @ddl := IF(@idx_exists > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE file_upload_owners ADD KEY idx_file_upload_owners_created_at (created_at)');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE file_upload_owners
|
||||
DROP INDEX idx_file_upload_owners_created_at;
|
||||
@@ -1,178 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 1. chat_conversations 表新增 distribution_site_id 与索引
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'chat_conversations' AND COLUMN_NAME = 'distribution_site_id') = 0,
|
||||
'ALTER TABLE chat_conversations ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER listing_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'chat_conversations' AND INDEX_NAME = 'idx_chat_conversations_distribution_site') = 0,
|
||||
'ALTER TABLE chat_conversations ADD KEY idx_chat_conversations_distribution_site (distribution_site_id, status)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. wallet_accounts 表新增 distribution_site_id 并将唯一键变更为 (user_id, distribution_site_id)
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_accounts' AND COLUMN_NAME = 'distribution_site_id') = 0,
|
||||
'ALTER TABLE wallet_accounts ADD COLUMN distribution_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER user_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_accounts' AND INDEX_NAME = 'uk_wallet_accounts_user_id') > 0,
|
||||
'ALTER TABLE wallet_accounts DROP KEY uk_wallet_accounts_user_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_accounts' AND INDEX_NAME = 'uk_wallet_accounts_user_site') = 1,
|
||||
'ALTER TABLE wallet_accounts DROP KEY uk_wallet_accounts_user_site',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_accounts' AND INDEX_NAME = 'uk_wallet_accounts_user_site') = 0,
|
||||
'ALTER TABLE wallet_accounts ADD UNIQUE KEY uk_wallet_accounts_user_site (user_id, distribution_site_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 3. wallet_ledger 表新增 distribution_site_id 列与联合索引
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_ledger' AND COLUMN_NAME = 'distribution_site_id') = 0,
|
||||
'ALTER TABLE wallet_ledger ADD COLUMN distribution_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER user_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_ledger' AND INDEX_NAME = 'idx_wallet_ledger_user_site') = 0,
|
||||
'ALTER TABLE wallet_ledger ADD KEY idx_wallet_ledger_user_site (user_id, distribution_site_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 4. withdrawal_requests 表新增 distribution_site_id 列与联合索引
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'withdrawal_requests' AND COLUMN_NAME = 'distribution_site_id') = 0,
|
||||
'ALTER TABLE withdrawal_requests ADD COLUMN distribution_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER user_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'withdrawal_requests' AND INDEX_NAME = 'idx_withdrawal_user_site') = 0,
|
||||
'ALTER TABLE withdrawal_requests ADD KEY idx_withdrawal_user_site (user_id, distribution_site_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'withdrawal_requests' AND INDEX_NAME = 'idx_withdrawal_user_site') > 0,
|
||||
'ALTER TABLE withdrawal_requests DROP KEY idx_withdrawal_user_site',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'withdrawal_requests' AND COLUMN_NAME = 'distribution_site_id') > 0,
|
||||
'ALTER TABLE withdrawal_requests DROP COLUMN distribution_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_ledger' AND INDEX_NAME = 'idx_wallet_ledger_user_site') > 0,
|
||||
'ALTER TABLE wallet_ledger DROP KEY idx_wallet_ledger_user_site',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_ledger' AND COLUMN_NAME = 'distribution_site_id') > 0,
|
||||
'ALTER TABLE wallet_ledger DROP COLUMN distribution_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_accounts' AND INDEX_NAME = 'uk_wallet_accounts_user_site') > 0,
|
||||
'ALTER TABLE wallet_accounts DROP KEY uk_wallet_accounts_user_site',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_accounts' AND INDEX_NAME = 'uk_wallet_accounts_user_id') = 0,
|
||||
'ALTER TABLE wallet_accounts ADD UNIQUE KEY uk_wallet_accounts_user_id (user_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_accounts' AND COLUMN_NAME = 'distribution_site_id') > 0,
|
||||
'ALTER TABLE wallet_accounts DROP COLUMN distribution_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'chat_conversations' AND INDEX_NAME = 'idx_chat_conversations_distribution_site') > 0,
|
||||
'ALTER TABLE chat_conversations DROP KEY idx_chat_conversations_distribution_site',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'chat_conversations' AND COLUMN_NAME = 'distribution_site_id') > 0,
|
||||
'ALTER TABLE chat_conversations DROP COLUMN distribution_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,46 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 1. notifications 表新增 distribution_site_id 并添加联合索引
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'notifications' AND COLUMN_NAME = 'distribution_site_id') = 0,
|
||||
'ALTER TABLE notifications ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER biz_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'notifications' AND INDEX_NAME = 'idx_notifications_user_site') = 0,
|
||||
'ALTER TABLE notifications ADD KEY idx_notifications_user_site (user_id, distribution_site_id, id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. 回填历史分销订单的站内信通知归属
|
||||
UPDATE notifications n
|
||||
JOIN rental_orders o ON n.biz_id = o.id
|
||||
SET n.distribution_site_id = o.distribution_site_id
|
||||
WHERE n.biz_type = 'order' AND o.distribution_site_id IS NOT NULL AND o.distribution_site_id > 0;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'notifications' AND INDEX_NAME = 'idx_notifications_user_site') > 0,
|
||||
'ALTER TABLE notifications DROP KEY idx_notifications_user_site',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'notifications' AND COLUMN_NAME = 'distribution_site_id') > 0,
|
||||
'ALTER TABLE notifications DROP COLUMN distribution_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,46 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 1. rental_listings 表新增 distribution_site_id 并添加索引
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND COLUMN_NAME = 'distribution_site_id') = 0,
|
||||
'ALTER TABLE rental_listings ADD COLUMN distribution_site_id BIGINT UNSIGNED NULL AFTER managed_admin_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND INDEX_NAME = 'idx_rental_listings_distribution_site') = 0,
|
||||
'ALTER TABLE rental_listings ADD KEY idx_rental_listings_distribution_site (distribution_site_id, status)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. 回填历史分销账号的归属站点
|
||||
UPDATE rental_listings l
|
||||
JOIN distribution_site_accounts dsa ON dsa.listing_id = l.id
|
||||
SET l.distribution_site_id = dsa.site_id
|
||||
WHERE l.distribution_site_id IS NULL;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND INDEX_NAME = 'idx_rental_listings_distribution_site') > 0,
|
||||
'ALTER TABLE rental_listings DROP KEY idx_rental_listings_distribution_site',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND COLUMN_NAME = 'distribution_site_id') > 0,
|
||||
'ALTER TABLE rental_listings DROP COLUMN distribution_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,11 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 修复 000072 中误将展示货架 (distribution_site_accounts) 历史商品回填为分销站点来源的问题
|
||||
-- 历史主站商品(在分销上传功能上线前创建的商品)来源应保持 distribution_site_id = NULL
|
||||
UPDATE rental_listings l
|
||||
JOIN distribution_site_accounts dsa ON dsa.listing_id = l.id
|
||||
SET l.distribution_site_id = NULL
|
||||
WHERE l.created_at < '2026-09-08 14:00:00';
|
||||
|
||||
-- +goose Down
|
||||
SELECT 1;
|
||||
@@ -1,113 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 1. distribution_sites 表增加品牌形象与前台装修字段
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'logo_url') = 0,
|
||||
'ALTER TABLE distribution_sites ADD COLUMN logo_url VARCHAR(512) NOT NULL DEFAULT \'\' AFTER display_account_enabled',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'favicon_url') = 0,
|
||||
'ALTER TABLE distribution_sites ADD COLUMN favicon_url VARCHAR(512) NOT NULL DEFAULT \'\' AFTER logo_url',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'banners_json') = 0,
|
||||
'ALTER TABLE distribution_sites ADD COLUMN banners_json TEXT NULL AFTER favicon_url',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'announcements_json') = 0,
|
||||
'ALTER TABLE distribution_sites ADD COLUMN announcements_json TEXT NULL AFTER banners_json',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'footer_info_json') = 0,
|
||||
'ALTER TABLE distribution_sites ADD COLUMN footer_info_json TEXT NULL AFTER announcements_json',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. distribution_site_accounts 增加自主加价字段
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_site_accounts' AND COLUMN_NAME = 'markup_amount_cent') = 0,
|
||||
'ALTER TABLE distribution_site_accounts ADD COLUMN markup_amount_cent BIGINT NOT NULL DEFAULT 0 AFTER display_status',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_site_accounts' AND COLUMN_NAME = 'markup_amount_cent') > 0,
|
||||
'ALTER TABLE distribution_site_accounts DROP COLUMN markup_amount_cent',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'footer_info_json') > 0,
|
||||
'ALTER TABLE distribution_sites DROP COLUMN footer_info_json',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'announcements_json') > 0,
|
||||
'ALTER TABLE distribution_sites DROP COLUMN announcements_json',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'banners_json') > 0,
|
||||
'ALTER TABLE distribution_sites DROP COLUMN banners_json',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'favicon_url') > 0,
|
||||
'ALTER TABLE distribution_sites DROP COLUMN favicon_url',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'distribution_sites' AND COLUMN_NAME = 'logo_url') > 0,
|
||||
'ALTER TABLE distribution_sites DROP COLUMN logo_url',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,58 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 下单时冻结货架加价,后续改价不影响历史订单。
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_orders' AND COLUMN_NAME = 'base_price_cent') = 0,
|
||||
'ALTER TABLE rental_orders ADD COLUMN base_price_cent BIGINT NOT NULL DEFAULT 0 AFTER platform_fee_cent',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_orders' AND COLUMN_NAME = 'shelf_markup_cent') = 0,
|
||||
'ALTER TABLE rental_orders ADD COLUMN shelf_markup_cent BIGINT NOT NULL DEFAULT 0 AFTER base_price_cent',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_orders' AND COLUMN_NAME = 'markup_site_id') = 0,
|
||||
'ALTER TABLE rental_orders ADD COLUMN markup_site_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER shelf_markup_cent',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_orders' AND COLUMN_NAME = 'markup_site_id') > 0,
|
||||
'ALTER TABLE rental_orders DROP COLUMN markup_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_orders' AND COLUMN_NAME = 'shelf_markup_cent') > 0,
|
||||
'ALTER TABLE rental_orders DROP COLUMN shelf_markup_cent',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_orders' AND COLUMN_NAME = 'base_price_cent') > 0,
|
||||
'ALTER TABLE rental_orders DROP COLUMN base_price_cent',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,71 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 补偿 000072 / 000073:货架展示不等于产地,禁止用绝对时间回滚。
|
||||
-- 本站上传特征:无 listing_uploads(外部导入走该表,source_channel=咸鱼/淘宝等),
|
||||
-- 且 distribution_site_accounts 与商品在同一发布事务内写入(created_at 相差不超过 5 秒)。
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND COLUMN_NAME = 'source_channel') = 0,
|
||||
'ALTER TABLE rental_listings ADD COLUMN source_channel VARCHAR(64) NOT NULL DEFAULT '''' AFTER distribution_site_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 1. 恢复被 000073 时间戳误清空的本站上传归属
|
||||
UPDATE rental_listings l
|
||||
INNER JOIN (
|
||||
SELECT dsa.listing_id, MIN(dsa.site_id) AS site_id
|
||||
FROM distribution_site_accounts dsa
|
||||
INNER JOIN rental_listings l2 ON l2.id = dsa.listing_id
|
||||
LEFT JOIN listing_uploads lu ON lu.listing_id = dsa.listing_id
|
||||
WHERE lu.id IS NULL
|
||||
AND TIMESTAMPDIFF(SECOND, l2.created_at, dsa.created_at) BETWEEN 0 AND 5
|
||||
GROUP BY dsa.listing_id
|
||||
) origin ON origin.listing_id = l.id
|
||||
SET l.distribution_site_id = origin.site_id
|
||||
WHERE l.distribution_site_id IS NULL OR l.distribution_site_id = 0;
|
||||
|
||||
-- 2. 清掉仅因货架回填产生的归属(外部导入,或 DSA 明显晚于商品创建)
|
||||
UPDATE rental_listings l
|
||||
LEFT JOIN listing_uploads lu ON lu.listing_id = l.id
|
||||
LEFT JOIN distribution_site_accounts origin_dsa
|
||||
ON origin_dsa.listing_id = l.id
|
||||
AND origin_dsa.site_id = l.distribution_site_id
|
||||
AND TIMESTAMPDIFF(SECOND, l.created_at, origin_dsa.created_at) BETWEEN 0 AND 5
|
||||
SET l.distribution_site_id = NULL
|
||||
WHERE l.distribution_site_id IS NOT NULL
|
||||
AND l.distribution_site_id <> 0
|
||||
AND (lu.id IS NOT NULL OR origin_dsa.id IS NULL);
|
||||
|
||||
-- 3. 回填来源渠道快照:分销站用 site_code,外部导入用 listing_uploads.source_channel
|
||||
UPDATE rental_listings l
|
||||
INNER JOIN distribution_sites s ON s.id = l.distribution_site_id
|
||||
SET l.source_channel = s.site_code
|
||||
WHERE l.distribution_site_id IS NOT NULL
|
||||
AND l.distribution_site_id <> 0
|
||||
AND l.source_channel = '';
|
||||
|
||||
UPDATE rental_listings l
|
||||
INNER JOIN (
|
||||
SELECT lu.listing_id, MAX(NULLIF(lu.source_channel, '')) AS source_channel
|
||||
FROM listing_uploads lu
|
||||
WHERE lu.listing_id IS NOT NULL
|
||||
GROUP BY lu.listing_id
|
||||
) upload ON upload.listing_id = l.id
|
||||
SET l.source_channel = COALESCE(upload.source_channel, '')
|
||||
WHERE (l.distribution_site_id IS NULL OR l.distribution_site_id = 0)
|
||||
AND l.source_channel = ''
|
||||
AND COALESCE(upload.source_channel, '') <> '';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'rental_listings' AND COLUMN_NAME = 'source_channel') > 0,
|
||||
'ALTER TABLE rental_listings DROP COLUMN source_channel',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -1,24 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- 跨站点查看/管理分销数据的显式权限。无站点成员不再视为超管。
|
||||
INSERT INTO permissions (code, name, resource, action) VALUES
|
||||
('distribution:all_view', '查看全部分销站点', 'distribution', 'all_view')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
resource = VALUES(resource),
|
||||
action = VALUES(action);
|
||||
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.code = 'distribution:all_view'
|
||||
WHERE r.code = 'super_admin';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DELETE rp
|
||||
FROM role_permissions rp
|
||||
JOIN permissions p ON p.id = rp.permission_id
|
||||
WHERE p.code = 'distribution:all_view';
|
||||
|
||||
DELETE FROM permissions WHERE code = 'distribution:all_view';
|
||||
Reference in New Issue
Block a user