Amazon SP-API 报告创建全流程与状态机:从创建、轮询到下载入库,新手避坑指南

Amazon SP-API 报告创建全流程与状态机:从创建、轮询到下载入库,新手避坑指南

本文基于 Selling Partner API(SP-API)Reports API v2021-06-30

SP-API 报告不是调用一次接口就能拿到数据,而是一套异步任务流程:先创建报告取得 reportId,再轮询处理状态,完成后获取 reportDocumentId 和临时下载地址,最后解压、解析并幂等入库。本文用流程图和状态机讲清每一步,并重点说明无数据时 CANCELLED 不等于失败,避免系统反复创建空报告。

一、先建立正确认识:创建报告,不等于得到报告

很多新手第一次接触 SP-API Reports API,会下意识写出下面这种逻辑:

text 复制代码
调用 createReport → 接收数据 → 保存数据库

但 Reports API 实际上是一个异步任务系统 。调用 createReport 成功,只代表 Amazon 接收了生成任务,并返回一个 reportId;真正的数据可能要过几秒、几分钟,甚至更久才准备好。

正确链路如下:
#mermaid-svg-SJISYfZhXaIa9Egi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SJISYfZhXaIa9Egi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SJISYfZhXaIa9Egi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SJISYfZhXaIa9Egi .error-icon{fill:#552222;}#mermaid-svg-SJISYfZhXaIa9Egi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SJISYfZhXaIa9Egi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SJISYfZhXaIa9Egi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SJISYfZhXaIa9Egi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SJISYfZhXaIa9Egi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SJISYfZhXaIa9Egi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SJISYfZhXaIa9Egi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SJISYfZhXaIa9Egi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SJISYfZhXaIa9Egi .marker.cross{stroke:#333333;}#mermaid-svg-SJISYfZhXaIa9Egi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SJISYfZhXaIa9Egi p{margin:0;}#mermaid-svg-SJISYfZhXaIa9Egi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SJISYfZhXaIa9Egi .cluster-label text{fill:#333;}#mermaid-svg-SJISYfZhXaIa9Egi .cluster-label span{color:#333;}#mermaid-svg-SJISYfZhXaIa9Egi .cluster-label span p{background-color:transparent;}#mermaid-svg-SJISYfZhXaIa9Egi .label text,#mermaid-svg-SJISYfZhXaIa9Egi span{fill:#333;color:#333;}#mermaid-svg-SJISYfZhXaIa9Egi .node rect,#mermaid-svg-SJISYfZhXaIa9Egi .node circle,#mermaid-svg-SJISYfZhXaIa9Egi .node ellipse,#mermaid-svg-SJISYfZhXaIa9Egi .node polygon,#mermaid-svg-SJISYfZhXaIa9Egi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SJISYfZhXaIa9Egi .rough-node .label text,#mermaid-svg-SJISYfZhXaIa9Egi .node .label text,#mermaid-svg-SJISYfZhXaIa9Egi .image-shape .label,#mermaid-svg-SJISYfZhXaIa9Egi .icon-shape .label{text-anchor:middle;}#mermaid-svg-SJISYfZhXaIa9Egi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SJISYfZhXaIa9Egi .rough-node .label,#mermaid-svg-SJISYfZhXaIa9Egi .node .label,#mermaid-svg-SJISYfZhXaIa9Egi .image-shape .label,#mermaid-svg-SJISYfZhXaIa9Egi .icon-shape .label{text-align:center;}#mermaid-svg-SJISYfZhXaIa9Egi .node.clickable{cursor:pointer;}#mermaid-svg-SJISYfZhXaIa9Egi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SJISYfZhXaIa9Egi .arrowheadPath{fill:#333333;}#mermaid-svg-SJISYfZhXaIa9Egi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SJISYfZhXaIa9Egi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SJISYfZhXaIa9Egi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SJISYfZhXaIa9Egi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SJISYfZhXaIa9Egi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SJISYfZhXaIa9Egi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SJISYfZhXaIa9Egi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SJISYfZhXaIa9Egi .cluster text{fill:#333;}#mermaid-svg-SJISYfZhXaIa9Egi .cluster span{color:#333;}#mermaid-svg-SJISYfZhXaIa9Egi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SJISYfZhXaIa9Egi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SJISYfZhXaIa9Egi rect.text{fill:none;stroke-width:0;}#mermaid-svg-SJISYfZhXaIa9Egi .icon-shape,#mermaid-svg-SJISYfZhXaIa9Egi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SJISYfZhXaIa9Egi .icon-shape p,#mermaid-svg-SJISYfZhXaIa9Egi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SJISYfZhXaIa9Egi .icon-shape .label rect,#mermaid-svg-SJISYfZhXaIa9Egi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SJISYfZhXaIa9Egi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SJISYfZhXaIa9Egi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SJISYfZhXaIa9Egi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 返回 reportId
IN_QUEUE / IN_PROGRESS
DONE
临时预签名 URL
CANCELLED
FATAL
确定报告类型和时间窗口
createReport
保存本地任务
getReport 轮询状态
取得 reportDocumentId
getReportDocument
下载报告文件
按 compressionAlgorithm 解压
解析 CSV / TSV / JSON
幂等写入业务表
记录取消或空数据终态
记录失败并按策略处理

Amazon 官方教程给出的主流程也是:请求报告、等待报告处理结束、通过 reportDocumentId 获取下载信息、下载报告。可参考 Tutorial: Request a report

可以把几个 ID 理解为:

名称 含义 是否直接包含数据
reportId 一次报告生成任务的编号
reportDocumentId 生成后的报告文档编号
预签名 URL 文档的临时下载地址 是,通过它下载

一个特别容易混淆的点是:reportId 不能拿来直接下载文件,必须等 DONE 后取得 reportDocumentId

二、创建报告前要准备什么

调用 createReport 时,最常见的参数包括:

json 复制代码
{
  "reportType": "GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA",
  "marketplaceIds": ["ATVPDKIKX0DER"],
  "dataStartTime": "2026-08-01T00:00:00Z",
  "dataEndTime": "2026-08-31T23:59:59Z",
  "reportOptions": {}
}

其中:

  • reportType:报告类型。不同类型支持的站点、时间范围、角色和 reportOptions 不同。
  • marketplaceIds:目标 Marketplace。不要把 Seller ID、站点域名或区域名误当成 Marketplace ID。
  • dataStartTimedataEndTime:数据窗口,不是任务创建时间;部分报告类型可能忽略它们。
  • reportOptions:某些报告特有的选项。不能把所有报告都当成相同参数模型。

创建前先做幂等检查

如果定时任务每次运行都无脑调用 createReport,很快会产生重复报告、浪费配额,还会让后续入库变得难以判断。

建议先生成一个业务幂等键:

text 复制代码
seller/store
+ authorization region
+ reportType
+ 排序后的 marketplaceIds
+ 标准化后的 dataStartTime/dataEndTime
+ 排序序列化后的 reportOptions

例如:

text 复制代码
US_STORE_01|NA|GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA|
ATVPDKIKX0DER|2026-08-01T00:00:00Z|2026-08-31T23:59:59Z|{}

同一个幂等键如果已经存在以下结果,通常不应再次创建:

  • 正在生成:IN_QUEUEIN_PROGRESS
  • 已生成并覆盖该窗口:DONE
  • 已确认是无数据窗口:由 Amazon 自动产生的 CANCELLED
  • DONE 且文档解析结果为 0 行:这同样是一次成功覆盖。

三、Amazon 官方状态机

Reports API 的 processingStatus 可以画成下面这张状态机:
#mermaid-svg-vLm0l1BHRgFXCerR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vLm0l1BHRgFXCerR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vLm0l1BHRgFXCerR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vLm0l1BHRgFXCerR .error-icon{fill:#552222;}#mermaid-svg-vLm0l1BHRgFXCerR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vLm0l1BHRgFXCerR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vLm0l1BHRgFXCerR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vLm0l1BHRgFXCerR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vLm0l1BHRgFXCerR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vLm0l1BHRgFXCerR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vLm0l1BHRgFXCerR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vLm0l1BHRgFXCerR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vLm0l1BHRgFXCerR .marker.cross{stroke:#333333;}#mermaid-svg-vLm0l1BHRgFXCerR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vLm0l1BHRgFXCerR p{margin:0;}#mermaid-svg-vLm0l1BHRgFXCerR defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-vLm0l1BHRgFXCerR g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-vLm0l1BHRgFXCerR g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-vLm0l1BHRgFXCerR g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-vLm0l1BHRgFXCerR g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-vLm0l1BHRgFXCerR g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-vLm0l1BHRgFXCerR .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-vLm0l1BHRgFXCerR .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-vLm0l1BHRgFXCerR .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-vLm0l1BHRgFXCerR .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vLm0l1BHRgFXCerR .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-vLm0l1BHRgFXCerR .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-vLm0l1BHRgFXCerR .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-vLm0l1BHRgFXCerR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vLm0l1BHRgFXCerR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vLm0l1BHRgFXCerR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vLm0l1BHRgFXCerR .edgeLabel .label text{fill:#333;}#mermaid-svg-vLm0l1BHRgFXCerR .label div .edgeLabel{color:#333;}#mermaid-svg-vLm0l1BHRgFXCerR .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-vLm0l1BHRgFXCerR .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-vLm0l1BHRgFXCerR .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-vLm0l1BHRgFXCerR .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-vLm0l1BHRgFXCerR .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-vLm0l1BHRgFXCerR .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vLm0l1BHRgFXCerR .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vLm0l1BHRgFXCerR #statediagram-barbEnd{fill:#333333;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vLm0l1BHRgFXCerR .cluster-label,#mermaid-svg-vLm0l1BHRgFXCerR .nodeLabel{color:#131300;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-vLm0l1BHRgFXCerR .note-edge{stroke-dasharray:5;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-note text{fill:black;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram-note .nodeLabel{color:black;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagram .edgeLabel{color:red;}#mermaid-svg-vLm0l1BHRgFXCerR #dependencyStart,#mermaid-svg-vLm0l1BHRgFXCerR #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-vLm0l1BHRgFXCerR .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vLm0l1BHRgFXCerR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} createReport 成功
Amazon 开始处理
主动取消或无数据自动取消
严重错误
生成完成
主动取消或无数据自动取消
严重错误
IN_QUEUE
IN_PROGRESS
CANCELLED
FATAL
DONE

状态含义如下:

Amazon 状态 是否终态 正确处理方式
IN_QUEUE 尚未开始或正在等待,稍后继续查询
IN_PROGRESS 正在生成,稍后继续查询
DONE 读取 reportDocumentId,下载并解析
CANCELLED 判断是主动取消还是无数据自动取消,不再轮询
FATAL 记录失败;若有 reportDocumentId,下载错误文档辅助诊断

Amazon 官方明确说明:CANCELLEDDONEFATAL 都表示处理已经结束;CANCELLED 既可能来自处理前的显式取消,也可能是因为没有数据可返回而被自动取消。详见 Verify that report processing is complete

四、最大的坑:CANCELLED 不一定是失败

这是本文最想强调的地方。

假设我们同步"某店铺 8 月份的 FBA 退货报告"。这个店铺整个月没有退货数据,Amazon 可能返回:

json 复制代码
{
  "reportId": "123456789",
  "processingStatus": "CANCELLED"
}

很多系统会这样写:

java 复制代码
if ("CANCELLED".equals(status) || "FATAL".equals(status)) {
    markFailed();
    createAgain();
}

这会形成一个死循环:
#mermaid-svg-sbTaJKjQgg7EXCtB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sbTaJKjQgg7EXCtB .error-icon{fill:#552222;}#mermaid-svg-sbTaJKjQgg7EXCtB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sbTaJKjQgg7EXCtB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sbTaJKjQgg7EXCtB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sbTaJKjQgg7EXCtB .marker.cross{stroke:#333333;}#mermaid-svg-sbTaJKjQgg7EXCtB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sbTaJKjQgg7EXCtB p{margin:0;}#mermaid-svg-sbTaJKjQgg7EXCtB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sbTaJKjQgg7EXCtB .cluster-label text{fill:#333;}#mermaid-svg-sbTaJKjQgg7EXCtB .cluster-label span{color:#333;}#mermaid-svg-sbTaJKjQgg7EXCtB .cluster-label span p{background-color:transparent;}#mermaid-svg-sbTaJKjQgg7EXCtB .label text,#mermaid-svg-sbTaJKjQgg7EXCtB span{fill:#333;color:#333;}#mermaid-svg-sbTaJKjQgg7EXCtB .node rect,#mermaid-svg-sbTaJKjQgg7EXCtB .node circle,#mermaid-svg-sbTaJKjQgg7EXCtB .node ellipse,#mermaid-svg-sbTaJKjQgg7EXCtB .node polygon,#mermaid-svg-sbTaJKjQgg7EXCtB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sbTaJKjQgg7EXCtB .rough-node .label text,#mermaid-svg-sbTaJKjQgg7EXCtB .node .label text,#mermaid-svg-sbTaJKjQgg7EXCtB .image-shape .label,#mermaid-svg-sbTaJKjQgg7EXCtB .icon-shape .label{text-anchor:middle;}#mermaid-svg-sbTaJKjQgg7EXCtB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sbTaJKjQgg7EXCtB .rough-node .label,#mermaid-svg-sbTaJKjQgg7EXCtB .node .label,#mermaid-svg-sbTaJKjQgg7EXCtB .image-shape .label,#mermaid-svg-sbTaJKjQgg7EXCtB .icon-shape .label{text-align:center;}#mermaid-svg-sbTaJKjQgg7EXCtB .node.clickable{cursor:pointer;}#mermaid-svg-sbTaJKjQgg7EXCtB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sbTaJKjQgg7EXCtB .arrowheadPath{fill:#333333;}#mermaid-svg-sbTaJKjQgg7EXCtB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sbTaJKjQgg7EXCtB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sbTaJKjQgg7EXCtB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sbTaJKjQgg7EXCtB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sbTaJKjQgg7EXCtB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sbTaJKjQgg7EXCtB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sbTaJKjQgg7EXCtB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sbTaJKjQgg7EXCtB .cluster text{fill:#333;}#mermaid-svg-sbTaJKjQgg7EXCtB .cluster span{color:#333;}#mermaid-svg-sbTaJKjQgg7EXCtB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sbTaJKjQgg7EXCtB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sbTaJKjQgg7EXCtB rect.text{fill:none;stroke-width:0;}#mermaid-svg-sbTaJKjQgg7EXCtB .icon-shape,#mermaid-svg-sbTaJKjQgg7EXCtB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sbTaJKjQgg7EXCtB .icon-shape p,#mermaid-svg-sbTaJKjQgg7EXCtB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sbTaJKjQgg7EXCtB .icon-shape .label rect,#mermaid-svg-sbTaJKjQgg7EXCtB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sbTaJKjQgg7EXCtB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sbTaJKjQgg7EXCtB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sbTaJKjQgg7EXCtB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建报告
Amazon 发现没有数据
返回 CANCELLED
系统误判为失败
再次创建同一窗口

结果是:

  • 重复消耗 createReport 配额;
  • 日志和告警里堆满"失败";
  • 低销量店铺或本来就没有业务数据的窗口永远同步不完;
  • 多个任务并发重建时,还可能进一步触发 429 限流。

正确做法:把"远程状态"和"业务结果"分开

CANCELLED 是 Amazon 的远程终态,但本地业务结果可以进一步解释为:

text 复制代码
Amazon processingStatus = CANCELLED
本地 outcome = EMPTY(已覆盖、无数据)

推荐的本地状态模型:

本地状态 含义 是否允许自动重建同一窗口
WAITING 对应 IN_QUEUE / IN_PROGRESS
IMPORTED DONE 且已经成功入库
EMPTY 无数据型 CANCELLED,或 DONE 但解析为 0 行
FAILED_RETRYABLE 网络、429、临时 5xx 等调用异常 不立即重建;先重试原调用或原 reportId
FAILED_FATAL Amazon FATAL 或不可恢复参数错误 按有限策略处理
CANCELLED_BY_USER 系统或用户主动取消 由业务决定,不要自动恢复

怎样区分"主动取消"和"无数据取消"

仅凭 processingStatus=CANCELLED 往往无法知道取消来源,所以系统设计必须补充上下文:

  1. 如果你的同步任务从不调用 cancelReport ,也没有人工取消入口,那么 Amazon 返回的 CANCELLED 可以按"空数据窗口"处理。
  2. 如果系统允许主动取消,应在调用取消接口前记录 cancel_requested_bycancel_requested_atcancel_source,之后再将对应报告解释为 CANCELLED_BY_USER
  3. 不要仅根据"没有 reportDocumentId"判断失败。无数据取消本来就可能没有可下载文档。

也就是说,不能简单宣称"所有 CANCELLED 都是成功",正确说法是:

对于没有主动取消行为的自动同步链路,Amazon 返回的 CANCELLED 通常应视为"该窗口已处理但无数据",而不是失败重建信号。

五、轮询应该怎样写

不推荐:在请求线程里 sleep 到完成

java 复制代码
String reportId = createReport(request);

while (true) {
    Thread.sleep(5000);
    Report report = getReport(reportId);
    if (isTerminal(report.getProcessingStatus())) {
        break;
    }
}

这种写法会长时间占用线程,服务重启后状态丢失,也不利于分布式部署。

推荐:持久化任务 + 调度器分批轮询

创建成功后把这些字段保存下来:

text 复制代码
report_id
seller/store
report_type
marketplace_ids
data_start_time / data_end_time
report_options
processing_status
next_poll_time
poll_error_count
report_document_id
created_time / processing_end_time
import_status / imported_time

调度器每轮只取 next_poll_time <= now 的少量任务:

java 复制代码
void poll(ReportTask task) {
    try {
        Report report = reportsApi.getReport(task.getReportId());

        switch (report.getProcessingStatus()) {
            case "IN_QUEUE", "IN_PROGRESS" ->
                scheduleNextPoll(task, backoff(task));

            case "DONE" ->
                downloadParseAndImport(task, report.getReportDocumentId());

            case "CANCELLED" ->
                finishAsEmptyOrUserCancelled(task);

            case "FATAL" ->
                handleFatal(task, report.getReportDocumentId());

            default ->
                keepUnknownStatusForObservation(task);
        }
    } catch (TooManyRequestsException e) {
        scheduleAccordingToRetryAfter(task, e);
    } catch (TemporaryNetworkException e) {
        retrySameReportId(task);
    }
}

这里有三个关键点:

  1. IN_QUEUEIN_PROGRESS 只是"还没完成",不是异常。
  2. 查询状态失败时,应继续查询原来的 reportId,不要立刻再创建一份报告。
  3. 对未知状态使用向前兼容策略:保存原值、告警并继续观察,不要把未来新增状态直接映射为 FATAL。Amazon 也提醒开发者,报告字段和字段值可能随时间增加,解析器应具备兼容性。参见 Reports API Use Case Guide

轮询间隔不要写死

可采用类似下面的退避策略:

text 复制代码
第 1 次:1 分钟后
第 2 次:2 分钟后
第 3 次:4 分钟后
之后:8~15 分钟,增加少量随机抖动

同时要读取实际响应中的 x-amzn-RateLimit-LimitRetry-After(如果返回),因为不同操作、不同账号的可用额度不一定完全相同。收到 429 时,正确动作是降速和退避,不是并发重试。

如果系统规模较大,可以订阅 REPORT_PROCESSING_FINISHED 通知,用 SQS 或 EventBridge 在报告进入终态时触发处理;但仍建议保留低频轮询作为补偿机制,防止通知配置或消费链路异常。

六、DONE 之后还有四步,不是直接"入库"

第一步:取得 reportDocumentId

只有报告存在可用文档时,getReport 才会返回 reportDocumentId。通常 DONE 应有文档;FATAL 也可能附带一个错误说明文档。

第二步:调用 getReportDocument

http 复制代码
GET /reports/2021-06-30/documents/{reportDocumentId}

返回内容通常包括:

json 复制代码
{
  "reportDocumentId": "DOC-EXAMPLE",
  "url": "https://...temporary-signed-url...",
  "compressionAlgorithm": "GZIP"
}

这个 URL 是临时预签名地址。建议保存 reportDocumentId,需要下载时重新调用 getReportDocument 获取地址,不要把 URL 当成永久地址长期复用

第三步:下载并解压

常见坑是直接把下载响应当文本解析,结果看到一堆乱码。应先检查 compressionAlgorithm

java 复制代码
InputStream raw = download(url);
InputStream content = "GZIP".equalsIgnoreCase(compressionAlgorithm)
        ? new GZIPInputStream(raw)
        : raw;

工程上还可以使用 GZIP 魔数 0x1f 0x8b 作为元数据缺失时的兜底,但应以 API 返回的压缩信息为主。

第四步:解析并幂等入库

不要默认所有报告都是 CSV。不同报告可能是 TSV、CSV、JSON、XML 或压缩包,字段也可能增加。

解析器应做到:

  • reportType 选择解析器;
  • 允许出现未知列,不因为新增列整份失败;
  • 校验必需列,但不要依赖固定列顺序;
  • 明确字符集、分隔符和换行方式;
  • 用稳定业务键或来源行摘要去重;
  • 下载成功、解析成功、入库成功分别记录,便于断点续跑。

七、建议把"报告状态"和"导入状态"拆开

Amazon 只负责生成文档,文档生成成功不代表你的业务数据已经导入成功。
#mermaid-svg-2KmLuF3ijei19R1R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2KmLuF3ijei19R1R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2KmLuF3ijei19R1R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2KmLuF3ijei19R1R .error-icon{fill:#552222;}#mermaid-svg-2KmLuF3ijei19R1R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2KmLuF3ijei19R1R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2KmLuF3ijei19R1R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2KmLuF3ijei19R1R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2KmLuF3ijei19R1R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2KmLuF3ijei19R1R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2KmLuF3ijei19R1R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2KmLuF3ijei19R1R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2KmLuF3ijei19R1R .marker.cross{stroke:#333333;}#mermaid-svg-2KmLuF3ijei19R1R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2KmLuF3ijei19R1R p{margin:0;}#mermaid-svg-2KmLuF3ijei19R1R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2KmLuF3ijei19R1R .cluster-label text{fill:#333;}#mermaid-svg-2KmLuF3ijei19R1R .cluster-label span{color:#333;}#mermaid-svg-2KmLuF3ijei19R1R .cluster-label span p{background-color:transparent;}#mermaid-svg-2KmLuF3ijei19R1R .label text,#mermaid-svg-2KmLuF3ijei19R1R span{fill:#333;color:#333;}#mermaid-svg-2KmLuF3ijei19R1R .node rect,#mermaid-svg-2KmLuF3ijei19R1R .node circle,#mermaid-svg-2KmLuF3ijei19R1R .node ellipse,#mermaid-svg-2KmLuF3ijei19R1R .node polygon,#mermaid-svg-2KmLuF3ijei19R1R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2KmLuF3ijei19R1R .rough-node .label text,#mermaid-svg-2KmLuF3ijei19R1R .node .label text,#mermaid-svg-2KmLuF3ijei19R1R .image-shape .label,#mermaid-svg-2KmLuF3ijei19R1R .icon-shape .label{text-anchor:middle;}#mermaid-svg-2KmLuF3ijei19R1R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2KmLuF3ijei19R1R .rough-node .label,#mermaid-svg-2KmLuF3ijei19R1R .node .label,#mermaid-svg-2KmLuF3ijei19R1R .image-shape .label,#mermaid-svg-2KmLuF3ijei19R1R .icon-shape .label{text-align:center;}#mermaid-svg-2KmLuF3ijei19R1R .node.clickable{cursor:pointer;}#mermaid-svg-2KmLuF3ijei19R1R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2KmLuF3ijei19R1R .arrowheadPath{fill:#333333;}#mermaid-svg-2KmLuF3ijei19R1R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2KmLuF3ijei19R1R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2KmLuF3ijei19R1R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2KmLuF3ijei19R1R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2KmLuF3ijei19R1R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2KmLuF3ijei19R1R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2KmLuF3ijei19R1R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2KmLuF3ijei19R1R .cluster text{fill:#333;}#mermaid-svg-2KmLuF3ijei19R1R .cluster span{color:#333;}#mermaid-svg-2KmLuF3ijei19R1R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2KmLuF3ijei19R1R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2KmLuF3ijei19R1R rect.text{fill:none;stroke-width:0;}#mermaid-svg-2KmLuF3ijei19R1R .icon-shape,#mermaid-svg-2KmLuF3ijei19R1R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2KmLuF3ijei19R1R .icon-shape p,#mermaid-svg-2KmLuF3ijei19R1R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2KmLuF3ijei19R1R .icon-shape .label rect,#mermaid-svg-2KmLuF3ijei19R1R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2KmLuF3ijei19R1R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2KmLuF3ijei19R1R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2KmLuF3ijei19R1R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 临时网络错误
格式或字段错误
数据库错误
Amazon: DONE
本地: READY_TO_DOWNLOAD
DOWNLOADING
PARSING
IMPORTING
IMPORTED
DOWNLOAD_RETRY
PARSE_FAILED
IMPORT_RETRY

如果只用一个 status 字段,很容易出现这种错误:Amazon 已经 DONE,但本地解析失败,页面却显示"同步成功"。推荐至少拆成:

text 复制代码
processing_status  // Amazon 生成状态
import_status      // 本地下载、解析、入库状态
business_outcome   // 有数据、空数据、主动取消等业务结果

这样才能精确回答三个问题:

  1. Amazon 是否生成完了?
  2. 文件是否已经被系统成功处理?
  3. 这个时间窗口到底是有数据、无数据,还是人为取消?

八、重试的正确边界

"失败就重建报告"看起来简单,实际上是最危险的策略之一。重试应该分层:

失败位置 示例 正确重试对象
createReport 调用前 本地限流器等待、参数校验失败 修正本地问题后再创建
createReport 响应不确定 客户端超时,但 Amazon 可能已创建 先查近期报告或依靠幂等协调,避免立刻重复创建
getReport 调用失败 网络超时、429、临时 5xx 重试同一个 reportId
IN_QUEUE / IN_PROGRESS 正常处理中 延迟后继续轮询同一个 reportId
CANCELLED 且无主动取消 无数据自动取消 记录 EMPTY,不重建
FATAL Amazon 处理失败 保存证据,有限次数创建替代报告
下载 URL 失效 预签名 URL 过期 用同一 reportDocumentId 重新获取 URL
解析或入库失败 格式变化、数据库异常 重试本地处理,不重建 Amazon 报告

为什么 createReport 超时最棘手

客户端收到超时,并不能证明 Amazon 没有创建成功。如果立刻重试,可能生成两份相同报告。

比较稳妥的方式是:

  • 本地对业务幂等键加唯一约束或分布式锁;
  • 保存"创建中"记录和请求指纹;
  • 超时后在合理时间范围内调用 getReports 查找同类型、同卖家、同创建时间附近的报告;
  • 无法确认时再按有限策略补建,而不是无限重试。

九、时间窗口是另一个高频坑

报告里的时间至少有三种口径:

  • 请求窗口时间dataStartTimedataEndTime
  • Amazon 处理时间createdTimeprocessingStartTimeprocessingEndTime
  • 报告业务日期:订单日、销售日、退货日等。

不能把它们混在一起,也不能依赖服务器默认时区。

建议:

  1. 调用 API 时使用带时区偏移的 ISO 8601 时间。
  2. 明确报告类型采用 UTC 还是 Marketplace 当地时间。
  3. 数据库中保存原始窗口和标准化窗口,幂等比较必须使用同一口径。
  4. 欧洲、北美等区域型报告可能覆盖多个站点,不能仅凭请求的单个 Marketplace 就武断给每一行归属国家。
  5. getReport 当前只提供近 90 天内创建的按需或计划报告信息,不能把 Amazon 当成永久任务历史库;关键状态应及时落地到自己的数据库。该限制见官方 Verify that report processing is complete

十、一个可落地的同步器整体设计

#mermaid-svg-ua00qCcKWR2QEY6R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ua00qCcKWR2QEY6R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ua00qCcKWR2QEY6R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ua00qCcKWR2QEY6R .error-icon{fill:#552222;}#mermaid-svg-ua00qCcKWR2QEY6R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ua00qCcKWR2QEY6R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ua00qCcKWR2QEY6R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ua00qCcKWR2QEY6R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ua00qCcKWR2QEY6R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ua00qCcKWR2QEY6R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ua00qCcKWR2QEY6R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ua00qCcKWR2QEY6R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ua00qCcKWR2QEY6R .marker.cross{stroke:#333333;}#mermaid-svg-ua00qCcKWR2QEY6R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ua00qCcKWR2QEY6R p{margin:0;}#mermaid-svg-ua00qCcKWR2QEY6R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ua00qCcKWR2QEY6R .cluster-label text{fill:#333;}#mermaid-svg-ua00qCcKWR2QEY6R .cluster-label span{color:#333;}#mermaid-svg-ua00qCcKWR2QEY6R .cluster-label span p{background-color:transparent;}#mermaid-svg-ua00qCcKWR2QEY6R .label text,#mermaid-svg-ua00qCcKWR2QEY6R span{fill:#333;color:#333;}#mermaid-svg-ua00qCcKWR2QEY6R .node rect,#mermaid-svg-ua00qCcKWR2QEY6R .node circle,#mermaid-svg-ua00qCcKWR2QEY6R .node ellipse,#mermaid-svg-ua00qCcKWR2QEY6R .node polygon,#mermaid-svg-ua00qCcKWR2QEY6R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ua00qCcKWR2QEY6R .rough-node .label text,#mermaid-svg-ua00qCcKWR2QEY6R .node .label text,#mermaid-svg-ua00qCcKWR2QEY6R .image-shape .label,#mermaid-svg-ua00qCcKWR2QEY6R .icon-shape .label{text-anchor:middle;}#mermaid-svg-ua00qCcKWR2QEY6R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ua00qCcKWR2QEY6R .rough-node .label,#mermaid-svg-ua00qCcKWR2QEY6R .node .label,#mermaid-svg-ua00qCcKWR2QEY6R .image-shape .label,#mermaid-svg-ua00qCcKWR2QEY6R .icon-shape .label{text-align:center;}#mermaid-svg-ua00qCcKWR2QEY6R .node.clickable{cursor:pointer;}#mermaid-svg-ua00qCcKWR2QEY6R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ua00qCcKWR2QEY6R .arrowheadPath{fill:#333333;}#mermaid-svg-ua00qCcKWR2QEY6R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ua00qCcKWR2QEY6R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ua00qCcKWR2QEY6R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ua00qCcKWR2QEY6R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ua00qCcKWR2QEY6R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ua00qCcKWR2QEY6R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ua00qCcKWR2QEY6R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ua00qCcKWR2QEY6R .cluster text{fill:#333;}#mermaid-svg-ua00qCcKWR2QEY6R .cluster span{color:#333;}#mermaid-svg-ua00qCcKWR2QEY6R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ua00qCcKWR2QEY6R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ua00qCcKWR2QEY6R rect.text{fill:none;stroke-width:0;}#mermaid-svg-ua00qCcKWR2QEY6R .icon-shape,#mermaid-svg-ua00qCcKWR2QEY6R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ua00qCcKWR2QEY6R .icon-shape p,#mermaid-svg-ua00qCcKWR2QEY6R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ua00qCcKWR2QEY6R .icon-shape .label rect,#mermaid-svg-ua00qCcKWR2QEY6R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ua00qCcKWR2QEY6R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ua00qCcKWR2QEY6R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ua00qCcKWR2QEY6R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生成业务窗口
createReport
reportId
getReport
DONE + documentId
CANCELLED
FATAL
getReportDocument
窗口规划器
report_sync_task
创建器
Amazon Reports API
状态轮询器
下载器
空窗口/主动取消判定
失败证据与有限补建
分类型解析器
幂等入库服务
业务事实表
窗口覆盖水位

一轮定时任务可以按下面的顺序执行:

text 复制代码
1. 先续跑已有 IN_QUEUE / IN_PROGRESS 报告
2. 再续跑 DONE 但尚未导入的报告
3. 将无主动取消记录的 CANCELLED 标为 EMPTY,并推进覆盖水位
4. 处理有限次数的 FATAL 替代报告
5. 最后才为真正缺失的窗口创建新报告

"先续跑、后创建"非常重要。它可以避免任务每次启动都先制造新报告,却不处理上一次留下的任务。

十一、常见错误码与处理方向

情况 常见原因 建议
400 参数、时间范围、reportOptions 不符合报告类型要求 不自动重试,先修参数
401 LWA Access Token 失效或请求认证有误 刷新令牌后有限重试一次
403 应用角色、卖家授权或资源权限不足 检查应用角色和授权范围
404 reportId 不属于当前卖家,或资源已不可查询 核对卖家身份和本地映射
429 超过操作限流 读取限流头、指数退避、降低并发
5xx / 503 Amazon 临时异常或维护 对同一步骤进行有上限的退避重试

要特别注意:不同 SP-API 操作有不同的使用计划。createReportgetReportgetReportDocument 不应共用一个粗暴的"全局每秒 N 次"计数器;至少要按操作和卖家/店铺维度控制。

十二、上线前检查清单

创建阶段

  • 已核对报告类型、站点、所需角色和参数限制。
  • 时间窗口带明确时区。
  • 幂等键包含卖家、类型、站点、窗口和 options。
  • 创建成功后立即保存 reportId
  • 创建超时不会马上无限重建。

轮询阶段

  • IN_QUEUE / IN_PROGRESS 被当作正常中间态。
  • 轮询失败重试原 reportId
  • 轮询使用退避、抖动和操作级限流。
  • 终态后停止轮询。
  • 未知状态不会被直接当作失败吞掉。

CANCELLED 与空数据

  • 系统能记录是否主动调用过取消接口。
  • 无主动取消链路中的 CANCELLED 作为空窗口覆盖。
  • CANCELLED 不会触发无限重建。
  • DONE 但 0 行也会标记为成功处理。
  • 空窗口可以推进同步水位,不依赖业务明细行存在。

下载与入库

  • 通过 reportDocumentId 获取新的临时 URL。
  • 根据 compressionAlgorithm 解压。
  • 按 reportType 选择解析器。
  • 允许报告新增非必需列。
  • 下载、解析、入库状态分别可观测。
  • 入库具备幂等键,可安全重放。

十三、结语

SP-API 报告同步最难的并不是调用接口,而是正确管理一个跨越"远程异步任务、本地文件处理、业务数据入库"的状态机。

只要记住下面四句话,就能避开大多数坑:

  1. createReport 成功只代表拿到了任务编号,不代表拿到了数据。
  2. IN_QUEUEIN_PROGRESS 是正常状态,不要当成失败。
  3. 没有主动取消行为时,CANCELLED 很可能表示无数据,应该结束该窗口而不是重复创建。
  4. Amazon DONE 与本地"导入成功"是两件事,必须分开记录。

当系统能够正确表达"处理中、已生成、已导入、无数据、主动取消、可重试失败、不可恢复失败"这些状态后,报告同步才真正从一段脚本变成了可靠的数据管道。

参考资料


说明:本文示例省略了 LWA、AWS SigV4、Restricted Data Token 等认证细节,重点解释 Reports API 的异步同步流程与状态管理。生产实现请以对应报告类型的最新官方文档和实际响应限流头为准。

相关推荐
VIP_CQCRE2 小时前
用一张图和一段音频生成数字人口播视频:Ace Data Cloud Dreamina API 接入指南
api·数字人·ai视频·acedatacloud
Raas1002 小时前
MAI Gateway(魔芋企业级AI网关)详解:企业为什么需要AI网关,一文读懂企业AI流量治理
大数据·人工智能·gateway·api·ai网关·mai gateway
Tanshu_API君2 小时前
身份证实名认证接口对接:PHP 接入二要素核验实战
php·api·实名认证·身份证实名认证·身份认证接口·运营商接口·运营商二要素
万邦科技Lafite3 小时前
1688一键创建订单付款API操作指南讲解
人工智能·微信·api·电商开放平台·淘宝开放平台·api开放接口
用户77833661321119 小时前
用 React Hook 封装搜索数据:useSerp 的防抖、缓存与错误处理
python·api
VIP_CQCRE2 天前
AceData Cloud MCP:把整个平台能力接入你的 AI 助手
ai·api·mcp·acedatacloud
XLYcmy2 天前
HTML/CSS/JS 基础与 Vue 技术栈深度解析
java·前端·css·html·vue3·vue2·api
VIP_CQCRE2 天前
用一个 API 接入 GPT-Image-2 与 Nano Banana:Ace Data Cloud 图像生成能力实践
ai·aigc·openai·api·图像生成
星核0penstarry3 天前
试一试用gr.Workflow把AI多步骤串联变成可视化画布
人工智能·python·ai作画·api·ai编程·工作流·api聚合平台