fix(distribution): 货架加价打通前台、下单与卖方分成
分站公开列表和详情展示当前货架加价;下单写入 base_price / shelf_markup / markup_site_id 快照并计入实付。 支付分账改用快照加价,不再误用 PlatformFeeCent;结账不退加价,事后改价不影响历史订单。
This commit is contained in:
@@ -35,6 +35,9 @@ type OrderDTO struct {
|
||||
DepositFreeManualQuotaCent int64 `json:"deposit_free_manual_quota_cent,omitempty"`
|
||||
DepositFreeUsedBeforeCent int64 `json:"deposit_free_used_before_cent,omitempty"`
|
||||
PlatformFeeCent *int64 `json:"platform_fee_cent,omitempty"`
|
||||
BasePriceCent int64 `json:"base_price_cent,omitempty"`
|
||||
ShelfMarkupCent int64 `json:"shelf_markup_cent,omitempty"`
|
||||
MarkupSiteID uint64 `json:"markup_site_id,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"`
|
||||
|
||||
@@ -19,6 +19,9 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
response.BadRequest(c, "订单信息不完整")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.DistributionSiteCode) == "" {
|
||||
req.DistributionSiteCode = strings.TrimSpace(c.GetHeader("X-Distribution-Site-Code"))
|
||||
}
|
||||
item, err := h.service.Create(c.Request.Context(), userID, req)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -57,7 +58,15 @@ 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pricing := buildOrderPricing(listing, account)
|
||||
basePriceCent := listing.PriceCent
|
||||
if shelfMarkupCent > 0 {
|
||||
pricing.RentAmountCent += shelfMarkupCent
|
||||
}
|
||||
distributionSite, distributionRule, err := distribution.ResolveOrderSourceTx(tx, req.DistributionSiteCode, listing.ID, account.ID, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -104,6 +113,9 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
DepositFreeManualQuotaCent: growthSnapshot.ManualDepositFreeQuotaCent,
|
||||
DepositFreeUsedBeforeCent: depositWaiver.UsedBeforeCent,
|
||||
PlatformFeeCent: pricing.PlatformFeeCent,
|
||||
BasePriceCent: basePriceCent,
|
||||
ShelfMarkupCent: shelfMarkupCent,
|
||||
MarkupSiteID: markupSiteID,
|
||||
RentOriginalAmountCent: rentOriginalAmountCent,
|
||||
RentDiscountAmountCent: rentDiscountAmountCent,
|
||||
PureCoinOriginalAmountCent: pricing.PureCoinAmountCent,
|
||||
@@ -156,6 +168,32 @@ 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) {
|
||||
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"`
|
||||
}
|
||||
err := tx.Table("distribution_site_accounts AS dsa").
|
||||
Select("dsa.site_id, COALESCE(dsa.markup_amount_cent, 0) AS markup").
|
||||
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
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
return 0, 0, err
|
||||
}
|
||||
if row.Markup < 0 {
|
||||
row.Markup = 0
|
||||
}
|
||||
return row.Markup, row.SiteID, nil
|
||||
}
|
||||
|
||||
func orderAccountSourceSnapshot(tx *gorm.DB, listingID uint64) (string, string, error) {
|
||||
var upload model.ListingUpload
|
||||
err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error
|
||||
|
||||
@@ -92,6 +92,9 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
DepositFreeManualQuotaCent: row.DepositFreeManualQuotaCent,
|
||||
DepositFreeUsedBeforeCent: row.DepositFreeUsedBeforeCent,
|
||||
PlatformFeeCent: &platformFeeCent,
|
||||
BasePriceCent: row.BasePriceCent,
|
||||
ShelfMarkupCent: row.ShelfMarkupCent,
|
||||
MarkupSiteID: row.MarkupSiteID,
|
||||
RentOriginalAmountCent: effectiveRentOriginalAmountCent(row.RentalOrder),
|
||||
RentDiscountAmountCent: row.RentDiscountAmountCent,
|
||||
PureCoinOriginalAmountCent: orderPureCoinOriginalAmountCent(row.RentalOrder),
|
||||
|
||||
@@ -290,8 +290,19 @@ func effectiveCheckoutDepositDeductCent(checkout model.OrderCheckout) int64 {
|
||||
return maxCent(checkout.OtherAmountCent, 0)
|
||||
}
|
||||
|
||||
func orderConsumptionPrepaidCent(order model.RentalOrder) int64 {
|
||||
prepaid := order.RentAmountCent
|
||||
if order.ShelfMarkupCent > 0 {
|
||||
prepaid -= order.ShelfMarkupCent
|
||||
if prepaid < 0 {
|
||||
prepaid = 0
|
||||
}
|
||||
}
|
||||
return prepaid
|
||||
}
|
||||
|
||||
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
|
||||
orderRentAmountCent := order.RentAmountCent
|
||||
orderRentAmountCent := orderConsumptionPrepaidCent(order)
|
||||
orderOwnerRentAmountCent := order.OwnerRentAmountCent
|
||||
orderDepositAmountCent := order.DepositAmountCent
|
||||
|
||||
|
||||
@@ -277,6 +277,99 @@ func TestRepositoryCreateAppliesRenterGrowthDiscount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrderSnapshotsShelfMarkup(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
if err := db.AutoMigrate(&model.DistributionSite{}, &model.DistributionSiteAccount{}, &model.DistributionRuleVersion{}, &model.DistributionOrderFinancial{}, &model.DistributionOrderParty{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo := NewRepository(db)
|
||||
now := time.Now()
|
||||
|
||||
owner := model.User{Phone: "13800002101"}
|
||||
renter := model.User{Phone: "13800002102"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account := model.GameAccount{
|
||||
OwnerID: owner.ID,
|
||||
ServerRegion: "国服",
|
||||
LoginPlatform: "steam",
|
||||
Title: "加价账号",
|
||||
AssetSummary: datatypes.JSON([]byte(`{
|
||||
"price_breakdown": {
|
||||
"seller_total_price": 80,
|
||||
"platform_markup_amount": 20
|
||||
}
|
||||
}`)),
|
||||
}
|
||||
if err := db.Create(&account).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listing := model.RentalListing{
|
||||
ListingNo: "L-MARKUP-1",
|
||||
AccountID: account.ID,
|
||||
OwnerID: owner.ID,
|
||||
PriceCent: 10000,
|
||||
DepositAmountCent: 1000,
|
||||
Status: listingStatusPublished,
|
||||
ReviewStatus: listingReviewStatusApproved,
|
||||
}
|
||||
if err := db.Create(&listing).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
site := model.DistributionSite{SiteCode: "markup_site", SiteName: "加价站", Domain: "markup.example.com", Status: "active", DisplayAccountEnabled: true}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&model.DistributionSiteAccount{
|
||||
SiteID: site.ID, AccountID: account.ID, ListingID: listing.ID, DisplayStatus: "enabled", MarkupAmountCent: 10000,
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rule := model.DistributionRuleVersion{
|
||||
SiteID: &site.ID, Mode: "two_party_platform_receive", Version: 1, Status: "active",
|
||||
PlatformReceiveRatio: 3.5, DistributorSellRatio: 4,
|
||||
ServiceFeeValue: 0, ServiceFeeUnit: "fixed_amount", PaymentFeeBPS: 50,
|
||||
EffectiveFrom: now.Add(-time.Hour),
|
||||
}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dto, err := repo.Create(t.Context(), renter.ID, CreateRequest{ListingID: listing.ID, DistributionSiteCode: "markup_site"})
|
||||
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.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)
|
||||
}
|
||||
|
||||
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 err := db.Model(&model.DistributionSiteAccount{}).Where("listing_id = ?", listing.ID).Update("markup_amount_cent", 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)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildOrderPricing 测试订单定价计算
|
||||
func TestBuildOrderPricingBasic(t *testing.T) {
|
||||
listing := model.RentalListing{
|
||||
|
||||
@@ -118,6 +118,35 @@ func TestBuildOrderPricingDoesNotDiscountAllExtraItemOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutDoesNotRefundShelfMarkup(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
RentAmountCent: 20000,
|
||||
OwnerRentAmountCent: 8000,
|
||||
PlatformFeeCent: 2000,
|
||||
BasePriceCent: 10000,
|
||||
ShelfMarkupCent: 10000,
|
||||
DepositAmountCent: 1000,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"asset_summary": {
|
||||
"price_breakdown": {
|
||||
"buyer_coin_base_price": 100,
|
||||
"seller_coin_base_price": 80,
|
||||
"consumable_price": 0
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}
|
||||
settlement := calculateCheckoutSettlement(order, 0, 100, 0)
|
||||
if settlement.RentRefundCent != 0 {
|
||||
t.Fatalf("full consume must not refund shelf markup, 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
RentAmountCent: 38300,
|
||||
@@ -527,4 +556,3 @@ func TestApplyAdminOrderFiltersSmartRouting(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user