Files
hfb_sys/backend/internal/modules/listing/external_adjustment.go
T
yml2213 5840b7da40 fix(listing): 外部上传按后台实时配置归类皮肤与折算消耗品,并补齐新增消耗品字段
- 皮肤归类:删除代码内硬编码的 externalSkinCatalog 快照,改为读取 listing.publish_options。
  修复运营在后台新增皮肤后,上传的商品被归入 other 的问题(详情页显示“其他皮肤”、
  按刀皮/金皮/红皮/武器皮分组筛选搜不到)
- 消耗品折算:externalResources 改为按 publish_options.quantity_items 生成条目与单价,
  修复 AWM 子弹按 0.6 元/发旧价折算等价格漂移(线上已调为 0.7 元/发)
- 上传契约:ExternalUploadInventory 新增 level6Ammo/barrettAmmo/gridCard9/redPacket45/
  coffeeBean/bulletPart 六个字段,字段名与消耗品 key 对齐;后续后台再新增消耗品,
  按同样 key 登记即可被上传链路识别
- 数量为 0 的消耗品不再写入资源清单,与网站自身发布流程(filter quantity > 0)保持一致
- 客服调整外部商品同样改用实时单价;Service 增加 externalPublishOptions 统一读配置并带默认值
- 验证:新增回归测试 TestExternalAccountToCreateRequestFollowsPublishOptions;
  并以线上 mobile-home-config 的真实配置 + 上传工具产出的 JSON 做跨仓库联调,
  确认“重塑→近战皮肤、骇爪-骇客少女→干员金皮”,9 项消耗品全部按线上单价生成
2026-10-11 17:44:27 +08:00

281 lines
9.6 KiB
Go

package listing
import (
"context"
"math"
"strings"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/systemconfig"
"gorm.io/gorm"
)
const maxExternalSaleRatioIncrement = 10.0
type externalAdjustmentResult struct {
Resources []any
ReferenceRatio float64
ReferenceCoinPrice float64
SaleRatio float64
SellerCoinPrice float64
SellerTotalPrice float64
ConsumablePrice float64
ActualDiscountAmount float64
PlatformPricing externalPlatformPricing
}
func (s *Service) AdjustExternalListing(
ctx context.Context,
adminID uint64,
listingID uint64,
req AdminExternalListingAdjustRequest,
meta AuditMeta,
) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if listingID == 0 || req.HafCoinAmount <= 0 || req.DepositAmountCent < 0 ||
req.AWMBullets < 0 || req.Level6Helmets < 0 || req.Level6Armor < 0 ||
strings.TrimSpace(req.Reason) == "" {
return nil, ErrExternalAdjustment
}
switch req.AdjustMode {
case ExternalAdjustModeDiscountAmount:
if req.DiscountAmountCent < 0 {
return nil, ErrExternalAdjustment
}
case ExternalAdjustModeSaleRatio:
if req.SaleRatio <= 0 || math.IsNaN(req.SaleRatio) || math.IsInf(req.SaleRatio, 0) {
return nil, ErrExternalAdjustment
}
default:
return nil, ErrExternalAdjustment
}
config, err := s.externalSalePriceConfig(ctx)
if err != nil {
return nil, err
}
// 消耗品单价同样取自后台实时配置,避免调整后的价格与网站现行单价不一致。
quantityItems := s.externalPublishOptions(ctx).QuantityItems
return s.repo.AdjustExternalListing(ctx, adminID, listingID, req, config, quantityItems, meta)
}
func (r *Repository) AdjustExternalListing(
ctx context.Context,
adminID uint64,
listingID uint64,
req AdminExternalListingAdjustRequest,
config salePriceConfig,
quantityItems []systemconfig.PublishQuantityItem,
meta AuditMeta,
) (*ListingDTO, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
listing, account, err := r.findForReviewUpdate(tx, listingID)
if err != nil {
return err
}
if listing.InTransaction || listing.Status == "rented" || listing.Status == "completed" {
return ErrListingLocked
}
switch listing.Status {
case "draft", "published", "offline", "abnormal":
default:
return ErrListingLocked
}
var uploadCount int64
if err := tx.Model(&model.ListingUpload{}).Where("listing_id = ?", listing.ID).Count(&uploadCount).Error; err != nil {
return err
}
if uploadCount == 0 {
return ErrExternalListingRequired
}
summary := decodeAssetSummary(account.AssetSummary)
if summary == nil {
summary = map[string]any{}
}
breakdown := ensurePriceBreakdown(summary)
referenceRatio := readSummaryNumber(breakdown["seller_reference_ratio"])
if referenceRatio <= 0 {
referenceRatio = readSummaryNumber(breakdown["seller_ratio"])
}
if referenceRatio <= 0 {
return ErrExternalAdjustment
}
before := externalAdjustmentAuditSnapshot(account, listing, summary, breakdown)
adjustment, err := calculateExternalListingAdjustment(req, summary, referenceRatio, config, quantityItems)
if err != nil {
return err
}
if adjustment.ConsumablePrice > 0 && req.DepositAmountCent <= yuanToCent(adjustment.ConsumablePrice) {
return ErrDepositTooLow
}
now := time.Now()
summary["resources"] = adjustment.Resources
summary["publish_ratio"] = adjustment.PlatformPricing.BuyerRatio
breakdown["seller_reference_ratio"] = adjustment.ReferenceRatio
breakdown["seller_ratio"] = adjustment.SaleRatio
breakdown["accelerated_sale_ratio"] = adjustment.SaleRatio
breakdown["seller_coin_base_price"] = adjustment.SellerCoinPrice
breakdown["seller_total_price"] = adjustment.SellerTotalPrice
breakdown["consumable_price"] = adjustment.ConsumablePrice
breakdown["buyer_coin_base_price"] = adjustment.PlatformPricing.BuyerCoinBasePrice
breakdown["buyer_total_price"] = adjustment.PlatformPricing.BuyerTotalPrice
breakdown["buyer_ratio"] = adjustment.PlatformPricing.BuyerRatio
breakdown["platform_markup_amount"] = adjustment.PlatformPricing.PlatformMarkupPrice
breakdown["platform_rule_type"] = adjustment.PlatformPricing.RuleType
breakdown["external_admin_adjust_mode"] = req.AdjustMode
breakdown["external_admin_discount_amount"] = adjustment.ActualDiscountAmount
breakdown["external_admin_adjust_reason"] = strings.TrimSpace(req.Reason)
breakdown["external_admin_adjusted_at"] = now.Format(time.RFC3339)
breakdown["external_admin_adjusted_by"] = adminID
summary["price_breakdown"] = breakdown
assetSummary, err := marshalAssetSummary(summary)
if err != nil {
return err
}
account.HafCoinAmount = req.HafCoinAmount
account.AssetSummary = assetSummary
account.Title = buildExternalListingTitle(
account.RankLevel,
readAssetString(summary, "season_insurance"),
float64(req.HafCoinAmount)/1000000,
account.LoginPlatform,
)
listing.PriceCent = yuanToCent(adjustment.PlatformPricing.BuyerTotalPrice)
listing.DepositAmountCent = req.DepositAmountCent
if err := tx.Save(account).Error; err != nil {
return err
}
if err := tx.Save(listing).Error; err != nil {
return err
}
after := externalAdjustmentAuditSnapshot(account, listing, summary, breakdown)
return appendAuditLog(tx, adminID, "listing.adjust_external", "listing", listing.ID, meta, map[string]any{
"listing_id": listing.ID,
"account_id": account.ID,
"owner_id": listing.OwnerID,
"adjust_mode": req.AdjustMode,
"requested_discount_amount_cent": req.DiscountAmountCent,
"requested_sale_ratio": req.SaleRatio,
"reason": strings.TrimSpace(req.Reason),
"before": before,
"after": after,
})
})
if err != nil {
return nil, err
}
return r.FindAdmin(ctx, listingID)
}
func calculateExternalListingAdjustment(
req AdminExternalListingAdjustRequest,
summary map[string]any,
referenceRatio float64,
config salePriceConfig,
quantityItems []systemconfig.PublishQuantityItem,
) (externalAdjustmentResult, error) {
coinM := float64(req.HafCoinAmount) / 1000000
coinWan := float64(req.HafCoinAmount) / 10000
referenceCoinPrice := roundMoney(coinWan / referenceRatio)
if coinM <= 0 || coinWan <= 0 || referenceCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrExternalAdjustment
}
saleRatio := roundRatio(req.SaleRatio)
if req.AdjustMode == ExternalAdjustModeDiscountAmount {
desiredCoinPrice := referenceCoinPrice - centToYuan(req.DiscountAmountCent)
if desiredCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrSaleRatioOutOfRange
}
saleRatio = roundRatio(coinWan / desiredCoinPrice)
}
if saleRatio < referenceRatio || saleRatio > referenceRatio+maxExternalSaleRatioIncrement {
return externalAdjustmentResult{}, ErrSaleRatioOutOfRange
}
sellerCoinPrice := roundMoney(coinWan / saleRatio)
if sellerCoinPrice <= 0 {
return externalAdjustmentResult{}, ErrExternalAdjustment
}
coreResources := externalResources(ExternalUploadInventory{
AWMBullets: req.AWMBullets,
Level6Helmets: req.Level6Helmets,
Level6Armor: req.Level6Armor,
}, quantityItems)
resources := mergeExternalCoreResources(summary, coreResources)
consumablePrice := consumableValue(map[string]any{"resources": resources})
sellerTotalPrice := roundMoney(sellerCoinPrice + consumablePrice)
platformPricing := calculateExternalPlatformPricing(
coinM,
saleRatio,
sellerCoinPrice,
consumablePrice,
config,
)
if platformPricing.BuyerCoinBasePrice <= 0 || platformPricing.BuyerTotalPrice <= 0 || platformPricing.BuyerRatio <= 0 {
return externalAdjustmentResult{}, ErrInvalidPrice
}
return externalAdjustmentResult{
Resources: resources,
ReferenceRatio: referenceRatio,
ReferenceCoinPrice: referenceCoinPrice,
SaleRatio: saleRatio,
SellerCoinPrice: sellerCoinPrice,
SellerTotalPrice: sellerTotalPrice,
ConsumablePrice: consumablePrice,
ActualDiscountAmount: roundMoney(referenceCoinPrice - sellerCoinPrice),
PlatformPricing: platformPricing,
}, nil
}
func mergeExternalCoreResources(summary map[string]any, core []any) []any {
coreKeys := map[string]struct{}{"awmAmmo": {}, "helmet6": {}, "armor6": {}}
result := append([]any{}, core...)
rawResources, _ := summary["resources"].([]any)
for _, raw := range rawResources {
row, ok := raw.(map[string]any)
if !ok {
continue
}
key, _ := row["key"].(string)
if _, isCore := coreKeys[key]; isCore {
continue
}
result = append(result, row)
}
return result
}
func externalAdjustmentAuditSnapshot(
account *model.GameAccount,
listing *model.RentalListing,
summary map[string]any,
breakdown map[string]any,
) map[string]any {
return map[string]any{
"haf_coin_amount": account.HafCoinAmount,
"awm_bullets": resourceQuantity(summary, "awmAmmo"),
"level6_helmets": resourceQuantity(summary, "helmet6"),
"level6_armor": resourceQuantity(summary, "armor6"),
"deposit_amount_cent": listing.DepositAmountCent,
"reference_ratio": readSummaryNumber(breakdown["seller_reference_ratio"]),
"sale_ratio": readSummaryNumber(breakdown["seller_ratio"]),
"seller_coin_base_price": readSummaryNumber(breakdown["seller_coin_base_price"]),
"seller_total_price": readSummaryNumber(breakdown["seller_total_price"]),
"buyer_ratio": readSummaryNumber(breakdown["buyer_ratio"]),
"buyer_total_price": readSummaryNumber(breakdown["buyer_total_price"]),
"listing_price_cent": listing.PriceCent,
}
}