فهرست منبع

refactor(接入文档): 文档按通道拆成两份,两条通道不再混在一个分组里

「收付款接口」这一组原本把两条通道的文档塞在同一份 md、同一个侧边栏分组里:
第 1–9 章是收付款通道(/payment/openapi/pay),第 10–11 章是门店与订单通道
(/payment/openapi/har)。两者是不同的产品,裁定必须分开,且本批只上门店与
订单那条。

拆法(零内容丢失 —— 两份合并回去逐字节等于原文 47169 B):
- docs/collection-openapi.md  30691 B / 738 CR —— 收付款,内容一字未动
- docs/har-openapi.md         14714 B / 263 CR —— 门店与订单
- 新增 docSlicer.ts:切片逻辑(含 2026-09-24 那个 CRLF 修复)只此一份,
  collectionDoc.ts / harDoc.ts 各只写「哪些标题拼成哪一条」
- index.vue:侧边栏拆两组,门店与订单露出;收付款那组由 SHOW_COLLECTION_DOC
  关掉(正文与切片都已就位,改成 true 即可放出,不必动 md)

门店与订单那份不再提 enterprise_id:
- 先确认代码侧非必填 —— 三个开放 DTO 均无校验注解、javadoc 写「选填」,
  管理端三处 required = false ⇒ 只改文档,后端零改动
- 12 处清除:差异表行、寻址段、三个接口的参数表、门店响应的归属字段、
  去重说明里的「两条寻址路径」、以及「不属于你的企业」那句
- 寻址改为只讲租户账号 + merchant_code 两条

验证:
- 真源码探针(esbuild 插件接管 ?raw,不 sed、不复制代码,跑的就是改后那几份 ts):
  harSections 1 条 / 8030 字符 / enterprise 出现 0 次;
  collectionSections 10 条 / 无 har_endpoints / 不误含第 10 章
- 两向变异:
  harDoc 两条 parts 标题都改坏 ⇒ 1→0 条,且收付款那 10 条不受影响;
  docSlicer 把 /\r?\n/ 改回 "\n" ⇒ 两条通道同时归零(复现原缺陷,证明修复承重);
  无害变异(改注释)⇒ 读数与基线逐字相同
  每个变异还原后核 sha256 逐字节相同、探针回绿
- pnpm ts:check 289 → 289(本仓基线非零,只能同机前后差分)
- pnpm build exit 0;产物里 \r\n 转义 = 1001 = 738 + 263,与两份 md 的 CR 合计
  逐字对上;HAR 段落内 enterprise = 0、merchant_code = 10(正对照);
  侧边栏「收付款接口」串在产物里 0 次(开关生效),同组其它 h3 仍在(正对照)
- prettier:新增的 docSlicer.ts 已合规;index.vue 的差异只在 711/807/824 三行,
  与本次改动的行区间零交集(既有问题,未动)

未部署、未 push。
alphaH 3 هفته پیش
والد
کامیت
f8ba701edf

+ 7 - 94
frontend/src/views/module_payment/apikey/collectionDoc.ts

@@ -1,89 +1,17 @@
 /**
- * 把《收付款 OpenAPI 接入文档》按章节切给「接入文档」tab 用。
+ * 《收付款 OpenAPI 接入文档》的侧边栏条目定义(正文 `docs/collection-openapi.md`)。
  *
- * 文档本身(`docs/collection-openapi.md`)是唯一真相,本文件只做切片,不复制任何正文 ——
- * 改 md,页面跟着变,不存在「文档改了页面没改」。
- *
- * 切片依据是标题原文,所以 md 里**标题被改字**会让对应条目从侧边栏消失(见下方 absent 逻辑):
- * 宁可少一条,也不要一个点进去空白的页面。
+ * 切片逻辑在 `docSlicer.ts`,本文件只写「哪些标题拼成哪一条」。
+ * 另一条通道(门店与订单)是独立产品、独立文档,见 `harDoc.ts` 与 `docs/har-openapi.md`。
  */
 import raw from "./docs/collection-openapi.md?raw";
-
-interface Block {
-  /** 标题层级:`##` 为 2,`###` 为 3 */
-  level: number;
-  /** 标题整行原文,例:`## 5. 接口详情` */
-  title: string;
-  /** 标题之后、下一个标题之前的正文 */
-  body: string;
-}
-
-function parseBlocks(doc: string): Block[] {
-  const blocks: Block[] = [];
-  let current: Block | null = null;
-  // ★ 必须按 /\r?\n/ 切,不能按 "\n" 切:本仓 core.autocrlf=true 且无 .gitattributes,
-  //   工作区的 md 是 CRLF,按 "\n" 切会给每行尾巴留一个 \r;而下面那个正则里 `.` 不匹配
-  //   \r、`$` 又没开 m 标志 ⇒ **整个文件一行标题都认不出来**,BLOCKS 为空、
-  //   每一条 take() 都返回 []、「收付款接口」整组在侧边栏渲染成一个空 ul。
-  //   2026-09-24 线上实测:md 1013 个 CR、parseBlocks 得 0 块、侧边栏 0 条;
-  //   同一份文件归一成 LF 后 36 块、11 条全回来。改回 "\n" 会让整组文档静默消失。
-  for (const line of doc.split(/\r?\n/)) {
-    const m = /^(#{1,6})\s+(.*)$/.exec(line);
-    if (m) {
-      if (current) blocks.push(current);
-      current = { level: m[1].length, title: line, body: "" };
-    } else if (current) {
-      current.body += line + "\n";
-    }
-  }
-  if (current) blocks.push(current);
-  return blocks;
-}
-
-const BLOCKS = parseBlocks(raw);
-
-/**
- * 取标题为 title 的块,及其后所有**层级更深**的子块。
- * 标题不存在时返回空数组。
- */
-function take(title: string): Block[] {
-  const start = BLOCKS.findIndex(b => b.title === title);
-  if (start < 0) return [];
-  const out = [BLOCKS[start]];
-  for (let i = start + 1; i < BLOCKS.length && BLOCKS[i].level > BLOCKS[start].level; i++) {
-    out.push(BLOCKS[i]);
-  }
-  return out;
-}
-
-function join(blocks: Block[]): string {
-  return blocks.map(b => `${b.title}\n${b.body}`).join("\n");
-}
-
-/**
- * GFM 任务列表 `- [ ]` → `☐`。
- *
- * markdown-it 默认不带 task-list 插件,`- [ ] 用样例自测签名` 会**原样**渲染出方括号,
- * 看上去像坏掉的 markdown。这里只在展示层替换,md 文件仍是标准 GFM 语法 ——
- * 在 GitHub、编辑器里打开依旧是真复选框。
- */
-function toDisplayCheckboxes(md: string): string {
-  return md.replace(/^(\s*)- \[ \] /gm, "$1- ☐ ").replace(/^(\s*)- \[[xX]\] /gm, "$1- ☑ ");
-}
-
-export interface CollectionDocSection {
-  /** 侧边栏与卡片标题上显示的名字 */
-  title: string;
-  /** 该条目对应的 markdown 正文 */
-  content: string;
-}
+import { buildSections, type DocSectionDef } from "./docSlicer";
 
 /**
  * 侧边栏条目定义。键即 `activeSection` 的值。
- * `parts` 里的标题必须与 md 里的标题**逐字一致**(含空格、全角括号)——
- * 这是唯一会静默失效的地方,改标题时务必同步改这里。
+ * `parts` 里的标题必须与 md 里的标题**逐字一致**(含空格、全角括号)。
  */
-const SECTIONS: Array<{ key: string; title: string; parts: string[] }> = [
+const SECTIONS: DocSectionDef[] = [
   {
     key: "collection_intro",
     title: "接入总则",
@@ -106,21 +34,6 @@ const SECTIONS: Array<{ key: string; title: string; parts: string[] }> = [
     title: "联调与常见问题",
     parts: ["## 8. 联调检查清单", "## 9. 常见问题"],
   },
-  // 另一条通道(/payment/openapi/har)。整章只给一个条目,不再拆成 10.1~10.5 ——
-  // 章首那张差异表(金额单位、幂等性)必须先读,拆开会让读者直接跳到 10.2 下单。
-  // 注意:`take("## 10. …")` 会一并吞掉其下所有 `###` 子块,所以这里**不能**改写成
-  // 「## 10 + 各 ### 分开列」—— 那样差异表会被重复渲染五次。
-  {
-    key: "har_endpoints",
-    title: "门店与订单",
-    parts: ["## 10. 门店与订单接口", "## 11. 门店与订单通道的联调检查清单"],
-  },
 ];
 
-export const collectionSections: Record<string, CollectionDocSection> = Object.fromEntries(
-  SECTIONS.flatMap(({ key, title, parts }) => {
-    const blocks = parts.flatMap(take);
-    // 标题对不上就整条不显示,避免点进去一片空白让人以为接口不存在
-    return blocks.length === 0 ? [] : [[key, { title, content: toDisplayCheckboxes(join(blocks)) }]];
-  })
-);
+export const collectionSections = buildSections(raw, SECTIONS);

+ 102 - 0
frontend/src/views/module_payment/apikey/docSlicer.ts

@@ -0,0 +1,102 @@
+/**
+ * 文档切片器 —— 把一份 markdown 按标题切成「接入文档」页侧边栏的条目。
+ *
+ * 两条通道各有一份 md、各有一组 SECTIONS,共用本文件:
+ * 收付款见 `collectionDoc.ts`(`docs/collection-openapi.md`),
+ * 门店与订单见 `harDoc.ts`(`docs/har-openapi.md`)。
+ *
+ * md 是唯一真相,本文件不复制任何正文 —— 改 md,页面跟着变,不存在「文档改了页面没改」。
+ */
+
+export interface DocBlock {
+  /** 标题层级:`##` 为 2,`###` 为 3 */
+  level: number;
+  /** 标题整行原文,例:`## 5. 接口详情` */
+  title: string;
+  /** 标题之后、下一个标题之前的正文 */
+  body: string;
+}
+
+/** 一个侧边栏条目的定义。`parts` 里的标题必须与 md 里逐字一致。 */
+export interface DocSectionDef {
+  /** 条目键,即页面里 `activeSection` 的值 */
+  key: string;
+  /** 侧边栏与卡片标题上显示的名字 */
+  title: string;
+  /** 拼成这一条正文的标题列表(按顺序取,可跨章节) */
+  parts: string[];
+}
+
+export interface DocSection {
+  title: string;
+  content: string;
+}
+
+export function parseBlocks(doc: string): DocBlock[] {
+  const blocks: DocBlock[] = [];
+  let current: DocBlock | null = null;
+  // ★ 必须按 /\r?\n/ 切,不能按 "\n" 切:本仓 core.autocrlf=true 且无 .gitattributes,
+  //   工作区的 md 是 CRLF,按 "\n" 切会给每行尾巴留一个 \r;而下面那个正则里 `.` 不匹配
+  //   \r、`$` 又没开 m 标志 ⇒ **整个文件一行标题都认不出来**,BLOCKS 为空、
+  //   每一条 take() 都返回 []、整组在侧边栏渲染成一个空 ul。
+  //   2026-09-24 线上实测:md 1013 个 CR、parseBlocks 得 0 块、侧边栏 0 条;
+  //   同一份文件归一成 LF 后 36 块、11 条全回来。改回 "\n" 会让整组文档静默消失。
+  for (const line of doc.split(/\r?\n/)) {
+    const m = /^(#{1,6})\s+(.*)$/.exec(line);
+    if (m) {
+      if (current) blocks.push(current);
+      current = { level: m[1].length, title: line, body: "" };
+    } else if (current) {
+      current.body += line + "\n";
+    }
+  }
+  if (current) blocks.push(current);
+  return blocks;
+}
+
+/**
+ * 取标题为 title 的块,及其后所有**层级更深**的子块。
+ * 标题不存在时返回空数组。
+ */
+function take(blocks: DocBlock[], title: string): DocBlock[] {
+  const start = blocks.findIndex((b) => b.title === title);
+  if (start < 0) return [];
+  const out = [blocks[start]];
+  for (let i = start + 1; i < blocks.length && blocks[i].level > blocks[start].level; i++) {
+    out.push(blocks[i]);
+  }
+  return out;
+}
+
+function join(blocks: DocBlock[]): string {
+  return blocks.map((b) => `${b.title}\n${b.body}`).join("\n");
+}
+
+/**
+ * GFM 任务列表 `- [ ]` → `☐`。
+ *
+ * markdown-it 默认不带 task-list 插件,`- [ ] 用样例自测签名` 会**原样**渲染出方括号,
+ * 看上去像坏掉的 markdown。这里只在展示层替换,md 文件仍是标准 GFM 语法 ——
+ * 在 GitHub、编辑器里打开依旧是真复选框。
+ */
+export function toDisplayCheckboxes(md: string): string {
+  return md.replace(/^(\s*)- \[ \] /gm, "$1- ☐ ").replace(/^(\s*)- \[[xX]\] /gm, "$1- ☑ ");
+}
+
+/**
+ * 按 defs 把 raw 切成 `{ key: { title, content } }`。
+ *
+ * 标题对不上就整条不显示 —— 宁可少一条,也不要一个点进去空白的页面。
+ * 这也是唯一会静默失效的地方:改 md 标题时务必同步改 defs 里的 parts。
+ */
+export function buildSections(raw: string, defs: DocSectionDef[]): Record<string, DocSection> {
+  const blocks = parseBlocks(raw);
+  return Object.fromEntries(
+    defs.flatMap(({ key, title, parts }) => {
+      const picked = parts.flatMap((p) => take(blocks, p));
+      return picked.length === 0
+        ? []
+        : [[key, { title, content: toDisplayCheckboxes(join(picked)) }]];
+    })
+  );
+}

+ 0 - 275
frontend/src/views/module_payment/apikey/docs/collection-openapi.md

@@ -736,278 +736,3 @@ Content-Type: application/x-www-form-urlencoded
 
 ---
 
-## 10. 门店与订单接口
-
-本章是**另一条通道**,路径前缀 `/payment/openapi/har`,与 5 章的收款接口不是同一个接口族。
-
-**相同的部分**:请求头与签名算法(3.2 / 3.3 逐字适用)、错误处理总则(3.6)、`enterprise_id` 的含义(2)。
-
-**不同的部分 —— 动手前先读完这张表**:
-
-| 项 | 5 章收款接口 | 本章 |
-|---|---|---|
-| 路径前缀 | `/payment/openapi/pay` | `/payment/openapi/har` |
-| 响应信封 | 三个下单接口 A 型,其余 B 型 | **3 个接口全部 B 型** |
-| `enterprise_id` | 必填 | **选填**:不传即按租户账号寻址,见下 |
-| 金额 | 整数,单位**分** | 小数,单位**元** |
-| 订单状态 | 文本枚举(`SUCCESS` / `PAYING`…),见 6 | **数字字符串**(`"0"` / `"1"`…),见 10.5 |
-| 下单幂等 | 同 `out_trade_no` 重发返回原单 | **不幂等**,每次调用都新建一笔,见 10.2 |
-| 退款 | 有(5.6 / 5.7) | **本章没有退款接口** |
-
-> ⚠️ **「不幂等」与「金额单位是元」这两条最容易出事。** 前者会让你在超时重发时收到两笔扣款,后者会把 1 元写成 1 分。请读完 10.2 的注意框再动手。
-
-**关于 `enterprise_id`**:本章三个接口的 `enterprise_id` **都是选填**。
-
-- **不传** ⇒ 平台按**你这条 API Key 所属的租户账号**寻址,取该租户名下由平台运营报备好的商户配置。租户下只有一个收款主体、也只配了一个商户号时,这是最省事的用法。
-- **传了** ⇒ 按该企业寻址,该企业必须已在平台完成商户报备,否则返回 `code=403`、`msg` 为 `无权限操作该企业`(平台有意不区分「这个企业不存在」与「不属于你」,避免被用来探测他人企业)。
-
-两种方式**并存**,不是替代:一个租户下有多个收款主体、需要分开记账时才传。**同一个租户下,两者的商户号是分开报备的** —— 之前一直不传的调用方,不要在某一笔里突然传一个从未报备过的 `enterprise_id`。
-
-**关于 `merchant_code`(新增,2026-09-24)**:平台**允许一个租户报备多个商户号**(例如你名下有多家门店主体,各自独立结算)。这时「这笔钱收进哪一个商户」必须由你在请求里说清楚,用 `merchant_code` 指定。
-
-- **不传** ⇒ 平台按顺序匹配:先看 `enterprise_id`,再看你租户下的商户配置。
-- **传了** ⇒ 一律以它为准(它**优先于** `enterprise_id`),且必须是你租户名下已报备的编号,否则返回 404。
-
-> ⚠️ **你租户下配了多个商户号、而你又不传 `merchant_code` 时,接口会直接**拒绝**(`code=400`,`msg` 为 `当前租户配置了多个商户编号,请在请求中指定 merchant_code`),不会替你挑一个。**这是有意的** —— 挑错了钱会进另一个商户的账户,而且全程没有任何报错,要等到对账才发现。**只配了一个商户号的调用方不受影响**,什么都不用改。
-
-> **怎么知道自己配了几个**:登录平台,在「商户配置」页可以看到你名下的全部商户号。三个接口都填同一个 `merchant_code` 即可。
-
-**门店与下单的关系**:10.2 里的 `shop_code` / `third_shop_code` 都是**选填**。门店不是下单的前置条件 —— 只有需要把交易归属到某个具体门店时才填。
-
----
-
-### 10.1 创建门店
-
-为你的一个线下门店建号。平台侧**没有门店查询接口**,本接口的返回就是你日后核对门店状态的依据,请把 `shop_id` 存下来。
-
-```
-POST /payment/openapi/har/shop/create
-```
-
-**请求**
-
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `enterprise_id` | string | 否 | 门店归属的企业标识。**不传即按你的租户账号寻址**,见本章开头 |
-| `merchant_code` | string | 否 | **你租户下配了多个商户号时必填**:门店建在哪一个商户下,见本章开头。只配了一个的不用传 |
-| `third_shop_id` | string | 是 | 你方的门店编码,≤32 字符。**同一个归属下不可重复**,见下 |
-| `shop_name` | string | 是 | 门店名称,≤128 字符 |
-
-**响应**(B 型,`data` 内)
-
-| 字段 | 说明 |
-|---|---|
-| `third_shop_id` | 原样返回 |
-| `shop_name` | 原样返回 |
-| `shop_id` | 平台门店编号,**建号成功才有值**;下单时作为 `shop_code` 传入 |
-| `create_status` | 建号结果,见下 |
-| `enterprise_id` | 归属企业。**按租户寻址建立的门店,这里是空的** |
-| `merchant_code` | 平台侧为该企业(或该租户)配置的商户编号(你无需关心,原样带回) |
-| `fail_reason` | `create_status` 非 `SUCCESS` 时的原因 |
-| `created_time` / `updated_time` | 记录时间 |
-
-`create_status` 取值:
-
-| 值 | 含义 | 你该做什么 |
-|---|---|---|
-| `SUCCESS` | 门店已建好,`shop_id` 可用 | 保存 `shop_id` |
-| `FAILED` | 明确被拒,不会建号 | 看 `fail_reason`,改好再调一次 |
-| `PENDING` | **结果未知** —— 调用超时或网络失败,无法确认门店是否已经建出来 | **不要直接重发**(可能建出两个门店),联系平台方人工核实 |
-
-> ⚠️ **`third_shop_id` 重复会被直接拒绝,而不是「返回原有门店」。** 同一个归属下重复提交同一个 `third_shop_id`,返回 `code=500`,`msg` 形如 `门店编码已存在: <你的编码>`。所以本接口的正确幂等做法是**先查你本地的记录,再决定要不要调**,而不是无脑重发。
->
-> 这里的「归属」就是上面说的两条寻址路径:**传了 `enterprise_id` 的按企业查重,不传的按租户查重**,两者互不干扰。也就是说,同一个 `third_shop_id` 在「你租户下」和「你某个企业下」可以各存在一份 —— 如果你两边都在用,请自己保证编码不要混。
-
-> ⚠️ **`PENDING` 不是「处理中」。** 门店创建**不自动重试** —— 重试可能在上游建出两个门店。收到 `PENDING` 时唯一正确的动作是找平台方核实,不要自行重发。
-
----
-
-### 10.2 下单收款
-
-同一个接口承担两种场景,由 `auth_code` 是否有值决定:
-
-| `auth_code` | 场景 | 行为 |
-|---|---|---|
-| **有值** | 被扫 —— 用户出示付款码,你扫码后把码传进来 | 立即发起扣款 |
-| **不传** | 主扫 —— 平台返回一条收款链接,你渲染成二维码给用户扫 | 只下单,等用户付款 |
-
-```
-POST /payment/openapi/har/order/create
-```
-
-**请求**
-
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `enterprise_id` | string | 否 | 收款企业标识。**不传即按你的租户账号寻址**,见本章开头 |
-| `merchant_code` | string | 否 | **你租户下配了多个商户号时必填**:这笔钱收进哪一个商户,见本章开头。它**优先于** `enterprise_id` |
-| `price` | number | 是 | 金额,**单位元**,必须 > 0。与 5 章的「整数分」不同,见下 |
-| `title` | string | 是 | 商品名称,≤64 字符 |
-| `trade_type` | string | 是 | `alipay_payment` 支付宝支付 / `alipay_stage` 花呗分期 |
-| `out_biz_no` | string | 否 | 你自己的订单号,≤64 字符,原样存下供你查询 |
-| `auth_code` | string | 否 | 用户的付款码。有值走被扫,不传走主扫 |
-| `stage_num` | string | 否 | 分期期数:`3` / `6` / `12` / `24`。`trade_type=alipay_stage` 时必填 |
-| `percent` | string | 否 | 分期手续费承担方:`100` 商户承担 / `0` 用户承担。`trade_type=alipay_stage` 时必填 |
-| `shop_code` | string | 否 | 平台门店编号(10.1 返回的 `shop_id`) |
-| `third_shop_code` | string | 否 | 你方门店编码,≤32 字符 |
-| `pay_timeout` | int | 否 | 支付过期时间,取值 2–60(单位由上游定义,平台原样透传) |
-| `extra_params` | string | 否 | 透传字段 |
-
-**响应**(B 型,`data` 内)
-
-| 字段 | 说明 |
-|---|---|
-| `order_no` | **平台订单号**,后续查询(10.3)用它 |
-| `out_biz_no` | 原样返回 |
-| `upstream_order_no` | 上游订单号。下单成功才有值,下单超时时为空 |
-| `platform_trade_no` | 平台交易号,支付成功后才有值 |
-| `pay_url` | **主扫**:收款链接,渲染成二维码即可。**被扫**:为空 |
-| `trade_status` | 订单状态,见 10.5 |
-| `price` | 订单金额(元) |
-| `title` / `trade_type` / `stage_num` / `percent` | 原样返回 |
-| `shop_code` / `third_shop_code` | 原样返回 |
-| `trade_time` | 支付成功时间 |
-| `rcv_amount` | 实收金额(元),支付成功后有值 |
-| `fee_amount` / `channel_amount` | 手续费 / 渠道金额 |
-| `refund_time` | 退款时间。本章无退款接口,该字段恒为空 |
-| `notify_status` | 平台向你推送回调的结果,见 10.4 |
-| `created_time` / `updated_time` | 记录时间 |
-
-> ⚠️ **本接口不幂等 —— 重复调用会产生两笔订单、两次扣款。** 每次调用都会生成一个新的 `order_no` 并向上游发起一笔新的交易,平台**不做** `out_biz_no` 去重(也不做唯一约束)。超时后不要直接重发:先用 **10.3 订单查询** 按 `out_biz_no` 查一次,查到就用那一笔的结果。
-
-> ⚠️ **`price` 的单位是「元」,是小数,不是「分」。** `1.5` 表示 1 元 5 角;5 章那一套要写 `total_amount=150` 才是同样的金额。**跨通道搬代码时这是最容易错的一处。**
-
-> ⚠️ **金额的写法会直接影响签名。** 签名是对你**实际发出的那个值**做字符串化后算的(3.3),而平台把 JSON 数字读成双精度数:你写 `0.010`,平台看到的是 `0.01` —— 签名因此对不上,只会返回「签名验证失败」,不会告诉你原因。**请写成不带多余零的最简小数**(`0.01` / `1.5` / `100`),不要写 `0.010`、`1.50`。写成字符串(`"price": "0.01"`)也可以,字符串原样参与签名,最不容易出错。
-
-> ⚠️ **下单失败时本地记录仍在,不要当作「什么都没发生」。** 上游调用失败会以 `code=500` 返回(`msg` 形如 `下单失败: <原因>`),但平台**已经落了本地记录并排入了状态轮询** —— 这笔交易仍可能成功。收到失败后请用 10.3 确认,不要凭这一次返回就向用户宣称交易未发生。
-
----
-
-### 10.3 订单查询
-
-```
-POST /payment/openapi/har/order/query
-```
-
-**请求**(`order_no` 与 `out_biz_no` **至少提供一个**;都提供时以 `order_no` 为准)
-
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `enterprise_id` | string | 否 | 企业标识。**不传即按你的租户账号寻址**,见本章开头 |
-| `merchant_code` | string | 否 | **你租户下配了多个商户号时必填**,与上面两个接口同一个值。传了就只查这个商户的订单 |
-| `order_no` | string | 二选一 | 平台订单号(10.2 响应里的 `order_no`) |
-| `out_biz_no` | string | 二选一 | 你自己的订单号 |
-
-**响应**(B 型,`data` 内)—— 字段与 10.2 的响应一致,另加:
-
-| 字段 | 说明 |
-|---|---|
-| `upstream_create_time` | 上游建单时间 |
-
-> **本接口查的是平台已记录的状态,不会当场向上游发起查询。** 订单状态由平台的任务自动收敛(下单约 30 秒后开始查,之后按退避节奏重试),所以你刚下单就查、拿到的可能仍是 `"0"`;过一会儿再查即可。
-
-> `order_no` 与 `out_biz_no` 都没传,返回 `code=500`,`msg` 为 `order_no 与 out_biz_no 至少填一个`。
-
-> ⚠️ **查不到订单时返回的是 `code=500`,不是 `404`。** 这与 5.4 的行为不同。`msg` 为 `订单不存在`。**不属于你的企业的订单也返回同一个 `msg`** —— 平台有意不区分「不存在」与「不属于你」,避免被用来探测他人订单是否存在。
-
----
-
-### 10.4 回调通知
-
-订单状态变化时,平台会向你推送一条回调。
-
-**回调地址取自你这把 API Key 上配置的 `return_url`**(未配置时退回该租户的默认回调地址)。**没配就不会推送**,此时只能靠主动查询(10.3)拿结果。
-
-```
-POST <你的 return_url>
-Content-Type: application/x-www-form-urlencoded
-```
-
-**请求体**(表单编码,三个字段):
-
-| 字段 | 说明 |
-|---|---|
-| `notify_id` | 本次通知的唯一 ID,可用于去重 |
-| `timestamp` | 平台发出通知的时间,Unix 毫秒 |
-| `content` | **JSON 字符串**,业务内容见下(注意:是字符串,需要再解析一次) |
-
-`content` 示例:
-
-```json
-{
-  "third_order_no": "HAR1234567890",
-  "out_biz_no": "T20260924001",
-  "order_no": "2026092499001",
-  "platform_trade_no": "2026092499002",
-  "merchant_code": "M0001",
-  "third_shop_code": "S001",
-  "price": 1.50,
-  "title": "测试商品",
-  "trade_type": "alipay_payment",
-  "status": "1",
-  "trade_time": "2026-09-24T10:30:00+08:00",
-  "refund_time": null,
-  "rcv_amount": 1.50,
-  "fee_amount": 0.01,
-  "channel_amount": 1.49
-}
-```
-
-金额字段是 **JSON 数字**(不是字符串);时间字段是 **ISO-8601 带时区偏移**的字符串。
-
-> ⚠️ **回调里的 `order_no` 与 10.2 响应里的 `order_no` 不是同一个号。**
-> 回调里的 `order_no` 是**上游订单号**(等于 10.2 响应里的 `upstream_order_no`);
-> 回调里的 `third_order_no` 才是**平台订单号**(等于 10.2 响应里的 `order_no`)。
-> 名字是反的,但对账必须靠它们,请按这层对应关系取用。
-> **最省事的做法:始终用你自己生成的 `out_biz_no` 作主键做幂等,不要依赖这两个 `order_no`。**
-
-**你方需要做的**:
-
-1. **应答**:处理成功后返回 **HTTP 2xx**。返回非 2xx 视为投递失败。
-2. **去重**:同一笔订单可能收到多次通知。请用 `out_biz_no` + `status` 做幂等。
-3. **快速应答**:先应答再异步处理。平台最多尝试 3 次(失败后隔 1 秒、2 秒各重试一次),收到 4xx 时不再重试。
-
-> ⚠️ **平台只在收到上游通知时才会向你推送回调。** 若上游那条通知丢了,平台会靠自己的任务把本地状态收敛正确,但**不会**补推一条给你 —— 此时 10.3 能查到正确结果,你的回调却永远不会响。**请务必给长期停在 `"0"` 的订单做主动查询兜底。**
-
-> ⚠️ **回调不带签名。** 与 7.2 的情况相同 —— 不能仅凭收到回调就认定收款成功。请以 **10.3 订单查询** 的结果为准,把回调当作「该查了」的触发信号。
-
----
-
-### 10.5 状态说明
-
-**`trade_status` 是数字字符串**(JSON 里带引号,不是数字):
-
-| 值 | 含义 | 是否终态 |
-|---|---|---|
-| `"0"` | 等待支付 | 否 |
-| `"1"` | **支付成功,钱已到账** | 是 |
-| `"2"` | 已退款 | 是 |
-| `"4"` | 退款中 | 否 |
-| `"8"` | 交易关闭 | 是 |
-
-判断「是否收到过钱」用 `"1"` / `"2"`;判断「是否还要等」用 `"0"` / `"4"`。
-
-**门店 `create_status`**:`SUCCESS` / `FAILED` / `PENDING`,见 10.1。
-
-**订单 `notify_status`**(平台向你推送回调的结果):
-
-| 值 | 含义 |
-|---|---|
-| `PENDING` | 尚未推送成功(含「你没有配置回调地址」这种情况) |
-| `SUCCESS` | 你方已成功接收 |
-| `FAILED` | 推送失败(对方返回非 2xx,或 3 次尝试均失败) |
-
----
-
-## 11. 门店与订单通道的联调检查清单
-
-- [ ] 认证与签名复用 3.3 的实现,先跑通 3.4 的样例
-- [ ] `price` 用**元**、写成最简小数(不要 `1.50`、`0.010`)
-- [ ] 下单**不重发** —— 超时先查 10.3,确认没有再决定
-- [ ] `auth_code` 有值走被扫、不传走主扫,两种都测过
-- [ ] 主扫拿到了 `pay_url` 并能渲染成二维码
-- [ ] 按 `trade_status` 的**数字字符串**分支处理(`"1"` 才是钱到账)
-- [ ] 回调接口能处理表单编码,并把 `content` **再解析一次** JSON
-- [ ] 回调按 `out_biz_no` 做幂等
-- [ ] 对长期停在 `"0"` 的订单有主动查询兜底
-- [ ] 门店 `create_status` 收到 `PENDING` 时走人工核实,不自行重发

+ 263 - 0
frontend/src/views/module_payment/apikey/docs/har-openapi.md

@@ -0,0 +1,263 @@
+## 10. 门店与订单接口
+
+本章是**另一条通道**,路径前缀 `/payment/openapi/har`,与 5 章的收款接口不是同一个接口族。
+
+**相同的部分**:请求头与签名算法(3.2 / 3.3 逐字适用)、错误处理总则(3.6)。
+
+**不同的部分 —— 动手前先读完这张表**:
+
+| 项 | 5 章收款接口 | 本章 |
+|---|---|---|
+| 路径前缀 | `/payment/openapi/pay` | `/payment/openapi/har` |
+| 响应信封 | 三个下单接口 A 型,其余 B 型 | **3 个接口全部 B 型** |
+| 金额 | 整数,单位**分** | 小数,单位**元** |
+| 订单状态 | 文本枚举(`SUCCESS` / `PAYING`…),见 6 | **数字字符串**(`"0"` / `"1"`…),见 10.5 |
+| 下单幂等 | 同 `out_trade_no` 重发返回原单 | **不幂等**,每次调用都新建一笔,见 10.2 |
+| 退款 | 有(5.6 / 5.7) | **本章没有退款接口** |
+
+> ⚠️ **「不幂等」与「金额单位是元」这两条最容易出事。** 前者会让你在超时重发时收到两笔扣款,后者会把 1 元写成 1 分。请读完 10.2 的注意框再动手。
+
+**关于寻址**:本章三个接口都按**你这条 API Key 所属的租户账号**寻址,取该租户名下由平台运营报备好的商户配置。你不需要在请求里指定归属。
+
+**关于 `merchant_code`(新增,2026-09-24)**:平台**允许一个租户报备多个商户号**(例如你名下有多家门店主体,各自独立结算)。这时「这笔钱收进哪一个商户」必须由你在请求里说清楚,用 `merchant_code` 指定。
+
+- **不传** ⇒ 平台取你租户名下的商户配置。
+- **传了** ⇒ 一律以它为准,且必须是你租户名下已报备的编号,否则返回 404。
+
+> ⚠️ **你租户下配了多个商户号、而你又不传 `merchant_code` 时,接口会直接**拒绝**(`code=400`,`msg` 为 `当前租户配置了多个商户编号,请在请求中指定 merchant_code`),不会替你挑一个。**这是有意的** —— 挑错了钱会进另一个商户的账户,而且全程没有任何报错,要等到对账才发现。**只配了一个商户号的调用方不受影响**,什么都不用改。
+
+> **怎么知道自己配了几个**:登录平台,在「商户配置」页可以看到你名下的全部商户号。三个接口都填同一个 `merchant_code` 即可。
+
+**门店与下单的关系**:10.2 里的 `shop_code` / `third_shop_code` 都是**选填**。门店不是下单的前置条件 —— 只有需要把交易归属到某个具体门店时才填。
+
+---
+
+### 10.1 创建门店
+
+为你的一个线下门店建号。平台侧**没有门店查询接口**,本接口的返回就是你日后核对门店状态的依据,请把 `shop_id` 存下来。
+
+```
+POST /payment/openapi/har/shop/create
+```
+
+**请求**
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| `merchant_code` | string | 否 | **你租户下配了多个商户号时必填**:门店建在哪一个商户下,见本章开头。只配了一个的不用传 |
+| `third_shop_id` | string | 是 | 你方的门店编码,≤32 字符。**重复提交会被拒绝**,见下 |
+| `shop_name` | string | 是 | 门店名称,≤128 字符 |
+
+**响应**(B 型,`data` 内)
+
+| 字段 | 说明 |
+|---|---|
+| `third_shop_id` | 原样返回 |
+| `shop_name` | 原样返回 |
+| `shop_id` | 平台门店编号,**建号成功才有值**;下单时作为 `shop_code` 传入 |
+| `create_status` | 建号结果,见下 |
+| `merchant_code` | 平台侧为你配置的商户编号(你无需关心,原样带回) |
+| `fail_reason` | `create_status` 非 `SUCCESS` 时的原因 |
+| `created_time` / `updated_time` | 记录时间 |
+
+`create_status` 取值:
+
+| 值 | 含义 | 你该做什么 |
+|---|---|---|
+| `SUCCESS` | 门店已建好,`shop_id` 可用 | 保存 `shop_id` |
+| `FAILED` | 明确被拒,不会建号 | 看 `fail_reason`,改好再调一次 |
+| `PENDING` | **结果未知** —— 调用超时或网络失败,无法确认门店是否已经建出来 | **不要直接重发**(可能建出两个门店),联系平台方人工核实 |
+
+> ⚠️ **`third_shop_id` 重复会被直接拒绝,而不是「返回原有门店」。** 重复提交同一个 `third_shop_id`,返回 `code=500`,`msg` 形如 `门店编码已存在: <你的编码>`。所以本接口的正确幂等做法是**先查你本地的记录,再决定要不要调**,而不是无脑重发。
+
+> ⚠️ **`PENDING` 不是「处理中」。** 门店创建**不自动重试** —— 重试可能在上游建出两个门店。收到 `PENDING` 时唯一正确的动作是找平台方核实,不要自行重发。
+
+---
+
+### 10.2 下单收款
+
+同一个接口承担两种场景,由 `auth_code` 是否有值决定:
+
+| `auth_code` | 场景 | 行为 |
+|---|---|---|
+| **有值** | 被扫 —— 用户出示付款码,你扫码后把码传进来 | 立即发起扣款 |
+| **不传** | 主扫 —— 平台返回一条收款链接,你渲染成二维码给用户扫 | 只下单,等用户付款 |
+
+```
+POST /payment/openapi/har/order/create
+```
+
+**请求**
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| `merchant_code` | string | 否 | **你租户下配了多个商户号时必填**:这笔钱收进哪一个商户,见本章开头 |
+| `price` | number | 是 | 金额,**单位元**,必须 > 0。与 5 章的「整数分」不同,见下 |
+| `title` | string | 是 | 商品名称,≤64 字符 |
+| `trade_type` | string | 是 | `alipay_payment` 支付宝支付 / `alipay_stage` 花呗分期 |
+| `out_biz_no` | string | 否 | 你自己的订单号,≤64 字符,原样存下供你查询 |
+| `auth_code` | string | 否 | 用户的付款码。有值走被扫,不传走主扫 |
+| `stage_num` | string | 否 | 分期期数:`3` / `6` / `12` / `24`。`trade_type=alipay_stage` 时必填 |
+| `percent` | string | 否 | 分期手续费承担方:`100` 商户承担 / `0` 用户承担。`trade_type=alipay_stage` 时必填 |
+| `shop_code` | string | 否 | 平台门店编号(10.1 返回的 `shop_id`) |
+| `third_shop_code` | string | 否 | 你方门店编码,≤32 字符 |
+| `pay_timeout` | int | 否 | 支付过期时间,取值 2–60(单位由上游定义,平台原样透传) |
+| `extra_params` | string | 否 | 透传字段 |
+
+**响应**(B 型,`data` 内)
+
+| 字段 | 说明 |
+|---|---|
+| `order_no` | **平台订单号**,后续查询(10.3)用它 |
+| `out_biz_no` | 原样返回 |
+| `upstream_order_no` | 上游订单号。下单成功才有值,下单超时时为空 |
+| `platform_trade_no` | 平台交易号,支付成功后才有值 |
+| `pay_url` | **主扫**:收款链接,渲染成二维码即可。**被扫**:为空 |
+| `trade_status` | 订单状态,见 10.5 |
+| `price` | 订单金额(元) |
+| `title` / `trade_type` / `stage_num` / `percent` | 原样返回 |
+| `shop_code` / `third_shop_code` | 原样返回 |
+| `trade_time` | 支付成功时间 |
+| `rcv_amount` | 实收金额(元),支付成功后有值 |
+| `fee_amount` / `channel_amount` | 手续费 / 渠道金额 |
+| `refund_time` | 退款时间。本章无退款接口,该字段恒为空 |
+| `notify_status` | 平台向你推送回调的结果,见 10.4 |
+| `created_time` / `updated_time` | 记录时间 |
+
+> ⚠️ **本接口不幂等 —— 重复调用会产生两笔订单、两次扣款。** 每次调用都会生成一个新的 `order_no` 并向上游发起一笔新的交易,平台**不做** `out_biz_no` 去重(也不做唯一约束)。超时后不要直接重发:先用 **10.3 订单查询** 按 `out_biz_no` 查一次,查到就用那一笔的结果。
+
+> ⚠️ **`price` 的单位是「元」,是小数,不是「分」。** `1.5` 表示 1 元 5 角;5 章那一套要写 `total_amount=150` 才是同样的金额。**跨通道搬代码时这是最容易错的一处。**
+
+> ⚠️ **金额的写法会直接影响签名。** 签名是对你**实际发出的那个值**做字符串化后算的(3.3),而平台把 JSON 数字读成双精度数:你写 `0.010`,平台看到的是 `0.01` —— 签名因此对不上,只会返回「签名验证失败」,不会告诉你原因。**请写成不带多余零的最简小数**(`0.01` / `1.5` / `100`),不要写 `0.010`、`1.50`。写成字符串(`"price": "0.01"`)也可以,字符串原样参与签名,最不容易出错。
+
+> ⚠️ **下单失败时本地记录仍在,不要当作「什么都没发生」。** 上游调用失败会以 `code=500` 返回(`msg` 形如 `下单失败: <原因>`),但平台**已经落了本地记录并排入了状态轮询** —— 这笔交易仍可能成功。收到失败后请用 10.3 确认,不要凭这一次返回就向用户宣称交易未发生。
+
+---
+
+### 10.3 订单查询
+
+```
+POST /payment/openapi/har/order/query
+```
+
+**请求**(`order_no` 与 `out_biz_no` **至少提供一个**;都提供时以 `order_no` 为准)
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| `merchant_code` | string | 否 | **你租户下配了多个商户号时必填**,与上面两个接口同一个值。传了就只查这个商户的订单 |
+| `order_no` | string | 二选一 | 平台订单号(10.2 响应里的 `order_no`) |
+| `out_biz_no` | string | 二选一 | 你自己的订单号 |
+
+**响应**(B 型,`data` 内)—— 字段与 10.2 的响应一致,另加:
+
+| 字段 | 说明 |
+|---|---|
+| `upstream_create_time` | 上游建单时间 |
+
+> **本接口查的是平台已记录的状态,不会当场向上游发起查询。** 订单状态由平台的任务自动收敛(下单约 30 秒后开始查,之后按退避节奏重试),所以你刚下单就查、拿到的可能仍是 `"0"`;过一会儿再查即可。
+
+> `order_no` 与 `out_biz_no` 都没传,返回 `code=500`,`msg` 为 `order_no 与 out_biz_no 至少填一个`。
+
+> ⚠️ **查不到订单时返回的是 `code=500`,不是 `404`。** 这与 5.4 的行为不同。`msg` 为 `订单不存在`。**不属于你的订单也返回同一个 `msg`** —— 平台有意不区分「不存在」与「不属于你」,避免被用来探测他人订单是否存在。
+
+---
+
+### 10.4 回调通知
+
+订单状态变化时,平台会向你推送一条回调。
+
+**回调地址取自你这把 API Key 上配置的 `return_url`**(未配置时退回该租户的默认回调地址)。**没配就不会推送**,此时只能靠主动查询(10.3)拿结果。
+
+```
+POST <你的 return_url>
+Content-Type: application/x-www-form-urlencoded
+```
+
+**请求体**(表单编码,三个字段):
+
+| 字段 | 说明 |
+|---|---|
+| `notify_id` | 本次通知的唯一 ID,可用于去重 |
+| `timestamp` | 平台发出通知的时间,Unix 毫秒 |
+| `content` | **JSON 字符串**,业务内容见下(注意:是字符串,需要再解析一次) |
+
+`content` 示例:
+
+```json
+{
+  "third_order_no": "HAR1234567890",
+  "out_biz_no": "T20260924001",
+  "order_no": "2026092499001",
+  "platform_trade_no": "2026092499002",
+  "merchant_code": "M0001",
+  "third_shop_code": "S001",
+  "price": 1.50,
+  "title": "测试商品",
+  "trade_type": "alipay_payment",
+  "status": "1",
+  "trade_time": "2026-09-24T10:30:00+08:00",
+  "refund_time": null,
+  "rcv_amount": 1.50,
+  "fee_amount": 0.01,
+  "channel_amount": 1.49
+}
+```
+
+金额字段是 **JSON 数字**(不是字符串);时间字段是 **ISO-8601 带时区偏移**的字符串。
+
+> ⚠️ **回调里的 `order_no` 与 10.2 响应里的 `order_no` 不是同一个号。**
+> 回调里的 `order_no` 是**上游订单号**(等于 10.2 响应里的 `upstream_order_no`);
+> 回调里的 `third_order_no` 才是**平台订单号**(等于 10.2 响应里的 `order_no`)。
+> 名字是反的,但对账必须靠它们,请按这层对应关系取用。
+> **最省事的做法:始终用你自己生成的 `out_biz_no` 作主键做幂等,不要依赖这两个 `order_no`。**
+
+**你方需要做的**:
+
+1. **应答**:处理成功后返回 **HTTP 2xx**。返回非 2xx 视为投递失败。
+2. **去重**:同一笔订单可能收到多次通知。请用 `out_biz_no` + `status` 做幂等。
+3. **快速应答**:先应答再异步处理。平台最多尝试 3 次(失败后隔 1 秒、2 秒各重试一次),收到 4xx 时不再重试。
+
+> ⚠️ **平台只在收到上游通知时才会向你推送回调。** 若上游那条通知丢了,平台会靠自己的任务把本地状态收敛正确,但**不会**补推一条给你 —— 此时 10.3 能查到正确结果,你的回调却永远不会响。**请务必给长期停在 `"0"` 的订单做主动查询兜底。**
+
+> ⚠️ **回调不带签名。** 与 7.2 的情况相同 —— 不能仅凭收到回调就认定收款成功。请以 **10.3 订单查询** 的结果为准,把回调当作「该查了」的触发信号。
+
+---
+
+### 10.5 状态说明
+
+**`trade_status` 是数字字符串**(JSON 里带引号,不是数字):
+
+| 值 | 含义 | 是否终态 |
+|---|---|---|
+| `"0"` | 等待支付 | 否 |
+| `"1"` | **支付成功,钱已到账** | 是 |
+| `"2"` | 已退款 | 是 |
+| `"4"` | 退款中 | 否 |
+| `"8"` | 交易关闭 | 是 |
+
+判断「是否收到过钱」用 `"1"` / `"2"`;判断「是否还要等」用 `"0"` / `"4"`。
+
+**门店 `create_status`**:`SUCCESS` / `FAILED` / `PENDING`,见 10.1。
+
+**订单 `notify_status`**(平台向你推送回调的结果):
+
+| 值 | 含义 |
+|---|---|
+| `PENDING` | 尚未推送成功(含「你没有配置回调地址」这种情况) |
+| `SUCCESS` | 你方已成功接收 |
+| `FAILED` | 推送失败(对方返回非 2xx,或 3 次尝试均失败) |
+
+---
+
+## 11. 门店与订单通道的联调检查清单
+
+- [ ] 认证与签名复用 3.3 的实现,先跑通 3.4 的样例
+- [ ] `price` 用**元**、写成最简小数(不要 `1.50`、`0.010`)
+- [ ] 下单**不重发** —— 超时先查 10.3,确认没有再决定
+- [ ] `auth_code` 有值走被扫、不传走主扫,两种都测过
+- [ ] 主扫拿到了 `pay_url` 并能渲染成二维码
+- [ ] 按 `trade_status` 的**数字字符串**分支处理(`"1"` 才是钱到账)
+- [ ] 回调接口能处理表单编码,并把 `content` **再解析一次** JSON
+- [ ] 回调按 `out_biz_no` 做幂等
+- [ ] 对长期停在 `"0"` 的订单有主动查询兜底
+- [ ] 门店 `create_status` 收到 `PENDING` 时走人工核实,不自行重发

+ 27 - 0
frontend/src/views/module_payment/apikey/harDoc.ts

@@ -0,0 +1,27 @@
+/**
+ * 《门店与订单接入文档》的侧边栏条目定义(正文 `docs/har-openapi.md`)。
+ *
+ * 切片逻辑在 `docSlicer.ts`,本文件只写「哪些标题拼成哪一条」。
+ * 这是与「收付款接口」并列的**另一条通道、另一个产品**,两份文档互不合并。
+ */
+import raw from "./docs/har-openapi.md?raw";
+import { buildSections, type DocSectionDef } from "./docSlicer";
+
+/**
+ * 侧边栏条目定义。键即 `activeSection` 的值。
+ * `parts` 里的标题必须与 md 里的标题**逐字一致**(含空格、全角括号)。
+ *
+ * 整章只给一个条目,不再拆成 10.1~10.5 —— 章首那张差异表(金额单位、幂等性)
+ * 必须先读,拆开会让读者直接跳到 10.2 下单。
+ * 注意:`take("## 10. …")` 会一并吞掉其下所有 `###` 子块,所以这里**不能**改写成
+ * 「## 10 + 各 ### 分开列」—— 那样差异表会被重复渲染五次。
+ */
+const SECTIONS: DocSectionDef[] = [
+  {
+    key: "har_endpoints",
+    title: "门店与订单",
+    parts: ["## 10. 门店与订单接口", "## 11. 门店与订单通道的联调检查清单"],
+  },
+];
+
+export const harSections = buildSections(raw, SECTIONS);

+ 41 - 7
frontend/src/views/module_payment/apikey/index.vue

@@ -441,10 +441,28 @@
                 </ul>
               </div>
               <!--
-                条目由 collectionDoc.ts 从 md 文档切出来 —— 侧边栏、卡片标题、正文
+                下面两组各由一支切片器从**各自的** md 切出来 —— 侧边栏、卡片标题、正文
                 三处同源。标题在 md 里被改字则对应条目自动消失(不是留下一个空白页)。
+                两条通道是两个产品、两份文档,不合并(2026-09-24 拆分)。
               -->
+
+              <!-- 门店与订单(harDoc.ts ← docs/har-openapi.md) -->
               <div class="sidebar-section">
+                <h3>门店与订单</h3>
+                <ul>
+                  <li
+                    v-for="(section, key) in harSections"
+                    :key="key"
+                    @click="activeSection = key"
+                    :class="{ active: activeSection === key }"
+                  >
+                    {{ section.title }}
+                  </li>
+                </ul>
+              </div>
+
+              <!-- 收付款接口(collectionDoc.ts ← docs/collection-openapi.md)—— 见 SHOW_COLLECTION_DOC -->
+              <div v-if="SHOW_COLLECTION_DOC" class="sidebar-section">
                 <h3>收付款接口</h3>
                 <ul>
                   <li
@@ -1344,12 +1362,13 @@ echo "签名验证结果: " . ($isValid ? '有效' : '无效') . "\n";
               </div>
 
               <!--
-                收付款接口 —— 正文由 MarkdownRenderer 渲染 collectionDoc.ts 切出的 md 片段。
+                md 切出来的条目(收付款 / 门店与订单两条通道)——
+                正文由 MarkdownRenderer 渲染切片器切出的片段。
                 这里刻意不写任何接口细节:页面上的每个字段名都来自那份 md,
                 而 md 的签名样例与字段名由 Java 侧契约测试钉住。
               -->
-              <div v-else-if="collectionSections[activeSection]" class="section-content">
-                <MarkdownRenderer :content="collectionSections[activeSection]?.content || ''" />
+              <div v-else-if="mdSections[activeSection]" class="section-content">
+                <MarkdownRenderer :content="mdSections[activeSection]?.content || ''" />
               </div>
             </el-card>
           </div>
@@ -1368,6 +1387,8 @@ import ApiKeyAPI, {
 } from "@/api/module_payment/apikey";
 import OpenAPI from "@/api/module_payment/openapi";
 import { collectionSections } from "./collectionDoc";
+import { harSections } from "./harDoc";
+import type { DocSection } from "./docSlicer";
 import CrudToolbarLeft from "@/components/CURD/CrudToolbarLeft.vue";
 import CrudToolbarRight from "@/components/CURD/CrudToolbarRight.vue";
 import PageSearch from "@/components/CURD/PageSearch.vue";
@@ -1386,6 +1407,19 @@ defineOptions({
 
 const activeTab = ref("management");
 const activeSection = ref("auth");
+
+/**
+ * 「收付款接口」这一组是否露出。
+ *
+ * 2026-09-24:文档按通道拆成两份(收付款 / 门店与订单),两者是不同产品,不再混在一起。
+ * 本批只上「门店与订单」,收付款那一组暂不露出 —— 正文、切片器、分组都已就位,
+ * 要放出来把这里改成 true 即可,不需要动 md、切片器或模板。
+ */
+const SHOW_COLLECTION_DOC = false;
+
+// 两条通道的正文合成一张查找表:正文渲染与卡片标题只认 key,不关心它属于哪一组。
+// 侧边栏仍按通道分两组渲染(见模板)。
+const mdSections: Record<string, DocSection> = { ...collectionSections, ...harSections };
 const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
 const dataFormRef = ref();
 const submitLoading = ref(false);
@@ -1479,9 +1513,9 @@ function toggleExpand(section: string) {
 }
 
 function getSectionTitle() {
-  // 收付款的标题与侧边栏同源(collectionDoc.ts),不在这里再抄一份
-  const collection = collectionSections[activeSection.value];
-  if (collection) return collection.title;
+  // 两条通道的标题都与侧边栏同源(collectionDoc.ts / harDoc.ts),不在这里再抄一份
+  const md = mdSections[activeSection.value];
+  if (md) return md.title;
   const titles: Record<string, string> = {
     auth: "认证方式",
     signature: "签名验证",