2026-09-30-lsy-checkout-link.md 6.6 KB

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