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 项消耗品全部按线上单价生成
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/modules/systemconfig"
|
||||
)
|
||||
|
||||
var externalOnlineTimePattern = regexp.MustCompile(`^\D*(\d{1,2})(?:\s*[::点.]\s*(\d{1,2}))?\D+(\d{1,2})(?:\s*[::点.]\s*(\d{1,2}))?\D*$`)
|
||||
@@ -37,11 +39,13 @@ func (s *Service) ImportExternalUpload(ctx context.Context, req ExternalUploadRe
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 皮肤分组与消耗品单价取自后台实时配置,避免代码里的快照与线上选项脱节。
|
||||
publishOptions := s.externalPublishOptions(ctx)
|
||||
clientUploadTime := parseClientUploadTime(req.UploadTime)
|
||||
results := make([]ExternalUploadResult, 0, len(items))
|
||||
resp := &ExternalUploadResponse{Total: len(items)}
|
||||
for index, item := range items {
|
||||
createReq := externalAccountToCreateRequest(uploaderName, req.UploadTime, item, salePriceConfig)
|
||||
createReq := externalAccountToCreateRequest(uploaderName, req.UploadTime, item, salePriceConfig, publishOptions)
|
||||
if err := validateRequest(createReq, rules); err != nil {
|
||||
if len(items) == 1 {
|
||||
return nil, err
|
||||
@@ -244,6 +248,7 @@ func externalAccountToCreateRequest(
|
||||
uploadTime int64,
|
||||
item ExternalAccountData,
|
||||
salePriceConfig salePriceConfig,
|
||||
publishOptions systemconfig.PublishOptionsDTO,
|
||||
) CreateRequest {
|
||||
hafCoinM := item.Currency.HafuCoin
|
||||
pureCoinPrice := item.Currency.RecycleRent
|
||||
@@ -254,7 +259,7 @@ func externalAccountToCreateRequest(
|
||||
skins := cleanStrings(item.Inventory.Skins)
|
||||
remark := strings.TrimSpace(item.Remark)
|
||||
onlineTimeText := normalizeExternalOnlineTimeText(item.OwnerOnlineTime)
|
||||
resources := externalResources(item.Inventory)
|
||||
resources := externalResources(item.Inventory, publishOptions.QuantityItems)
|
||||
consumablePrice := consumableValue(map[string]any{"resources": resources})
|
||||
pricing := calculateExternalPlatformPricing(
|
||||
hafCoinM,
|
||||
@@ -273,7 +278,7 @@ func externalAccountToCreateRequest(
|
||||
"stamina_level": staminaLevel,
|
||||
"load_level": loadLevel,
|
||||
"resources": resources,
|
||||
"skin_groups": externalSkinGroups(skins),
|
||||
"skin_groups": externalSkinGroups(skins, publishOptions.SkinGroups),
|
||||
"online_time_text": onlineTimeText,
|
||||
// 外部导入没有该字段时按不可凌晨响应处理,避免旧数据误入夜间专区。
|
||||
"early_morning_response": "否",
|
||||
@@ -420,60 +425,71 @@ func levelText(value int) string {
|
||||
return strconv.Itoa(value) + "级"
|
||||
}
|
||||
|
||||
func externalResources(inventory ExternalUploadInventory) []any {
|
||||
resources := []any{
|
||||
map[string]any{"key": "awmAmmo", "label": "AWM子弹", "price": "0.6元/发", "quantity": inventory.AWMBullets, "mode": "收费"},
|
||||
map[string]any{"key": "helmet6", "label": "6头", "price": "1.5元/个", "quantity": inventory.Level6Helmets, "mode": "收费"},
|
||||
map[string]any{"key": "armor6", "label": "6甲", "price": "2.5元/个", "quantity": inventory.Level6Armor, "mode": "收费"},
|
||||
// inventoryQuantities 把上传契约的库存字段映射回 publish_options.quantity_items 的 key。
|
||||
// 历史字段名(awmBullets / level6Helmets / level6Armor)保持不变,以兼容既有上传客户端。
|
||||
func inventoryQuantities(inventory ExternalUploadInventory) map[string]int {
|
||||
return map[string]int{
|
||||
"awmAmmo": inventory.AWMBullets,
|
||||
"helmet6": inventory.Level6Helmets,
|
||||
"armor6": inventory.Level6Armor,
|
||||
"level6Ammo": inventory.Level6Ammo,
|
||||
"barrettAmmo": inventory.BarrettAmmo,
|
||||
"gridCard9": inventory.GridCard9,
|
||||
"redPacket45": inventory.RedPacket45,
|
||||
"coffeeBean": inventory.CoffeeBean,
|
||||
"bulletPart": inventory.BulletPart,
|
||||
}
|
||||
}
|
||||
|
||||
// externalResources 按后台实时配置的消耗品清单生成资源数组:条目顺序、名称与单价取自配置,
|
||||
// 数量取自上传数据;未填(<=0)的条目与网站自身发布流程保持一致地省略。
|
||||
func externalResources(inventory ExternalUploadInventory, items []systemconfig.PublishQuantityItem) []any {
|
||||
quantities := inventoryQuantities(inventory)
|
||||
resources := make([]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
quantity := quantities[item.Key]
|
||||
if quantity <= 0 {
|
||||
continue
|
||||
}
|
||||
resources = append(resources, map[string]any{
|
||||
"key": item.Key,
|
||||
"label": item.Label,
|
||||
"price": item.Price,
|
||||
"quantity": quantity,
|
||||
"mode": "收费",
|
||||
})
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
// 与 systemconfig 默认 publish_options 皮肤分组保持一致,避免详情页出现英文分组名。
|
||||
var externalSkinCatalog = []struct {
|
||||
key string
|
||||
names []string
|
||||
}{
|
||||
{"melee", []string{"坠星者", "暗星", "龙牙", "信条", "怜悯", "赤枭", "影锋", "黑海", "北极星", "电锯惊魂", "处刑者"}},
|
||||
{"operatorGold", []string{
|
||||
"露娜-古墓丽影联动", "蛊-不羁人生", "红狼-电锯惊魂", "露娜-金牌射手",
|
||||
"牧羊人-街头之星", "蜂医-危险物质", "蜂医-送葬人", "无名-夜鹰",
|
||||
"威龙-壮志凌云", "威龙-蛟龙特战队", "威龙-铁面判官", "威龙-吴彦祖",
|
||||
}},
|
||||
{"operatorRed", []string{"凌霄成卫", "蚀金玫瑰", "水墨云图", "午夜邮差", "天际线", "维什戴尔"}},
|
||||
{"weapon", []string{
|
||||
"M7棱镜攻势S2", "腾龙气象感应", "MP7电玩高手S2", "M250电玩高手S2",
|
||||
"AS Val悬赏令", "K416命运", "M4A1棱镜攻势", "SCAR-H电玩高手",
|
||||
"QBZ95王牌之剑", "AUG气象感应", "Vector美杜莎", "KC17-造物纪元",
|
||||
}},
|
||||
}
|
||||
|
||||
func externalSkinGroups(skins []string) map[string][]string {
|
||||
groups := map[string][]string{}
|
||||
// externalSkinGroups 依据后台实时配置的皮肤分组归类上传的皮肤名,
|
||||
// 避免代码里的字典快照与运营在后台维护的选项脱节(新增皮肤会被误判为“其他皮肤”)。
|
||||
func externalSkinGroups(skins []string, groups []systemconfig.PublishOptionGroup) map[string][]string {
|
||||
result := map[string][]string{}
|
||||
for _, skin := range skins {
|
||||
skin = strings.TrimSpace(skin)
|
||||
if skin == "" {
|
||||
continue
|
||||
}
|
||||
key := classifyExternalSkin(skin)
|
||||
groups[key] = append(groups[key], skin)
|
||||
key := classifyExternalSkin(skin, groups)
|
||||
result[key] = append(result[key], skin)
|
||||
}
|
||||
return groups
|
||||
return result
|
||||
}
|
||||
|
||||
func classifyExternalSkin(skin string) string {
|
||||
func classifyExternalSkin(skin string, groups []systemconfig.PublishOptionGroup) string {
|
||||
// 先精确匹配,再做包含匹配,兼容“红狼-电锯惊魂 / 电锯惊魂”这类写法。
|
||||
for _, group := range externalSkinCatalog {
|
||||
for _, name := range group.names {
|
||||
for _, group := range groups {
|
||||
for _, name := range group.Options {
|
||||
if skin == name {
|
||||
return group.key
|
||||
return group.Key
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, group := range externalSkinCatalog {
|
||||
for _, name := range group.names {
|
||||
for _, group := range groups {
|
||||
for _, name := range group.Options {
|
||||
if strings.Contains(skin, name) || strings.Contains(name, skin) {
|
||||
return group.key
|
||||
return group.Key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user