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。dataStartTime、dataEndTime:数据窗口,不是任务创建时间;部分报告类型可能忽略它们。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_QUEUE、IN_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 官方明确说明:CANCELLED、DONE、FATAL 都表示处理已经结束;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 往往无法知道取消来源,所以系统设计必须补充上下文:
- 如果你的同步任务从不调用
cancelReport,也没有人工取消入口,那么 Amazon 返回的CANCELLED可以按"空数据窗口"处理。 - 如果系统允许主动取消,应在调用取消接口前记录
cancel_requested_by、cancel_requested_at或cancel_source,之后再将对应报告解释为CANCELLED_BY_USER。 - 不要仅根据"没有
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);
}
}
这里有三个关键点:
IN_QUEUE和IN_PROGRESS只是"还没完成",不是异常。- 查询状态失败时,应继续查询原来的
reportId,不要立刻再创建一份报告。 - 对未知状态使用向前兼容策略:保存原值、告警并继续观察,不要把未来新增状态直接映射为
FATAL。Amazon 也提醒开发者,报告字段和字段值可能随时间增加,解析器应具备兼容性。参见 Reports API Use Case Guide。
轮询间隔不要写死
可采用类似下面的退避策略:
text
第 1 次:1 分钟后
第 2 次:2 分钟后
第 3 次:4 分钟后
之后:8~15 分钟,增加少量随机抖动
同时要读取实际响应中的 x-amzn-RateLimit-Limit 和 Retry-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 // 有数据、空数据、主动取消等业务结果
这样才能精确回答三个问题:
- Amazon 是否生成完了?
- 文件是否已经被系统成功处理?
- 这个时间窗口到底是有数据、无数据,还是人为取消?
八、重试的正确边界
"失败就重建报告"看起来简单,实际上是最危险的策略之一。重试应该分层:
| 失败位置 | 示例 | 正确重试对象 |
|---|---|---|
createReport 调用前 |
本地限流器等待、参数校验失败 | 修正本地问题后再创建 |
createReport 响应不确定 |
客户端超时,但 Amazon 可能已创建 | 先查近期报告或依靠幂等协调,避免立刻重复创建 |
getReport 调用失败 |
网络超时、429、临时 5xx | 重试同一个 reportId |
IN_QUEUE / IN_PROGRESS |
正常处理中 | 延迟后继续轮询同一个 reportId |
CANCELLED 且无主动取消 |
无数据自动取消 | 记录 EMPTY,不重建 |
FATAL |
Amazon 处理失败 | 保存证据,有限次数创建替代报告 |
| 下载 URL 失效 | 预签名 URL 过期 | 用同一 reportDocumentId 重新获取 URL |
| 解析或入库失败 | 格式变化、数据库异常 | 重试本地处理,不重建 Amazon 报告 |
为什么 createReport 超时最棘手
客户端收到超时,并不能证明 Amazon 没有创建成功。如果立刻重试,可能生成两份相同报告。
比较稳妥的方式是:
- 本地对业务幂等键加唯一约束或分布式锁;
- 保存"创建中"记录和请求指纹;
- 超时后在合理时间范围内调用
getReports查找同类型、同卖家、同创建时间附近的报告; - 无法确认时再按有限策略补建,而不是无限重试。
九、时间窗口是另一个高频坑
报告里的时间至少有三种口径:
- 请求窗口时间 :
dataStartTime、dataEndTime; - Amazon 处理时间 :
createdTime、processingStartTime、processingEndTime; - 报告业务日期:订单日、销售日、退货日等。
不能把它们混在一起,也不能依赖服务器默认时区。
建议:
- 调用 API 时使用带时区偏移的 ISO 8601 时间。
- 明确报告类型采用 UTC 还是 Marketplace 当地时间。
- 数据库中保存原始窗口和标准化窗口,幂等比较必须使用同一口径。
- 欧洲、北美等区域型报告可能覆盖多个站点,不能仅凭请求的单个 Marketplace 就武断给每一行归属国家。
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 操作有不同的使用计划。createReport、getReport 和 getReportDocument 不应共用一个粗暴的"全局每秒 N 次"计数器;至少要按操作和卖家/店铺维度控制。
十二、上线前检查清单
创建阶段
- 已核对报告类型、站点、所需角色和参数限制。
- 时间窗口带明确时区。
- 幂等键包含卖家、类型、站点、窗口和 options。
- 创建成功后立即保存
reportId。 - 创建超时不会马上无限重建。
轮询阶段
-
IN_QUEUE / IN_PROGRESS被当作正常中间态。 - 轮询失败重试原
reportId。 - 轮询使用退避、抖动和操作级限流。
- 终态后停止轮询。
- 未知状态不会被直接当作失败吞掉。
CANCELLED 与空数据
- 系统能记录是否主动调用过取消接口。
- 无主动取消链路中的
CANCELLED作为空窗口覆盖。 -
CANCELLED不会触发无限重建。 -
DONE但 0 行也会标记为成功处理。 - 空窗口可以推进同步水位,不依赖业务明细行存在。
下载与入库
- 通过
reportDocumentId获取新的临时 URL。 - 根据
compressionAlgorithm解压。 - 按 reportType 选择解析器。
- 允许报告新增非必需列。
- 下载、解析、入库状态分别可观测。
- 入库具备幂等键,可安全重放。
十三、结语
SP-API 报告同步最难的并不是调用接口,而是正确管理一个跨越"远程异步任务、本地文件处理、业务数据入库"的状态机。
只要记住下面四句话,就能避开大多数坑:
createReport成功只代表拿到了任务编号,不代表拿到了数据。IN_QUEUE和IN_PROGRESS是正常状态,不要当成失败。- 没有主动取消行为时,
CANCELLED很可能表示无数据,应该结束该窗口而不是重复创建。 - Amazon
DONE与本地"导入成功"是两件事,必须分开记录。
当系统能够正确表达"处理中、已生成、已导入、无数据、主动取消、可重试失败、不可恢复失败"这些状态后,报告同步才真正从一段脚本变成了可靠的数据管道。
参考资料
- Amazon SP-API Reports API v2021-06-30 Use Case Guide
- Amazon SP-API Tutorial: Request a report
- Amazon SP-API: Verify that report processing is complete
说明:本文示例省略了 LWA、AWS SigV4、Restricted Data Token 等认证细节,重点解释 Reports API 的异步同步流程与状态管理。生产实现请以对应报告类型的最新官方文档和实际响应限流头为准。