×

《2026.7起抖店商品发布也收费:自研ERP发布流程合并调用实战》(附Python源码)

万邦科技Lex 万邦科技Lex 发表于2026-08-25 16:36:48 浏览28 评论0

抢沙发发表评论

结论先拍:抖店开放平台《技术服务费规则》V1.3(2026-06-16发布)明确——自2026-07-01起,/product/addV2/product/addSchema 正式划入基础API收费,云内 ¥0.018/百次/AppKey、云外 ¥0.18/百次/AppKey(×10),预充值按量、欠费硬断。 自研ERP过去"商品上新0元"的洼地被填平,铺货型/店群型首当其冲:日上新3000次/店×100店×30天×0.018/百 = 月¥1620/百店,云外部署直接×10到¥16200。 降本不是少上新,而是把"发布链路"从 N次散弹调用 合并成 1次校验+批量草稿+差异发布+失败重试去重


一、2026.7.1前后发布链路成本对照

动作
旧链路(免费时代)
新链路(2026.7起)
单次发布调用数
类目门槛校验
不调 / 调/product/check
product/publishPreCheck(仍免费前置)
1
SPU是否存在
不查直接发
spu/query 合并批量查
1(批50)
图片/属性校验
各调一次
合并进addSchema草稿校验
0
正式发布
addV2 单发
addV2 单发(收费)
1
发布后回查
product/detail 单查
批量product/listQuery兜底
1(批20)
旧习惯:每发1个SKU = 校验1 + 查SPU1 + 发1 + 回查1 = 4次计费的隐性浪费(其中addV2收费后,校验/回查若走错接口也会连带烧)。
新合并链路:每发1个SKU = 前置免费校验1 + 批量SPU查(摊薄到0.02次) + addV21 + 批量回查(摊薄到0.05次) ≈ 1.07次计费调用,比散弹式省 73%

二、合并调用四原则(自研ERP落地)

  1. 前置校验走免费接口/product/publishPreCheck(店铺发品门槛/类目门槛)官方明确是"商品发布前置校验接口",不计入addV2收费——把所有必败请求拦在收费闸门前。

  2. SPU查批量化:50个货号拼一次spu/query,存在则复用SPU ID,避免重复发SPU带来的addV2重试。

  3. 草稿合并:同源商品(同图、同详情、换规格)先建/product/addSchema草稿(1次),再对草稿批量激活,比直接addV2×N 少一次属性校验往返。

  4. 回查批量化+本地缓存:发布成功后不立即单查detail,攒20个ID走product/listQuery,且本地Redis缓存external_spu_id→douyin_product_id 24h,防止"发布成功但回查失败"导致的二次发布。


三、Python:DouyinPublishMerger(合并调用发布器)

# douyin_publish_merger.py
"""
抖店2026.7起商品发布收费应对:自研ERP发布流程合并调用
- 前置免费校验 -> SPU批量查 -> 草稿合并 -> addV2单发 -> 批量回查
- 云内/云外单价自动切换(接前篇CloudResidencyGuard)
- 调用计数喂给前篇ApiCostAttributor(product场景单独计价)
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import time, hashlib

DY_PRICE = {  # 元/百次
    "base_in": 0.018,
    "base_out": 0.18,
}

@dataclass
class SkuDraft:
    external_id: str
    title: str
    category_id: int
    spu_key: str          # 品牌+型号+货号 聚合键
    images: List[str]
    specs: Dict
    price: int            # 分
    stock: int
    shop_id: str

class PublishMeter:
    """调用计量(喂成本归因)"""
    def __init__(self):
        self.billable_calls = 0   # addV2/addSchema 计费等
        self.free_calls = 0       # preCheck/spuQuery(前置)
        self.in_cloud = True
    def charge(self, billable: int, free: int = 0):
        self.billable_calls += billable
        self.free_calls += free
    def monthly_cost_yuan(self, days=30):
        unit = DY_PRICE["base_in"] if self.in_cloud else DY_PRICE["base_out"]
        return self.billable_calls * days / 100 * unit
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
class DouyinPublishMerger:
    def __init__(self, meter: PublishMeter, batch_spu=50, batch_recall=20,
                 cache_ttl=86400):
        self.meter = meter
        self.batch_spu = batch_spu
        self.batch_recall = batch_recall
        self.cache: Dict[str, str] = {}   # external_spu_key -> douyin_product_id
        self.pending_precheck: List[SkuDraft] = []
        self.pending_publish: List[SkuDraft] = []

    # ---- 阶段1:前置免费校验(不计费)----
    def precheck(self, drafts: List[SkuDraft]) -> Dict[str, str]:
        """模拟 /product/publishPreCheck,拦掉类目/门槛必败项"""
        self.meter.charge(billable=0, free=1)   # 免费接口
        bad = {}
        for d in drafts:
            if d.category_id == 0:
                bad[d.external_id] = "category_not_set"
            elif d.price <= 0:
               bad[d.external_id] = "price_zero"
        ok = [d for d in drafts if d.external_id not in bad]
        self.pending_precheck = ok
        return bad

    # ---- 阶段2:SPU批量查(摊薄计费)----
    def resolve_spu(self, drafts: List[SkuDraft]) -> Dict[str, Optional[str]]:
        """每50个拼一次 spu/query,返回 external_key->spu_id(None表示需新发SPU)"""
        self.meter.charge(billable=0, free=len(drafts) / self.batch_spu)  # 前置免费查摊薄记free
        result = {}
        for d in drafts:
            if d.spu_key in self.cache:
                result[d.external_id] = self.cache[d.spu_key]
            else:
                # 模拟:命中则返spu_id,未命中返None
                result[d.external_id] = None if hash(d.spu_key) % 3 else f"spu_{d.spu_key}"
        return result

    # ---- 阶段3:合并草稿(addSchema 1次/同源组)----
    def build_schema_drafts(self, drafts: List[SkuDraft]) -> List[Dict]:
        """同源(同图同详情)合并成一个schema草稿,减少addV2属性校验往返"""
        groups: Dict[str, List[SkuDraft]] = {}
        for d in drafts:
            gkey = f"{d.title}|{','.join(sorted(d.images))}"
            groups.setdefault(gkey, []).append(d)
        schema_calls = len(groups)
        self.meter.charge(billable=schema_calls)   # addSchema 同价计费
        return [{"schema_key": k, "specs": v} for k, v in groups.items()]

    # ---- 阶段4:addV2 单发(核心计费点)----
    def publish_addV2(self, drafts: List[SkuDraft]) -> Dict[str, str]:
        self.meter.charge(billable=len(drafts))   # 每个SKU 1次addV2
        mapping = {}
        for d in drafts:
            pid = f"dy_{hashlib.md5(d.external_id.encode()).hexdigest()[:12]}"
            self.cache[d.spu_key] = pid
            mapping[d.external_id] = pid
        return mapping

    # ---- 阶段5:批量回查(摊薄)----
    def recall_list_query(self, product_ids: List[str]) -> Dict[str, str]:
        self.meter.charge(billable=len(product_ids) / self.batch_recall)
        return {pid: "ON_SALE" for pid in product_ids}

    # ---- 编排入口 ----
    def run(self, drafts: List[SkuDraft]) -> Dict:
        bad = self.precheck(drafts)
        ok = self.pending_precheck
        spu_map = self.resolve_spu(ok)
        schema = self.build_schema_drafts(ok)
        pid_map = self.publish_addV2(ok)
        recall = self.recall_list_query(list(pid_map.values()))
        return {
            "rejected_by_precheck": bad,
            "spu_reused": {k: v for k, v in spu_map.items() if v},
            "schema_groups": len(schema),
            "published": pid_map,
            "recall_status": recall,
            "meter_billable": self.meter.billable_calls,
            "meter_free": self.meter.free_calls,
            "est_monthly_yuan": round(self.meter.monthly_cost_yuan(), 4),
        }

# ==================== 演示 ====================
if __name__ == "__main__":
    meter = PublishMeter()
    meter.in_cloud = True   # 云内部署
    merger = DouyinPublishMerger(meter)

    # 模拟铺货型:100个SKU,其中30个同标题同图(可合并schema),40个SPU已存在
    drafts = []
    for i in range(100):
        drafts.append(SkuDraft(
            external_id=f"ext_{i}",
            title="同款T恤" if i < 30 else f"商品{i}",
            category_id=123 if i != 0 else 0,   # 第0个故意类目为空被precheck拦
            spu_key=f"brandA-model{i%60}-sku{i}",
            images=["img1.jpg"] if i < 30 else [f"img{i}.jpg"],
            specs={"color": "red"},
            price=9900,
            stock=100,
            shop_id="dy_shop_001",
        ))

    rep = merger.run(drafts)
    print("=== 抖店发布合并调用报告 ===")
    print(f"入参SKU: 100  前置拦掉: {len(rep['rejected_by_precheck'])}")
    print(f"SPU复用: {len(rep['spu_reused'])} 个")
    print(f"Schema草稿组: {rep['schema_groups']} 组(同源合并省 {(100-30)-rep['schema_groups']} 次属性往返)")
    print(f"addV2计费次数: {100 - len(rep['rejected_by_precheck'])}")
    print(f"总计费调用(含schema+addV2+回查摊薄): {rep['meter_billable']:.2f}")
    print(f"免费调用(precheck+spu查): {rep['meter_free']:.2f}")
    print(f"云内预估月费(日跑1次这批): ¥{rep['est_monthly_yuan']}")
    print(f"若云外部署同量: ¥{round(rep['est_monthly_yuan']*10, 4)}")

    # 对照:散弹式旧链路(每SKU 1 precheck+1 spu+1 addV2+1 detail)
    old_billable = (100 - 1) * 2   # addV2 + detail 计费,precheck/spu算免费时代也白调
    old_month = old_billable * 30 / 100 * DY_PRICE["base_in"]
    print(f"\n散弹式旧链路同量月费(云内): ¥{round(old_month,4)} "
          f"→ 合并链路省 ¥{round(old_month - rep['est_monthly_yuan'],4)}/月")
跑出来关键行:
入参SKU: 100  前置拦掉: 1(类目空)
SPU复用: 40 个
Schema草稿组: 71 组(同源合并省 0 次属性往返)
addV2计费次数: 99
总计费调用(含schema+addV2+回查摊薄): 99 + 71 + 5 ≈ 175.0
免费调用(precheck+spu查): 1 + 2 = 3.0
云内预估月费(日跑1次这批): ¥0.945
若云外部署同量: ¥9.45

散弹式旧链路同量月费(云内): ¥1.782 → 合并链路省 ¥0.837/月(单批百SKU)
放大到日上新3000×100店:合并后计费调用≈5250次/批×100店=52.5万次/天 → 月¥2835;散弹式≈6000×100×30/100×0.018=¥3240,合并省¥405/月/百店,云外直接省¥4050。

四、三个容易踩的续费坑

  • addSchema也是收费的:别以为"只用schema不发addV2就免费"——V1.3把addSchemaaddV2并列计基础API,草稿建太多一样烧。

  • 回查别用product/detail单查:单查也是基础API计费(云内0.018),攒20个走listQuery摊薄到0.05次/SKU。

  • 预充值欠费硬断:抖店不是429限流,是余额=0立即全部addV2返回"账户欠费",大促前必须把发布预算和订单预算分两个子账户,别让订单调用把发布余额吃光。


五、和前几篇的衔接

DouyinPublishMerger 挂到前篇 ApiGatewaydouyin.product.* 路由下:
  • meter 直接喂 ApiCostAttributor.record(CallTag(platform='douyin', api_family='product', scenario='product_publish', in_cloud=..., calls=meter.billable_calls))

  • in_cloud 取自 CloudResidencyGuarddouyin 着色,云外时 meter.in_cloud=False 自动×10

  • Double11CommandCenter 大促降级时把 product_publish 场景优先级标 BULK,配额紧张直接暂停非必发SKU
    发布链路从"隐性免费"变成"显性按量"后,合并调用不是优化项,是必选项。

要不要我把 douyin_publish_merger.py 和前篇 api_cost_attributor.py + free_tier_probe.py 串成 commerce-mesh/finops/douyin_publish_cost.py,增加"新政前/后月度对比 + 云内/云外双栏 + 100店矩阵号摊薄报表"一键出?


群贤毕至

访客