×

🔁《跨境二手商品等级描述不符触发自动下架:ERP 上传数据的 A/B 级校验闭环》(附 Python 源码)

万邦科技Lex 万邦科技Lex 发表于2026-09-15 10:37:20 浏览16 评论0

抢沙发发表评论

核心事实先钉住:Back Market 官方成色是 Premium / Excellent / Good / Fair,且“所有等级都必须 100% 功能正常、电池至少 80%(Premium 90%)”;成色只描述外观,功能不一致就是违反 Quality Charter。 行业里 A/B/C 只是内部口径,“A 标成 B 卖”叫 undergrade(亏钱),“B 标成 A 卖”叫 overgrading(退货+搜索降权+匿名测试单翻车)。 所以 ERP 要做的不是“传个性别字段”,而是入库质检 → 内部等级 → 平台等级 → 文案/图片/电池/维修记录一致性 → 发布前 A/B 校验 → 售后退货回流再校准的闭环。

🔁《跨境二手商品等级描述不符触发自动下架:ERP 上传数据的 A/B 级校验闭环》(附 Python 源码)

一、惨案公式(为什么 A/B 级会搞死店铺)

内部QC说:  "屏幕有发丝划痕, 算 B"
运营图省事: "标 A 好卖, 价格+8%"
买家收到:  "A 应该有几乎无划痕"
平台机制:  描述外观 ≠ 实物 → 退货/客诉 → 退货率↑ → 算法降权/下架/匿名测试单 → 账户风险
Back Market 自己放匿名测试单(anonymous test orders),卖家自己都分不清是不是真买家;A 级设备如果屏幕有划痕,本身就是违约
行业术语:
  • Overgrading = 等级往高标(最烧钱:退货+逆向物流+搜索降权)

  • Grading drift = 技师久了越来越松(不是故意骗,但结果一样)

  • Undergrading = 往低标(亏毛利,不违规)

ERP 的活:让“标出去的等级”永远 ≤ “实物检出来的等级”

二、统一等级模型(内部 A/B/C ↔ 平台 Premium/Excellent/Good/Fair)

内部       外观标准                         电池        平台(Back Market)   抖音/闲鱼
A / LikeNew  无划痕/无磕碰/近乎新           ≥90%       Premium/Excellent  99新/准新
B / Good     微划痕/轻微磨损/不影响功能     ≥80%       Good              95新/正常使用
C / Fair     可见划痕/磕碰/掉漆/轻凹       ≥80%       Fair              9新/明显使用
D / Parts    功能缺陷/屏裂/无法正常使用     -          不允许多数翻新平台  For parts
关键点:功能不通过 = 不能当 A/B/C 卖;电池不达标 = 不能标 Premium;屏幕有划痕 = 不能标 Excellent/Premium。
这就是“A/B 级校验”的本质——多字段联合判定,不是单个下拉框

三、完整源码:Grade Integrity Loop

# grade_integrity_loop.py
"""
跨境二手 A/B级描述不符防御闭环
- QcRecord: 入库质检(划痕/磕碰/电池/功能/维修)
- InternalGrade: 由QC推导内部等级 (A/B/C/D)
- PlatformGradeMapper: 内部等级 -> BackMarket/Mercari/Vinted/eBay 平台等级
- ListingAssertion: 发布前校验 (标称等级 <= 实物等级, 文案/图片/电池一致)
- ReturnSignalLoop: 退货原因码回流 -> 发现 overgrading / grading drift
- AutoTakedownGuard: 高危SKU禁止发布 / 已发布触发降级或下架申请
"""
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from enum import Enum


# ---------------- 等级枚举 ----------------
class InternalGrade(Enum):
    A = "A"          # like new
    B = "B"          # good / light wear
    C = "C"          # fair / visible wear
    D = "D"          # parts / not functional


class BMGrade(Enum):
    PREMIUM = "Premium"
    EXCELLENT = "Excellent"
    GOOD = "Good"
    FAIR = "Fair"


# ---------------- 入库质检 ----------------
@dataclass
class QcRecord:
    sku: str
    screen_scratch: bool = False        # 屏幕划痕
    body_micro_scratch: bool = False    # 机身发丝划痕
    visible_scratches: bool = False     # 肉眼可见划痕
    dents: bool = False                 # 凹痕
    battery_health: int = 100           # %
    powers_on: bool = True
    camera_ok: bool = True
    wifi_ok: bool = True
    repaired_parts: List[str] = field(default_factory=list)   # ["screen","battery"]
    qc_operator: str = ""
    photos: int = 0                     # 实拍角度数


def infer_internal_grade(qc: QcRecord) -> InternalGrade:
    """功能不正常 -> D; 电池/外观联合判定 A/B/C"""
    if not (qc.powers_on and qc.camera_ok and qc.wifi_ok):
        return InternalGrade.D
    if qc.screen_scratch:
        return InternalGrade.C
    if qc.visible_scratches or qc.dents:
        return InternalGrade.C
    if qc.battery_health >= 90 and not qc.body_micro_scratch:
        return InternalGrade.A
    if qc.battery_health >= 80:
        return InternalGrade.B
    return InternalGrade.C


# ---------------- 平台映射 ----------------
class PlatformGradeMapper:
    BM_MAP = {
        InternalGrade.A: BMGrade.PREMIUM,
        InternalGrade.B: BMGrade.GOOD,
        InternalGrade.C: BMGrade.FAIR,
        InternalGrade.D: None,   # 不允许作为翻新可售
    }

    def to_backmarket(self, g: InternalGrade) -> Optional[BMGrade]:
        return self.BM_MAP.get(g)

    def to_label(self, platform: str, g: InternalGrade) -> str:
        if platform == "backmarket":
            bm = self.to_backmarket(g)
            return bm.value if bm else "NOT_LISTABLE"
        if platform == "generic":
            return {InternalGrade.A: "99新/准新",
                    InternalGrade.B: "95新/正常使用",
                    InternalGrade.C: "9新/明显使用",
                    InternalGrade.D: "故障/配件机"}[g]
        return g.value


# ---------------- 发布前 A/B 校验 ----------------
@dataclass
class ListingDraft:
    sku: str
    platform: str
    claimed_grade: str          # 运营填的: Premium / A / 99新 ...
    title: str = ""
    desc: str = ""
    battery_in_desc: Optional[int] = None
    photo_count: int = 0
    qc: QcRecord = None


class GradeViolation(Exception):
    def __init__(self, issues: List[str]):
        self.issues = issues
        super().__init__("; ".join(issues))


class ListingAssertion:
    def __init__(self, mapper: PlatformGradeMapper):
        self.mapper = mapper

    def assert_listing(self, draft: ListingDraft):
        qc = draft.qc
        if qc is None:
            raise GradeViolation(["无QC记录, 禁止发布"])
        real = infer_internal_grade(qc)
        real_bm = self.mapper.to_backmarket(real)

        issues = []

        # 1. 功能不达标却想卖可售等级
        if real == InternalGrade.D:
            issues.append("功能不达标(InternalGrade=D), 不允许作为翻新可售")

        # 2. 标称等级高于实物等级 = overgrading
        claimed_internal = self._claimed_to_internal(draft.platform, draft.claimed_grade)
        if claimed_internal is not None and self._rank(claimed_internal) > self._rank(real):
            issues.append(
                f"overgrading: 标称 {draft.claimed_grade}({claimed_internal.value}) "
                f"> 实物 {real.value}")

        # 3. Back Market: Premium 必须电池>=90 且无可辨划痕
        if draft.platform == "backmarket" and draft.claimed_grade == "Premium":
            if qc.battery_health < 90:
                issues.append(f"Premium 电池需>=90%, 实际 {qc.battery_health}%")
            if qc.body_micro_scratch or qc.screen_scratch:
                issues.append("Premium 不允许任何可见/发丝划痕")

        # 4. 文案与电池不一致
        if draft.battery_in_desc is not None and draft.battery_in_desc != qc.battery_health:
            issues.append(
                f"描述写电池 {draft.battery_in_desc}%, QC实测 {qc.battery_health}%")

        # 5. 实拍图不足 -> 外观争议无证据
        if qc.photos < 3 or draft.photo_count < 3:
            issues.append("实拍图<3张, 外观争议时无法自证")

        # 6. 维修记录未披露
        if qc.repaired_parts and "换" not in draft.desc and "维修" not in draft.desc:
            issues.append(f"换过部件 {qc.repaired_parts} 但未在描述披露")

        if issues:
            raise GradeViolation(issues)
        return real_bm

    def _claimed_to_internal(self, platform: str, claimed: str) -> Optional[InternalGrade]:
        claimed = claimed.lower()
        if claimed in ("premium", "a", "99新", "准新"):
            return InternalGrade.A
        if claimed in ("excellent", "good", "b", "95新", "正常使用"):
            return InternalGrade.B
        if claimed in ("fair", "c", "9新", "明显使用"):
            return InternalGrade.C
        if claimed in ("parts", "d", "for parts", "配件机"):
            return InternalGrade.D
        return None

    def _rank(self, g: InternalGrade) -> int:
        return {InternalGrade.A: 3, InternalGrade.B: 2,
                InternalGrade.C: 1, InternalGrade.D: 0}[g]


# ---------------- 售后回流:发现 overgrading ----------------
@dataclass
class ReturnCase:
    sku: str
    claimed_grade: str
    return_reason: str   # "not_as_described", "scratch", "battery", "dead_pixel"...
    qc: QcRecord = None


class ReturnSignalLoop:
    def __init__(self):
        self.by_sku_grade: Dict[tuple, int] = {}

    def ingest(self, case: ReturnCase):
        key = (case.sku, case.claimed_grade)
        self.by_sku_grade[key] = self.by_sku_grade.get(key, 0) + 1

    def overgrading_alert(self, threshold: int = 3) -> List[Dict]:
        alerts = []
        for (sku, claimed), cnt in self.by_sku_grade.items():
            if cnt >= threshold:
                alerts.append({
                    "sku": sku, "claimed": claimed,
                    "return_cases": cnt,
                    "suspect": "overgrading / grading drift",
                    "action": "暂停该SKU发布 + 盲检复评 + 已上架降级",
                })
        return alerts


# ---------------- 自动下架防御 ----------------
class AutoTakedownGuard:
    def __init__(self, assertion: ListingAssertion, returns: ReturnSignalLoop):
        self.assertion = assertion
        self.returns = returns

    def pre_publish(self, draft: ListingDraft) -> Dict:
        try:
            bm = self.assertion.assert_listing(draft)
            return {"ok": True, "backmarket_grade": bm.value if bm else None}
        except GradeViolation as e:
            return {"ok": False, "block": True,
                    "issues": e.issues, "action": "禁止发布"}

    def post_return_heal(self, draft: ListingDraft) -> Dict:
        """退货聚集后: 自动降级到实物等级重新发, 不硬撑A"""
        real = infer_internal_grade(draft.qc)
        bm = PlatformGradeMapper().to_backmarket(real)
        return {
            "action": "relist_as_real_grade",
            "new_claimed_grade": real.value,
            "backmarket_grade": bm.value if bm else "NOT_LISTABLE",
        }

# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
# ==================== DEMO ====================
if __name__ == "__main__":
    mapper = PlatformGradeMapper()
    assertion = ListingAssertion(mapper)
    returns = ReturnSignalLoop()
    guard = AutoTakedownGuard(assertion, returns)

    print("=== 1. B级实物被运营标成 Premium (overgrading) ===")
    qc_b = QcRecord(sku="IP13-001", body_micro_scratch=True,
                    battery_health=85, photos=4, repaired_parts=["battery"])
    draft = ListingDraft(sku="IP13-001", platform="backmarket",
                         claimed_grade="Premium",
                         desc="准新机 电池健康 95%",
                         battery_in_desc=95, photo_count=4, qc=qc_b)
    print("  ", guard.pre_publish(draft))
    # -> block: overgrading + Premium电池不足 + 维修未披露

    print("\n=== 2. 真实A级 (无划痕/电池93) 标 Premium ===")
    qc_a = QcRecord(sku="IP13-002", battery_health=93, photos=5)
    draft_a = ListingDraft(sku="IP13-002", platform="backmarket",
                           claimed_grade="Premium",
                           desc="无划痕 电池93% 官方级翻新",
                           battery_in_desc=93, photo_count=5, qc=qc_a)
    print("  ", guard.pre_publish(draft_a))
    # -> ok: Premium

    print("\n=== 3. 售后回流: 同一SKU标A被退3次 ===")
    for _ in range(3):
        returns.ingest(ReturnCase("IP13-003", "A", "not_as_described",
                                  QcRecord(screen_scratch=True, battery_health=82)))
    print("  alerts:", returns.overgrading_alert())

    print("\n=== 4. 自动愈合: 不再硬标A, 按实物降级 ===")
    qc_c = QcRecord(sku="IP13-003", screen_scratch=True, battery_health=82, photos=3)
    draft_c = ListingDraft(sku="IP13-003", platform="backmarket",
                           claimed_grade="A", desc="屏幕有轻微划痕 电池82%",
                           battery_in_desc=82, photo_count=3, qc=qc_c)
    print("  ", guard.post_return_heal(draft_c))
    # -> relist as C / Fair

    print("\n=== 5. 功能坏机想当B卖 ===")
    qc_dead = QcRecord(sku="IP13-004", powers_on=False, camera_ok=False)
    draft_dead = ListingDraft(sku="IP13-004", platform="backmarket",
                              claimed_grade="Good", qc=qc_dead)
    print("  ", guard.pre_publish(draft_dead))
    # -> block: InternalGrade=D 不允许可售
跑出来核心就是:
1. {'ok': False, 'block': True, 'issues': [
     'overgrading: 标称 Premium(A) > 实物 B',
     'Premium 电池需>=90%, 实际 85%',
     '描述写电池 95%, QC实测 85%',
     '换过部件 [...]/电池... 但未在描述披露'], 'action': '禁止发布'}

2. {'ok': True, 'backmarket_grade': 'Premium'}

3. alerts: [{'sku':'IP13-003','claimed':'A','return_cases':3,
             'suspect':'overgrading / grading drift','action':'暂停该SKU发布+盲检复评'}]

4. {'action':'relist_as_real_grade','new_claimed_grade':'C','backmarket_grade':'Fair'}

5. {'ok': False, 'block': True, 'issues':['功能不达标(InternalGrade=D)...']}

四、A/B 级校验闭环(架构图)

入库QC ──► InternalGrade(A/B/C/D)
   │
   ├─ PlatformGradeMapper ──► BackMarket(Premium/Excellent/Good/Fair)
   │                        └─ 抖音/闲鱼/Mercari/Vinted 本地成色词
   │
   ▼
ListingAssertion (发布闸门)
   • claimed <= real
   • 电池/划痕/维修/实拍图 一致性
   • 平台宪章对齐 (BM: 功能全过/电池80%/Premium90%)
   │
   ├─ 不通过 ──► 禁止发布 + 告警 + 留审计日志
   └─ 通过   ──► 上架
                │
                ▼
          ReturnCases 回流
                │
                ▼
          ReturnSignalLoop
          (同SKU+同标称等级退货>=N → overgrading警报)
                │
                ▼
          AutoTakedownGuard.post_return_heal()
          硬标A的 → 按实物降级为B/C
          严重漂移 → 停发 + 盲检复评(反向打分)
盲检复评(reverse blind grading) 是行业里防 grading drift 最稳的办法:让另一个技师在不看原等级的情况下重新检,和原结果比对。

五、6 条铁律

  1. 标称等级永远 ≤ 实物等级:宁可 undergrade 亏毛利,别 overgrading 丢账户。

  2. A/B/C 不是形容词,是字段组合:划痕、磕碰、电池、功能、维修记录、实拍张数,全进规则引擎。

  3. Back Market 成色只管外观,功能必须全过:把“A 级但触屏偶尔失灵”发出去 = 违反 Quality Charter。

  4. 电池数字是高危字段:描述写 95%、QC 写 85%,平台/买家一投诉就实锤“描述不符”。

  5. 退货原因码是唯一真相源:按 SKU × 标称等级 聚合,退货集中 = 等级标准漂了,不是“买家事多”。

  6. 自动下架防御要双向:发布前拦错标 + 售后后自动降级;别等平台算法把店铺流量关了再救。


六、和前几篇串起来

  • 前篇 Back Market Compliance Gate:主体/VAT/OAuth/Quality Charter 是“能不能卖”;

  • 本篇 Grade Integrity Loop:成色 A/B 一致性是“卖得久不久”;

  • 前篇 Vinted GPSR / Mercari 限流 / eBay ConditionGuard:都是“平台替你守买家体验,ERP 必须先把规则内建”;

  • 前篇 ModeSelector:认证服务商模式值不值——看这种闭环要养多少质检/SOP 就懂了。

二手跨境电商的底层公式:
可信等级 = QC 证据 × 平台宪章 × 描述一致性
三者任一断,A 级也会变“描述不符”。
要不要我把 grade_integrity_loop.py 合进 commerce-mesh/core/:做成 GradeEngine + QcChecklistRegistry(按手机/平板/服饰/奢品挂不同清单)+ ReturnDriftDetector,并和前篇 ObservabilityMiddleware 出一张 Overgrading 风险面板(按 SKU / 技师 / 班次 / 类目拆分)?


群贤毕至

访客