Przeglądaj źródła

docs(计划): 收录「收款链接 + 自助收银台」实现计划

用户 2026-09-30 两句话定范围,计划是那两句话的展开,连同本仓既有裁定一起落成
Global Constraints(每条都指得出出处),实现按它逐条对上。

★ 计划里的**文件布局有两处与落地不一致**,写在这里以免下一个人拿计划当现状读:
- 计划写 `src/api/module_payment/checkout.ts`(独立 axios 实例单独成文件),
  落地是**直接写在顾客页里**:那个实例只服务于这一屏,单独一个文件会让
  「为什么不用 @/utils/request」这条理由离调用点隔一层。
- 计划写 `CheckoutLinkDialog.vue` 单独组件,落地是**并进 `merchant/index.vue`**:
  弹窗与那一列按钮是同一件事,拆开后 `linkVisible` 这个状态要在两个文件之间来回传。

计划其余部分(建表 SQL、服务层三个方法、两条发链接的腿、免登录判据、金额下限、
`notify_url` 有意留空)与落地逐条一致。
alphaH 2 tygodni temu
rodzic
commit
1e925a21a4
1 zmienionych plików z 95 dodań i 0 usunięć
  1. 95 0
      .claude/plan/2026-09-30-lsy-checkout-link.md

+ 95 - 0
.claude/plan/2026-09-30-lsy-checkout-link.md

@@ -0,0 +1,95 @@
+# 乐收赢「收款链接 + 自助收银台」(checkout)实现计划
+
+> 用户 2026-09-30 两句话定了范围,本计划是那两句话的展开:
+> ①「链接仅做到打开checkout完成输入金额并然后调用真实主扫api并拉起响应中的支付链接就行了,
+>   但是checkout要做的漂亮,不含其他功能」
+> ②「checkout链接获取的按钮可以做到系统页面上,顺便封装成二维码,链接和二维码都可以拿走直接用」
+
+**Goal:** 给每个乐收赢商户一条**永久可复用**的 https 收款链接;顾客打开它 → 输金额 → 确认 →
+后端调**真实主扫**(`/v3/qrpay`)拿 `code_url` → 浏览器直接跳过去拉起支付宝/微信。
+
+**Architecture:**
+- 一条极简 token 表 `pay_lsy_checkout_link`(租户+商户+token,token 全局唯一)。
+- 链接由**两条腿**发出,共用同一个服务:管理端按钮(系统页面)与开放 API。
+- 顾客侧两个**免登录**端点:读商户显示名 / 按金额建真主扫单。
+- 顾客页是 Vue 免登录路由 `/#/checkout/:token`,只做输入金额→跳转,**不做**轮询/结果页。
+
+**Tech Stack:** Spring Boot 3 + MyBatis-Plus(本模块既有约定)、Vue3 + Element Plus、`qrcode` npm(已在依赖里)。
+
+**Spec:** 无独立 spec;依据是用户这两句话 + 本仓既有裁定(下「不可省的约束」逐条都指得出出处)。
+
+## Global Constraints(不可省的既有约束,逐条有出处)
+
+1. **浏览器不能持有签名凭据** ⇒ 必须有公开后端端点代签(`TenantApiKeyAuthFilter` 那套 HMAC 只能在服务端)。
+2. **无认证路径的定位键必须全局唯一** ⇒ token 用 `UNIQUE` 索引(通则「甲·预防」,
+   见 `TenantInnerInterceptor.CONDITIONAL_TABLES` 上方那段)。
+3. **单号 `out_trade_no` 的唯一性是 `(tenant_id, out_trade_no)`,不是全局** ⇒ 服务端生成
+   单号必须是**雪花**(全局唯一),不能是时间戳自增那种会跨租户撞的形态。
+4. **`pay_lsy_order.mch_no` NOT NULL 且无默认值** ⇒ `insert` 必须在装配 mch_no 之后。
+5. **主扫下单成功 ≠ 收款成功** ⇒ 落 `PAYING`(`createQrOrder` 里的既有判据,不改)。
+6. **`notify_url` 有意留空**(同收银台那条腿):上游回调走平台级 `leshouying.notify-url`,
+   与订单这一列无关;下游通知按 `resolveUrl` 回退到 `open_conf`。
+   ★ 这条不能靠「照抄开放 API」得到 —— 那会把订单快照填成我们自己的回调入口 = 自环。
+7. **金额下限**复用 `LsyAmountFloor.MIN`(1 元,我方业务线),文案同源。
+8. **工单不写进 URL**:`mch_no` 属上游身份,本仓既有裁定不给租户看 ⇒ 链接里只有不透明 token。
+9. 提交一律 `git add <显式路径>`;`application.yml` 永不 add、永不读。
+
+## 文件一览
+
+**后端(新建)**
+- `java/src/main/resources/db/lsy-checkout-link.sql` —— 建表(**只写不执行**,交用户)
+- `.../leshouying/entity/LsyCheckoutLinkEntity.java`
+- `.../leshouying/mapper/LsyCheckoutLinkMapper.java`
+- `.../leshouying/service/LsyCheckoutLinkService.java`
+- `.../leshouying/controller/LeshouyingCheckoutController.java` —— **免登录**两个端点
+- `.../leshouying/dto/LsyCheckoutPayDTO.java` / `LsyCheckoutInfoVO.java` / `LsyCheckoutOrderVO.java`
+
+**后端(改)**
+- `core/security/SecurityConfig.java` —— 白名单加 `/payment/checkout/**`
+- `core/tenant/TenantInnerInterceptor.java` —— `CONDITIONAL_TABLES` 加本表(含 token 全局唯一那条理由)
+- `.../controller/LeshouyingMerchantController.java` —— 管理端「取收款链接」端点
+- `.../controller/open/LeshouyingOpenController.java` —— 开放 API「取收款链接」端点
+- `core/security/TenantApiKeyAuthFilter.java` —— `OPENAPI_ACCOUNT_PATHS` 补第 8 条
+
+**前端(新建)**
+- `src/api/module_payment/checkout.ts` —— **独立 axios 实例**(绕开管理端拦截器)
+- `src/views/checkout/index.vue` —— 免登录顾客页(漂亮、移动优先)
+- `src/views/module_payment/collection/merchant/CheckoutLinkDialog.vue` —— 链接+二维码弹窗
+
+**前端(改)**
+- `src/router/index.ts`(静态路由加一条,无 Layout)、`src/plugins/permission.ts`(两分支之前放行)
+- `src/api/module_payment/collection.ts`(管理端取链接)
+- `src/views/module_payment/collection/merchant/index.vue`(操作列加按钮)
+
+## 任务
+
+### Task 1: 建表 SQL(只写不执行)
+`pay_lsy_checkout_link`:id/tenant_id/mch_no/token/merchant_name/status/created_time/updated_time;
+`uk_lsy_checkout_token (token)` 全局唯一 + `uk_lsy_checkout_tenant_mch (tenant_id, mch_no)` 一商户一条。
+风格照 `V1.8__create_pay_lsy_tables.sql`(BIGINT 主键、`TIMESTAMP WITH TIME ZONE`、COMMENT 齐全)。
+
+### Task 2: 服务层(TDD)
+`LsyCheckoutLinkService`:
+- `issue(tenantId, mchNo, merchantName)` —— 按 (tenant_id, mch_no) 找,没有就建(token = fastSimpleUUID);
+  重入返回**同一条**(链接永久复用);每次刷新显示名快照。
+- `resolve(String token)` —— 按 token 定位,0 行 ⇒ 404「收款链接无效或已停用」。
+- `buildUrl(entity, request)` —— 前缀取 `leshouying.checkout-base-url`(可空⇒按请求 Host 推导)。
+
+### Task 3: 免登录顾客端点(TDD)
+`LeshouyingCheckoutController`(`/payment/checkout`):
+- `GET /{token}` → `{ merchantName }`
+- `POST /{token}/order` `{amount}` → 建真主扫单(`CS`+雪花)→ 返回 `{ outTradeNo, codeUrl }`
+- **顺序固定**:解析 token → 把租户写进 SecurityContext → 本地闸 → 落库 → 上游调用。
+  (不设上下文 ⇒ 拦截器追加 `tenant_id=0` ⇒ code_url 落库静默 0 行,见约束 2/3 的同类失效。)
+
+### Task 4: 两条发链接的腿 + 白名单/拦截面/清单
+管理端 `POST /payment/collection/merchant/{mchNo}/checkout-link`(权限串复用 `:merchant:list`,
+理由:租户本来就该拿到自己商户的收款链接;租户号取自会话,请求体里没有租户维度,不构成跨租户原语)
+开放 API `POST /payment/openapi/pay/checkout/link`(走既有 `requireMerchant` 分流)。
+
+### Task 5: 前端顾客页 + 管理端按钮与二维码
+顾客页:金额输入(元→分)→ 确认 → `location.href = codeUrl`;失败态一句话。
+管理端:操作列「收款链接」→ 弹窗显示链接 + 二维码(`qrcode` 已在依赖里)+ 复制 + 下载 PNG。
+
+### Task 6: 文档 + 构建 + 交付说明
+`collection-openapi.md` 补开放 API 那一条;jar/dist 重建;交付说明写清用户侧要做的动作。