Explorar el Código

docs(收款工作台): 收款码不分渠道 - 删 channel 列,H5 腿改顾客点选

用户问「能做到一个码微信和支付宝都能用吗」,答案是能、且原设计已经就是这样:
码里编的是我们自己的地址 /payment/cashier/{codeNo}(§4.5),分流按 UA 做,
与码本身无关(§4.4)。所谓「微信码」与「支付宝码」二维码图像逐像素相同,
channel 唯一作用是管理端列表上那个标签。

证据强度已在文档里分清:上游 getauth2 同端点两渠道 UA 都被静默授权 = 实测;
「因此顾客一定能付成」= 推断,真机没走过完整一单(§11.2 第 1/2 条)。

裁定落点:
- §0.9 口径第 ② 条(区分微信/支付宝两种二维码)划掉,新增第 ④ 条裁定
- §4.2 删 pay_receive_code.channel 列,补理由链
- §4.4 普通浏览器腿:h5pay 的 pay_type 必填(page 196236551),且 UA 推不出
  顾客装了哪个 App ⇒ 收银台页给两个按钮,顾客点选
- §7 DDL 与 db/receive-code.sql 同步删列(表尚未创建,无需 ALTER)
- §11.2 新增第 6 条:/v3/qrpay 聚合码在普通浏览器是否分流(未实测,验通则按钮可去)
alphaH hace 3 semanas
padre
commit
013c3c8513

+ 32 - 12
docs/superpowers/specs/2026-09-22-receive-workbench-design.md

@@ -20,14 +20,15 @@
 8. 常设约束:验签绝不跳过;文案不暴露上游;开发测试期先用文档给的测试信息把支付与收款生命周期走通,线上必须先进件。
 9. **(2026-09-22 追加,推翻第 1 条里「一单一码」的隐含前提)群收款场景**:商户拿到**一个长期固定的收款码**发到群里,群里任何人扫这个码进收银台,**顾客自己输入金额**、点确认,**直接唤起对应的支付 App**。用户逐字口径:
    - 「其实就是提供一个链接或者二维码,指向系统收银台前端,链接或二维码需要**绑定生成的商户**」
-   - 「需要**区分微信和支付宝两种二维码**,商户生成微信二维码则发到微信群,支付宝二维码则发给支付宝用户,输入完金额后**直接要唤起对应的 app 支付**」
+   - ~~「需要**区分微信和支付宝两种二维码**,商户生成微信二维码则发到微信群,支付宝二维码则发给支付宝用户,输入完金额后**直接要唤起对应的 app 支付**」~~ **(2026-09-22 撤销,见下方第 ④ 条裁定)**
    - 「**可建多个,带名称**」
    - 扫码环境三者都要覆盖:**微信群 / 微信内置浏览器、手机相机 / 普通浏览器、支付宝扫**
 
    本条的实测可行性见 §2.4,落地设计见 §4。**§4 已按本条重写**;第 1 条中「返回收银台页面链接」的结论仍成立,但「一单一码、商户先输金额、收银台只做展示」不成立。
 
-   用户就本条追加的三条裁定(2026-09-22):
-   - **渠道不符不用管**:「微信扫不了支付宝的码,反过来也是一样,不用管」⇒ 不做硬拦、不做提示(落点 §4.2)
+   用户就本条追加的裁定(2026-09-22):
+   - **渠道不符不用管**:「微信扫不了支付宝的码,反过来也是一样,不用管」⇒ 不做硬拦、不做提示(落点 §4.2)。**该条 2026-09-22 晚些时候因第 ④ 条失去对象**:码上不再有渠道,「不符」无从谈起。
+   - **④ 不区分渠道**:用户问「能做到一个码微信和支付宝都能用吗?」⇒ **能,且原设计已经就是这样**(证据链见 §4.2)⇒ 裁定**去掉 `pay_receive_code.channel` 列**,并撤销上面第 ② 条口径。普通浏览器那条腿改由顾客点选渠道(落点 §4.4)。
    - **收银台域名与 `notify-url` 同源**(落点 §5.3)
    - **开放接口不改**:「一般人不会抓包,就算抓包也抓不到什么有用的信息」⇒ 原「`codeUrl` → `cashierUrl`」提案作废(落点 §4.7)
    - **金额范围**:下限 `0.01` 元、**上限不额外限制**(落点 §5.3)
@@ -129,7 +130,7 @@
 |---|---|---|
 | 微信内置浏览器(群里扫码) | `v3/jspay` `pay_type=400` | `WeixinJSBridge` **原生唤起微信支付** |
 | 支付宝内置浏览器 | `v3/jspay` `pay_type=300` | `ali_trade_no` 调起支付宝 |
-| 普通浏览器(手机相机扫 / 电脑打开) | `v3/h5pay` | 返回跳转 URL,`location` 过去 |
+| 普通浏览器(手机相机扫 / 电脑打开) | `v3/h5pay` | 返回跳转 URL,`location` 过去;**渠道由顾客在页面上点选**(见下) |
 
 **仍待上游确认**(外部依赖,不阻塞本地开发,测试环境已证明不校验):
 1. `redirect_uri` 在**正式环境**是否需要预先登记白名单(测试环境实测未校验);
@@ -205,20 +206,29 @@ DDL(唯一一处,见 §7):
 | `code_no` | **URL 短码,32 位随机,不可枚举**(沿用旧设计对收银台令牌的要求) |
 | `name` | 名称,商户自取(「前台」「3 号桌」) |
 | `merchant_id` | → `pay_lsy_merchant.id`。**码绑定到哪个商户**,用户口径第 ① 条 |
-| `channel` | **复用既有枚举 `LsyPayType`**:`WECHAT`/`ALIPAY`(上游值 `400`/`300`)。用户口径第 ② 条 |
+| ~~`channel`~~ | **2026-09-22 删除** —— 码不分渠道,理由见下 |
 | `status` | 启用 / 停用 |
 | `remark` | 备注 |
 | `created_time` / `updated_time` | 同全库约定 |
 
 约束:`code_no` 全局唯一;`(tenant_id, name)` 唯一(同名无法管理)。
 
-⚠ **`channel` 是「这个码发给谁」的标记,不参与任何服务端判断。** 真实走哪条通道**只以 UA 为准**(§4.4)。
+**为什么不分渠道(2026-09-22 定)**
 
-用户 2026-09-22 裁定:「微信扫不了支付宝的码,反过来也是一样,**不用管**」⇒ 不做硬拦,**也不做「渠道不符」的提示**。`channel` 只用于管理端列表的区分与筛选,**收银台逻辑里一行都不读它**。
+用户问「能做到一个码微信和支付宝都能用吗?」——**能,而且原设计就已经是这样**,只是被一个多余的列挡住了:
 
-技术备注(不影响上面的裁定,也不改变任何实现):**我们的二维码内容是自己的 URL**(§4.5),两种码长得一模一样,区别只在管理端那个 `channel` 标签。所以「微信扫不了支付宝的码」在技术上未必成立。但**成立与否结论都一样**——因为分流是按 UA 做的(§4.4):**实测**同一个 `getauth2` 端点,微信 UA 与支付宝 UA **都会**被静默授权,只有其他 UA 才报错。真扫到了,顾客仍会被带到能付成的那条通道。
+1. 二维码里编的是**我们自己的地址** `https://<收银台域名>/payment/cashier/{codeNo}`(§4.5),**不是**微信或支付宝的专属链接;
+2. 两个 App 的「扫一扫」扫到 URL 都用各自内置浏览器打开(**平台既定行为,本项目未实测**);
+3. 进了收银台页之后**分流按 UA 做,与码本身无关**(§4.4)。
 
-⚠ 分清上面两句的性质:「两个渠道都静默授权」是**实测**;「所以顾客一定能付成」是我由实测推出的**推断**,没在真机走过完整一单。这是**联调时要验的项**,不是已证事实。
+⇒ 所谓「微信码」与「支付宝码」的二维码图像**逐像素相同**;`channel` 唯一作用是管理端列表上那个标签。故删列,§0.9 口径第 ② 条同时撤销。
+
+**证据强度要分清**(§11.2 第 1/2 条要验):
+
+- **实测** —— 上游 `getauth2` 同一个端点,微信 UA 与支付宝 UA **都会被静默授权**,只有其他 UA 报错(§2.4)。
+- **推断(未实测)** —— 因此顾客一定能付成。**没在真机走过完整一单**,不是已证事实。
+
+用户此前「微信扫不了支付宝的码,反过来也是一样,不用管」那条裁定**仍然成立**,且现在自动满足:码上根本没有渠道,「不符」无从发生。
 
 ### 4.3 免鉴权收银台页
 
@@ -278,6 +288,16 @@ GET /payment/cashier/{codeNo}/auth
 
 **普通浏览器**(手机相机扫、电脑打开):`WeixinJSBridge` 不存在,两个 JSAPI 都不适用,走 H5 支付。服务端下单后**自己 302** 过去,上游链接不下发到浏览器。
 
+⚠ **这一条腿的渠道必须顾客选**(2026-09-22 定,因为 `channel` 列已删,§4.2)。理由是两头都堵死了:
+
+- `h5pay` 的 `pay_type` 是**必填**(`M`:300 支付宝 / 400 微信 / 600 云闪付,page 196236551)——**不能省**;
+- 普通浏览器里 UA 只能告诉我们「不是微信也不是支付宝」,**推不出顾客装了哪个 App**,也没有 `channel` 可读。
+
+⇒ **收银台页在这一条腿上渲染两个按钮(微信支付 / 支付宝支付)**,顾客点哪个就用哪个 `pay_type` 下单。
+另两条腿(微信/支付宝内置浏览器)**不出现按钮**——UA 已知,直接唤起。
+
+> 可选优化(**未实测,不作为本期方案**):上游 `/v3/qrpay` 是聚合码接口,描述逐字「创建订单生成**包含微信、支付宝**的二维码链接」,**没有 `pay_type` 入参**。若它在**普通浏览器**里打开也能分流,这条腿就不需要顾客选了。列入 §11.2 第 6 条待验;验通了可以去掉按钮。
+
 **字段名口径**:`pay` 只回微信/支付宝自己的唤起参数名。`appId`/`timeStamp`/`nonceStr`/`packages`/`signType`/`paySign` 是微信 JSAPI 的**既定名,必须原样**(改了 `WeixinJSBridge` 不认),但它们属于微信、不暴露上游。**不回上游订单号、不回上游域名、不回上游任何字段名。**
 
 **支付结果的真实性**:`WeixinJSBridge` 的回调**只用来做提示、不作为成功依据**——用户中途退出、网络抖动都会让它丢。真实状态由前端轮询 `GET ./{codeNo}/status` 得到,最终以上游异步回调落库的状态为准(回调链路在批次一之前就有,不在本轮改动范围)。
@@ -517,7 +537,6 @@ CREATE TABLE IF NOT EXISTS pay_receive_code (
     code_no         VARCHAR(64)  NOT NULL,
     name            VARCHAR(64)  NOT NULL,
     merchant_id     BIGINT       NOT NULL,
-    channel         VARCHAR(16)  NOT NULL,
     status          VARCHAR(32)  NOT NULL DEFAULT 'ENABLED',
     remark          VARCHAR(255),
     created_time    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
@@ -588,7 +607,7 @@ CREATE INDEX        IF NOT EXISTS idx_receive_code_mch ON pay_receive_code (merc
 
 **已裁定:**
 
-5. ~~渠道不符怎么办~~ → **不用管**(用户逐字:「微信扫不了支付宝的码,反过来也是一样,不用管」)。落点见 §4.2:不做硬拦、不做提示,`channel` 不参与服务端判断。**已落地文档。**
+5. ~~渠道不符怎么办~~ → **不用管**(用户逐字:「微信扫不了支付宝的码,反过来也是一样,不用管」)。**已落地文档,且 2026-09-22 晚些时候因删掉 `channel` 列而自动满足**:码上不再有渠道,「不符」无从发生。§4.2 有完整理由链。
 7. ~~收银台对外域名~~ → **与 `notify-url` 同源**(用户答「可以」)。落点见 §5.3。**已落地文档。**
 
 **已裁定:**
@@ -608,5 +627,6 @@ CREATE INDEX        IF NOT EXISTS idx_receive_code_mch ON pay_receive_code (merc
 | 1 | **真机走通完整一单**(三通道各一次) | 未做。本文档所有「能付成」都是从实测推出的**推断** | 微信内扫码 → 输金额 → 唤起 → 支付成功 → 回调入账 |
 | 2 | 支付宝侧的**回跳与唤起**(不是字段名——字段名文档已明写,见 §2.4 订正) | 微信侧实测可达;支付宝侧只验到「UA 会被静默授权」这一步 | 同上,走一单真支付宝:看回跳带没带 `openid`、`jspay` 回没回 `ali_trade_no` |
 | 3 | **上游回跳时 `?openid=` 是怎么拼上去的** | 文档示例里 redirect_uri 带 query 时拼 `&openid=`,还会插一个 `/`;**我们的 redirect_uri 不带 query**,那到底拼 `?` 还是 `&`、会不会插 `/`——**没实测过**(探针没走完整趟 OAuth) | 真机看回跳 URL 原文;若拼错,`/auth` 侧要能容错解析 |
-| 4 | 普通浏览器(手机相机扫)走 `v3/h5pay` 的兜底 | `h5pay` 本身实测过(对照探针 `PAY_SUCCESS`),但**没串进收银台链路** | 用相机扫码走一遍 |
+| 4 | 普通浏览器(手机相机扫)走 `v3/h5pay` 的兜底,**含顾客点选渠道那两个按钮** | `h5pay` 本身实测过(对照探针 `PAY_SUCCESS`),但**没串进收银台链路**;两个按钮是 2026-09-22 新增的设计,**零实测** | 用相机扫码走一遍:**两个渠道各点一次**,看 400 和 300 是否都跳到对应 App |
 | 5 | 上游 `redirect_uri` 正式环境是否要预登记白名单 | 未知(测试环境实测未校验) | 问上游(§11.1 第 8 条) |
+| 6 | **(可选优化)`/v3/qrpay` 的聚合码 `code_url` 在普通浏览器里能不能两端分流** | 未测。文档只写「在支付宝或微信中打开支付链接完成支付」,**没写普通浏览器**。若成立,第 4 条的按钮可以去掉,一条腿也变成零交互 | 建一笔 `qrpay` 单,把 `code_url` 在 Chrome/Safari 里打开,看是否给出渠道选择或自动分流 |

+ 6 - 4
java/src/main/resources/db/receive-code.sql

@@ -3,7 +3,11 @@
 --
 -- 背景: 用户口径(design 文档 §0.9)—— 商户拿**一个长期固定的二维码**发到群里,
 --       群里任何人扫这个码进收银台,**顾客自己输金额**、点确认,直接唤起支付 App。
---       码可建多个、可命名、可停用,绑定到某个商户,区分微信/支付宝两种。
+--       码可建多个、可命名、可停用,绑定到某个商户。
+--
+-- ★ 不分渠道(2026-09-22 定): 一个码微信和支付宝都能扫。码里编的是**我们自己的地址**
+--   /payment/cashier/{code_no},分流按扫码环境的 UA 做,与码本身无关 —— 所以没有 channel 列。
+--   理由与证据链见 design 文档 §4.2。**普通浏览器(系统相机扫)那条腿的渠道由顾客点选**(§4.4)。
 --
 -- ★ 与旧设计的根本区别: **不是一单一码**。码只承载「入口」,不承载金额;
 --   订单在顾客点「确认支付」那一刻才创建。
@@ -16,7 +20,6 @@ CREATE TABLE IF NOT EXISTS pay_receive_code (
     code_no         VARCHAR(64)  NOT NULL,
     name            VARCHAR(64)  NOT NULL,
     merchant_id     BIGINT       NOT NULL,
-    channel         VARCHAR(16)  NOT NULL,
     status          VARCHAR(32)  NOT NULL DEFAULT 'ENABLED',
     remark          VARCHAR(255),
     created_time    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
@@ -42,7 +45,6 @@ COMMENT ON COLUMN pay_receive_code.tenant_id    IS '租户ID(由 MyBatis-Plus
 COMMENT ON COLUMN pay_receive_code.code_no      IS 'URL 短码(32 位随机,不可枚举;出现在公开收银台地址上,全局唯一)';
 COMMENT ON COLUMN pay_receive_code.name         IS '码名称(商户自取,如「前台」「3号桌」;同租户内唯一)';
 COMMENT ON COLUMN pay_receive_code.merchant_id  IS '绑定的商户,指向 pay_lsy_merchant.id';
-COMMENT ON COLUMN pay_receive_code.channel      IS '渠道(复用 LsyPayType 枚举名:WECHAT / ALIPAY;上游值分别为 400 / 300)';
 COMMENT ON COLUMN pay_receive_code.status       IS '状态(ENABLED 启用 / DISABLED 停用;停用后收银台返回「链接无效」页)';
 COMMENT ON COLUMN pay_receive_code.remark       IS '备注';
 COMMENT ON COLUMN pay_receive_code.created_time IS '创建时间';
@@ -50,5 +52,5 @@ COMMENT ON COLUMN pay_receive_code.updated_time IS '更新时间';
 
 -- 只读核对(执行后可手工跑一遍确认)
 --   SELECT column_name, data_type, is_nullable FROM information_schema.columns
---    WHERE table_name = 'pay_receive_code' ORDER BY ordinal_position;      -- 期望 10 列
+--    WHERE table_name = 'pay_receive_code' ORDER BY ordinal_position;      -- 期望 9 列
 --   SELECT indexname FROM pg_indexes WHERE tablename = 'pay_receive_code'; -- 期望含上述三条