fix(distribution): 货架加价打通前台、下单与卖方分成
分站公开列表和详情展示当前货架加价;下单写入 base_price / shelf_markup / markup_site_id 快照并计入实付。 支付分账改用快照加价,不再误用 PlatformFeeCent;结账不退加价,事后改价不影响历史订单。
This commit is contained in:
@@ -26,6 +26,9 @@ type RentalOrder struct {
|
|||||||
DepositFreeManualQuotaCent int64 `gorm:"not null;default:0" json:"deposit_free_manual_quota_cent"`
|
DepositFreeManualQuotaCent int64 `gorm:"not null;default:0" json:"deposit_free_manual_quota_cent"`
|
||||||
DepositFreeUsedBeforeCent int64 `gorm:"not null;default:0" json:"deposit_free_used_before_cent"`
|
DepositFreeUsedBeforeCent int64 `gorm:"not null;default:0" json:"deposit_free_used_before_cent"`
|
||||||
PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"`
|
PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
|
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"`
|
||||||
RentOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
RentOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
RentDiscountAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
RentDiscountAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
PureCoinOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
PureCoinOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
|
|||||||
@@ -186,7 +186,10 @@ func SnapshotPaidOrderTx(tx *gorm.DB, orderID uint64, grossPaidCent int64) error
|
|||||||
if err := tx.First(&rule, *order.DistributionRuleVersionID).Error; err != nil {
|
if err := tx.First(&rule, *order.DistributionRuleVersionID).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
markup := order.PlatformFeeCent
|
markup := order.ShelfMarkupCent
|
||||||
|
if markup < 0 {
|
||||||
|
markup = 0
|
||||||
|
}
|
||||||
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: grossPaidCent, MarkupAmountCent: markup})
|
result, err := calculateForRule(rule, SettlementInput{GrossPaidAmountCent: grossPaidCent, MarkupAmountCent: markup})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -279,7 +279,8 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
|||||||
OwnerID: 1,
|
OwnerID: 1,
|
||||||
RenterID: 2,
|
RenterID: 2,
|
||||||
Status: "renting",
|
Status: "renting",
|
||||||
PlatformFeeCent: 300,
|
PlatformFeeCent: 999,
|
||||||
|
ShelfMarkupCent: 300,
|
||||||
DistributionSiteID: &site.ID,
|
DistributionSiteID: &site.ID,
|
||||||
DistributionRuleVersionID: &rule.ID,
|
DistributionRuleVersionID: &rule.ID,
|
||||||
}
|
}
|
||||||
@@ -300,6 +301,9 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
|||||||
if financial.GrossPaidAmountCent != grossPaid || financial.SiteID != site.ID {
|
if financial.GrossPaidAmountCent != grossPaid || financial.SiteID != site.ID {
|
||||||
t.Fatalf("unexpected financial snapshot: %+v", financial)
|
t.Fatalf("unexpected financial snapshot: %+v", financial)
|
||||||
}
|
}
|
||||||
|
if financial.MarkupAmountCent != 300 {
|
||||||
|
t.Fatalf("markup must come from shelf snapshot, got %d", financial.MarkupAmountCent)
|
||||||
|
}
|
||||||
|
|
||||||
var parties []model.DistributionOrderParty
|
var parties []model.DistributionOrderParty
|
||||||
if err := db.Where("order_id = ?", order.ID).Order("id ASC").Find(&parties).Error; err != nil || len(parties) != 2 {
|
if err := db.Where("order_id = ?", order.ID).Order("id ASC").Find(&parties).Error; err != nil || len(parties) != 2 {
|
||||||
@@ -347,3 +351,64 @@ func TestSnapshotPaidOrderTxAndApplyRefundTx(t *testing.T) {
|
|||||||
t.Fatalf("expected net paid %d, got %d", grossPaid-refundAmount, updatedFinancial.NetPaidAmountCent)
|
t.Fatalf("expected net paid %d, got %d", grossPaid-refundAmount, updatedFinancial.NetPaidAmountCent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSnapshotPaidOrderTxAddsShelfMarkupToSeller(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}
|
||||||
|
if err := db.Create(&site).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rule := model.DistributionRuleVersion{
|
||||||
|
SiteID: &site.ID, Mode: ModeTwoPartyPlatformReceive, Version: 1, Status: "active",
|
||||||
|
PlatformReceiveRatio: 3.5, DistributorSellRatio: 4,
|
||||||
|
ServiceFeeValue: 0, ServiceFeeUnit: ServiceFeeUnitFixedAmount, PaymentFeeBPS: 50,
|
||||||
|
EffectiveFrom: now.Add(-time.Hour),
|
||||||
|
}
|
||||||
|
if err := db.Create(&rule).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
order := model.RentalOrder{
|
||||||
|
OrderNo: "ORD-MARKUP-SELL", ListingID: 1, AccountID: 2, OwnerID: 3, RenterID: 4, Status: "renting",
|
||||||
|
PlatformFeeCent: 2000, ShelfMarkupCent: 10000,
|
||||||
|
DistributionSiteID: &site.ID, DistributionRuleVersionID: &rule.ID, DistributionMode: ModeTwoPartyPlatformReceive,
|
||||||
|
}
|
||||||
|
if err := db.Create(&order).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := SnapshotPaidOrderTx(db, order.ID, 20000); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var parties []model.DistributionOrderParty
|
||||||
|
if err := db.Where("order_id = ?", order.ID).Find(&parties).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var seller, platform *model.DistributionOrderParty
|
||||||
|
for i := range parties {
|
||||||
|
switch parties[i].PartyRole {
|
||||||
|
case "distributor_sell":
|
||||||
|
seller = &parties[i]
|
||||||
|
case "platform_receive":
|
||||||
|
platform = &parties[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,38 +88,38 @@ func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) {
|
|||||||
|
|
||||||
func parsePublicListQuery(c *gin.Context) PublicListQuery {
|
func parsePublicListQuery(c *gin.Context) PublicListQuery {
|
||||||
query := PublicListQuery{
|
query := PublicListQuery{
|
||||||
Page: parsePositiveInt(c.DefaultQuery("page", "1"), 1),
|
Page: parsePositiveInt(c.DefaultQuery("page", "1"), 1),
|
||||||
PageSize: parsePositiveInt(c.DefaultQuery("page_size", "20"), 20),
|
PageSize: parsePositiveInt(c.DefaultQuery("page_size", "20"), 20),
|
||||||
Keyword: strings.TrimSpace(c.Query("keyword")),
|
Keyword: strings.TrimSpace(c.Query("keyword")),
|
||||||
Sort: strings.TrimSpace(c.Query("sort")),
|
Sort: strings.TrimSpace(c.Query("sort")),
|
||||||
Zone: strings.TrimSpace(c.Query("zone")),
|
Zone: strings.TrimSpace(c.Query("zone")),
|
||||||
Server: parseCSVQuery(c.Query("server")),
|
Server: parseCSVQuery(c.Query("server")),
|
||||||
Region: parseCSVQuery(c.Query("region")),
|
Region: parseCSVQuery(c.Query("region")),
|
||||||
LoginMethod: parseCSVQuery(firstNonEmpty(c.Query("login_method"), c.Query("login"))),
|
LoginMethod: parseCSVQuery(firstNonEmpty(c.Query("login_method"), c.Query("login"))),
|
||||||
Rank: parseCSVQuery(c.Query("rank")),
|
Rank: parseCSVQuery(c.Query("rank")),
|
||||||
Insurance: parseCSVQuery(c.Query("insurance")),
|
Insurance: parseCSVQuery(c.Query("insurance")),
|
||||||
Stamina: parseCSVQuery(c.Query("stamina")),
|
Stamina: parseCSVQuery(c.Query("stamina")),
|
||||||
Load: parseCSVQuery(c.Query("load")),
|
Load: parseCSVQuery(c.Query("load")),
|
||||||
SkinGroup: parseCSVQuery(c.Query("skin_group")),
|
SkinGroup: parseCSVQuery(c.Query("skin_group")),
|
||||||
SkinName: parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))),
|
SkinName: parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))),
|
||||||
MinCoin: parseOptionalFloat(c.Query("min_coin")),
|
MinCoin: parseOptionalFloat(c.Query("min_coin")),
|
||||||
MaxCoin: parseOptionalFloat(c.Query("max_coin")),
|
MaxCoin: parseOptionalFloat(c.Query("max_coin")),
|
||||||
MinRatio: parseOptionalFloat(c.Query("min_ratio")),
|
MinRatio: parseOptionalFloat(c.Query("min_ratio")),
|
||||||
MaxRatio: parseOptionalFloat(c.Query("max_ratio")),
|
MaxRatio: parseOptionalFloat(c.Query("max_ratio")),
|
||||||
MinPrice: parseOptionalFloat(c.Query("min_price")),
|
MinPrice: parseOptionalFloat(c.Query("min_price")),
|
||||||
MaxPrice: parseOptionalFloat(c.Query("max_price")),
|
MaxPrice: parseOptionalFloat(c.Query("max_price")),
|
||||||
MinDeposit: parseOptionalFloat(c.Query("min_deposit")),
|
MinDeposit: parseOptionalFloat(c.Query("min_deposit")),
|
||||||
MaxDeposit: parseOptionalFloat(c.Query("max_deposit")),
|
MaxDeposit: parseOptionalFloat(c.Query("max_deposit")),
|
||||||
MinTotal: parseOptionalFloat(c.Query("min_total")),
|
MinTotal: parseOptionalFloat(c.Query("min_total")),
|
||||||
MaxTotal: parseOptionalFloat(c.Query("max_total")),
|
MaxTotal: parseOptionalFloat(c.Query("max_total")),
|
||||||
MinFireLevel: parseOptionalFloat(c.Query("min_fire_level")),
|
MinFireLevel: parseOptionalFloat(c.Query("min_fire_level")),
|
||||||
MaxFireLevel: parseOptionalFloat(c.Query("max_fire_level")),
|
MaxFireLevel: parseOptionalFloat(c.Query("max_fire_level")),
|
||||||
MinDailyLoss: parseOptionalFloat(c.Query("min_daily_loss")),
|
MinDailyLoss: parseOptionalFloat(c.Query("min_daily_loss")),
|
||||||
MaxDailyLoss: parseOptionalFloat(c.Query("max_daily_loss")),
|
MaxDailyLoss: parseOptionalFloat(c.Query("max_daily_loss")),
|
||||||
MinSecretKD: parseOptionalFloat(c.Query("min_secret_kd")),
|
MinSecretKD: parseOptionalFloat(c.Query("min_secret_kd")),
|
||||||
MaxSecretKD: parseOptionalFloat(c.Query("max_secret_kd")),
|
MaxSecretKD: parseOptionalFloat(c.Query("max_secret_kd")),
|
||||||
ResourceRanges: parseResourceRanges(c),
|
ResourceRanges: parseResourceRanges(c),
|
||||||
DistributionSiteCode: strings.TrimSpace(firstNonEmpty(c.Query("distribution_site_code"), c.Query("site_code"))),
|
DistributionSiteCode: strings.TrimSpace(firstNonEmpty(c.GetHeader("X-Distribution-Site-Code"), c.Query("distribution_site_code"), c.Query("site_code"))),
|
||||||
}
|
}
|
||||||
if query.PageSize > 50 {
|
if query.PageSize > 50 {
|
||||||
query.PageSize = 50
|
query.PageSize = 50
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package listing
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
filemodule "hfb_sys/backend/internal/modules/file"
|
filemodule "hfb_sys/backend/internal/modules/file"
|
||||||
"hfb_sys/backend/pkg/response"
|
"hfb_sys/backend/pkg/response"
|
||||||
@@ -24,7 +25,8 @@ func (h *Handler) FindPublic(c *gin.Context) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item, err := h.service.FindPublic(c.Request.Context(), id)
|
siteCode := strings.TrimSpace(firstNonEmpty(c.GetHeader("X-Distribution-Site-Code"), c.Query("distribution_site_code"), c.Query("site_code")))
|
||||||
|
item, err := h.service.FindPublic(c.Request.Context(), id, siteCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeListingError(c, err)
|
writeListingError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ func (r *Repository) ListPublic(ctx context.Context, query PublicListQuery) (*Pu
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items := publicListings(rowsToDTO(rows))
|
items := publicListings(rowsToDTO(rows))
|
||||||
|
if err := r.applyShelfMarkup(ctx, query.DistributionSiteCode, items); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
baseQuery := query
|
baseQuery := query
|
||||||
baseQuery.Zone = ""
|
baseQuery.Zone = ""
|
||||||
items = filterPublicListings(items, baseQuery)
|
items = filterPublicListings(items, baseQuery)
|
||||||
@@ -228,15 +231,63 @@ func copyPublicZoneCounts(counts map[string]int64) map[string]int64 {
|
|||||||
return copied
|
return copied
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) FindPublic(ctx context.Context, id uint64) (*ListingDTO, error) {
|
func (r *Repository) FindPublic(ctx context.Context, id uint64, siteCode string) (*ListingDTO, error) {
|
||||||
dto, err := r.findDTO(ctx, "l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
|
dto, err := r.findDTO(ctx, "l.id = ? AND l.status = ? AND l.review_status = ? AND l.in_transaction = ?", id, "published", "approved", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyPublicListingURLs(dto)
|
applyPublicListingURLs(dto)
|
||||||
|
markups, err := r.shelfMarkupByListingID(ctx, siteCode, []uint64{dto.ID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto.PriceCent += markups[dto.ID]
|
||||||
return dto, nil
|
return dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) applyShelfMarkup(ctx context.Context, siteCode string, items []ListingDTO) error {
|
||||||
|
if siteCode == "" || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ids := make([]uint64, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
ids = append(ids, item.ID)
|
||||||
|
}
|
||||||
|
markups, err := r.shelfMarkupByListingID(ctx, siteCode, ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range items {
|
||||||
|
items[i].PriceCent += markups[items[i].ID]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) shelfMarkupByListingID(ctx context.Context, siteCode string, listingIDs []uint64) (map[uint64]int64, error) {
|
||||||
|
result := make(map[uint64]int64, 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"`
|
||||||
|
}
|
||||||
|
var rows []markupRow
|
||||||
|
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").
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) FindPublicCoverKey(ctx context.Context, id uint64) (string, error) {
|
func (r *Repository) FindPublicCoverKey(ctx context.Context, id uint64) (string, error) {
|
||||||
return r.FindPublicScreenshotKey(ctx, id, 0)
|
return r.FindPublicScreenshotKey(ctx, id, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ func (s *Service) ListMine(ctx context.Context, ownerID uint64, siteCode ...stri
|
|||||||
return s.repo.ListMine(ctx, ownerID, siteCode...)
|
return s.repo.ListMine(ctx, ownerID, siteCode...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) FindPublic(ctx context.Context, id uint64) (*ListingDTO, error) {
|
func (s *Service) FindPublic(ctx context.Context, id uint64, siteCode string) (*ListingDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
}
|
}
|
||||||
return s.repo.FindPublic(ctx, id)
|
return s.repo.FindPublic(ctx, id, siteCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) FindPublicCoverKey(ctx context.Context, id uint64) (string, error) {
|
func (s *Service) FindPublicCoverKey(ctx context.Context, id uint64) (string, error) {
|
||||||
|
|||||||
@@ -348,8 +348,6 @@ func TestApplyPublicListingURLsHidesSourceChannel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func uint64Pointer(value uint64) *uint64 {
|
func uint64Pointer(value uint64) *uint64 {
|
||||||
return &value
|
return &value
|
||||||
}
|
}
|
||||||
@@ -703,7 +701,7 @@ func TestListPublicWithDistributionSiteCode(t *testing.T) {
|
|||||||
site := model.DistributionSite{SiteCode: "site_alpha", SiteName: "分销A", Domain: "a.com", Status: "active", DisplayAccountEnabled: true}
|
site := model.DistributionSite{SiteCode: "site_alpha", SiteName: "分销A", Domain: "a.com", Status: "active", DisplayAccountEnabled: true}
|
||||||
db.Create(&site)
|
db.Create(&site)
|
||||||
|
|
||||||
dsa := model.DistributionSiteAccount{SiteID: site.ID, AccountID: account1.ID, ListingID: l1.ID, DisplayStatus: "enabled"}
|
dsa := model.DistributionSiteAccount{SiteID: site.ID, AccountID: account1.ID, ListingID: l1.ID, DisplayStatus: "enabled", MarkupAmountCent: 10000}
|
||||||
db.Create(&dsa)
|
db.Create(&dsa)
|
||||||
|
|
||||||
res, err := repo.ListPublic(context.Background(), PublicListQuery{
|
res, err := repo.ListPublic(context.Background(), PublicListQuery{
|
||||||
@@ -715,6 +713,24 @@ func TestListPublicWithDistributionSiteCode(t *testing.T) {
|
|||||||
if len(res.Items) != 1 || res.Items[0].ListingNo != "L101" {
|
if len(res.Items) != 1 || res.Items[0].ListingNo != "L101" {
|
||||||
t.Fatalf("expected only L101, got %+v", res.Items)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
mainDetail, err := repo.FindPublic(context.Background(), l1.ID, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindPublic main err: %v", err)
|
||||||
|
}
|
||||||
|
if mainDetail.PriceCent != 1000 {
|
||||||
|
t.Fatalf("main site must keep original price, got %d", mainDetail.PriceCent)
|
||||||
|
}
|
||||||
|
|
||||||
allRes, err := repo.ListPublic(context.Background(), PublicListQuery{})
|
allRes, err := repo.ListPublic(context.Background(), PublicListQuery{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -946,5 +962,3 @@ func TestListMineSiteIsolation(t *testing.T) {
|
|||||||
t.Fatalf("expected 2 listings, got %d", len(allItems))
|
t.Fatalf("expected 2 listings, got %d", len(allItems))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ type OrderDTO struct {
|
|||||||
DepositFreeManualQuotaCent int64 `json:"deposit_free_manual_quota_cent,omitempty"`
|
DepositFreeManualQuotaCent int64 `json:"deposit_free_manual_quota_cent,omitempty"`
|
||||||
DepositFreeUsedBeforeCent int64 `json:"deposit_free_used_before_cent,omitempty"`
|
DepositFreeUsedBeforeCent int64 `json:"deposit_free_used_before_cent,omitempty"`
|
||||||
PlatformFeeCent *int64 `json:"platform_fee_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"`
|
RentOriginalAmountCent int64 `json:"rent_original_amount_cent,omitempty"`
|
||||||
RentDiscountAmountCent int64 `json:"rent_discount_amount_cent,omitempty"`
|
RentDiscountAmountCent int64 `json:"rent_discount_amount_cent,omitempty"`
|
||||||
PureCoinOriginalAmountCent int64 `json:"pure_coin_original_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, "订单信息不完整")
|
response.BadRequest(c, "订单信息不完整")
|
||||||
return
|
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)
|
item, err := h.service.Create(c.Request.Context(), userID, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeOrderError(c, err)
|
writeOrderError(c, err)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
@@ -57,7 +58,15 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rentHours := estimateOrderDurationHours(snapshot)
|
rentHours := estimateOrderDurationHours(snapshot)
|
||||||
|
shelfMarkupCent, markupSiteID, err := loadShelfMarkupTx(tx, req.DistributionSiteCode, listing.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
pricing := buildOrderPricing(listing, account)
|
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())
|
distributionSite, distributionRule, err := distribution.ResolveOrderSourceTx(tx, req.DistributionSiteCode, listing.ID, account.ID, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -104,6 +113,9 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
|||||||
DepositFreeManualQuotaCent: growthSnapshot.ManualDepositFreeQuotaCent,
|
DepositFreeManualQuotaCent: growthSnapshot.ManualDepositFreeQuotaCent,
|
||||||
DepositFreeUsedBeforeCent: depositWaiver.UsedBeforeCent,
|
DepositFreeUsedBeforeCent: depositWaiver.UsedBeforeCent,
|
||||||
PlatformFeeCent: pricing.PlatformFeeCent,
|
PlatformFeeCent: pricing.PlatformFeeCent,
|
||||||
|
BasePriceCent: basePriceCent,
|
||||||
|
ShelfMarkupCent: shelfMarkupCent,
|
||||||
|
MarkupSiteID: markupSiteID,
|
||||||
RentOriginalAmountCent: rentOriginalAmountCent,
|
RentOriginalAmountCent: rentOriginalAmountCent,
|
||||||
RentDiscountAmountCent: rentDiscountAmountCent,
|
RentDiscountAmountCent: rentDiscountAmountCent,
|
||||||
PureCoinOriginalAmountCent: pricing.PureCoinAmountCent,
|
PureCoinOriginalAmountCent: pricing.PureCoinAmountCent,
|
||||||
@@ -156,6 +168,32 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
|||||||
return r.FindForUser(ctx, renterID, createdID)
|
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) {
|
func orderAccountSourceSnapshot(tx *gorm.DB, listingID uint64) (string, string, error) {
|
||||||
var upload model.ListingUpload
|
var upload model.ListingUpload
|
||||||
err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error
|
err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
|||||||
DepositFreeManualQuotaCent: row.DepositFreeManualQuotaCent,
|
DepositFreeManualQuotaCent: row.DepositFreeManualQuotaCent,
|
||||||
DepositFreeUsedBeforeCent: row.DepositFreeUsedBeforeCent,
|
DepositFreeUsedBeforeCent: row.DepositFreeUsedBeforeCent,
|
||||||
PlatformFeeCent: &platformFeeCent,
|
PlatformFeeCent: &platformFeeCent,
|
||||||
|
BasePriceCent: row.BasePriceCent,
|
||||||
|
ShelfMarkupCent: row.ShelfMarkupCent,
|
||||||
|
MarkupSiteID: row.MarkupSiteID,
|
||||||
RentOriginalAmountCent: effectiveRentOriginalAmountCent(row.RentalOrder),
|
RentOriginalAmountCent: effectiveRentOriginalAmountCent(row.RentalOrder),
|
||||||
RentDiscountAmountCent: row.RentDiscountAmountCent,
|
RentDiscountAmountCent: row.RentDiscountAmountCent,
|
||||||
PureCoinOriginalAmountCent: orderPureCoinOriginalAmountCent(row.RentalOrder),
|
PureCoinOriginalAmountCent: orderPureCoinOriginalAmountCent(row.RentalOrder),
|
||||||
|
|||||||
@@ -290,8 +290,19 @@ func effectiveCheckoutDepositDeductCent(checkout model.OrderCheckout) int64 {
|
|||||||
return maxCent(checkout.OtherAmountCent, 0)
|
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 {
|
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
|
||||||
orderRentAmountCent := order.RentAmountCent
|
orderRentAmountCent := orderConsumptionPrepaidCent(order)
|
||||||
orderOwnerRentAmountCent := order.OwnerRentAmountCent
|
orderOwnerRentAmountCent := order.OwnerRentAmountCent
|
||||||
orderDepositAmountCent := order.DepositAmountCent
|
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 测试订单定价计算
|
// TestBuildOrderPricing 测试订单定价计算
|
||||||
func TestBuildOrderPricingBasic(t *testing.T) {
|
func TestBuildOrderPricingBasic(t *testing.T) {
|
||||||
listing := model.RentalListing{
|
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) {
|
func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) {
|
||||||
order := model.RentalOrder{
|
order := model.RentalOrder{
|
||||||
RentAmountCent: 38300,
|
RentAmountCent: 38300,
|
||||||
@@ -527,4 +556,3 @@ func TestApplyAdminOrderFiltersSmartRouting(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
-- +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;
|
||||||
Reference in New Issue
Block a user