[{"content":"源码仅供学习参考用，请勿用于其他用途\nimport pandas as pd import numpy as np import math # ================= 1. 读取与合并 ================= df_sz = pd.read_excel(\u0026#39;对账单.xlsx\u0026#39;, sheet_name=\u0026#39;深圳\u0026#39;, skiprows=4, keep_default_na=False, dtype=str) df_sz[\u0026#39;发货地\u0026#39;] = \u0026#39;深圳\u0026#39; df_sh = pd.read_excel(\u0026#39;对账单.xlsx\u0026#39;, sheet_name=\u0026#39;上海\u0026#39;, skiprows=4, keep_default_na=False, dtype=str) df_sh[\u0026#39;发货地\u0026#39;] = \u0026#39;上海\u0026#39; df_all = pd.concat([df_sz, df_sh], ignore_index=True) df_all = df_all[df_all[\u0026#39;原单号\u0026#39;] != \u0026#39;\u0026#39;] # ================= 2. 数据类型提前统一清洗 ================= # 将所有参与计算的列一次性转化为浮点数，无法转换的直接变 0，省去后续无尽的报错检查 num_cols =[\u0026#39;称重\u0026#39;, \u0026#39;抛重\u0026#39;, \u0026#39;长宽高之和\u0026#39;, \u0026#39;计费重量\u0026#39;, \u0026#39;欠费总金额\u0026#39;] for col in num_cols: df_all[col] = pd.to_numeric(df_all[col], errors=\u0026#39;coerce\u0026#39;).fillna(0) # ================= 3. 渠道清洗与标准化 ================= df_map = pd.read_excel(\u0026#39;映射表.xlsx\u0026#39;) channel_map = dict(zip(df_map[\u0026#39;账单中的渠道字段\u0026#39;], df_map[\u0026#39;清洗后\u0026#39;])) df_all[\u0026#39;渠道_清洗\u0026#39;] = df_all[\u0026#39;渠道\u0026#39;].map(channel_map).fillna(df_all[\u0026#39;渠道\u0026#39;]) conditions = [ df_all[\u0026#39;渠道_清洗\u0026#39;].str.contains(\u0026#39;佐川\u0026#39;, na=False), df_all[\u0026#39;渠道_清洗\u0026#39;].str.contains(\u0026#39;黑猫\u0026#39;, na=False) ] choices = [df_all[\u0026#39;发货地\u0026#39;] + df_all[\u0026#39;渠道_清洗\u0026#39;], \u0026#39;黑猫投函\u0026#39;] df_all[\u0026#39;标准渠道名称\u0026#39;] = np.select(conditions, choices, default=df_all[\u0026#39;渠道_清洗\u0026#39;]) # ================= 4. 处理计费重量 (5%免抛逻辑) ================= df_all[\u0026#39;系统计费重量\u0026#39;] = 0.0 total_tickets = len(df_all) max_exemptions = int(total_tickets * 0.05) count_over_120 = len(df_all[df_all[\u0026#39;长宽高之和\u0026#39;] \u0026gt;= 120]) mask_under_120 = df_all[\u0026#39;长宽高之和\u0026#39;] \u0026lt; 120 mask_120_140 = (df_all[\u0026#39;长宽高之和\u0026#39;] \u0026gt;= 120) \u0026amp; (df_all[\u0026#39;长宽高之和\u0026#39;] \u0026lt; 140) mask_over_140 = df_all[\u0026#39;长宽高之和\u0026#39;] \u0026gt;= 140 # 规则1 \u0026amp; 规则3 (利用 Numpy 向量化取最大值) df_all.loc[mask_under_120, \u0026#39;系统计费重量\u0026#39;] = df_all.loc[mask_under_120, \u0026#39;称重\u0026#39;] df_all.loc[mask_over_140, \u0026#39;系统计费重量\u0026#39;] = df_all.loc[mask_over_140, [\u0026#39;称重\u0026#39;, \u0026#39;抛重\u0026#39;]].max(axis=1) # 规则2 idx_120_140 = df_all[mask_120_140].index.tolist() if count_over_120 \u0026lt;= max_exemptions: df_all.loc[idx_120_140, \u0026#39;系统计费重量\u0026#39;] = df_all.loc[idx_120_140, \u0026#39;称重\u0026#39;] else: excess_count = count_over_120 - max_exemptions if excess_count \u0026gt;= len(idx_120_140): df_all.loc[idx_120_140, \u0026#39;系统计费重量\u0026#39;] = df_all.loc[idx_120_140,[\u0026#39;称重\u0026#39;, \u0026#39;抛重\u0026#39;]].max(axis=1) else: exempt_idx = idx_120_140[:(len(idx_120_140) - excess_count)] throw_idx = idx_120_140[(len(idx_120_140) - excess_count):] df_all.loc[exempt_idx, \u0026#39;系统计费重量\u0026#39;] = df_all.loc[exempt_idx, \u0026#39;称重\u0026#39;] df_all.loc[throw_idx, \u0026#39;系统计费重量\u0026#39;] = df_all.loc[throw_idx, [\u0026#39;称重\u0026#39;, \u0026#39;抛重\u0026#39;]].max(axis=1) df_all[\u0026#39;重量\u0026#39;] = df_all[\u0026#39;系统计费重量\u0026#39;] df_all = df_all[df_all[\u0026#39;重量\u0026#39;] \u0026gt; 0] # ================= 5. 计算基础运费 (提速优化) ================= df_rule = pd.read_excel(\u0026#39;规则表.xlsx\u0026#39;) # 【优化】：按渠道名称将规则分组缓存为字典，避免 apply 循环中重复检索 DataFrame，提速极其明显！ rules_dict = {channel: group for channel, group in df_rule.groupby(\u0026#39;渠道名称\u0026#39;)} def compute_fee_fast(row): channel = row[\u0026#39;标准渠道名称\u0026#39;] weight = row[\u0026#39;重量\u0026#39;] sum_cm = row[\u0026#39;长宽高之和\u0026#39;] # 前面已经转为float了，这里直接用 if channel not in rules_dict: return 0.0 lookup_weight = max(weight, 10.01) if sum_cm \u0026gt;= 120 else weight sub = rules_dict[channel] # 直接从字典取规则，瞬间完成 matched = sub[(sub[\u0026#39;重量下限 (含)\u0026#39;] \u0026lt;= lookup_weight) \u0026amp; (lookup_weight \u0026lt; sub[\u0026#39;重量上限 (不含)\u0026#39;])] if matched.empty: return 0.0 rule = matched.iloc[0] if weight \u0026lt;= rule[\u0026#39;首重 (kg)\u0026#39;]: return rule[\u0026#39;首重费\u0026#39;] else: extra_units = math.ceil(round((weight - rule[\u0026#39;首重 (kg)\u0026#39;]) / rule[\u0026#39;续重单位 (kg)\u0026#39;], 5)) return rule[\u0026#39;首重费\u0026#39;] + extra_units * rule[\u0026#39;续重单价\u0026#39;] df_all[\u0026#39;运费\u0026#39;] = df_all.apply(compute_fee_fast, axis=1) # ================= 6. 其他附加费 (全面向量化优化) ================= df_all[\u0026#39;检品费\u0026#39;] = 2.0 # 6.1 偏远费优化：使用 np.select，告别逐行扫描 is_remote_addr = df_all[\u0026#39;收件地址\u0026#39;].str.contains(\u0026#39;北海道|沖縄\u0026#39;, na=False) is_sagawa = df_all[\u0026#39;标准渠道名称\u0026#39;].str.contains(\u0026#39;佐川\u0026#39;, na=False) is_jhs = df_all[\u0026#39;标准渠道名称\u0026#39;].str.contains(\u0026#39;聚划算\u0026#39;, na=False) remote_conds =[ (~is_remote_addr), # 不偏远 (is_remote_addr \u0026amp; is_sagawa \u0026amp; (df_all[\u0026#39;长宽高之和\u0026#39;] \u0026gt;= 120)), # 偏远+佐川+\u0026gt;=120 (is_remote_addr \u0026amp; is_sagawa \u0026amp; (df_all[\u0026#39;长宽高之和\u0026#39;] \u0026lt; 120)), # 偏远+佐川+\u0026lt;120 (is_remote_addr \u0026amp; is_jhs) # 偏远+聚划算 ] remote_vals =[0, 70, 50, 20] df_all[\u0026#39;计算偏远费\u0026#39;] = np.select(remote_conds, remote_vals, default=0) # 6.2 超长费优化：使用 pd.cut 区间映射神器 # 因为 \u0026gt;=240 都是 300 元，所以最后一段直接写 240 到 无穷大(float(\u0026#39;inf\u0026#39;)) bins =[-1, 159.99, 180, 200, 220, 240, float(\u0026#39;inf\u0026#39;)] labels =[0, 100, 120, 150, 200, 300] # 直接映射 df_all[\u0026#39;计算超长费\u0026#39;] = pd.cut(df_all[\u0026#39;长宽高之和\u0026#39;], bins=bins, labels=labels).astype(float) # ================= 7. 汇总与输出 ================= df_all[\u0026#39;运费\u0026#39;] = df_all[\u0026#39;运费\u0026#39;] + df_all[\u0026#39;检品费\u0026#39;] + df_all[\u0026#39;计算偏远费\u0026#39;] + df_all[\u0026#39;计算超长费\u0026#39;] # 计算差异 df_all[\u0026#39;运费差异\u0026#39;] = df_all[\u0026#39;运费\u0026#39;] - df_all[\u0026#39;欠费总金额\u0026#39;] df_all.to_excel(\u0026#39;对账单_运费计算结果.xlsx\u0026#39;, index=False) print(\u0026#34;✅ 对账计算完成！已输出文件。\u0026#34;) ","date":"2026-05-02T00:00:00Z","permalink":"https://nbcaishui.com/p/%E7%89%A9%E6%B5%81%E5%AF%B9%E8%B4%A6%E5%8D%95%E6%BA%90%E7%A0%81/","title":"物流对账单源码"},{"content":" 做跨境电商的人都懂：每天早上第一件事，就是登录 OZON 后台，一个店铺一个店铺地看昨天的数据——销售额、退款、广告费、有效订单、取消订单……几家店铺切换下来，半小时过去了，Excel 上还没写一个字。\n于是我花了一点时间，把这件事彻底自动化了。本文记录整个过程，既是技术复盘，也是对\u0026quot;数据获取\u0026quot;这件事本身的一些思考。\n一、问题的起点：后台数据到底有多\u0026quot;散\u0026quot; 在 OZON 卖家后台，看一天的经营数据，要跳转至少 4 个不同的页面：\n需要的数据 后台路径 痛点 订单金额、订单量 订单管理 → 分别看 FBO 和 FBS FBO（OZON仓发货）、FBS（自发货）是两套独立列表 取消订单 同上，需筛选状态 要手动勾选\u0026quot;已取消\u0026quot;才能看到 退款金额 财务 → 经营状况 → 退款 和订单是完全独立的口径 广告花费 广告后台（独立系统，独立登录） 账号体系都和卖家后台分开 产品销售明细 分析 → 按 SKU 看 还要自己导出、筛掉销售额为 0 的 关键认知：OZON 的销售数据和广告数据，是两套完全独立的 API 体系，连鉴权方式都不一样。这是整个事情第一个需要跨过的门槛。\n如果店铺数量是 5 个，上面的动作要重复 5 次。如果还要汇总\u0026quot;近 7 天\u0026quot;做趋势分析——那基本上一上午就废了。\n二、网页后台 vs API：同样的数据，两种获取方式 这是全文的核心对比。我把整个数据获取流程拆成 4 块，逐块对比。\n对比 1：销售订单金额 网页后台做法\n登录卖家后台 进入\u0026quot;订单 → FBO\u0026quot;，筛选昨天的下单时间，记录订单金额 进入\u0026quot;订单 → FBS\u0026quot;，同样筛选昨天，记录订单金额 把状态为\u0026quot;已取消\u0026quot;的单子手动剔除 把上面两块加起来 API 做法\nFBO 和 FBS 是两个独立的接口，但参数结构几乎一样：\n# 拉取 FBS 订单，按莫斯科时区过滤，自动翻页 def _fetch_fbs_orders(store, day): result, offset = [], 0 while True: resp = requests.post( \u0026#34;https://api-seller.ozon.ru/v3/posting/fbs/list\u0026#34;, headers=seller_headers(store), json={ \u0026#34;dir\u0026#34;: \u0026#34;asc\u0026#34;, \u0026#34;filter\u0026#34;: { \u0026#34;since\u0026#34;: day + \u0026#34;T00:00:00+03:00\u0026#34;, # 莫斯科时间 +03:00 \u0026#34;to\u0026#34;: day + \u0026#34;T23:59:59+03:00\u0026#34; }, \u0026#34;limit\u0026#34;: 50, \u0026#34;offset\u0026#34;: offset, }, timeout=30) postings = resp.json().get(\u0026#34;result\u0026#34;, {}).get(\u0026#34;postings\u0026#34;, []) result += postings if len(postings) \u0026lt; 50: # 不足一页，说明拉完了 break offset += 50 return result 拿到数据后，用状态码筛掉取消的订单，剩下的累加金额就是\u0026quot;有效订单金额\u0026quot;：\nCANCELLED = {\u0026#34;cancelled\u0026#34;, \u0026#34;cancelled_employer\u0026#34;} # 两种取消状态 valid = [p for p in all_orders if p.get(\u0026#34;status\u0026#34;) not in CANCELLED] result_a = sum( float(item[\u0026#34;price\u0026#34;]) * int(item[\u0026#34;quantity\u0026#34;]) for p in valid for item in p.get(\u0026#34;products\u0026#34;, []) ) 几个非写不可的注释：\n时间必须用莫斯科时间（UTC+3），否则跨零点的订单会错位到另一天。OZON 后台显示的就是莫斯科时间。 FBS 和 FBO 是两套接口：/v3/posting/fbs/list 和 /v2/posting/fbo/list，连 API 版本号都不一样。 分页用 offset 推进，每页最多 50 条，直到返回不足 50 条才算拉完。 对比 2：退款金额 网页后台做法\n进入\u0026quot;财务 → 经营状况 → 退款\u0026quot;，记录昨天的退款金额。\nAPI 做法\n退款这里有个坑：它不在订单接口里，而在财务流水接口里，而且需要按 transaction_type: \u0026quot;returns\u0026quot; 过滤：\ndef _fetch_returns_amount(store, day): total, page = 0.0, 1 while True: resp = requests.post( \u0026#34;https://api-seller.ozon.ru/v3/finance/transaction/list\u0026#34;, headers=seller_headers(store), json={ \u0026#34;filter\u0026#34;: { \u0026#34;date\u0026#34;: { \u0026#34;from\u0026#34;: day + \u0026#34;T00:00:00.000Z\u0026#34;, \u0026#34;to\u0026#34;: day + \u0026#34;T23:59:59.000Z\u0026#34; }, \u0026#34;operation_type\u0026#34;: [], \u0026#34;transaction_type\u0026#34;: \u0026#34;returns\u0026#34; # 关键：只要退款 }, \u0026#34;page\u0026#34;: page, \u0026#34;page_size\u0026#34;: 100 }, timeout=30) result = resp.json().get(\u0026#34;result\u0026#34;, {}) for op in result.get(\u0026#34;operations\u0026#34;, []): total += op.get(\u0026#34;accruals_for_sale\u0026#34;, 0) if page \u0026gt;= result.get(\u0026#34;page_count\u0026#34;, 1): break page += 1 return abs(round(total, 2)) # 退款是负数，取绝对值 几个非写不可的注释：\n退款金额字段是 accruals_for_sale，返回的是负数（账上扣减），所以最后要 abs()。 这个接口的时间用的是 UTC 格式（.000Z），和订单接口的莫斯科时间格式不同。同一个 API 家族，不同接口的时间格式居然不一致，这是 OZON API 最反直觉的地方之一。 分页用 page 和 page_size，和订单接口的 offset/limit 也不一样。 对比 3：广告数据 这块最复杂，因为广告是另一套完全独立的 API：\n维度 销售 API 广告 API 域名 api-seller.ozon.ru api-performance.ozon.ru 鉴权方式 Client-Id + Api-Key 直接塞 header OAuth2，先拿 token 再请求 数据格式 JSON CSV（分号分隔，逗号做小数点） 账号体系 卖家账号 广告主账号（独立申请） 网页后台做法：登录广告后台（另一套登录态），进入\u0026quot;推广 → 推广分析→选择日期\u0026quot;，直接记录当天的广告消费或者导出 CSV，手动整理。\nAPI 做法：\n# 第一步：拿 access_token def get_perf_token(store): resp = requests.post( \u0026#34;https://api-performance.ozon.ru/api/client/token\u0026#34;, json={ \u0026#34;client_id\u0026#34;: store[\u0026#34;perf_id\u0026#34;], \u0026#34;client_secret\u0026#34;: store[\u0026#34;perf_secret\u0026#34;], \u0026#34;grant_type\u0026#34;: \u0026#34;client_credentials\u0026#34; }, timeout=30) return resp.json()[\u0026#34;access_token\u0026#34;] # 第二步：用 token 拿数据，注意返回的是 CSV def _fetch_ad_detail(store, date_from, date_to): token = get_perf_token(store) resp = requests.get( \u0026#34;https://api-performance.ozon.ru/api/client/statistics/daily\u0026#34;, headers={\u0026#34;Authorization\u0026#34;: f\u0026#34;Bearer {token}\u0026#34;}, params={\u0026#34;dateFrom\u0026#34;: date_from, \u0026#34;dateTo\u0026#34;: date_to}, timeout=30) # CSV 用分号分隔，小数点是逗号（俄欧习惯） df = pd.read_csv(StringIO(resp.text.strip()), sep=\u0026#34;;\u0026#34;, decimal=\u0026#34;,\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) df.columns = [\u0026#34;活动ID\u0026#34;, \u0026#34;活动名称\u0026#34;, \u0026#34;日期\u0026#34;, \u0026#34;展示量\u0026#34;, \u0026#34;点击量\u0026#34;, \u0026#34;广告费(RUB)\u0026#34;, \u0026#34;广告订单量\u0026#34;, \u0026#34;广告销售额(RUB)\u0026#34;] # 关键：OZON 会在 CSV 末尾自动附加一行\u0026#34;合计行\u0026#34;，活动ID 为空 # 如果不过滤掉，所有金额都会被重复计算一遍 df = df[df[\u0026#34;活动ID\u0026#34;].notna() \u0026amp; (df[\u0026#34;活动ID\u0026#34;].astype(str).str.strip() != \u0026#34;\u0026#34;)] return df 几个踩过的坑：\n俄语区 CSV 用分号做分隔符、逗号做小数点，所以 pd.read_csv 必须同时传 sep=\u0026quot;;\u0026quot; 和 decimal=\u0026quot;,\u0026quot;，否则数字全错。 CSV 末尾会自动附加一行合计行（活动 ID 为空），第一次没注意，广告费直接翻倍。 归因机制：用户先点击了广告 A，又点击了广告 B，最后下单了——OZON 会把这笔销售额同时计入 A 和 B。所以广告销售额加总可能大于实际订单额，这不是 bug，是平台的归因策略。 通过 API 拿到的广告费，和网页端看到的可能会差几卢布，是尾数精度差异造成的，对经营决策没有影响。 对比 4：产品级销售明细 网页后台做法：进入\u0026quot;分析 → 我的商品销售\u0026quot;，选日期，导出 Excel，手动剔除销售额为 0 的行。\nAPI 做法：走 /v1/analytics/data，这是一个很通用的 OLAP 风格接口，可以指定维度（dimension）和指标（metrics）：\nresp = requests.post( \u0026#34;https://api-seller.ozon.ru/v1/analytics/data\u0026#34;, headers=seller_headers(store), json={ \u0026#34;date_from\u0026#34;: day, \u0026#34;date_to\u0026#34;: day, \u0026#34;metrics\u0026#34;: [\u0026#34;revenue\u0026#34;, \u0026#34;ordered_units\u0026#34;], # 想要什么指标 \u0026#34;dimension\u0026#34;: [\u0026#34;sku\u0026#34;], # 按什么维度切 \u0026#34;limit\u0026#34;: 1000 }, timeout=30) 这个接口设计得最好——一个接口搞定所有维度组合，想按 SKU 切就按 SKU，想按类目切就按类目。\n三、最难的一个问题：1 号下单、2 号取消怎么办？ 这是整个项目里最容易被忽略、也最容易让数据失真的一个问题。\n想象这个场景：\n11 月 1 日 客户下单，金额 1000 卢布 11 月 2 日 客户取消订单 如果每天跑日报，只拉\u0026quot;当天下单的订单\u0026quot;：\n11 月 1 日的日报：1000 卢布订单 ✅ 没问题 11 月 2 日的日报：0 单（这天没人下单） 可是 1 号那条数据永远是\u0026quot;有效订单\u0026quot;，永远不会更新成\u0026quot;已取消\u0026quot; ❌ 换句话说，按下单日拉数据 + 当天下单当天判定状态 = 永远捕捉不到跨天取消。\n解决方案：每周一次重新统计 + 用实时状态 我写了两个脚本：\nozon_day.py（日报）：每天早上跑一次，生成昨天的日报。这是快报，用来快速感知。\nozon_week.py（周报）：拉取最近 7 天的所有订单，但判定状态时用的是API 返回的当前状态。\n关键代码：\n# 一次拉取 7 天窗口内的订单 fbs_orders = _fetch_fbs(store, date_from, date_to) # date_from=7天前, date_to=昨天 fbo_orders = _fetch_fbo(store, date_from, date_to) # 按下单日分桶，但用\u0026#34;当前状态\u0026#34;判定是否取消 for p in all_orders: d = _order_day(p) # 下单日期（莫斯科时间） if p.get(\u0026#34;status\u0026#34;) in CANCELLED: # 这里是实时状态，不是历史快照 cancel_cnt[d] += 1 else: valid_amt[d] += _posting_amount(p) 这样一来：\n11 月 1 日下单、11 月 2 日取消的那条订单，在 11 月 2 日跑周报时，会被归到\u0026quot;11 月 1 日 → 已取消\u0026quot;。 1 号的\u0026quot;有效订单金额\u0026quot;会被正确地向下修正 1000 卢布。 这是网页后台也做不到的事情——后台要么给你看\u0026quot;按下单日的静态数据\u0026quot;，要么给你看\u0026quot;按处理日的流水\u0026quot;，没有\u0026quot;按下单日 + 最新状态\u0026quot;这个视角。\n当然也还是有边界——如果订单是 8 天前下的、今天才取消，周报就覆盖不到了。但对实际经营来说，一周窗口已经足够。\n四、两种方式的数据差异： 这是全文最该被收藏的部分。同样的\u0026quot;昨天销售额\u0026quot;，后台看到的和 API 拿到的，可能对不上。原因：\n差异点 说明 影响 时间口径 FBS 看 in_process_at，FBO 看 created_at；财务流水用 UTC 跨零点的订单可能归属不同日期 广告费尾数 API 返回的数值精度和网页端显示的截断方式不同 差几卢布，不影响决策 广告归因 一单可能被多个广告计入销售额 广告销售额之和 \u0026gt; 实际销售额（正常现象） 退款归属 退款按【财务模块-店铺经营状况】\u0026ldquo;扣款日\u0026quot;归属，不是按\u0026quot;原订单日\u0026rdquo; 和订单金额不是严格同一批单子的 取消订单 日报只能看到\u0026quot;当天下单当天取消\u0026quot;，周报能看\u0026quot;当天下单 N 天内取消\u0026quot; 日报的取消数会偏低，周报更准 导出明细 OZON 导出的销售明细 CSV 默认包含已取消订单 如果直接求和，会虚高，需要筛选掉已取消订单 总结：API 能拿到比后台更完整、更灵活的数据，但代价是你要自己搞清楚每个字段的口径。后台看似简单，其实平台替你做了很多隐式的口径选择——而这些选择并不总是符合你的经营视角。\n五、产出：两个 Excel 日报脚本跑完后，生成一个 Excel 文件，包含：\n汇总：每个店铺一行 销售明细：店铺 × 日期× SKU 广告明细：按广告活动拆分，带 ROI 和广告费占比 周报脚本跑完后，生成一个 Excel 文件，包含：\n汇总：每个店铺一行，近 7 天合计 每日明细：店铺 × 日期，每天一行 销售明细：按 SKU 拆分 广告明细：按广告活动拆分，带 ROI 和广告费占比 每天早上我只需要双击一下 py 文件，几分钟后打开 Excel 就能看到所有店铺的完整经营情况。\n原来需要2小时的手工操作，现在变成了 5-10 分钟。\n六、写在最后：为什么这件事值得做 网页后台是平台帮你做好的\u0026quot;一个视角\u0026quot;，它足够好，但不一定是你最需要的那个视角。 API 是原材料，你可以按自己的口径重新组装数据。\n后台不会帮你自动合并20个店铺的数据，API 会。 后台不会帮你做\u0026quot;按下单日 + 最新状态\u0026quot;的口径，API 会。 后台不会帮你把销售和广告并列对比算 ROI，API 会。 这也是我之后会继续挖的方向——不是把脚本写得多花哨，而是把每一个\u0026quot;重复动作\u0026quot;还原成它背后的数据问题，然后用最直接的方式解决它。\n本文涉及的接口包括：OZON 卖家 API（订单、财务、分析）和 OZON 广告 API（统计）。 所有接口的鉴权信息、店铺配置被抽离到独立的 ozon_config.py，主脚本只负责逻辑。\n","date":"2026-04-24T00:00:00Z","permalink":"https://nbcaishui.com/p/%E4%BB%8E%E6%AF%8F%E5%A4%A9%E6%89%93%E5%BC%80%E5%90%8E%E5%8F%B0%E6%88%AA%E5%9B%BE%E5%88%B0%E4%B8%80%E9%94%AE%E7%94%9F%E6%88%90%E6%97%A5%E6%8A%A5%E6%88%91%E5%A6%82%E4%BD%95%E7%94%A8-ozon-api-%E9%87%8D%E6%9E%84%E8%B7%A8%E5%A2%83%E7%94%B5%E5%95%86%E7%9A%84%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E6%B5%81%E7%A8%8B/","title":"从\"每天打开后台截图\"到\"一键生成日报\"：我如何用 OZON API 重构跨境电商的数据获取流程"},{"content":" 同一笔订单，后台显示买家付了 5129 卢布，API 返回的价格是 6983 卢布，最后打到账上的却是 5330.99 卢布。\n三个数字，三种口径，到底该信哪个？\n这篇就把这事一次讲透。\n一、开门见山：WB 的数据有一层\u0026quot;滤镜\u0026quot; Wildberries（下称 WB）是俄罗斯最大的电商平台之一。我对接它的 API 做日报自动化的时候，撞上一个在 OZON 从没遇到过的问题——后台显示的数字，和 API 返回的数字，差了一千多卢布。\n不是精度问题，不是尾数差，是真真切切差了一大截。\n搞懂这事，得先过两个坎：单号搞不清对不上账、价格和佣金的结算机制反直觉。下面一个一个来。\n二、第一个坑：WB 有三种\u0026quot;订单号\u0026quot;，搞混就对不上账 这个坑隐蔽但特别致命——你拿代码拉出来的订单号，往 WB 后台一搜，搜不到。\n为什么？因为 WB 系统里其实有三个不同用途的\u0026quot;单号\u0026quot;，各管各的。\n1. gNumber — 买家购物车的订单号 什么意思：买家一次购物车结算出来的号（Номер заказа покупателя） 特点：一个买家一次买 3 件你的衣服，API 里会是 3 条数据，但 gNumber 是同一个 用途：主要是客服跟买家对话时用 坑：在 WB 后台（FBS 发货台、财务明细）里基本搜不到 2. 备货任务号（Номер сборочного задания）— 网页发货用的号 什么意思：每一件实体商品对应的打包单号，长得像 1234567-8901 特点：你在 FBS（Маркетплейс）列表里天天看到的就是它 坑：/orders 统计接口里压根没这个字段——那个接口是财务口径，不管仓储 3. Srid — 系统底层唯一 ID（最关键） 什么意思：WB 系统底层的绝对唯一 ID 在哪看：只在**财务明细表（Детализация）**里才有专门一列 用途：要把 Python 拉的日报和周末下载的财务明细表做 VLOOKUP 对账，只有 Srid 能对上，别的号都不行 一张表总结：\n单号 哪里能看到 干嘛用 gNumber /orders 接口、客服场景 跟买家对账 备货任务号 FBS 网页列表 打包发货 Srid 财务明细表 对账、追到钱 我自己踩的坑：第一版日报脚本用了 API 返回的 gNumber 当\u0026quot;订单号\u0026quot;存 Excel。第二周想对账，把这些号一个一个扔进 WB 后台——全部搜不到。那一刻人都麻了。后来才反应过来，WB 的单号不是\u0026quot;一号走天下\u0026quot;，而是\u0026quot;一件货在不同环节有不同身份证\u0026quot;。\n修正方案：日报里三个号都留着，对账用 Srid，发货用备货任务号，对买家用 gNumber。不浪费一个字段。\n三、第二个坑：你以为的\u0026quot;销售额\u0026quot;到底是哪个价？ 这块最有意思——WB 的价格口径有三层，网页给你看的、API 返回的、最后打到账上的，是三个完全不同的数。\n拿一台 NOTE 13 PRO 手机的真实订单做例子。\n三个价：谁是谁 ① 你自己定的价（Цена розничная с учетом скидки）：6983.03 ₽\n你在后台设置的价（扣掉你自己给的折扣之后） WB 认为这笔生意是按这个价成交的 API 里的 priceWithDisc 字段就是它 ② 买家实际付的钱（Вайлдберриз реализовал Товар）：5129.00 ₽\n买家真掏出来的钱 网页端\u0026quot;买家支付额\u0026quot;显示的就是这个 API 里的 finishedPrice 对应它 比 ① 少了 1854.03 ₽ ③ 差额去哪了？SPP 平台补贴：1854.03 ₽\n买家之所以少付，是因为 WB 自己给他贴了这 1854 卢布 重点来了：这 1854 卢布 WB 会补给你 换句话说：买家觉得自己薅到羊毛，但你没亏。WB 自掏腰包承担了差价。\n佣金怎么扣：不是直接扣钱，而是\u0026quot;改佣金率\u0026quot; 这是 WB 结算最骚的地方——它不直接从你账上扣 1854，而是降低对你收的佣金比例来实现这个补贴：\n项目 数值 说明 原始佣金率（Размер кВВ, %） 26.55% 合同上说好的类目佣金 实际执行佣金率（Итоговый кВВ, %） 20.72% 因为 WB 补贴，帮你往下调了 佣金金额 6983.03 × 20.72% = 1446.88 ₽ 按 ① 号价算，不是按 ② 银行收款手续费（Комиссия за эквайринг） 205.16 ₽ 买家刷卡，银行拿的钱 最后到手多少（Вознаграждение за реализованный товар）：\n你的定价 (6983.03) − 佣金 (1446.88) − 银行手续费 (205.16) = 5330.99 ₽ 为啥你到手的钱（5330.99）比买家付的钱（5129）还多？ 这是整套机制最反直觉的地方。答案：\nWB 承认这单是按 6983 成交的（按 ① 号价算佣金） 买家只掏了 5129（② 比 ① 少了 1854） WB 通过砍佣金率，把那 1854 里的大部分又塞回到你账上 所以你账上进的钱 5330.99 \u0026gt; 买家付的 5129。\n平台宁愿少收你佣金、甚至贴钱，也要让买家觉得\u0026quot;在 WB 买东西就是便宜\u0026quot;。对平台来说这不是亏损，是获客成本。\n四、亚马逊、ozon的平台补贴是怎么样的？ 几乎每个主流平台都有类似机制，但实现方式天差地别。\n三个平台放一张表里对比：\n维度 Wildberries（SPP） OZON（折扣积分） Amazon（Discount Provided by Amazon） 谁决定打折 平台自动 平台自动 平台自动 差额谁出 平台（通过降低佣金率） 平台（通过发\u0026quot;积分\u0026quot;抵佣金） 平台全额出 卖家要不要主动参加 默认就参加，很难退 默认参加，可申请退出 自动参加，可 opt-out（但不可逆） 卖家最终按啥结算 按你的\u0026quot;定价\u0026quot;结算 按你的\u0026quot;定价\u0026quot;算，差额用积分抵佣金 按你的\u0026quot;原始挂牌价\u0026quot;全额拿到钱 佣金基数 你的定价（高） 你的定价（高） 全价（高） 报表里能不能看到补贴 能，但要自己拿两个佣金率反推 能，有专门\u0026quot;折扣积分\u0026quot;字段 能，财务报表里有 \u0026ldquo;Amazon funded\u0026rdquo; 对卖家数据的误导程度 最高 中等 较低 OZON：也有，叫\u0026quot;折扣积分\u0026quot;（Баллы за скидки） OZON 的逻辑和 WB 的 SPP 非常像，但摊得比 WB 明白：\n平台给买家打折 100 卢布 → OZON 按 1:1 给卖家发 100 个\u0026quot;积分\u0026quot; 这些积分专门用来抵扣 OZON 要收的佣金 积分最多能覆盖佣金的 99% 不能跨结算周期用，用不完就归零 跟 WB 的本质区别：WB 直接改写佣金率，让你看到的佣金数字就是最终数字；OZON 是先按正常佣金扣你的钱，再用积分补回来。\n所以 OZON 的财务报表里有一列清清楚楚的\u0026quot;折扣积分\u0026quot;，卖家一眼就能反应过来\u0026quot;这笔钱是平台帮我出的\u0026quot;。WB 就没这列，你得自己算。\nAmazon：也有，而且最痛快 先说清楚，Amazon 有两套完全不同的机制，别搞混了：\n① 卖家自己发的 coupon / promotion\n卖家自己掏钱。卖家设折扣价，买家少付的钱全部从卖家结算额里扣 Amazon 还额外收 coupon 手续费（2025 年 6 月改革后是 $5 固定费 + 2.5% 销售额抽成，之前是每次兑换收 $0.60） 就是一般说的\u0026quot;Amazon 促销\u0026quot;，跟 WB SPP 没关系 ② Discount Provided by Amazon（平台出钱打折）\n这个才是类比 WB SPP 的东西 卖家照样按原挂牌价拿到全额货款，Amazon 自己承担折扣那部分 佣金按全价算，不会因为打了折就少收 所有卖家默认自动加入，可以账户级别 opt-out，但一旦退出就不能再回来 Amazon 自己选哪些商品降价、降多少，卖家事先可能都不知道 Amazon 这套和 WB SPP 的不同在于：\n更痛快——直接按全价给你结算，没有\u0026quot;先扣再补\u0026quot;的复杂流程 但可能踩到卖家跟上游品牌方的 MAP（最低广告价）协议，因为 Amazon 擅自降价可能破坏你和品牌商之间的价格政策 总结 OZON 是\u0026quot;账面上清清楚楚告诉你\u0026rsquo;这部分我帮你出了\u0026rsquo;\u0026quot;。 Amazon 是\u0026quot;直接给你全款，我自己消化差价\u0026quot;。 WB 是\u0026quot;把补贴藏进佣金率里，自己算去\u0026quot;。\nWB 的 SPP 最阴。不是阴在\u0026quot;存在补贴\u0026quot;——补贴本身对卖家是好事——而是阴在它没有任何一个字段直接告诉你补贴了多少。\n你看到的全是计算结果，没有计算过程。原始佣金率 26.55% 和实际佣金率 20.72% 摆在那，你得自己反应过来\u0026quot;哦，这俩差了 5.83%，乘以定价 6983，就是……不对，还得加上银行手续费的变化，还得考虑……\u0026quot;——大多数卖家根本算不清这笔账，看到\u0026quot;买家只付了 5129\u0026quot;就以为自己亏了。\n五、网页端 vs API：同样的订单，差异在哪 网页端能看到什么 买家支付额（5129） 订单状态、发货信息 佣金总额（汇总的，不拆） 简化版财务摘要 API 能多看到什么 ① 你的定价 6983（真实成交额，佣金基数） ③ SPP 补贴相关字段 原始佣金率 vs 实际佣金率的差 每一笔银行手续费 Srid——对账必备 差异对照表 维度 网页后台 API \u0026ldquo;销售额\u0026quot;显示 买家支付价（5129），看着偏低 定价（6983），真实成交额 佣金 只给总额 原始率 + 实际率 + 差额都有 SPP 补贴 某个栏目汇总 明细字段可见 单号 备货任务号（能搜） gNumber（搜不到） 对账能力 弱，字段不够 强，有 Srid 数据延迟 有平台加工延迟 接近实时 核心差异一句话：\n网页端给你的是\u0026quot;买家视角\u0026rdquo;（他付了多少），API 给你的是\u0026quot;结算视角\u0026quot;（这单值多少钱）。 只看网页端，容易把每一单都当成\u0026quot;少赚了\u0026quot;，吓自己。\n六、怎么跟老板汇报：三个数字讲清楚 到这一步，汇报就很简单了：\n\u0026ldquo;老板，这单 NOTE 13 PRO 我们挂牌卖 6983，买家因为平台 SPP 补贴只掏了 5129，WB 扣了 20% 左右的佣金和手续费，最后打给我们 5330.99 卢布。我们不仅没亏，平台还替我们承担了一部分折扣成本。\u0026rdquo;\n三个数字、两个动作（平台补、平台扣）、一个结论（没亏，反而被补了）。这就是合格财务该有的汇报颗粒度——不是把数字念一遍，而是把\u0026quot;钱的流向\u0026quot;讲清楚。\n七、技术细节：API 鉴权和\u0026quot;单 token\u0026quot;设计 比起 OZON 把\u0026quot;销售 API\u0026quot;和\u0026quot;广告 API\u0026quot;拆成两套完全独立的鉴权体系，WB 这边简洁很多：\n只有一个 token——一把钥匙开所有门（订单、统计、财务、商品、广告、仓储） 鉴权方式：HTTP header 里塞 Authorization: Bearer \u0026lt;token\u0026gt; 域名：不同业务走不同子域名，但共用同一个 token 好处是开发者友好，坏处是权限粒度粗——这个 token 一旦泄露，整个店铺裸奔。所以 token 管理要格外小心。\n关键代码示意：\nheaders = {\u0026#34;Authorization\u0026#34;: WB_TOKEN} # 订单统计接口：按下单时间拉数据 resp = requests.get( \u0026#34;https://statistics-api.wildberries.ru/api/v1/supplier/orders\u0026#34;, headers=headers, params={\u0026#34;dateFrom\u0026#34;: \u0026#34;2024-01-01T00:00:00\u0026#34;}, timeout=30 ) 注释：\ndateFrom 用 ISO 8601 格式，不带时区的话 WB 默认按莫斯科时间（UTC+3）处理 统计接口有每分钟一次的调用限制，频繁轮询会被 429 怼回来 财务明细接口按周结算，不是按天——和订单接口口径不一样 要拿 Srid，必须走财务明细接口，订单统计接口里没这字段 八、最后 对接完 WB 之后，我最深的感受是：\n做跨境电商，最容易踩雷的不是\u0026quot;算错数\u0026quot;，而是\u0026quot;用错口径\u0026quot;。\nWB 这个例子里，如果你拿\u0026quot;买家支付额\u0026quot;去算毛利、算 ROAS、算广告投产比——每个指标都会系统性偏低。偏低 20%~30% 的数据，足以让一个本该加投的品被误杀，也足以让一个本该下架的品被继续烧钱。\nAPI 的价值不只是自动化。更重要的是——它逼着你把\u0026quot;那个模糊的销售额\u0026quot;一层一层拆开，看清楚每一层到底是什么。\n后台给你一个数字，那是结论 API 给你一组数字，那是过程 财务的专业度，就藏在\u0026quot;过程\u0026quot;里。看到的层数越多，判断就越准。这也是我坚持把每个平台的 API 都吃透一遍的原因——不是为了写脚本，是为了不被平台的默认视角带偏。\n","date":"2026-04-24T00:00:00Z","permalink":"https://nbcaishui.com/p/%E5%9C%A8-wildberries-%E5%8D%96%E8%B4%A7%E4%BD%A0%E4%BB%A5%E4%B8%BA%E7%9A%84%E9%94%80%E5%94%AE%E9%A2%9D%E5%8F%AF%E8%83%BD%E6%98%AF%E5%81%87%E7%9A%84%E4%B8%80%E6%AC%A1%E5%85%B3%E4%BA%8E%E4%BB%B7%E6%A0%BC%E4%BD%A3%E9%87%91%E5%92%8C%E5%8D%95%E5%8F%B7%E7%9A%84%E6%B7%B1%E5%BA%A6%E6%8B%86%E8%A7%A3/","title":"在 Wildberries 卖货，你以为的\"销售额\"可能是假的——一次关于价格、佣金和单号的深度拆解"},{"content":"本文介绍如何在钉钉中为特定人员（如管理层）设置免打卡权限。 首先进入智能人事。 ","date":"2026-04-20T00:00:00Z","permalink":"https://nbcaishui.com/p/%E5%A6%82%E4%BD%95%E5%9C%A8%E9%92%89%E9%92%89%E4%B8%AD%E8%AE%BE%E7%BD%AE%E6%9F%90%E4%BA%9B%E4%BA%BA%E6%AF%94%E5%A6%82%E8%80%81%E6%9D%BF%E4%B8%8D%E7%94%A8%E6%89%93%E5%8D%A1/","title":"如何在钉钉中设置某些人（比如老板）不用打卡？"},{"content":"本文介绍如何通过钉钉邀请新员工加入企业组织。 ","date":"2026-04-20T00:00:00Z","permalink":"https://nbcaishui.com/p/%E5%A6%82%E4%BD%95%E5%9C%A8%E9%92%89%E9%92%89%E4%B8%AD%E9%82%80%E8%AF%B7%E6%96%B0%E5%91%98%E5%B7%A5/","title":"如何在钉钉中邀请新员工？"}]