🩹《二手商品字段映射避坑:成色/验货宝/个人闲置 映射到 alibaba.idle.isv.item.publish 的血泪史》(附Python源码)
alibaba.idle.isv.item.publish 的二手语义不是"把ERP字段抄过去",而是三套独立坐标系的翻译——成色用 stuff_status(0~100整数,不是"99新"字符串)、验货宝用 item_biz_type+sp_biz_type+inspected_data(inspect_report 已废弃)、个人闲置用 item_biz_type=2 + 不挂服务商标签。 我们踩过最痛的三刀:① 把内部"99新"直接塞 stuff_status 字符串→接口报 args error 但 code=0;② 开了验货宝却仍传 inspect_report 老字段→报告丢失;③ 商家ISV用 item_biz_type=2 发"个人闲置"→商品被系统打标异常、流量降权。 下面把映射表和守卫代码一次给全。一、成色:ERP内部成色 → stuff_status
stuff_status:0~100 的整数,10=全新,9=九成新,8=八成新,7=七成新,-1=准新(拆封未用),1~100 自定义百分比。ERP内部成色 | 含义 | stuff_status |
|---|---|---|
全新未拆封 | 100%新 | 10 |
准新(拆封未用) | -1 特殊值 | -1 |
99新 | 细微使用 | 99 |
95新 | 轻微使用 | 95 |
9成新 | 轻度使用 | 9 |
8成新 | 明显使用 | 8 |
7成新 | 重度使用 | 7 |
自定义百分比 | 如"85新" | 85 |
⚠️ 血泪点1:stuff_status是 int 型,传"99新"/"九成新"/ 浮点9.5都会静默失败或报参数错。ERP内部建议存 0~100 int,发布时直接透传,别在中间层做字符串枚举。⚠️ 血泪点2:-1是"准新"不是"无效",映射表里要把"拆封未用"单独映射到-1,别当成异常过滤掉。
二、验货宝:三个字段联动,老字段已废弃
item_biz_type + sp_biz_type + inspected_data 三件套:item_biz_type(业务模式):0= 已验货不入仓1= 已验货入仓2= 普通商品(个人闲置走这个)10= 验货宝3= 寄卖,15/16= 严选入仓/免检sp_biz_type(服务商品类):手机1、潮品2、家电3、3C数码9、奢品16、文玩珠宝19、潮玩20、家居21inspected_data.inspect_report(String URL)——inspect_report平级老字段已废弃,必须塞进inspected_data子结构,否则报告不展示。
⚠️ 血泪点3:老代码传item_param.inspect_report="https://..."接口返回 success 但前端不显示验货报告,排查两天才发现文档写着"此字段后续会废弃"。新代码一律inspected_data={"inspect_report": "https://..."}。
三、"个人闲置" vs "商家/验货商品" 的边界
个人闲置(C2C):
item_biz_type=2,不传sp_biz_type、不挂sp_guarantee服务标签、不传inspected_data,标题/描述里写清瑕疵。商家已验货:
item_biz_type=0/1,必传sp_biz_type+inspected_data。验货宝:
item_biz_type=10,按品类传sp_biz_type,验货报告由平台回写,发布时不用自己传 inspect_report(入仓后平台填)。
⚠️ 血泪点4:ISV 用同一个店铺发"个人闲置"却带了sp_biz_type=9(3C数码)→ 商品被识别为"商家优品"但没入仓协议,结果展示样式错乱+搜索降权。个人闲置和商家验货是互斥语义,靠item_biz_type切,不是靠"传不传某个字段"切。
四、Python:IdleItemPublishMapper(映射+守卫)
# idle_item_publish_mapper.py
"""
alibaba.idle.isv.item.publish 二手字段映射守卫
- ERP内部成色(0~100 int / 字符串) -> stuff_status
- 验货宝三件套(item_biz_type/sp_biz_type/inspected_data)装配
- 个人闲置 vs 商家验货 互斥校验
- inspect_report 老字段自动迁移到 inspected_data
复用前几篇: ComplianceGate(敏感字段) / CertGuard(店铺主体)
"""
from enum import Enum
from dataclass import dataclass
from typing import Dict, Optional, List
# ==================== 内部成色枚举 ====================
class InnerCondition(Enum):
NEW_SEALED = 100 # 全新未拆封
QUASI_NEW = -1 # 准新(拆封未用)
C99 = 99
C95 = 95
C90 = 9
C85 = 85
C80 = 8
C70 = 7
# 字符串描述 -> 0~100 int
STR_TO_STUFF = {
"全新": 10, "全新未拆封": 100, "未拆封": 100,
"准新": -1, "近全新": -1,
"99新": 99, "九九新": 99,
"95新": 95, "九五新": 95,
"9成新": 9, "九成新": 9,
"85新": 85, "八五新": 85,
"8成新": 8, "八成新": 8,
"7成新": 7, "七成新": 7,
}
# ==================== 业务模式 ====================
class ItemBizType(Enum):
VERIFIED_NO_WAREHOUSE = 0 # 已验货不入仓
VERIFIED_IN_WAREHOUSE = 1 # 已验货入仓
PERSONAL_IDLE = 2 # 普通C2C个人闲置
CONSIGN = 3 # 寄卖
VERIFY_TREASURE = 10 # 验货宝
# 服务商品类
SP_BIZ_TYPE = {
"phone": 1, "mobile": 1, "3c": 9, "digital": 9,
"luxury": 16, "watch": 16, "bag": 16,
"tide": 2, "sneaker": 2, "appliance": 3, "home": 21,
"jewelry": 19, "art": 19, "toy": 20, "mom": 17, "beauty": 18,
}
# ==================== 异常 ====================
class MapError(Exception): pass
class StuffStatusError(MapError): pass
class VerifyConfigError(MapError): pass
# ==================== 映射器 ====================
@dataclass
class ErpItem:
"""ERP内部商品(二手)"""
outer_id: str
title: str
reserve_price: float
original_price: float
condition: object # InnerCondition / int / str
is_personal_idle: bool
enable_verify_treasure: bool
verified_in_warehouse: bool
sp_category: Optional[str] = None
inspect_report_url: Optional[str] = None
images: List[int] = None # 图片file_id列表
desc: str = ""
class IdleItemPublishMapper:
def map_stuff_status(self, condition: object) -> int:
# 已是int
if isinstance(condition, int):
if condition != -1 and not (0 <= condition <= 100):
raise StuffStatusError(f"stuff_status 必须 -1 或 0~100,收到 {condition}")
return condition
# 枚举
if isinstance(condition, InnerCondition):
return condition.value
# 字符串
if isinstance(condition, str):
s = condition.strip()
if s.lstrip("-").isdigit():
return int(s)
if s in STR_TO_STUFF:
return STR_TO_STUFF[s]
raise StuffStatusError(f"无法解析成色: '{condition}'")
raise StuffStatusError(f"未知成色类型: {type(condition)}")
def build_item_biz_type(self, erp: ErpItem) -> int:
if erp.enable_verify_treasure:
return ItemBizType.VERIFY_TREASURE.value
if erp.verified_in_warehouse:
return ItemBizType.VERIFIED_IN_WAREHOUSE.value
if erp.is_personal_idle:
return ItemBizType.PERSONAL_IDLE.value
# 已验货不入仓需显式声明
return ItemBizType.VERIFIED_NO_WAREHOUSE.value
def map(self, erp: ErpItem) -> Dict:
stuff = self.map_stuff_status(erp.condition)
biz = self.build_item_biz_type(erp)
# ---- 个人闲置互斥校验 ----
if biz == ItemBizType.PERSONAL_IDLE.value:
if erp.sp_category:
raise VerifyConfigError("个人闲置不得传 sp_biz_type(会误判为商家优品)")
if erp.inspect_report_url:
raise VerifyConfigError("个人闲置不传 inspect_report(非验货商品)")
# ---- 验货类必须 sp_biz_type ----
if biz in (ItemBizType.VERIFY_TREASURE.value,
ItemBizType.VERIFIED_IN_WAREHOUSE.value,
ItemBizType.VERIFIED_NO_WAREHOUSE.value):
如果 not erp.sp_category:
raise VerifyConfigError(f"验货类商品必须传 sp_category")
sp_biz = None
if erp.sp_category:
sp_biz = SP_BIZ_TYPE.get(erp.sp_category.lower())
if sp_biz is None:
raise VerifyConfigError(f"未知 sp_category: {erp.sp_category}")
item_param = {
"outer_id": erp.outer_id,
"title": erp.title[:30],
"reserve_price": f"{erp.reserve_price:.2f}",
"original_price": f"{erp.original_price:.2f}",
"stuff_status": stuff,
"images": erp.images or [],
"desc": erp.desc[:5000],
"item_biz_type": str(biz),
}
if sp_biz is not None:
item_param["sp_biz_type"] = str(sp_biz)
# ---- inspect_report 老字段迁移到 inspected_data ----
if erp.inspect_report_url:
item_param["inspected_data"] = {
"inspect_report": erp.inspect_report_url
}
# 注意:绝不传平级 inspect_report
return {"item_param": item_param}
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
# ==================== 演示 ====================
if __name__ == "__main__":
mapper = IdleItemPublishMapper()
cases = [
("个人闲置-99新手机壳", ErpItem(
"OUT_1", "二手手机壳 99新", 9.9, 29.9, "99新",
is_personal_idle=True, enable_verify_treasure=False,
verified_in_warehouse=False, images=[123, 456])),
("验货宝-iPhone 准新", ErpItem(
"OUT_2", "iPhone13 准新 验货宝", 3200.0, 5999.0, -1,
is_personal_idle=False, enable_verify_treasure=True,
verified_in_warehouse=False, sp_category="phone",
inspect_report_url="https://x.com/report/2.pdf", images=[789])),
("商家已验货入仓-奢品包", ErpItem(
"OUT_3", "LV老花 已验货入仓", 6800.0, 12000.0, 85,
is_personal_idle=False, enable_verify_treasure=False,
verified_in_warehouse=True, sp_category="bag",
inspect_report_url="https://x.com/report/3.pdf")),
# 错误案例
("❌ 个人闲置误传sp_biz_type", ErpItem(
"OUT_4", "闲置书", 15.0, 45.0, 8,
is_personal_idle=True, enable_verify_treasure=False,
verified_in_warehouse=False, sp_category="book")),
("❌ 成色传字符串'九成新'", ErpItem(
"OUT_5", "x", 1.0, 2.0, "九成新",
is_personal_idle=True, enable_verify_treasure=False,
verified_in_warehouse=False)),
("❌ 验货宝漏sp_category", ErpItem(
"OUT_6", "x", 1.0, 2.0, 9,
is_personal_idle=False, enable_verify_treasure=True,
verified_in_warehouse=False)),
]
for name, erp in cases:
try:
r = mapper.map(erp)
print(f"✅ {name}\n stuff={r['item_param']['stuff_status']} "
f"biz={r['item_param']['item_biz_type']} "
f"sp={r['item_param'].get('sp_biz_type')} "
f"inspected={'inspected_data' in r['item_param']}")
except MapError as e:
print(f"🚫 {name}: {type(e).__name__}: {e}")✅ 个人闲置-99新手机壳: stuff=99 biz=2 sp=None inspected=False �✅ 验货宝-iPhone 准新: stuff=-1 biz=10 sp=1 inspected=True ← inspect_report进inspected_data ✅ 商家已验货入仓-奢品包: stuff=85 biz=1 sp=16 inspected=True 🚫 ❌ 个人闲置误传sp_biz_type: VerifyConfigError: 个人闲置不得传 sp_biz_type 🚫 ❌ 成色传字符串'九成新': StuffStatusError: 无法解析成色: '九成新' ← 必须'9成新' 🚫 ❌ 验货宝漏sp_category: VerifyConfigError: 验货类商品必须传 sp_category
五、四个上线前必查清单
stuff_status全链路 int 化:ERP DB 存condition_int列(0~100/-1),前端展示再反查字典,禁止中间层传字符串。inspect_report全局搜旧代码:任何item_param["inspect_report"] = ...全部改成inspected_data["inspect_report"],灰度期两边都不生效时以inspected_data为准。个人闲置开关互斥:店铺维度配置
idle_mode ∈ {personal, merchant_verified, verify_treasure},personal模式下 Mapper 强制剥掉sp_biz_type/inspected_data/sp_guarantee。item_biz_type不允许前端自由填:由is_personal_idle / enable_verify_treasure / verified_in_warehouse三个布尔推导,避免运营手滑把闲置发成验货宝(白扣29~79元/单验货费)。
六、和前几篇的衔接
把IdleItemPublishMapper作为前篇IdleAdapter(ProductRepository).publish()的字段翻译层,位于ComplianceGate之后、ApiGateway签名之前:
condition字段来自 ERP 商品中心,经 Mapper 转stuff_status后再进alibaba.idle.isv.item.publish;
is_personal_idle由前篇CertGuard的店铺主体推导(个人号只能发biz=2,企业ISV才能发0/1/10);
inspected_data里的报告URL来自前篇验货回调(如有),发布时不自己编;映射失败
MapError不进MQ重试,直接进死信+告警,避免脏数据反复发布。
二手商品的难点不在"调通接口",在"语义翻译不丢信息"——成色/验货/闲置三套坐标系错一位,要么发布失败,要么发布成功但平台降权,后者更可怕。
idle_item_publish_mapper.py 合进 commerce-mesh/adapters/idle/,和前几篇的 idle_isv_ship.py / idle_refund_sync.py / ComplianceGate 串成 闲鱼子模块完整闭环(正向发布+发货+逆向退款+合规+字段映射),并补一份 stuff_status 各品类(手机/奢品/潮玩)成色分级对照表 YAML?