# 乐收赢「收款链接 + 自助收银台」(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 重建;交付说明写清用户侧要做的动作。