×

《二手商品字段映射避坑:成色/验货宝/个人闲置 映射到 alibaba.idle.isv.item.publish 的血泪史》(附Python源码)

万邦科技Lex 万邦科技Lex 发表于2026-09-01 15:05:07 浏览24 评论0

抢沙发发表评论

🩹《二手商品字段映射避坑:成色/验货宝/个人闲置 映射到 alibaba.idle.isv.item.publish 的血泪史》(附Python源码)

结论先拍alibaba.idle.isv.item.publish 的二手语义不是"把ERP字段抄过去",而是三套独立坐标系的翻译——成色用 stuff_status(0~100整数,不是"99新"字符串)、验货宝用 item_biz_type+sp_biz_type+inspected_datainspect_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
⚠️ 血泪点1stuff_statusint 型,传 "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、家居21

  • inspected_data.inspect_report(String URL)—— inspect_report 平级老字段已废弃,必须塞进 inspected_data 子结构,否则报告不展示。

⚠️ 血泪点3:老代码传 item_param.inspect_report="https://..." 接口返回 success 但前端不显示验货报告,排查两天才发现文档写着"此字段后续会废弃"。新代码一律 inspected_data={"inspect_report": "https://..."}

三、"个人闲置" vs "商家/验货商品" 的边界

这是 ISV 最容易混的:
  • 个人闲置(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

五、四个上线前必查清单

  1. stuff_status 全链路 int 化:ERP DB 存 condition_int 列(0~100/-1),前端展示再反查字典,禁止中间层传字符串。

  2. inspect_report 全局搜旧代码:任何 item_param["inspect_report"] = ... 全部改成 inspected_data["inspect_report"],灰度期两边都不生效时以 inspected_data 为准。

  3. 个人闲置开关互斥:店铺维度配置 idle_mode ∈ {personal, merchant_verified, verify_treasure}personal 模式下 Mapper 强制剥掉 sp_biz_type/inspected_data/sp_guarantee

  4. 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?


群贤毕至

访客