# 收款工作台 + 商户进件 设计 **日期**:2026-09-22 **上游**:乐收赢(代称「上游」,所有对用户可见处不得出现其名称、域名、字段名) **前序 spec**:`docs/superpowers/specs/2026-09-21-leshouying-payment-channel-design.md` --- ## 0. 需求口径(本轮最终版) 用户在本次会话中逐条给出的口径,按最终版记录: 1. 主扫(生成二维码收款)要返回**渲染好的收银台页面链接**,页面**不鉴权**,商户可直接用,不需要自己开发收银台页面。有效期**默认三天**(上游默认值)。 2. 生成二维码的入口**不做在小程序端、也不做在现有后台**,而是**一个全新独立的前端工程**。 3. 该工程包含四页:**登录**(复用现有平台账号)、**二维码收款(主扫)**、**扫描二维码收款(被扫)**、**订单明细**。 4. **被扫同样需要**(用户自己纠正了早先「被扫全部注释」的说法:被扫也是收款)。 5. 乐收赢与原有企业逻辑**完全不相干**,一律**租户账号级**;开放接口允许租户账号直接请求,唯一前置是**完成商户进件**。 6. **原有的开放接口不变**。 7. **商户进件本轮一并做**,取值依据上游文档。 8. 常设约束:验签绝不跳过;文案不暴露上游;开发测试期先用文档给的测试信息把支付与收款生命周期走通,线上必须先进件。 --- ## 1. 批次划分 一次性做完上面全部内容会把「能立刻验收的东西」和「依赖外部输入才能通电的东西」绑在一起。按**可独立验收**切成三批: | 批次 | 内容 | 能否独立验收 | 阻塞项 | |---|---|---|---| | **一** | 乐收赢去企业化(租户账号级) | 能 | 无 | | **二** | 收款工作台:免鉴权收银台页 + 管理端收款接口 + 独立前端工程 | 能(端到端跑通一笔主扫、一笔被扫) | 无 | | **三** | 商户进件 | **不能** | 客户经理凭据 + 上游配置回调地址,见 §6 | 批次一必须先做:批次二的管理端接口按租户级设计,建立在它之上。 批次三与批次一、二无代码依赖,但它的联调依赖外部输入,**不应挡住批次二的发布**。 --- ## 2. 实测依据 以下读数均为本次会话实测,不是推断。 ### 2.1 上游 `code_url` 的真实形状 | 项 | 读数 | |---|---| | 样例值 | `https://test.gdyfsk.com/yfpay/s/s-00006` | | 长度 | **39 字符** | | 路径模式 | `/yfpay/s/s-0000N`,**N 连号**(实测见 s-00006…s-00009) | | 是不是图片直链 | **不是**。是上游自己的短链收银台页 | | 上游是否另给图片 | **不给**。`/v3/qrpay` 应答里只有 `code_url` 一个 URL 字段 | 上游文档 §12(主扫场景)原文:**「商户系统按协议生成支付二维码」**。即二维码由**我们**生成。 **推论(决定了整个收银台方案)**:我们页面上那张二维码里编的内容**就是 `code_url`**。顾客扫码后落到上游域名,这是渠道的既定事实,我们改不了。我们能控制的是**商户拿到手的入口**:一个属于我们、不暴露上游、可保存的链接。 ### 2.2 收银台令牌选型 | 项 | 读数 | |---|---| | `pay_lsy_order` 行数 | 7 | | `pay_trace_no` 非空 / 去重 | 7 / 7(0 个 NULL) | | 唯一索引 | `uk_lsy_order_pay_trace_no` 已存在 | | 生成方式 | `IdUtil.fastSimpleUUID()`,32 位随机十六进制,不可枚举 | ⇒ 用 `pay_trace_no` 当公开页面令牌,**零 DDL**。 ### 2.3 其它现状 | 项 | 读数 | |---|---| | `pay_lsy_order` 按 scene/status | QRCODE:CLOSED 3、QRCODE:PAYING 1、H5:EXPIRED 1、BARCODE:CREATED 1、BARCODE:FAIL 1 | | `pay_lsy_merchant` | 1 行,`enterprise_id='2088780645490698'`,`mch_no='Y52211090004'`,`shop_no=NULL` | | `pay_lsy_refund` / `pay_lsy_notify_log` | 各 0 行 | | `LsyOrderStatus.PENDING` | `{CREATED, PAYING}`,**含 PAYING** | | 我们调的上游端点 | `/v3/qrpay`(主扫)、`/v3/pay`(被扫)、`/v3/h5pay`、`/v3/qrclose`、`/v3/cancelorder`、`/v3/queryorder`、`/v3/refundorder`、`/v3/queryrefund` | | 上游基础地址 | `application.yml` 的 `leshouying.base-url`,当前指向测试环境 | | 我方机构号 | `application.yml` 的 `leshouying.inst-no`(有值,主扫/被扫已跑通) | | 二维码库 | **不在依赖树里**(已用依赖树正对照验证过测法有效);Hutool `hutool-all 5.8.34` 在,但 `QrCodeUtil` 依赖的 zxing 是 optional 依赖,不会传递进来 | | 后端 HTML 模板引擎 | **无**(pom 里没有 Thymeleaf) | | 现有前端栈 | Vue 3.5 / vue-router 4.5 / Pinia 3 / Element Plus 2.11 / axios 1.10 / pnpm 9.15.3 / Vite | | 登录接口 | `POST /api/v1/system/auth/login` | | 令牌传递 | 请求头 `Authorization: Bearer ` | --- ## 3. 批次一:乐收赢去企业化 ### 3.1 目标 乐收赢的商户映射、下单、查询全部按**租户**寻址,不再要求、不再校验 `enterprise_id`。原有按企业寻址的能力**保留且行为不变**。 ### 3.2 方案 **不变式**:租户级商户 = `pay_lsy_merchant` 中 `enterprise_id IS NULL` 的那一行。 不新增列、不新增表。`pay_lsy_order.enterprise_id` 建表时已是可空,**订单表零 DDL**。 DDL(唯一一处,见 §7): - `pay_lsy_merchant.enterprise_id` 去掉 `NOT NULL` - 新增**租户级唯一索引**:`(tenant_id) WHERE enterprise_id IS NULL` 后者是必须的:现有唯一索引 `uk_lsy_merchant_tenant_ent` 建在 `(tenant_id, enterprise_id)` 上,而 PostgreSQL 唯一索引中 **NULL 互不相等**,不加这条局部索引,同一租户可以插入任意多行「租户级商户」而数据库一声不吭。这与本仓先例 `db/batch-account-level.sql` 的处理方式一致。 代码侧(按名字锚定,不写行号): - `LsyMerchantService`:**新增** `requireByTenant(Long tenantId)`;`requireByEnterprise` **一字不动**。 - 三个开放接口 DTO(主扫 / 被扫 / H5 的 create 入参):`enterpriseId` 由 `@NotBlank` 改为可选。 - `LeshouyingOpenController` 的三个下单方法:`enterpriseId` 为空时走 `requireByTenant`,非空时走 `requireByEnterprise`。**不是替换,是分流**。 - `LsyMerchantEntity` / `LsyOrderEntity` 的 `enterpriseId` 字段保留。 - 商户映射的管理端页面(`LeshouyingMerchantController` + 现有前端页):允许企业字段留空。 ### 3.3 兼容性 原有按企业下单的调用方**不受影响**:传了 `enterpriseId` 就还是老路径、还是老报错文案。删掉 `enterpriseId` 字段是**方向相反的改动**,不做。 --- ## 4. 批次二:收款工作台 ### 4.1 组成 ``` 独立前端工程 frontend-receive/ ──JWT──▶ 管理端收款接口 /payment/collection/receive/** │ │ │ (展示二维码图片) ├─▶ LsyOrderService(既有) ▼ │ 收银台页 /payment/cashier/{token}/qr.png ────────┘ ▲ │ 免鉴权 │ 商户(或顾客)浏览器 ◀── cashier_url ── 开放接口 /payment/openapi/pay/qrcode/create ``` ### 4.2 免鉴权收银台页 **端点**(`SecurityConfig.WHITE_LIST` 与 `JwtAuthFilter.WHITE_LIST_PATHS` 同步新增 `/payment/cashier/**`): | 端点 | 产出 | |---|---| | `GET /payment/cashier/{payTraceNo}` | HTML 页面 | | `GET /payment/cashier/{payTraceNo}/qr.png` | 二维码 PNG | | `GET /payment/cashier/{payTraceNo}/status` | JSON:金额、状态、剩余有效期 | **页面内容**:金额、二维码、倒计时、订单状态(支付中 / 已支付 / 已失效)。文案零上游痕迹。 **实现方式**:HTML 一个静态文件放在 `src/main/resources/` 下,控制器读入后**原样**以 `produces = text/html` 返回。**不引入 Thymeleaf**——本服务目前是纯 JSON 服务,为一个页面引入模板引擎不划算。 **服务端不做任何占位符替换。** 页面上每一个变量都从 `/status` 取,由 JS 用 `textContent` 写入 DOM;令牌由 JS 从 `location.pathname` 里读。这样服务端没有任何拼接点,**注入面为零**。 ⚠ 如果以后有人往这段 HTML 里加服务端替换,那就必须同时对值做 HTML 转义——金额来自数据库、令牌来自路径,两者都不可信。**当前设计里没有这个位置,加的时候要有意识。** `/status` 应答:`amount`、`status`、`codeUrlExpireTime`、`paid`。 **令牌校验**:`{payTraceNo}` 不存在 ⇒ 返回一个「链接无效」页,**不是 404 白页**(这是顾客会看到的页面)。令牌本身 32 位随机,不可枚举。 **上游短链可枚举这件事**:`code_url` 是 `s-0000N` 连号,理论上邻单可被猜出。这是上游的设计,我们改不了。**我们这边能做的两件事**:(1) 我们的入口令牌不可枚举;(2) **不把 `code_url` 通过任何接口明文外发**(见 §4.5)。风险等级低(猜到了也只能是「替别人付钱」),记录在案,不作为阻塞。 ### 4.3 二维码生成 - 依赖:**新增 `com.google.zxing:core`**。已实测它不在依赖树里,Hutool 的 `QrCodeUtil` 没有它会在运行时炸。 - 生成点:**只有一处** —— `qr.png` 端点。 - 二维码内容:`code_url` 原文。 - 尺寸/容错级别:选一个能印能扫的默认值即可,不作配置项(YAGNI)。 前端不生成二维码:新工程因此**不需要引入任何二维码 npm 包**,`` 即可。 ### 4.4 管理端收款接口 路径前缀 `/payment/collection/receive`,鉴权走既有 JWT 过滤器。 | 端点 | 用途 | 权限串 | |---|---|---| | `POST /qrcode` | 主扫下单,输入金额 | `module_payment:receive:qrcode` | | `POST /barcode` | 被扫下单,输入付款码 | `module_payment:receive:barcode` | | `GET /page` | 订单明细分页 | `module_payment:receive:order:list` | | `GET /{outTradeNo}` | 订单详情 | `module_payment:receive:order:list` | | `POST /{outTradeNo}/query` | 主动向上游查一次 | `module_payment:receive:order:list` | **不复用** `module_payment:leshouying:order:list`。理由:能查订单不该自动等于能发起收款。 主扫下单应答:`outTradeNo`、`payTraceNo`、`cashierUrl`、`qrImageUrl`、`amount`、`expireTime`。 **不回 `codeUrl`**——理由与 §4.5 一致。 被扫下单:入参 `authCode`(付款码)+ `amount`。走既有 `LsyOrderService.createBarcodeOrder`。 ### 4.5 开放接口变更 `POST /payment/openapi/pay/qrcode/create` 的应答:`codeUrl` 字段**换成 `cashierUrl`**。 理由有两条,都需要: 1. 商户拿到的应该是**我们的**链接,点开是**我们的**收银台页。直接把 `code_url` 发出去,商户点开看到的是上游域名,等于暴露上游。 2. 顺带收窄暴露面:`code_url` 是上游可枚举短链(§4.2),不再外发。 这是**破坏性变更**。成立的前提是用户确认过「乐收赢所有代码都没有上过生产」。落地前要再核一次 `LsyPayResultVO.codeUrl` 有没有别的消费方。 ### 4.6 独立前端工程 `frontend-receive/` - 栈与现有 `frontend/` 一致(Vue 3 + Vite + Element Plus + Pinia + vue-router + axios + pnpm),不引入新框架。 - 登录:调 `POST /api/v1/system/auth/login`,复用现有平台账号,不新建账号体系。 - 令牌:`Authorization: Bearer`,与现有 `frontend/src/utils/request.ts` 的拦截器同构。 - 四页: 1. **登录** 2. **二维码收款(主扫)**:输入金额 → 生成 → 页面展示二维码 + 金额 + 倒计时,支持保存/打印 3. **扫描二维码收款(被扫)**:输入框接扫码枪(键盘模拟输入 + 回车触发)→ 提交 → 展示结果 4. **订单明细**:分页列表 + 详情 - 构建:`base: '/receive/'`,产物独立;nginx 新增 `location /receive`。 - 不作多租户切换 UI:租户从 JWT 里来。 ### 4.7 主扫有效期与状态收敛 **改**:`leshouying.default-qr-expire-minutes` 由 `15` 改为 `4320`(三天,上游文档默认值,也是用户要的),`QR_MAX_EXPIRE_MINUTES` 保持 4320。 **随之必须改的**:`LsyReconcileScheduler.expireStaleOrders` 当前按 `absolute-expire-hours = 24` 把所有 `PENDING`(= `{CREATED, PAYING}`,**含 PAYING**)且创建超 24 小时的订单置 `EXPIRED`。主扫单在拿到 `code_url` 后停在 `PAYING`,**正好落在网里**。 今天的 15 分钟有效期让两者永远碰不上;改成三天后,第 24 小时本地记 `EXPIRED`、而上游链接活到第 72 小时。**第 25 到 72 小时之间付的款会变成「钱收了、本地账是过期」**,而现有的两条补救通道都是死的,属于永久性错账。 **规则**:主扫单**自带收敛链**(`code_url_expire_time` → `/v3/qrclose` → `CLOSED`,由 `LsyReconcileScheduler.closeExpiredQrOrders` 负责),因此把它排除出 24 小时绝对兜底网。 排除条件要**精确**,不能一刀切掉整个 `QRCODE`:`pay_scene='QRCODE'` **且 `code_url_expire_time IS NOT NULL`** 才排除。理由:`QRCODE` 卡在 `CREATED`(上游没回 `code_url`,例如下单直接失败)时**没有任何网**,它仍然需要绝对兜底。 H5 与被扫的 24 小时不动。 --- ## 5. 批次二的 DDL / 菜单 / 配置 ### 5.1 会不会有 DDL 批次一有(§7 一条)。批次二**没有**:收银台复用 `pay_trace_no`,订单表列已够用。 ### 5.2 权限与菜单 新增权限串(§4.4 三个)。**菜单与权限的 SQL 由我写好、由用户执行**。 ⚠ **SQL 未执行前,收款接口一律 403**。这不是缺陷,是预期;验收顺序上要排在 SQL 之后。 ### 5.3 新增配置项 | 键 | 用途 | |---|---| | `leshouying.cashier-base-url` | 拼 `cashierUrl` 用。默认取现有对外域名,与 `notify-url` 同源 | 不新增:二维码尺寸、颜色、容错级别。 --- ## 6. 批次三:商户进件 ### 6.1 接口清单(全部来自上游文档,本次逐页拉取) | # | 接口 | 路径 | 用途 | |---|---|---|---| | 1 | 创建商户 | `POST /yfpay/api/merchant/create` | 录入资料,成功返回商户号 | | 2 | 商户审核异步通知 | **我方提供固定地址**,上游配置 | 审核通过/驳回 | | 3 | 更新商户信息 | `POST /yfpay/api/merchant/update` | 增量更新 | | 4 | 获取商户信息 | `POST /yfpay/api/merchant/info` | 基本资料 + 各通道审核状态 + 子商户号 | | 5 | 创建门店 | `POST /yfpay/api/shop/create` | 返回 `shop_no` | | 6 | 门店列表 | `POST /yfpay/api/shop/list` | | | 7 | 提交商户入网信息 | `POST /yfpay/api/merchant/commitMerchantInfo` | 资质、门店位置、经纬度 | | 8 | 查询商户入网状态 | `POST /yfpay/api/merchant/reportQuery` | | | 9 | 安心签个人版开户 | `POST /yfpay/api/merchant/anxinSignPerson` | 通道专用 | | 10 | 企业和个体安心签开户 | `POST /yfpay/api/merchant/anxinSignCompany` | 通道专用 | | 11 | 商户审核主动查询 | `POST /yfpay/api/merchant/query` | | | 12 | 获取省市区 | `POST /yfpay/api/info/area` | 三级联动 | | 13 | 获取行业类目 | `POST /yfpay/api/merchant/getTopMccByType` | | | 14 | 图片上传 | `POST /yfpay/api/info/upload/img` | multipart,返回 `img_url` | 报文体系与支付类不同:路径是 `/yfpay/api/*` 不是 `/v3/*`,成功判定看 `result_code` + `result_msg`,**没有** `PAY_SUCCESS` 这类业务码。签名规则一致(字典序拼接 + 机构密钥 MD5)。 **创建商户**的必填字段共 **27 个**(含 `inst_no` / `trace_no` / `sign` / `notify_url`),另有 15 个资质图片字段按商户类型分档必填。附表给的是 7 类商户(企业对公 / 企业对私法人 / 企业对私非法人 / 个体对公法人 / 个体对私法人 / 个体对私非法人 / 小微)× 每个字段的必填勾选。 ### 6.2 文档缺陷(实测,逐条对上原文) **(a) `merchant_type` 值域,4 处里 1 处矛盾。** | 出处 | 值域 | |---|---| | 创建商户·请求参数表 | `1企业,2个体工商户,3小微商户` | | 创建商户·附表(7 类商户) | 1=企业、2=个体、3=小微 | | 获取商户信息·返回参数 | `1企业,2个体工商户,3小微个人` | | 获取行业类目·请求参数 | `1个体工商户 2企业 3政府组织及事业单位4小微商户5其他机构组织` | **3 处一致、1 处把 1 和 2 对调**。而且「获取行业类目」那一页的接口描述是从「获取省市区」页**整段复制**过来的(原文写着「通过此接口可获取进件时的省市区列表信息。此接口为三级联动设计…」),是明显的复制粘贴残留。 ⇒ **裁定:`1=企业,2=个体工商户,3=小微商户`**,以三处一致者为准,把行业类目页记为文档缺陷。**这不是猜测**:一页有明显复制痕迹的文档,对抗三处互相印证的取值。 **(b) `reviewed_status`:两处真冲突。** | 出处 | 值域 | 冲突点 | |---|---|---| | 商户审核异步通知 | 参数表 `1.审核通过,2.审核驳回`,类型 `String(4)` | **同页请求示例写的是 `"reviewed_status":"审核通过"`**(中文串,正好 4 字),且示例里多出参数表没有的 `settlement_status` | | 商户审核主动查询 | `0审核通过,1审核中,9审核驳回,10通道报备中,98停用,99关闭` | 示例 `reviewed_status: 0` 配 `reviewed_reason: "审核通过"`——**驳回原因字段里填的是「审核通过」** | | 查询商户入网状态 | 返回参数表 `0.审核中,1.审核通过,2.审核驳回,3.预审核通过 4.待提交 5.已提交 6.重新进件` | **同页的接口描述块给的是另一套**:`0已发起进件 / 1返回渠道子商户进件流程成功的终态 / 2重新修改信息进件 / 3银行审核中 / 4需要调用入网接口 / 5银行审核中 / 6需要重新调用创建商户接口`。同一个 0-6 值域、同一页、两个映射 | 前两处是**跨接口**的,只要各按各的解析就不冲突。**第三处是同一页对一个字段给了两个映射**,无法靠「各按各的」绕开。 **(c) 商户号字段名不一致。** 创建商户与更新商户的返回参数表写 `shoubei_merchant_no`,两页的返回示例都写 `merchant_no`。而主动查询、入网状态查询的入参叫 `merchant_no`。⇒ 创建成功后到底从哪个 key 取号,**只有真跑一次才知道**。这是可实测项,不需要外部输入。 **(d) 类型不一致。** `merchant_mcc_code` 在创建商户是 `String(4)`(示例 `204`),行业类目接口返回的 `mcc_code` 是**数字**(`1/7/32/47/86/125`),获取商户信息返回示例是字符串 `"292"`。 ### 6.3 阻塞项(拿不到依据就不写那一部分) | # | 缺什么 | 为什么我拿不到 | |---|---|---| | 1 | `channel_id`(通道 id) | 文档原文「**由客户经理提供**」,只给了几个举例(19/96/71) | | 2 | `bd_id`(业务经理 id) | 文档只给举例 `288/686` | | 3 | `rate_code`(费率模板) | 文档只说格式规则(万分之四十五填 `RT0045`),实际填什么要谈 | | 4 | `account_bank_code`(结算银行支行联行号) | 要真实联行号,编不出来 | | 5 | `channel_type` | 行业类目接口的必填项,文档写「由客户经理提供」 | | 6 | 进件审核通知地址的**上游侧配置** | 文档原文「由合作机构提供一个固定的接口地址,**平台配置后方可使用**」。我方地址我能提供,上游侧的配置动作我做不了 | | 7 | 测试环境的 `channel_id` 等是否与生产不同 | 文档没写 | `merchant_mcc_code`(行业类目)也依赖 `channel_type`,一并卡在 #5。 **没有 1–5,创建商户这个接口一步都走不通**——不是「跑不通某一分支」,是第一条请求就会被上游拒。因此批次三拆成两半: ### 6.4 批次三的拆分 **三-A:无歧义、可独立完成并验收** 图片上传、省市区、门店创建、门店列表、获取商户信息、商户审核主动查询。 这六个接口的入参、出参、值域在文档里都干净,不依赖任何未知凭据(只需要已有的 `inst_no` 与机构密钥)。图片上传尤其关键——它是创建商户那些资质图片字段取值的唯一来源。 **三-B:创建商户 + 入网流程** 代码可以写完(必填性、字段名、类型都明确,§6.2 的 (a) 已裁定),但**通电需要 §6.3 的 1–5 与 6**。 做法:字段组装与签名一次做对,值从配置读(不硬编码、不留「测试用假值」)。**在凭据到位前,这一半不部署、不接生产**,与用户「线上版本肯定是要进件再用的」一致。 **三-C:审核状态解析器** `reviewed_status` 的三套值域(§6.2 (b))全部按各自接口实现,**不做归一化猜测**。同一套语义在不同接口可能对应不同字面值,任何「统一成一个枚举」的冲动都会被文档打脸。入网状态那一页的两个映射冲突**记录在代码注释里指向文档**,等联调时用真实应答确定。 ### 6.5 上游 `notify_url` 的能力边界 异步通知要「由合作机构提供一个固定的接口地址」。现有配置里已有支付回调地址(`leshouying.notify-url`),进件审核通知是**另一个**固定地址,需要单独一个配置项和一条白名单路径。上游侧配置动作需要用户去要。 --- ## 7. 全部 DDL(一条) ```sql -- pay_lsy_merchant 去企业化:允许租户级商户 ALTER TABLE pay_lsy_merchant ALTER COLUMN enterprise_id DROP NOT NULL; -- 租户级商户唯一性。现有 uk_lsy_merchant_tenant_ent 建在 (tenant_id, enterprise_id) 上, -- 而 PostgreSQL 唯一索引里 NULL 互不相等 ⇒ 没有这条局部索引,同租户可插任意多行租户级商户。 CREATE UNIQUE INDEX IF NOT EXISTS uk_lsy_merchant_tenant_level ON pay_lsy_merchant (tenant_id) WHERE enterprise_id IS NULL; ``` 订单表、门店相关表**无 DDL**。 按本仓规则:SQL 我写,**只有用户执行**。 --- ## 8. 安全约束 1. **验签绝不跳过**。进件接口同样是签名请求,一样走既有签名逻辑,不开任何旁路。 2. **收银台页免鉴权,但不免令牌校验**。无令牌 / 错令牌 → 明确的「链接无效」页。令牌 32 位随机。 3. **`/payment/cashier/**` 进白名单时逐条精确匹配**,不用 `/**` 粗放放行——本仓既有过滤器注释专门警告过这个坑。 4. **`code_url` 不外发**。任何面向商户或前端的应答里不出现它。 5. **文案零上游痕迹**:页面、错误消息、应答字段名里不出现上游名称、域名、其专有字段名。 6. **凭据不进仓库**。进件新增的 `channel_id` 等一律走配置 + 环境变量,照 `application.yml` 现有的 `${ENV:default}` 写法。顺带记录一条**既存观察**(本轮不改):`leshouying.key` 的默认值目前是明文写在被 git 跟踪的 `application.yml` 里,正式密钥上线前应改为无默认值的环境变量。 7. 秘密只打印长度,不打印值。 --- ## 9. 测试策略 - **批次一**:`requireByTenant` / `requireByEnterprise` 分流各一组;`enterpriseId` 为空与非空两条下单路径。现有 418 个测试必须继续全绿。 - **批次二**: - 收银台:令牌不存在 → 无效页;令牌存在 → `/status` 金额/状态/过期时间正确;页面 HTML **与令牌无关**(同一段静态内容,挡的是「以后有人加回服务端替换」)。 - `qr.png`:返回 PNG 魔数,且内容能被解码回 `code_url`(拿 zxing 自己解一遍,别只断言「非空」)。 - 开放接口:`/qrcode/create` 应答**不含** `codeUrl`、**含** `cashierUrl`。 - 有效期:`expireStaleOrders` 对 `QRCODE + code_url_expire_time 非空` 的订单**不再**置 EXPIRED,对 `QRCODE + 无 code_url` 与 H5 **仍然**置 EXPIRED。这条要**两向**测:该红的红,不该红的绿。 - 免鉴权:`/payment/cashier/**` 无 token 可达;`/payment/collection/receive/**` 无 token **不可达**。 - **批次三**:三-A 的六个接口按报文契约单测 + 签名单测;三-B 的字段组装与必填校验单测,**其联网行为不做自动化测试**(会被外部环境卡住),改为拿到凭据后手工跑一次并留证据。 - `mvn test` 需要库隧道在位(`PaymentApplicationContextTest` 的已知依赖)。 --- ## 10. 明确不做 1. **当面付(`module/payment/facetoface`)不退役、不改动**。用户口径是「乐收赢与原有功能完全不相干」「原有的开放接口不变」。 2. **被扫的既有实现不动**:`/v3/pay`、`LsyOrderPollScheduler` 的密集轮询、付款码脱敏层、相关测试全部保留,只是在新的独立前端里多一个入口。 3. **小程序端不动**。 4. **不做多商户切换 UI**:租户级一个商户映射,够用。 5. **不做收银台 SSR 框架**:不引 Thymeleaf。 6. **不做二维码外观配置**。 7. **不修 `code_url` 可枚举**:上游的事。 --- ## 11. 待用户确认 1. **批次顺序**:一 → 二 → 三 是否认可?三-A 能否单独先做(它不依赖任何外部输入)? 2. **§6.3 的 1–6 谁来要**:`channel_id` / `bd_id` / `rate_code` / `account_bank_code` / `channel_type` 需要找客户经理;进件审核通知地址需要上游配置。 3. **§6.2 (a) 我裁定 `1=企业, 2=个体工商户, 3=小微商户`**,是否认可(依据是三处互相印证 vs 一处有复制痕迹)? 4. **§4.5 开放接口应答 `codeUrl` → `cashierUrl` 是破坏性变更**,确认可以改(前提是乐收赢确实没上过生产)。