🛡️《eBay 二手专区 × ERP:Vintage/Refurbished 商品属性与平台自动下架防御》(附 Python 源码)
stuff_status=95 塞进去"那么简单——New=1000/New other=1500/Certified Refurbished=2000/Seller Refurbished=2500/Used=3000/For parts=7000是按类目生效的;Manufacturer Refurbished已废弃,美/澳等地2000现在是 Certified Refurbished,要用得先过 eBay Refurbished Program 审核;手机类(9355)里
Seller Refurbished=2500早就被禁,必须改用2010/2020/2030或退到Used=3000;真正吓人的是账号级限制(如 MC011):不是单条 listing 违规,而是活跃 listing 被移除、打款被hold、新建 listing 被禁。
一、eBay 二手/翻新 ConditionID 实战表
ConditionID | 含义 | ERP 处理 |
|---|---|---|
1000 | New | 全新,别和二手混 |
1500 | New other (see details) | 近全新,描述必填 |
1750 | New with defects | 全新但有瑕疵,必须写 defect |
2000 | Certified Refurbished(原 Manufacturer Refurbished) | 需 Refurbished Program 预审,否则建单/改单被 block |
2010 / 2020 / 2030 | Excellent / Very Good / Good – Refurbished | 手机类等类目替代 Seller Refurbished |
2500 | Seller Refurbished | 部分类目禁用(手机美/加/英/德/澳) |
3000 | Used / Pre-owned | 二手主力;时装类进一步拆 Excellent/Good/Fair |
4000 / 5000 / 6000 | Very Good / Good / Acceptable | 图书/收藏/服饰场景 |
7000 | For parts or not working | 垃圾机/配件机,别进"可售库存" |
关键坑:ConditionID 不是全局常量,是"类目 × 站点 × 卖家资格"的三维函数。ERP 必须调getItemConditionPolicies(Metadata API)按类目拉白名单,不能写死一张表。
二、自动下架的两种性质(ERP 必须分开处理)
平台动作 ├── 1. Listing 级违规 │ · Condition 不在该类目允许列表 │ · Item Specifics / 类目不匹配 │ · 品牌/Catalog 要求未满足 │ → 单条 Ended / Blocking error / Warning→Error │ └── 2. 账号级限制 (MC011 等) · 缺陷率/迟发率/取消率/追踪上传不达标 · 突然爆量、代发供应链不稳 · 高风险品类(电子/品牌/医疗) → 活跃 listing 被移除 + 打款 hold + 禁止新建
MC011 不是 API 错误码,是信任与安全审查状态。ERP 不能"重试解决",只能:检测 → 暂停自动发布 → 拉指标 → 人工申诉。
三、完整源码:ConditionGuard + ListingDefense
# ebay_secondhand_defense.py
"""
eBay 二手/翻新 ERP 防御层
- ConditionPolicyCache: 按 category_id 拉取允许的 ConditionID (模拟 getItemConditionPolicies)
- ConditionGuard: 发布前校验 (Vintage/Refurbished/For-parts 分流)
- ListingDefense: 轮询/消息发现 Ended -> 分类 -> Revise / Relist / 人工
- AccountHealthProbe: MC011 类账号级风险探测 (缺陷率/迟发/取消/追踪)
复用前几篇: unified_adapter_layer.Product / MarketplaceAdapter 思路
"""
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime, timezone
# ==================== Condition 策略 ====================
class EbayCondition(Enum):
NEW = 1000
NEW_OTHER = 1500
CERTIFIED_REFURB = 2000
EXCELLENT_REFURB = 2010
VERYGOOD_REFURB = 2020
GOOD_REFURB = 2030
SELLER_REFURB = 2500
USED = 3000
VERY_GOOD = 4000
GOOD = 5000
ACCEPTABLE = 6000
FOR_PARTS = 7000
# 类目 -> 允许 condition + 特殊约束
CATEGORY_POLICY: Dict[str, Dict] = {
"9355": { # Cell Phones & Smartphones (US/UK/DE/CA/AU)
"allowed": [2000, 2010, 2020, 2030, 3000],
"forbidden": [2500], # Seller Refurbished 禁用
"refurb_requires_program": True,
"note": "手机类必须用 CR/Refurb 分级,2500 已禁",
},
"15724": { # Vintage & Antique 服饰/收藏
"allowed": [3000, 4000, 5000, 6000],
"forbidden": [2000, 2500],
"refurb_requires_program": False,
"note": "Vintage 走 Used 分级,不允许 Refurbished",
},
"31387": { # Home & Garden 部分叶子类目
"allowed": [2000, 2500, 3000, 4000, 5000],
"forbidden": [],
"refurb_requires_program": True, # eBay Refurbished Program 叶子类目超2200个
"note": "Refurb 需程序资格",
},
}
REFURB_IDS = {2000, 2010, 2020, 2030, 2500}
# ==================== 策略缓存 ====================
class ConditionPolicyCache:
def __init__(self, policies: Dict = None):
self.policies = policies or CATEGORY_POLICY
def allowed(self, category_id: str) -> List[int]:
return self.policies.get(category_id, {}).get(
"allowed", [1000, 1500, 3000])
def is_refurb(self, condition_id: int) -> bool:
return condition_id in REFURB_IDS
def refurb_requires_program(self, category_id: str) -> bool:
return self.policies.get(category_id, {}).get("refurb_requires_program", False)
# ==================== 发布前守卫 ====================
@dataclass
class GuardResult:
ok: bool
level: str = "ok" # ok / warn / block
reasons: List[str] = field(default_factory=list)
suggested_condition: Optional[int] = None
class ConditionGuard:
def __init__(self, cache: ConditionPolicyCache,
refurb_program_approved: bool = False):
self.cache = cache
self.approved = refurb_program_approved
def check(self, category_id: str, condition_id: int,
description: str = "", has_defect: bool = False) -> GuardResult:
reasons = []
allowed = self.cache.allowed(category_id)
# 1. 类目不允许该 condition
if condition_id not in allowed:
reasons.append(f"condition {condition_id} 不在类目 {category_id} 白名单")
# 自动降级建议
if condition_id in REFURB_IDS and 3000 in allowed:
return GuardResult(False, "block", reasons, suggested_condition=3000)
return GuardResult(False, "block", reasons)
# 2. Refurb 但没过程序
if self.cache.is_refurb(condition_id) and \
self.cache.refurb_requires_program(category_id) and not self.approved:
reasons.append("该类目 Refurbished 需 eBay Refurbished Program 预审")
if 3000 in allowed:
return GuardResult(False, "block", reasons, suggested_condition=3000)
return GuardResult(False, "block", reasons)
# 3. For parts 不能进可售库存
if condition_id == 7000 and "for_parts" not in (description or "").lower():
reasons.append("For parts 商品必须描述中明确不可正常使用")
# 4. New other / defects 必须有描述
if condition_id in (1500, 1750) and not description.strip():
reasons.append(f"condition {condition_id} 必须有 conditionDescription")
if reasons and condition_id in (1500, 1750):
return GuardResult(False, "warn", reasons)
if reasons:
return GuardResult(False, "block", reasons)
return GuardResult(True, "ok", ["condition 合规"])
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
# ==================== 下架防御 ====================
class ListingState(Enum):
ACTIVE = "Active"
ENDED_BY_SELLER = "Ended"
ENDED_BY_EBAY = "Ended" # 需结合 EndReason / 违规标记判断
SOLD_OUT_HIDDEN = "Ended" # OutOfStockControl 隐藏但可补货
@dataclass
class ListingSnapshot:
item_id: str
category_id: str
condition_id: int
selling_state: str
end_reason: Optional[str]
quantity: int
violations: List[str] = field(default_factory=list)
class ListingDefense:
"""发现 Ended -> 分类 -> 决策"""
def classify(self, snap: ListingSnapshot) -> str:
# 1. 账号级限制导致
if "MC011" in snap.violations or "ACCOUNT_RESTRICTED" in snap.violations:
return "account_restriction" # 不能自动 Relist
# 2. 条件/类目违规
if any(v.startswith("CONDITION") or v.startswith("CATEGORY") for v in snap.violations):
return "policy_violation"
# 3. 卖光但开了 Out-of-Stock 控制
if snap.quantity == 0 and snap.selling_state == "Ended":
return "out_of_stock"
# 4. 正常售罄
if snap.quantity == 0:
return "sold_out"
# 5. 平台无理由结束
return "platform_ended"
def decide(self, snap: ListingSnapshot) -> Dict:
kind = self.classify(snap)
if kind == "account_restriction":
return {"action": "halt_automation",
"next": "人工申诉 + 拉 Seller Standards",
"auto": False}
if kind == "policy_violation":
return {"action": "revise_then_relist",
"next": "ConditionGuard 修正 condition/category 后 Revise",
"auto": True}
if kind == "out_of_stock":
return {"action": "keep_hidden_until_replenish",
"next": "补货后 Revise quantity,不开新 listing",
"auto": True}
if kind == "sold_out":
return {"action": "relist_or_archive",
"next": "有复用模板则 Relist,否则归档",
"auto": True}
return {"action": "investigate",
"next": "查 GetItem 的 ListingRecommendations/Violations",
"auto": False}
# ==================== 账号健康探针 (MC011 预防) ====================
@dataclass
class SellerHealth:
defect_rate: float = 0.0 # <2% 安全
late_ship_rate: float = 0.0 # 越低越好
cancel_rate: float = 0.0 # 越低越好
tracking_upload_rate: float = 0.0 # 越高越好
out_of_stock_rate: float = 0.0 # eBay Refurbished 要求 <2%
snad_return_rate: float = 0.0 # <4%
class AccountHealthProbe:
THRESHOLDS = {
"defect_rate": 0.02,
"late_ship_rate": 0.03,
"cancel_rate": 0.02,
"tracking_upload_rate": 0.95, # 至少 95%
"out_of_stock_rate": 0.02,
"snad_return_rate": 0.04,
}
def risk(self, h: SellerHealth) -> Dict:
alerts = []
if h.defect_rate > self.THRESHOLDS["defect_rate"]:
alerts.append("缺陷率超 2% → MC011 高风险")
if h.late_ship_rate > self.THRESHOLDS["late_ship_rate"]:
alerts.append("迟发率超 3%")
if h.cancel_rate > self.THRESHOLDS["cancel_rate"]:
alerts.append("取消率超 2% → 库存/代发问题")
if h.tracking_upload_rate < self.THRESHOLDS["tracking_upload_rate"]:
alerts.append("有效追踪上传率 <95%")
if h.out_of_stock_rate > self.THRESHOLDS["out_of_stock_rate"]:
alerts.append("Out-of-stock 率超 2% → Refurbished 资格危险")
if h.snad_return_rate > self.THRESHOLDS["snad_return_rate"]:
alerts.append("SNAD 退货率超 4%")
return {
"mc011_risk": "HIGH" if alerts else "LOW",
"alerts": alerts,
"auto_publish_allowed": len(alerts) == 0,
}
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
# ==================== 演示 ====================
if __name__ == "__main__":
cache = ConditionPolicyCache()
guard = ConditionGuard(cache, refurb_program_approved=False)
defense = ListingDefense()
health = AccountHealthProbe()
print("=== 1. 手机类用 Seller Refurbished (2500) → 应 block 并降级 Used ===")
r = guard.check("9355", 2500, description="iPhone 13 seller refurb")
print(f" ok={r.ok} level={r.level} suggest={r.suggested_condition} reasons={r.reasons}")
print("\n=== 2. 手机类用 Certified Refurb (2000) 但没过程序 → block ===")
r = guard.check("9355", 2000, description="certified refurb iPhone")
print(f" ok={r.ok} level={r.level} suggest={r.suggested_condition} reasons={r.reasons}")
print("\n=== 3. Vintage 类用 Refurbished → block ===")
r = guard.check("15724", 2500, description="vintage jacket")
print(f" ok={r.ok} level={r.level} reasons={r.reasons}")
print("\n=== 4. 手机类用 Used=3000 → ok ===")
r = guard.check("9355", 3000, description="used iPhone 13 95% battery")
print(f" ok={r.ok} level={r.level} reasons={r.reasons}")
print("\n=== 5. 下架分类: 账号级限制 ===")
snap = ListingSnapshot("IT-1", "9355", 3000, "Ended",
end_reason=None, quantity=3, violations=["MC011"])
print(f" classify={defense.classify(snap)} → {defense.decide(snap)}")
print("\n=== 6. 下架分类: condition 违规 → revise 后 relist ===")
snap = ListingSnapshot("IT-2", "9355", 2500, "Ended",
end_reason="Policy violation", quantity=2,
violations=["CONDITION_NOT_ALLOWED"])
print(f" classify={defense.classify(snap)} → {defense.decide(snap)}")
print("\n=== 7. 下架分类: 卖光但开 Out-of-Stock ===")
snap = ListingSnapshot("IT-3", "31387", 3000, "Ended",
end_reason=None, quantity=0,
violations=[])
print(f" classify={defense.classify(snap)} → {defense.decide(snap)}")
print("\n=== 8. 账号健康: 代发翻车前兆 ===")
h = SellerHealth(defect_rate=0.03, late_ship_rate=0.05,
cancel_rate=0.04, tracking_upload_rate=0.88,
out_of_stock_rate=0.03, snad_return_rate=0.06)
print(f" {health.risk(h)}")=== 1. 手机类用 Seller Refurbished (2500) → 应 block 并降级 Used ===
ok=False level=block suggest=3000 reasons=['condition 2500 不在类目 9355 白名单']
=== 2. 手机类用 Certified Refurb (2000) 但没过程序 → block ===
ok=False level=block suggest=3000 reasons=['该类目 Refurbished 需 eBay Refurbished Program 预审']
=== 5. 下架分类: 账号级限制 ===
classify=account_restriction → {'action': 'halt_automation', 'auto': False, ...}
=== 6. 下架分类: condition 违规 → revise 后 relist ===
classify=policy_violation → {'action': 'revise_then_relist', 'auto': True, ...}
=== 8. 账号健康: 代发翻车前兆 ===
{'mc011_risk': 'HIGH', 'auto_publish_allowed': False,
'alerts': ['缺陷率超 2% → MC011 高风险', '迟发率超 3%', '取消率超 2% → 库存/代发问题', ...]}四、ERP 落地五条铁律
ConditionID 不写死:按
category_id + site + seller_eligibility动态拉getItemConditionPolicies,否则手机类用 2500、时尚类用 2000 都会后期被 eBay 强制结束。Refurbished ≠ 随便标:
2000/2010/2020/2030走 eBay Refurbished Program,要预审、要 30 天退货、要保修/物流标准;没资格就用Used=3000。For parts(7000) 不进可售库存:它和"二手可售"是两套供应链,混在一起会拉高 SNAD 退货率,反过来诱发 MC011。
Ended 必须先分类再动作:
账号限制 → 停自动化
条件/类目违规 → Guard 修正后 Revise
0 库存 → 补货后 Revise quantity(别开新 listing 制造重复刊登)
售罄 → Relist/归档
MC011 是经营问题不是 API 问题:追踪上传率、迟发率、取消率、SNAD 率、out-of-stock 率全部进
AccountHealthProbe,红色就停自动发布,比"被限制后救账号"便宜 100 倍。
五、和前几篇的衔接
前篇
MarketplaceAdapter的领域模型里,condition_int到 eBay 要经EbayConditionResolver(统一 0~100 → eBay ConditionID + conditionDescription)。前篇
ComplianceGate在国内管图片/字段,在 eBay 就变成 ConditionGuard + CategoryPolicyCache。前篇
StockEngine的"0 库存"语义在 eBay 要接 OutOfStockControlPreference:卖光不结束 listing、补货即恢复,避免频繁 Relist 被算重复刊登。前篇
ObservabilityMiddleware在这里多挂一个 AccountHealthProbe 面板:MC011 风险 HIGH = 自动发布开关置灰。
eBay 二手专区的本质:成色不是字段,是"类目 × 资格 × 信任分"的三维对象;ERP 的活不是发上去,而是别让平台替你删。
ebay_secondhand_defense.py 扩成 commerce-mesh/adapters/ebay/:真实 getItemConditionPolicies 缓存预热、Trading/Inventory API 的 Revise/Relist 幂等、MC011 风险评分落库、以及和闲鱼/Back Market/Mercari 统一进 AccountHealthProbe 的多平台信任分看板?