结论先拍:抖店开放平台《技术服务费规则》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落地)
前置校验走免费接口:
/product/publishPreCheck(店铺发品门槛/类目门槛)官方明确是"商品发布前置校验接口",不计入addV2收费——把所有必败请求拦在收费闸门前。SPU查批量化:50个货号拼一次
spu/query,存在则复用SPU ID,避免重复发SPU带来的addV2重试。草稿合并:同源商品(同图、同详情、换规格)先建
/product/addSchema草稿(1次),再对草稿批量激活,比直接addV2×N 少一次属性校验往返。回查批量化+本地缓存:发布成功后不立即单查detail,攒20个ID走
product/listQuery,且本地Redis缓存external_spu_id→douyin_product_id24h,防止"发布成功但回查失败"导致的二次发布。
三、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)
四、三个容易踩的续费坑
addSchema也是收费的:别以为"只用schema不发addV2就免费"——V1.3把addSchema和addV2并列计基础API,草稿建太多一样烧。回查别用
product/detail单查:单查也是基础API计费(云内0.018),攒20个走listQuery摊薄到0.05次/SKU。预充值欠费硬断:抖店不是429限流,是余额=0立即全部
addV2返回"账户欠费",大促前必须把发布预算和订单预算分两个子账户,别让订单调用把发布余额吃光。
五、和前几篇的衔接
把DouyinPublishMerger挂到前篇ApiGateway的douyin.product.*路由下:
meter直接喂ApiCostAttributor.record(CallTag(platform='douyin', api_family='product', scenario='product_publish', in_cloud=..., calls=meter.billable_calls))
in_cloud取自CloudResidencyGuard的douyin着色,云外时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店矩阵号摊薄报表"一键出?