| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- #!/usr/bin/env node
- import fs from 'node:fs/promises';
- const TABLES_FILE = new URL('../logs/jd-official-upgrade-tables-2026-08-24.json', import.meta.url);
- const SP_PROBE_FILE = new URL('../logs/jd-sp-api-probe-2026-08-24.json', import.meta.url);
- const JOS_PROBE_FILE = new URL('../logs/jd-jos-api-probe-2026-08-24.json', import.meta.url);
- const DOC_FILE = new URL('../docs/JD-API-PERMISSIONS.md', import.meta.url);
- const escapeCell = (value) => String(value ?? '').replace(/\r?\n/g, ' ').replace(/\|/g, '\\|').trim();
- const compact = (value, limit = 100) => {
- const text = escapeCell(value).replace(/\s+/g, ' ');
- return text.length > limit ? `${text.slice(0, limit)}…` : text;
- };
- function parseOfficial(tables) {
- const endpoints = new Map();
- const mappingRows = [];
- for (const table of tables.filter((entry) => entry.ti >= 4 && entry.ti <= 11)) {
- let current = null;
- for (const cells of table.rows.slice(1)) {
- if (/^(GET|POST|PUT|PATCH|DELETE)\s/.test(cells[2] || '')) {
- const [method, ...rest] = cells[2].split(/\s+/);
- const path = `/${rest.join(' ')}`;
- current = {
- domain: cells[0] || '', subdomain: cells[1] || '', method, path,
- name: cells[3] || '', jos: [],
- };
- if (!endpoints.has(`${method} ${path}`)) endpoints.set(`${method} ${path}`, current);
- else current = endpoints.get(`${method} ${path}`);
- }
- if (!current) continue;
- const josApi = cells[4] || '';
- if (josApi) {
- const mapping = {
- domain: current.domain, spMethod: current.method, spPath: current.path, spName: current.name,
- josApi, josName: cells[5] || '', josCategory: cells[6] || '',
- };
- current.jos.push(mapping);
- mappingRows.push(mapping);
- }
- }
- }
- return { endpoints: [...endpoints.values()], mappingRows };
- }
- function groupCount(rows, field) {
- const result = new Map();
- for (const row of rows) result.set(row[field], (result.get(row[field]) || 0) + 1);
- return [...result.entries()].sort((a, b) => b[1] - a[1]);
- }
- function spResultTextSafe(result) {
- if (!result) return '未执行(非 GET)';
- if (result.status === '成功') return '成功(HTTP 200,`success: true`)';
- const code = result.code ? `,\`${escapeCell(result.code)}\`` : '';
- const message = result.message ? `,${compact(result.message, 64)}` : '';
- return `HTTP ${result.httpStatus}${code}${message}`;
- }
- function josResultText(result) {
- const code = result.code ? `,\`${escapeCell(result.code)}\`` : '';
- const message = result.message ? `,${compact(result.message, 64)}` : '';
- return `${escapeCell(result.status)}(HTTP ${result.httpStatus}${code}${message})`;
- }
- const tables = JSON.parse(await fs.readFile(TABLES_FILE, 'utf8'));
- const spProbe = JSON.parse(await fs.readFile(SP_PROBE_FILE, 'utf8'));
- const josProbe = JSON.parse(await fs.readFile(JOS_PROBE_FILE, 'utf8'));
- const official = parseOfficial(tables);
- const spByKey = new Map(spProbe.results.map((row) => [`${row.method} ${row.path}`, row]));
- const josByMethod = new Map(josProbe.results.map((row) => [row.josApi, row]));
- const officialJosMethods = new Set(official.mappingRows.map((row) => row.josApi).filter((value) => /^jingdong\./.test(value)));
- const testedOfficialJos = josProbe.results.filter((row) => officialJosMethods.has(row.josApi));
- const extraJos = josProbe.results.filter((row) => !officialJosMethods.has(row.josApi));
- const nonGet = official.endpoints.filter((row) => row.method !== 'GET');
- const spPassed = spProbe.results.filter((row) => row.status === '成功');
- const spBlocked = spProbe.results.filter((row) => ['99904030008', '99904030005'].includes(row.code));
- const spOther = spProbe.results.filter((row) => row.status !== '成功' && !['99904030008', '99904030005'].includes(row.code));
- const josPassed = josProbe.results.filter((row) => row.status === '方法响应');
- const josBlocked = josProbe.results.filter((row) => ['无接口调用权限', '要求云鼎调用'].includes(row.status));
- const josOther = josProbe.results.filter((row) => !['方法响应', '无接口调用权限', '要求云鼎调用'].includes(row.status));
- const endpointByKey = new Map(official.endpoints.map((row) => [`${row.method} ${row.path}`, row]));
- const lines = [];
- lines.push('# 京东官方 API 接口权限实测清单');
- lines.push('');
- lines.push('> 测试日期:2026-08-24 ');
- lines.push('> 配置来源:`docs/JD-CONNECTION-CONFIG.local.md`,店铺 Token 来源为 Parse `EcomAuth`。 ');
- lines.push('> 官方清单来源:[京东 SP-API 升级说明](https://open.jd.com/v2/#/doc/api?apiCateId=200436&articleId=1100596&gwType=1)。');
- lines.push('');
- lines.push('## 1. 测试范围和结果');
- lines.push('');
- lines.push('| 接口体系 | 官方接口数 | 已实际请求 | 通过/返回方法响应 | 明确拒绝 | 其他错误 |');
- lines.push('| --- | ---: | ---: | ---: | ---: | ---: |');
- lines.push(`| SP-API | ${official.endpoints.length} | ${spProbe.results.length} | ${spPassed.length} | ${spBlocked.length} | ${spOther.length} |`);
- lines.push(`| JOS 1.0 | ${officialJosMethods.size} 个官方映射方法 + ${extraJos.length} 个评价方法 | ${josProbe.results.length} | ${josPassed.length} | ${josBlocked.length} | ${josOther.length} |`);
- lines.push('');
- lines.push(`SP-API 的 ${official.endpoints.length} 个接口均已发起请求,包括 ${spProbe.results.filter((row) => row.method === 'GET').length} 个 GET 和 ${spProbe.results.filter((row) => row.method !== 'GET').length} 个非 GET。非 GET 使用占位路径参数和空 JSON 请求体执行,没有提交有效业务数据。`);
- lines.push('');
- lines.push('判定口径:');
- lines.push('');
- lines.push('- SP-API 返回 `success: true`:通过。');
- lines.push('- JOS 返回对应方法响应对象,且未返回网关权限错误:方法已响应。');
- lines.push('- `99904030008`、JOS `24/88`:明确无接口调用权限。');
- lines.push('- `99904030005`、JOS `73`:明确要求云鼎调用。');
- lines.push('- 参数错误或后端服务错误:本次未通过,不等同于已拥有完整业务调用能力。');
- lines.push('');
- lines.push('## 2. 未通过接口的申请条件和申请方式');
- lines.push('');
- lines.push('### 2.1 当前失败结果分别怎么处理');
- lines.push('');
- lines.push('| 实测结果 | 当前涉及范围 | 申请条件 | 申请入口与操作 |');
- lines.push('| --- | --- | --- | --- |');
- lines.push('| SP `99904030008`;JOS `24/88` | 商品、订单物流、售后、财务、会员、客服、商家、通用服务 | 当前应用类型和应用场景下,该接口权限包必须出现在应用概览页的“可申请”列表 | 开发者后台 → 应用中心 → 应用列表 → 当前应用 → 应用概览 → 权限包清单 → 申请;填写申请原因并提交 |');
- lines.push('| SP `99904030005`;JOS `73` | 订单、部分售后、财务以及其他敏感接口 | 应用审核完成;使用与开放平台店铺主账号一致的京东账号激活京东云并完成企业实名认证;完成云鼎入驻和应用部署 | 先完成接口权限包申请,再进入云鼎入驻页选择“零售”提交申请;云鼎审核通过后在云鼎环境部署并调用 |');
- lines.push('| SP `99904000013`、`99904000001` 及各业务参数错误码;JOS `61/400` | 本轮使用空请求体、占位 ID 或缺省参数的接口 | 这类响应是请求参数校验结果,本轮响应不能判定为缺少权限包 | 按具体接口文档补齐必填参数、有效业务 ID、场景字段和请求体后再次调用;不走权限包申请 |');
- lines.push('| HTTP 503、JOS `65/67` | 商品、商家、通用等接口 | 本轮返回业务后端或服务异常,没有形成权限拒绝结论 | 使用完整参数复测;仍返回服务异常时,通过开放平台工单提交接口、时间和 requestId |');
- lines.push('');
- lines.push('### 2.2 普通接口权限包');
- lines.push('');
- lines.push('京东官方把权限包分为“默认开通”和“可申请”:');
- lines.push('');
- lines.push('- 默认开通:应用创建后直接拥有。');
- lines.push('- 可申请:需要单独申请,运营审核通过后获得。');
- lines.push('');
- lines.push('申请步骤:');
- lines.push('');
- lines.push('1. 登录[京东开放平台开发者后台](https://open-console.jd.com/v2#/app/index)。');
- lines.push('2. 进入“应用中心 → 应用列表”,点击当前应用。');
- lines.push('3. 在应用概览页下方查看权限包清单。');
- lines.push('4. 在“可申请”权限包上点击“申请”。');
- lines.push('5. 填写申请原因并提交。');
- lines.push('6. 官方记录的审核周期为 1—3 个工作日。');
- lines.push('');
- lines.push('如果所需接口在“已获得”和“可申请”中都不存在,官方给出的处理方式是提交工单,或加入咚咚群 `10220332728` 咨询。');
- lines.push('');
- lines.push('当前 `99904030008` 的 SP-API 分布:');
- lines.push('');
- lines.push('| 数据域 | 接口数 |');
- lines.push('| --- | ---: |');
- for (const [domain, count] of [...new Map(spProbe.results.filter((row) => row.code === '99904030008').reduce((items, row) => items.set(row.domain, (items.get(row.domain) || 0) + 1), new Map())).entries()].sort((a, b) => b[1] - a[1])) {
- lines.push(`| ${escapeCell(domain)} | ${count} |`);
- }
- lines.push('');
- lines.push('### 2.3 云鼎敏感接口');
- lines.push('');
- lines.push('官方云鼎入驻条件和流程:');
- lines.push('');
- lines.push('1. 先在京东开放平台创建并完成应用审核;官方云鼎文档注明,自研应用审核完成后再执行云鼎入驻。');
- lines.push('2. 与开放平台商家店铺主账号一致的京东账号,需要在京东云激活并完成企业实名认证。');
- lines.push('3. 使用该账号进入[云鼎入驻页](https://yd.jdcloud.com/#/enter),业务选择“零售”;官方记录的审核周期为 1—2 个工作日。');
- lines.push('4. 审核通过后,在云鼎应用管理平台完成应用部署。');
- lines.push('5. 按云鼎文档配置云资源;文档将对外提供 HTTP/HTTPS 的 Web 服务器、访问 JDO 敏感接口的应用服务器,以及存储敏感字段的云 RDS 列为基础部署资源。');
- lines.push('6. 从云鼎环境重新调用订单、售后、财务等返回 `99904030005` / JOS `73` 的接口。');
- lines.push('');
- lines.push('当前 `99904030005` 的 SP-API 分布:');
- lines.push('');
- lines.push('| 数据域 | 接口数 |');
- lines.push('| --- | ---: |');
- for (const [domain, count] of [...new Map(spProbe.results.filter((row) => row.code === '99904030005').reduce((items, row) => items.set(row.domain, (items.get(row.domain) || 0) + 1), new Map())).entries()].sort((a, b) => b[1] - a[1])) {
- lines.push(`| ${escapeCell(domain)} | ${count} |`);
- }
- lines.push('');
- lines.push('### 2.4 自营供应链接口的特殊流程');
- lines.push('');
- lines.push('京东官方注明:自营供应链接口权限除在应用的 API 权限管理中申请权限包外,还需要使用核心产品线账号向主采销发起邮件;采销邮件审核通过后,平台才审核开通。该流程只适用于自营供应链接口。');
- lines.push('');
- lines.push('### 2.5 服务对象和店铺授权前置条件');
- lines.push('');
- lines.push('- 自研 POP 应用需要添加 POP 店铺账号作为服务对象;自营应用需要添加供应商账号。');
- lines.push('- 同一公司账号可自动审核;同一主体、同法人、同集团等关系需要提交关系证明,官方记录的人工审核时限为 1 个工作日。');
- lines.push('- 服务对象绑定成功后才能执行店铺授权并获得 Token。当前项目已经取得有效 Token,此项不是本次商品接口成功/失败差异的阻断点。');
- lines.push('');
- lines.push('### 2.6 官方依据');
- lines.push('');
- lines.push('- [接口权限包申请](https://open.jd.com/v2/#/doc/guide?listId=1911)');
- lines.push('- [开放平台新手指南](https://open.jd.com/v2/#/doc/begin-guide?listId=1900)');
- lines.push('- [自研与 ISV 授权 Token 获取](https://open.jd.com/v2/#/doc/dev-guide?listId=1100981)');
- lines.push('- [云鼎入驻流程](https://yd-doc.jdcloud.com/)');
- lines.push('');
- lines.push('## 3. 通过的接口');
- lines.push('');
- lines.push('### 3.1 SP-API:业务响应成功');
- lines.push('');
- lines.push('| 数据域 | 方法与路径 | 官方名称 | 结果 |');
- lines.push('| --- | --- | --- | --- |');
- for (const row of spPassed) lines.push(`| ${escapeCell(row.domain)} | \`${row.method} ${row.path}\` | ${escapeCell(row.name)} | HTTP ${row.httpStatus},\`success: true\` |`);
- lines.push('');
- lines.push('### 3.2 JOS:方法已响应');
- lines.push('');
- lines.push('| 数据域 | JOS 方法 | 对应 SP-API | 结果 |');
- lines.push('| --- | --- | --- | --- |');
- for (const row of josPassed) {
- const sp = row.spPath ? `\`${row.spMethod} ${row.spPath}\`` : '商品评价旧接口';
- lines.push(`| ${escapeCell(row.domain)} | \`${escapeCell(row.josApi)}\` | ${sp} | HTTP ${row.httpStatus},返回方法响应对象 |`);
- }
- lines.push('');
- lines.push('## 4. 未通过的 SP-API');
- lines.push('');
- lines.push('### 4.1 明确被京东网关拒绝');
- lines.push('');
- lines.push('| 数据域 | 方法与路径 | 官方名称 | 实测结果 |');
- lines.push('| --- | --- | --- | --- |');
- for (const row of spBlocked) lines.push(`| ${escapeCell(row.domain)} | \`${row.method} ${row.path}\` | ${escapeCell(row.name)} | ${spResultTextSafe(row)} |`);
- lines.push('');
- lines.push('### 4.2 参数或服务错误,本次未成功');
- lines.push('');
- lines.push('| 数据域 | 方法与路径 | 官方名称 | 实测结果 |');
- lines.push('| --- | --- | --- | --- |');
- for (const row of spOther) lines.push(`| ${escapeCell(row.domain)} | \`${row.method} ${row.path}\` | ${escapeCell(row.name)} | ${spResultTextSafe(row)} |`);
- lines.push('');
- lines.push('## 5. 未通过的 JOS 接口');
- lines.push('');
- lines.push('### 5.1 明确无权限或要求云鼎');
- lines.push('');
- lines.push('| 数据域 | JOS 方法 | 对应 SP-API | 实测结果 |');
- lines.push('| --- | --- | --- | --- |');
- for (const row of josBlocked) {
- const sp = row.spPath ? `\`${row.spMethod} ${row.spPath}\`` : '无对应 SP 路径';
- lines.push(`| ${escapeCell(row.domain)} | \`${escapeCell(row.josApi)}\` | ${sp} | ${josResultText(row)} |`);
- }
- lines.push('');
- lines.push('### 5.2 参数或服务错误,本次未成功');
- lines.push('');
- lines.push('| 数据域 | JOS 方法 | 对应 SP-API | 实测结果 |');
- lines.push('| --- | --- | --- | --- |');
- for (const row of josOther) {
- const sp = row.spPath ? `\`${row.spMethod} ${row.spPath}\`` : '无对应 SP 路径';
- lines.push(`| ${escapeCell(row.domain)} | \`${escapeCell(row.josApi)}\` | ${sp} | ${josResultText(row)} |`);
- }
- lines.push('');
- lines.push('## 6. 项目内已经实现的京东调用');
- lines.push('');
- lines.push('| 项目位置 | 京东接口 | 用途 |');
- lines.push('| --- | --- | --- |');
- lines.push('| `tools/jd-import-products.mjs` | `GET /sp-product/v0/products` | 商品分页读取 |');
- lines.push('| `tools/jd-import-products.mjs` | `GET /sp-product/v0/skus` | SKU 读取 |');
- lines.push('| `tools/jd-import-products.mjs` | `GET /sp-product/v0/sku-stocks` | SKU 库存读取 |');
- lines.push('| `tools/jd-import-products.mjs` | `GET /sp-product/v0/products/{productId}` | 商品详情读取 |');
- lines.push('| `Saas-voc-server/scripts/sync-jd-listings.ts` | 商品列表、商品详情 | Listing 全量同步 |');
- lines.push('');
- lines.push('## 7. 测试文件');
- lines.push('');
- lines.push('- `logs/jd-official-upgrade-tables-2026-08-24.json`');
- lines.push('- `logs/jd-sp-api-probe-2026-08-24.json`');
- lines.push('- `logs/jd-jos-api-probe-2026-08-24.json`');
- lines.push('- `tools/jd-api-permission-probe.mjs`');
- lines.push('- `tools/jd-jos-api-permission-probe.mjs`');
- await fs.writeFile(DOC_FILE, `${lines.join('\n')}\n`, 'utf8');
- console.log(`Generated ${DOC_FILE.pathname}: ${official.endpoints.length} SP endpoints, ${official.mappingRows.length} mapping rows.`);
|