基本约定
- 服务地址示例:
http://localhost:3000。部署后替换为实际内部地址。 - 内部使用:所有接口无需登录、Cookie、Authorization 或 API Key。
- JSON 写入请求必须设置
Content-Type: application/json。 - 经营日期按北京时间(Asia/Shanghai);日期格式
YYYY-MM-DD。 collectedAt为工具实际取到指标值的时间,必须包含时区,如2026-10-19T09:30:00+08:00。- 金额均为人民币元,不是分。推荐十进制字符串以保留精度;不能带千分位、货币符号或
%。 - 单次 1–500 条记录、请求体不超过 1 MiB;任何校验或写入失败,整批回滚。
- 页面
/为看板;/admin为无需验证的数据管理后台。 - 在线文档:API 接入指南;Markdown 原文:
GET /api/v1/docs?format=markdown;机器可读规范:GET /api/v1/openapi。
1. 店铺列表与维护
GET/api/v1/shops
响应 200:
{"shops":[{"account":"rakuten-main","id":"256730481","name":"日系运动店"}]}
PUT/api/v1/shops
创建或按店铺ID更新账号、店铺名称。店铺ID是唯一键,与账号无关;同一账号可以对应多家店铺,店铺名称也不要求唯一。店铺ID一旦关联数据,不应更改。
{"account":"rakuten-main","id":"256730481","name":"日系运动店"}
响应 200:{"shop":{"account":"rakuten-main","id":"256730481","name":"日系运动店"}}。
三个字段均必填。account 为账号,1–200 字符;id 为店铺ID,1–100 位字母、数字、下划线、点或连字符;name 为店铺名称,1–200 字符。首尾空白不接受,账号和 ID 均传字符串以保留前导零。账号不是登录凭据,接口不接收密码。
迁移前已有店铺的账号可能返回 null,后台显示 —;可编辑或导入补齐。迁移保留原店铺ID和经营数据,不会生成虚构账号。
DELETE/api/v1/shops?id=256730481
删除此店铺和它的所有经营数据。不存在返回 404 NOT_FOUND。已收录的采集请求历史保留,不会因重试旧请求恢复已删除数据。
批量删除使用 DELETE /api/v1/shops,设置 Content-Type: application/json,请求体为 {"ids":["shop-a","shop-b"]}。每次 1–500 项,重复 ID 自动去重,整批校验并在同一事务中删除;响应包含 deletedShops 实际删除店铺数。已不存在的店铺可安全重试。
POST/api/v1/shops/import
批量新增或按店铺ID更新账号、店铺名称。后台「店铺管理 → Excel 导入店铺」也使用此接口。
{
"shops": [
{"account": "rakuten-main", "id": "001234", "name": "运动旗舰店"},
{"account": "rakuten-main", "id": "shop-002", "name": "生活优选店"}
]
}
| 字段 | 必填 | 说明 |
|---|---|---|
| shops | 是 | 1–1000 个店铺对象;同一批次不能重复 ID |
| shops[].account | 是 | 账号,1–200 字符;多个店铺可使用相同账号 |
| shops[].id | 是 | 字符串,规则同单店维护;文本 ID 保留前导零 |
| shops[].name | 是 | 1–200 字符,已有 ID 更新账号与名称 |
响应 200:
{"total":2,"inserted":2,"updated":0}
整批在事务内写入;任一行不合法返回 422,不会部分导入。请求体最多 1 MiB。相同 ID 不会重复创建,重复提交会按当前库中已有 ID 统计为更新。
Excel 表头匹配
后台支持 .xlsx 文件(最多 5 MiB、每批 1000 家店铺),可下载空白模板、选择工作表并预览。自动扫描前 30 行寻找表头,列顺序不限,忽略空格与大小写;多余列不导入。
| 目标字段 | 可识别表头 |
|---|---|
| 账号 | 账号、帐号、店铺账号、店铺帐号、account、shopAccount、storeAccount |
| 店铺 ID | 店铺 ID、店铺编号、店铺编码、店铺号、shopId、storeId、id |
| 店铺名称 | 店铺名称、店铺名、店名、shopName、storeName、name |
模板列顺序为 账号、店铺id、店铺名称,三列表头均需匹配。前导零或较长的账号、ID 必须在 Excel 中设为文本,避免数字精度损失。重复 ID(即使账号不同)、缺失字段或非法值会标出行号,修正全部错误后才能确认导入。旧版 .xls 请先另存为 .xlsx。
2. 分项/批量录入指标
POST/api/v1/ingestion
{
"requestId":"455ff1f4-8250-4c71-9599-833a09a0c7b1",
"source":"golden-eye-crawler",
"records":[
{
"shopId":"256730481",
"shopAccount":"rakuten-main",
"shopName":"日系运动店",
"period":{"grain":"day","startDate":"2026-10-18","endDate":"2026-10-18"},
"collectedAt":"2026-10-19T09:30:00+08:00",
"metrics":{"gmv":"12345.67"}
}
]
}
| 字段 | 必填 | 说明 |
|---|---|---|
| requestId | 是 | UUID;每个新批次生成一次。网络重试必须使用相同 ID 和原请求体 |
| source | 是 | 1–100 字符的来源名,例如 golden-eye-crawler |
| records | 是 | 1–500 条;允许混合店铺、不同日期及指标;只接收日数据 |
| shopId | 是 | 稳定店铺 ID,格式同店铺维护接口 |
| shopAccount | 新店铺需要 | 店铺账号,1–200 字符;已有店铺可省略,省略时保留原账号 |
| shopName | 新店铺需要 | 已有店铺可省略;提供时更新店铺名称;新店铺自动创建 |
| period | 是 | 周期对象,见下表 |
| collectedAt | 是 | 含时区的 ISO 8601 时间,最多毫秒;不得超出服务端当前时间 5 分钟 |
| metrics | 是 | 至少一个支持的指标;省略表示不更新,不是清空 |
未定义的字段会返回 422,避免拼写错误被静默忽略。
只导入日数据
写入及删除仅支持 grain: "day",startDate 与 endDate 必须相同,年份支持 2000–2099。数据日期必须早于 collectedAt 对应的北京时间日期,不能录入当天或未来数据。
原先导入 week / month / mtd 的请求现在返回 422。采集器只需导入每日五项指标,周、月、MTD 均自动计算,无需重复上传。 修改或删除某天后,相关计算结果及环比立即随下一次查询重算,不保存独立的周/月/MTD 观测值,不读取旧快照。
周、月及 MTD 的计算口径
| 报表 | 计算范围 | 环比参照 |
|---|---|---|
| day | 当天 | 上一自然日 |
| week | 周一至周日 | 上一周 |
| month | 自然月第一天至最后一天 | 上一自然月 |
| mtd | 月初至指定日期,本月默认到北京时间昨天 | 上月相同日期范围,超出上月天数时取上月月末 |
所有计算排除北京时间当天及未来数据,未结束的周/月只使用截至昨天的已有日记录。缺少日期不补零,金额和访客累加已有值;没有任何输入时返回空值。MTD 无上月对应数据时不显示环比,不回退到整月比较。环比分母为零时返回空值。
- 正向 = 累计成交金额,逆向 = 累计退款金额,GMV = 正向 − 逆向。
- 每日成交客户数 = 当日成交金额 / 当日客单价。
- 每日商品访客数 = 当日成交客户数 / 当日商品成交转化率。
- 周/月/MTD 客单价 = 累计成交金额 / 累计成交客户数。
- 周/月/MTD 商品成交转化率 = 累计成交客户数 / 累计商品访客数。
- 店铺访客数直接逐日累加,表示累计人次,不代表跨日去重人数。
推算人数保留小数,不平均每日比率,不用店铺访客数替代商品访客数,不用父单量替代成交客户数。任一天的必要输入不足或分母为零时,相应累计比率为空;成交金额为零时客户数计为零。只有一个有效日输入时保留该日来源客单价和商品转化率。
销售构成按店铺展示所选周及此前 4 周,末尾追加 MTD;报告周期仅选择周。历史月份 MTD 统计月初至所选周结束日。本月按昨天计算,表格和 CSV 不显示 MTD 截至日期。计算值附带已录入天数,CSV 同步携带说明。
指标名称与口径
| metrics 字段 | 对应抓取值 | 范围与精度 |
|---|---|---|
| gmv | 成交金额,正向、未扣退款(沿用原字段名) | 非负数,最多 2 位小数 |
| refundAmount | 退款金额 | 非负数,最多 2 位小数;无退款传 0 |
| aov | 客单价 | 非负数,最多 6 位小数,保留抓取原值 |
| visitors | 店铺访客数 | 非负整数,当天去重访客数 |
| productConversionRate | 商品成交转化率 | 0–1 小数,最多 8 位小数;2.5% 传 0.025 |
| parentOrders | 父单量(可选扩展) | 非负整数;保留原始父单量,不参与客户数推算,未抓取可不传 |
数值最多 15 位整数。null、空字符串、NaN、负退款、带 % 的字符串均不接受。零是有效数据,与缺失不同。
看板正向 = 采集 gmv,逆向 = 采集 refundAmount(正数),GMV = 正向 − 逆向。 退款可以超过成交金额,此时看板 GMV 为负。成交或退款任一缺失,GMV 显示 —;无退款必须明确传 0。
金额字段口径已更新:写入接口及 /api/v1/metrics、/api/v1/records 中的 metrics.gmv 是原始成交金额;/api/dashboard 的 metrics[].gmv、daily[].gmv 是扣退款后的计算结果。现有采集观测值按成交金额解释,不改写原始值或请求历史。若采集器此前提交的是已扣退款的净额,需按新口径重新采集,并用新的 requestId、真实的新 collectedAt 更新。
原始日客单价和商品成交转化率保留来源值;跨日、跨店合计采用上面的推算公式,不直接平均。推算客户数不会写入父单量。
五个任务分别抓取的流程
- 成交金额任务提交
{ "gmv": "12345.67" }。 - 退款任务提交
{ "refundAmount": "345.67" }。 - 客单价任务提交
{ "aov": "246.9134" }。 - 访客任务提交
{ "visitors": 5000 }。 - 转化率任务提交
{ "productConversionRate": "0.025" }。
每个任务使用相同的 shopId + grain=day + startDate=endDate,各自生成 requestId 和实际 collectedAt。可以只交一项,也可以一次交齐五项。最终组合为一条完整店铺周期记录,各任务不会清空其他任务的指标。
同一请求允许一个店铺周期出现多次,但同一个指标不能重复;重复指标返回 422。首次创建同一店铺时,批次里需提供 shopAccount 和 shopName,可以位于该店铺的不同记录;同一请求内同店铺的账号和名称必须各自一致。已有店铺只传 shopId 和指标即可,省略账号、名称会保留现值。
成功响应
{
"requestId":"455ff1f4-8250-4c71-9599-833a09a0c7b1",
"records":1,
"metrics":1,
"inserted":1,
"updated":0,
"stale":0,
"unchanged":0,
"replayed":false
}
统计单位:records 是请求记录数;其余 inserted / updated / stale / unchanged 是指标数量,它们之和等于 metrics。
- 同 requestId、相同规范化内容重试:返回首次结果,
replayed=true,不重复入库。 - 同 requestId、不同内容:409
IDEMPOTENCY_CONFLICT,不写入。 - 更新采用每项指标自身的 collectedAt,不是请求到达顺序。
- 新采集时间更新旧值;较旧时间只计入 stale,不覆盖当前值。
- 相同时间、相同值计入 unchanged;相同时间、不同值返回 409
TIMESTAMP_CONFLICT,整批回滚。 source只标识来源,不作为隔离键;多个工具写同一店铺周期指标时,以较新 collectedAt 为准。- 后台编辑与抓取工具使用同一条更新链路。删除数据不会删除请求历史;若需要重新录入已删除值,使用新的 requestId。
3. 查询某店铺某周期的录入完整度
GET/api/v1/metrics?shopId=256730481&grain=day&startDate=2026-10-18&endDate=2026-10-18
{
"shopId":"256730481",
"period":{"id":"2026-10-18:2026-10-18:day","grain":"day","startDate":"2026-10-18","endDate":"2026-10-18"},
"metrics":{
"gmv":{"value":"12345.67000000","collectedAt":"2026-10-19T01:30:00.000Z","source":"golden-eye-crawler","requestId":"455ff1f4-8250-4c71-9599-833a09a0c7b1"}
},
"complete":false,
"missingMetrics":["refundAmount","aov","visitors","productConversionRate"]
}
读取 value 返回十进制字符串,避免浮点精度丢失;返回时间为 UTC。没有记录时返回 200、空 metrics 和全部五项 missingMetrics。parentOrders 不影响 complete。
查询计算后的周、月及 MTD
同一 GET /api/v1/metrics 支持 grain=week|month|mtd,只读计算结果,不创建记录。例如:
GET /api/v1/metrics?shopId=256730481&grain=mtd&startDate=2026-10-01&endDate=2026-10-19
响应中的 calculated: true 表示日数据计算结果;每项 metrics 仍返回 value 字符串,source 为 daily-calculation,没有虚构的 requestId 或采集时间。metrics.gmv 为累计成交金额,顶层 netGmv 为扣退款后的 GMV。growth 包含 GMV、访客、客单价、商品转化率环比,无比较数据或分母为零时为 null。
coverage 包含 recordedDays(已录入天数)、expectedDays(统计范围应有天数,最多到昨天)、completeDays(五项指标齐全的天数)、metricDays(各指标有值天数)。complete 仅在所有应有日期的五项输入齐全且结果可计算时为 true。没有日数据时 metrics 为空、complete 为 false。
日查询返回 calculated: false,保留实际采集时间和来源。写入、分页记录和删除接口只操作 day;不能编辑或删除计算出的周/月/MTD,应修改对应日数据。
4. 分页查询与删除指标记录
GET/api/v1/records?page=1&pageSize=20&shopId=256730481&grain=day
page、pageSize 默认 1、20。page 最大 100000,pageSize 最大 500,界面支持每页 20 / 50 / 100 / 200 / 500 条。shopId、grain 可省略;grain 仅允许 day。默认按开始日期倒序、店铺 ID、粒度排序。可传 sortBy(shopId、shopName、grain、startDate、endDate、gmv、refundAmount、aov、visitors、productConversionRate、missingMetrics)及 sortOrder(asc / desc)。服务端对完整筛选结果排序后分页,数值按数字排序,缺失值始终置底;不支持的字段或方向返回 422。
可选 startDate、endDate(有效的 YYYY-MM-DD,2000–2099)筛选完整落在范围内的记录:记录开始日期不早于 startDate,结束日期不晚于 endDate。起止日期可单独传入,同时传入时开始不能晚于结束。
{"total":1,"page":1,"pageSize":20,"records":[{"shopId":"256730481","shopName":"日系运动店","periodId":"2026-10-18:2026-10-18:day","grain":"day","startDate":"2026-10-18","endDate":"2026-10-18","metrics":{"gmv":"12345.67000000"},"collectedAt":"2026-10-19T01:30:00.000Z","missingMetrics":["refundAmount","aov","visitors","productConversionRate"]}]}
DELETE/api/v1/records?shopId=256730481&grain=day&startDate=2026-10-18&endDate=2026-10-18
删除这一个店铺周期的全部采集指标。响应 {"deletedMetrics":5}。不存在时返回 {"deletedMetrics":0}。请求历史保留。
批量删除使用 DELETE /api/v1/records,设置 Content-Type: application/json:
{"records":[{"shopId":"256730481","grain":"day","startDate":"2026-10-18","endDate":"2026-10-18"}]}
每次 1–500 条完整记录标识,重复标识自动去重。整批通过校验后在同一事务中删除,失败全部回滚;响应 deletedMetrics 为实际删除指标数量。后台的全选仅选当前页,切换页码或筛选条件会清空选择。
5. 看板查询
GET /api/dashboard 返回前端经营看板数据。录入成功后刷新看板即可读取;不会缓存旧值,也不会回退到本地示例数据。
未录入的指标显示 —;筛选合计累加全部筛选结果中已录入的值,不受分页影响。全部未录入时显示 —,已录入的零值计为零。合计环比按本期与参照期的已录入 GMV 合计计算,不累加或平均各店环比。日、周、月报告周期由已有日数据自动生成,经营数据默认优先月维度。
前台支持具体起止日期。占比自动计算为当前店铺 GMV / 同期全部店铺已录入 GMV 合计,未录入店铺不参与分母,店铺筛选、搜索和分页不改变分母。所有日期均保留完整指标列;月报与按月查看对比上一自然月,自定义范围对比紧邻的等长上一期。参照期数据不因日期筛选而丢失,并在经营表和导出中保留。
所有周期和自定义范围均按已有日记录计算,UV 为累计人次,客单价和转化率按推算人数计算。分母为零或缺失时不显示误导性的占比或环比。
6. 错误与重试
错误响应统一为:
{"error":{"code":"VALIDATION_ERROR","message":"提交数据不符合接口约定。","details":[{"path":"records[0].metrics.productConversionRate","message":"比例范围 0–1;2.5% 应传 0.025"}]}}
| HTTP | code | 处理方式 |
|---|---|---|
| 400 | INVALID_JSON | 修正 JSON |
| 404 | NOT_FOUND | 删除店铺时检查 ID |
| 409 | IDEMPOTENCY_CONFLICT | 新内容生成新的 requestId |
| 409 | TIMESTAMP_CONFLICT | 核对来源值,以实际新的采集时间提交修正 |
| 413 | PAYLOAD_TOO_LARGE | 拆分到 1 MiB 以内,每个新批次使用新 requestId |
| 415 | UNSUPPORTED_MEDIA_TYPE | 设置 application/json |
| 422 | VALIDATION_ERROR / UNKNOWN_SHOP | 修正字段、日期、数值;新店铺提供 shopAccount 和 shopName |
| 503 | DATABASE_UNAVAILABLE | 使用原 requestId 和原请求体退避重试 |
遇到网络超时也使用原 requestId 和原请求体重试,避免服务端已提交但客户端没收到响应造成重复。重试时不要改变 collectedAt。业务校验错误不应自动无限重试。
7. Python 工具调用
仓库提供 scripts/submit-metrics.py(只依赖 Python 标准库)。将请求写到 batch.json:
python scripts/submit-metrics.py --base-url http://localhost:3000 --file batch.json
脚本原样发送文件,不补造 ID 和时间;网络错误或 503 最多重试 3 次,始终保留相同请求体。可直接把 HTTP 调用部分集成到抓取程序。
import json, uuid
from datetime import datetime, timezone, timedelta
from urllib.request import Request, urlopen
# 在抓取成功后生成一次并持久化;失败重试复用此 payload。
payload = {
"requestId": str(uuid.uuid4()),
"source": "my-crawler",
"records": [{
"shopId": "256730481",
"shopAccount": "rakuten-main",
"shopName": "日系运动店",
"period": {"grain": "day", "startDate": "2026-10-18", "endDate": "2026-10-18"},
"collectedAt": datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="milliseconds"),
"metrics": {"gmv": "12345.67", "refundAmount": "345.67", "aov": "246.9134", "visitors": 5000, "productConversionRate": "0.025"}
}]
}
request = Request("http://localhost:3000/api/v1/ingestion",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}, method="POST")
with urlopen(request, timeout=30) as response:
print(json.load(response))
8. 数据库结构
全部表位于独立 schema shop_dashboard。
| 表 | 主键/粒度 | 用途 |
|---|---|---|
| shops | id(唯一主键) | account 账号、id 店铺ID、name 店铺名称;账号与名称不唯一 |
| collection_periods | id;唯一 grain + start_date + end_date | 仅 day 日周期,数据库约束起止日期必须相同 |
| metric_observations | shop_id + period_id + metric | 每项指标的当前值、实际采集时间、来源及请求 ID |
| ingestion_requests | request_id | 幂等去重、原始规范化请求、写入结果和接收时间;保留历史 |
| snapshots | id | 原报表快照结构,当前无预置数据 |
| periods | id | 原报表周期,含 MTD/YTD |
| period_metrics | snapshot_id + shop_id + period_id | 原周期指标、广告与动销 SPU 扩展字段 |
| daily_metrics | snapshot_id + shop_id + date | 原日指标结构 |
| listing_snapshots | snapshot_id + shop_id + period_id | 京麦/火种上架数量扩展 |
新抓取工具只需调用采集接口,由接口写入前四张表;无需创建整份快照,也不需直接操作 PostgreSQL。旧快照相关表保留结构但不再参与看板读取。广告、动销/上架 SPU 暂不在这版抓取接口范围内,未录入时展示 —。
数据关系
| 来源表 | 关系 | 指标表关联字段 |
|---|---|---|
| shops | 一家店铺 → 多项周期指标 | metric_observations.shop_id |
| collection_periods | 一个日期 → 多家店铺指标 | metric_observations.period_id |
| ingestion_requests | 一次写入请求 → 多项最新指标 | metric_observations.request_id |
每项指标以 shop_id + period_id + metric 唯一定位,按自身 collectedAt 更新。请求历史单独保存,便于重试去重与追溯。
每日销售按已录入日记录汇总 GMV 和 UV,空数据店铺不使其他店铺的数据变空;合计统计当前范围已录入的日数据,不要求整月录满。每行环比仍读取上一自然日,合计环比对比参照范围内的日数据。UV 合计未跨店、跨日去重。转化率和客单价不直接平均,缺少计算依据时可切换“按店铺明细”查看各店原始值。销售构成、每日销售及后台表格均支持表头排序,先排序后分页,导出沿用前台排序。