|
@@ -18,6 +18,13 @@
|
|
|
6. **原有的开放接口不变**。
|
|
6. **原有的开放接口不变**。
|
|
|
7. **商户进件本轮一并做**,取值依据上游文档。
|
|
7. **商户进件本轮一并做**,取值依据上游文档。
|
|
|
8. 常设约束:验签绝不跳过;文案不暴露上游;开发测试期先用文档给的测试信息把支付与收款生命周期走通,线上必须先进件。
|
|
8. 常设约束:验签绝不跳过;文案不暴露上游;开发测试期先用文档给的测试信息把支付与收款生命周期走通,线上必须先进件。
|
|
|
|
|
+9. **(2026-09-22 追加,推翻第 1 条里「一单一码」的隐含前提)群收款场景**:商户拿到**一个长期固定的收款码**发到群里,群里任何人扫这个码进收银台,**顾客自己输入金额**、点确认,**直接唤起对应的支付 App**。用户逐字口径:
|
|
|
|
|
+ - 「其实就是提供一个链接或者二维码,指向系统收银台前端,链接或二维码需要**绑定生成的商户**」
|
|
|
|
|
+ - 「需要**区分微信和支付宝两种二维码**,商户生成微信二维码则发到微信群,支付宝二维码则发给支付宝用户,输入完金额后**直接要唤起对应的 app 支付**」
|
|
|
|
|
+ - 「**可建多个,带名称**」
|
|
|
|
|
+ - 扫码环境三者都要覆盖:**微信群 / 微信内置浏览器、手机相机 / 普通浏览器、支付宝扫**
|
|
|
|
|
+
|
|
|
|
|
+ 本条的实测可行性见 §2.4,落地设计见 §4。**§4 已按本条重写**;第 1 条中「返回收银台页面链接」的结论仍成立,但「一单一码、商户先输金额、收银台只做展示」不成立。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
@@ -28,7 +35,7 @@
|
|
|
| 批次 | 内容 | 能否独立验收 | 阻塞项 |
|
|
| 批次 | 内容 | 能否独立验收 | 阻塞项 |
|
|
|
|---|---|---|---|
|
|
|---|---|---|---|
|
|
|
| **一** | 乐收赢去企业化(租户账号级) | 能 | 无 |
|
|
| **一** | 乐收赢去企业化(租户账号级) | 能 | 无 |
|
|
|
-| **二** | 收款工作台:免鉴权收银台页 + 管理端收款接口 + 独立前端工程 | 能(端到端跑通一笔主扫、一笔被扫) | 无 |
|
|
|
|
|
|
|
+| **二** | 收款工作台:**长期收款码**(§0.9)+ 免鉴权收银台页 + 三通道唤起 + 管理端接口 + 独立前端工程 | 能(端到端跑通一笔群收款、一笔被扫) | 无(三通道的正式环境白名单属上线前事项,见 §2.4) |
|
|
|
| **三** | 商户进件 | **不能** | 客户经理凭据 + 上游配置回调地址,见 §6 |
|
|
| **三** | 商户进件 | **不能** | 客户经理凭据 + 上游配置回调地址,见 §6 |
|
|
|
|
|
|
|
|
批次一必须先做:批次二的管理端接口按租户级设计,建立在它之上。
|
|
批次一必须先做:批次二的管理端接口按租户级设计,建立在它之上。
|
|
@@ -82,6 +89,45 @@
|
|
|
| 登录接口 | `POST /api/v1/system/auth/login` |
|
|
| 登录接口 | `POST /api/v1/system/auth/login` |
|
|
|
| 令牌传递 | 请求头 `Authorization: Bearer <accessToken>` |
|
|
| 令牌传递 | 请求头 `Authorization: Bearer <accessToken>` |
|
|
|
|
|
|
|
|
|
|
+### 2.4 「群收款 + 顾客输金额 + 原生唤起」的实测可行性(§0.9 的依据)
|
|
|
|
|
+
|
|
|
|
|
+**背景**:本设计早先记过一条「JSAPI `v3/jspay` 需 `open_id` + 服务商公众号授权,链路长一个量级,用户已确认本期不做」。用户 2026-09-22 提出 §0.9 后,该条口径被推翻,并对上游文档做了逐页复核 + 打测试环境实测。结论:**链路完全开放,且那个「服务商公众号」不是我们要申请的东西**。
|
|
|
|
|
+
|
|
|
|
|
+**文档侧读数**(上游文档站,页码可复核):
|
|
|
|
|
+
|
|
|
|
|
+| 项 | 原文口径 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `v3/jspay` 场景 | 「用于支付宝生活号支付,公众号支付,**微信浏览器打开链接支付**,微信扫码打开链接支付」 |
|
|
|
|
|
+| `v3/jspay` 必填 | `inst_no`、`mch_no`、`pay_type`(300支付宝/400微信)、`pay_trace_no`、`pay_time`、`total_amount`、**`open_id`**、`sign`;`appid` 选填且「取值优先于后台配置」 |
|
|
|
|
|
+| `v3/jspay` 返回 | 微信:`appId`/`timeStamp`/`nonceStr`/`packages`/`signType`/`paySign` —— **正是 `WeixinJSBridge.invoke('getBrandWCPayRequest')` 所需**;支付宝:`ali_trade_no`「用于调起支付宝JSAPI」 |
|
|
|
|
|
+| 获取 openid | 「用于**服务商模式(服务商公众号)**授权获取用户信息……使用子商户模式(独立公众号)发起支付的商户请通过相应的直连接口获取」 |
|
|
|
|
|
+| 关单 | `v3/closeorder` 正文:「订单关闭接口,**二维码支付,JSAPI支付可用**」 |
|
|
|
|
|
+
|
|
|
|
|
+**测试环境实测**(我方向上游机构号 + 测试商户号,2026-09-22):
|
|
|
|
|
+
|
|
|
|
|
+| 探针 | 读数 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `GET /jsapi/getauth2`(签名已修正后) | 不再报「签名验证失败」,改报「**请在微信或支付宝中打开!**」⇒ 该接口对我们是**开放的**,只是有 UA 门禁 |
|
|
|
|
|
+| `GET /jsapi/getauth2` + **微信 UA** | `302 → open.weixin.qq.com/connect/oauth2/authorize?appid=wx81ba…&scope=snsapi_base&…` ⇒ 服务商公众号是**上游的**,且 `snsapi_base` 为**静默授权**(无授权弹窗) |
|
|
|
|
|
+| `GET /jsapi/getauth2` + **支付宝 UA** | `302 → openauth.alipay.com`,`scope=auth_base`(同为静默)⇒ **同一个端点按 UA 自动分流两渠道** |
|
|
|
|
|
+| `GET /jsapi/getauth2` + **普通浏览器 UA** | 落回错误页(预期,无对应 JSAPI) |
|
|
|
|
|
+| `redirect_uri` 往返 | 我们传的 `redirect_uri` 被**原样**嵌在上游 `auth_hosted.html?...&redirect_uri=<我们的>` 里 ⇒ 上下文可穿过整个 OAuth 往返 |
|
|
|
|
|
+| `POST /v3/jspay`(假 openid) | `return_code=SUCCESS` / `return_msg='无效的openid'` / `result_code=PAY_FAIL` ⇒ **接口可达、签名通过、参数校验走到了 openid 这一层,只差一个真 openid** |
|
|
|
|
|
+| `POST /v3/h5pay`(对照组,pay_type=300) | `PAY_SUCCESS`,`trade_no=200001070X4326…`,`code_url` 137 字符 ⇒ **普通浏览器有兜底通道** |
|
|
|
|
|
+
|
|
|
|
|
+**由此确定的三条通道**(按 UA 分流,服务端不需问用户):
|
|
|
|
|
+
|
|
|
|
|
+| 扫码环境 | 通道 | 唤起方式 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| 微信内置浏览器(群里扫码) | `v3/jspay` `pay_type=400` | `WeixinJSBridge` **原生唤起微信支付** |
|
|
|
|
|
+| 支付宝内置浏览器 | `v3/jspay` `pay_type=300` | `ali_trade_no` 调起支付宝 |
|
|
|
|
|
+| 普通浏览器(手机相机扫 / 电脑打开) | `v3/h5pay` | 返回跳转 URL,`location` 过去 |
|
|
|
|
|
+
|
|
|
|
|
+**仍待上游确认**(外部依赖,不阻塞本地开发,测试环境已证明不校验):
|
|
|
|
|
+1. `redirect_uri` 在**正式环境**是否需要预先登记白名单(测试环境实测未校验);
|
|
|
|
|
+2. 支付宝回跳时携带用户标识的**字段名**(文档只写了微信侧示例的 `openid`,支付宝侧未知,联调时实测);
|
|
|
|
|
+3. 正式环境参数(域名 / 机构号 / 密钥)。
|
|
|
|
|
+
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 3. 批次一:乐收赢去企业化
|
|
## 3. 批次一:乐收赢去企业化
|
|
@@ -122,106 +168,167 @@ DDL(唯一一处,见 §7):
|
|
|
### 4.1 组成
|
|
### 4.1 组成
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
-独立前端工程 frontend-receive/ ──JWT──▶ 管理端收款接口 /payment/collection/receive/**
|
|
|
|
|
|
|
+独立前端工程 frontend-receive/ ──JWT──▶ 管理端接口 /payment/collection/receive/**
|
|
|
│ │
|
|
│ │
|
|
|
- │ (展示二维码图片) ├─▶ LsyOrderService(既有)
|
|
|
|
|
- ▼ │
|
|
|
|
|
- 收银台页 /payment/cashier/{token}/qr.png ────────┘
|
|
|
|
|
- ▲
|
|
|
|
|
- │ 免鉴权
|
|
|
|
|
- │
|
|
|
|
|
- 商户(或顾客)浏览器 ◀── cashier_url ── 开放接口 /payment/openapi/pay/qrcode/create
|
|
|
|
|
|
|
+ │ 管理「长期收款码」:建 / 改名 / 停用 ├─▶ 收款码表 pay_receive_code(新增)
|
|
|
|
|
+ │ 下载二维码 PNG │
|
|
|
|
|
+ ▼ ▼
|
|
|
|
|
+ 商户把码贴到群里 ──▶ 顾客扫 ──▶ 收银台页 /payment/cashier/{codeNo}
|
|
|
|
|
+ │
|
|
|
|
|
+ │ 免鉴权
|
|
|
|
|
+ ├─ 微信内置浏览器 ─▶ 静默授权拿 openid ─▶ JSAPI 原生唤起微信支付
|
|
|
|
|
+ ├─ 支付宝内置浏览器 ─▶ 静默授权拿 userid ─▶ 支付宝 JSAPI 唤起
|
|
|
|
|
+ └─ 普通浏览器 ─▶ H5 支付 ─▶ 跳转唤起
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 4.2 免鉴权收银台页
|
|
|
|
|
|
|
+**与旧版 §4 的根本区别**:码**不是一单一码**,而是商户的固定资产(可建多个、可命名、可停用、长期有效);码**不承载金额**,金额由顾客在收银台输入;订单在顾客点「确认支付」时才创建。
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 收款码(新增表 `pay_receive_code`)
|
|
|
|
|
+
|
|
|
|
|
+一个商户可建**多个**收款码,**可命名**、可停用,**长期有效**。码只承载「入口」,不承载金额。
|
|
|
|
|
+
|
|
|
|
|
+| 列 | 说明 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `id` | 主键,雪花 |
|
|
|
|
|
+| `tenant_id` | 租户,由 MyBatis-Plus 拦截器填(归属只能来自认证上下文,同 `pay_lsy_merchant` 的约束) |
|
|
|
|
|
+| `code_no` | **URL 短码,32 位随机,不可枚举**(沿用旧设计对收银台令牌的要求) |
|
|
|
|
|
+| `name` | 名称,商户自取(「前台」「3 号桌」) |
|
|
|
|
|
+| `merchant_id` | → `pay_lsy_merchant.id`。**码绑定到哪个商户**,用户口径第 ① 条 |
|
|
|
|
|
+| `channel` | **复用既有枚举 `LsyPayType`**:`WECHAT`/`ALIPAY`(上游值 `400`/`300`)。用户口径第 ② 条 |
|
|
|
|
|
+| `status` | 启用 / 停用 |
|
|
|
|
|
+| `remark` | 备注 |
|
|
|
|
|
+| `created_time` / `updated_time` | 同全库约定 |
|
|
|
|
|
+
|
|
|
|
|
+约束:`code_no` 全局唯一;`(tenant_id, name)` 唯一(同名无法管理)。
|
|
|
|
|
+
|
|
|
|
|
+⚠ **`channel` 是「这个码发给谁」的标记,不是服务端的硬闸。** 真实走哪条通道**以 UA 为准**(§4.4):微信码被支付宝扫到时,仍然让顾客用支付宝付得成,只是页面上给一句提示。理由:顾客扫了码就是要付钱,拦下来对商户是损失。**这条是我的默认取值,用户可推翻**——若要改成硬拦(「本码为微信收款码,请在微信中打开」),改动只在收银台页一处。
|
|
|
|
|
+
|
|
|
|
|
+### 4.3 免鉴权收银台页
|
|
|
|
|
|
|
|
**端点**(`SecurityConfig.WHITE_LIST` 与 `JwtAuthFilter.WHITE_LIST_PATHS` 同步新增 `/payment/cashier/**`):
|
|
**端点**(`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:金额、状态、剩余有效期 |
|
|
|
|
|
|
|
+| `GET /payment/cashier/{codeNo}` | HTML 页面 |
|
|
|
|
|
+| `GET /payment/cashier/{codeNo}/info` | JSON:商户名、码名称、渠道、可用状态 |
|
|
|
|
|
+| `GET /payment/cashier/{codeNo}/auth` | 302 到上游静默授权(§4.4) |
|
|
|
|
|
+| `POST /payment/cashier/{codeNo}/pay` | 入参金额 + 授权标识 → 建单 + 下单 → 返回唤起参数 |
|
|
|
|
|
+| `GET /payment/cashier/{codeNo}/status` | JSON:该笔订单状态(前端轮询用) |
|
|
|
|
|
|
|
|
-**页面内容**:金额、二维码、倒计时、订单状态(支付中 / 已支付 / 已失效)。文案零上游痕迹。
|
|
|
|
|
|
|
+**页面内容**:商户名、金额输入框、「确认支付」按钮、支付结果。文案零上游痕迹。
|
|
|
|
|
|
|
|
**实现方式**:HTML 一个静态文件放在 `src/main/resources/` 下,控制器读入后**原样**以 `produces = text/html` 返回。**不引入 Thymeleaf**——本服务目前是纯 JSON 服务,为一个页面引入模板引擎不划算。
|
|
**实现方式**:HTML 一个静态文件放在 `src/main/resources/` 下,控制器读入后**原样**以 `produces = text/html` 返回。**不引入 Thymeleaf**——本服务目前是纯 JSON 服务,为一个页面引入模板引擎不划算。
|
|
|
|
|
|
|
|
-**服务端不做任何占位符替换。** 页面上每一个变量都从 `/status` 取,由 JS 用 `textContent` 写入 DOM;令牌由 JS 从 `location.pathname` 里读。这样服务端没有任何拼接点,**注入面为零**。
|
|
|
|
|
|
|
+**服务端不做任何占位符替换。** 页面上每一个变量(商户名、码名称)都从 `/info` 取,由 JS 用 `textContent` 写入 DOM;`codeNo` 由 JS 从 `location.pathname` 里读。这样服务端没有任何拼接点,**注入面为零**。
|
|
|
|
|
+
|
|
|
|
|
+⚠ 如果以后有人往这段 HTML 里加服务端替换,那就必须同时对值做 HTML 转义——**商户名和码名称是商户自己填的自由文本**、`codeNo` 来自路径,两者都不可信。**当前设计里没有这个位置,加的时候要有意识。**
|
|
|
|
|
+
|
|
|
|
|
+**授权标识的处理**:OAuth 回跳到 `?openid=…` 后,页面捕获它、存进 `sessionStorage`、随即用 `history.replaceState` 把 URL 上的标识抹掉(免得被转发或截图带出去)。它**只用来向上游换唤起参数,不参与我方的任何鉴权**——所以即使被人改成别的值,结果也只是唤起失败,不构成越权。
|
|
|
|
|
|
|
|
-⚠ 如果以后有人往这段 HTML 里加服务端替换,那就必须同时对值做 HTML 转义——金额来自数据库、令牌来自路径,两者都不可信。**当前设计里没有这个位置,加的时候要有意识。**
|
|
|
|
|
|
|
+**`codeNo` 校验**:不存在或已停用 ⇒ 返回一个「链接无效」页,**不是 404 白页**(这是顾客会看到的页面)。`codeNo` 32 位随机,不可枚举。
|
|
|
|
|
|
|
|
-`/status` 应答:`amount`、`status`、`codeUrlExpireTime`、`paid`。
|
|
|
|
|
|
|
+**上游短链可枚举这件事(旧版遗留,新版已基本消解)**:上游的 `code_url` 是 `s-0000N` 连号,邻单理论上可被猜出。旧设计要发链接给商户,所以必须处理;**新设计里顾客只拿到我们的 `/payment/cashier/{codeNo}`,上游链接只在服务端 302(普通浏览器兜底那条腿)或回调里出现,从不下发到浏览器**。风险等级:低。
|
|
|
|
|
|
|
|
-**令牌校验**:`{payTraceNo}` 不存在 ⇒ 返回一个「链接无效」页,**不是 404 白页**(这是顾客会看到的页面)。令牌本身 32 位随机,不可枚举。
|
|
|
|
|
|
|
+### 4.4 支付唤起(三通道)
|
|
|
|
|
|
|
|
-**上游短链可枚举这件事**:`code_url` 是 `s-0000N` 连号,理论上邻单可被猜出。这是上游的设计,我们改不了。**我们这边能做的两件事**:(1) 我们的入口令牌不可枚举;(2) **不把 `code_url` 通过任何接口明文外发**(见 §4.5)。风险等级低(猜到了也只能是「替别人付钱」),记录在案,不作为阻塞。
|
|
|
|
|
|
|
+**微信内置 / 支付宝内置 —— 静默授权 + 原生唤起**
|
|
|
|
|
|
|
|
-### 4.3 二维码生成
|
|
|
|
|
|
|
+```
|
|
|
|
|
+顾客扫长期码 → 收银台页 /payment/cashier/{codeNo}
|
|
|
|
|
+ │ 页面加载:sessionStorage 里没有授权标识 ⇒ location.replace('./{codeNo}/auth')
|
|
|
|
|
+ ▼
|
|
|
|
|
+GET /payment/cashier/{codeNo}/auth
|
|
|
|
|
+ │ 服务端按既有签名规则签 inst_no / mch_no / redirect_uri
|
|
|
|
|
+ │ (redirect_uri 参与签名时不 encodeURI —— 上游文档明写的特殊口径)
|
|
|
|
|
+ ▼
|
|
|
|
|
+302 → 上游 getauth2 → 上游按 UA 分流(不需要我们判断):
|
|
|
|
|
+ 微信 → open.weixin.qq.com scope=snsapi_base(静默,无授权弹窗)
|
|
|
|
|
+ 支付宝 → openauth.alipay.com scope=auth_base (静默)
|
|
|
|
|
+ ▼
|
|
|
|
|
+上游回跳 → 收银台?openid=…
|
|
|
|
|
+ │ 页面捕获 → 存 sessionStorage → history.replaceState 抹掉
|
|
|
|
|
+ ▼
|
|
|
|
|
+顾客输入金额 → 点「确认支付」→ POST ./{codeNo}/pay {金额, 授权标识}
|
|
|
|
|
+ │ 服务端:查码 → 取绑定商户 → 建单 → 调上游 JSAPI 下单(pay_type 由 UA 决定)
|
|
|
|
|
+ ▼
|
|
|
|
|
+返回唤起参数 → 前端 WeixinJSBridge.invoke('getBrandWCPayRequest', …) → 收银台直接弹出
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**`redirect_uri` 不带任何业务参数。** 金额是顾客回跳之后才输入的,没有东西需要穿过这趟往返——这比「把金额挂在 redirect_uri 上带过去」干净,也少一个可被篡改的入口。
|
|
|
|
|
+
|
|
|
|
|
+**普通浏览器**(手机相机扫、电脑打开):`WeixinJSBridge` 不存在,两个 JSAPI 都不适用,走 H5 支付。服务端下单后**自己 302** 过去,上游链接不下发到浏览器。
|
|
|
|
|
+
|
|
|
|
|
+**字段名口径**:`pay` 只回微信/支付宝自己的唤起参数名。`appId`/`timeStamp`/`nonceStr`/`packages`/`signType`/`paySign` 是微信 JSAPI 的**既定名,必须原样**(改了 `WeixinJSBridge` 不认),但它们属于微信、不暴露上游。**不回上游订单号、不回上游域名、不回上游任何字段名。**
|
|
|
|
|
+
|
|
|
|
|
+**支付结果的真实性**:`WeixinJSBridge` 的回调**只用来做提示、不作为成功依据**——用户中途退出、网络抖动都会让它丢。真实状态由前端轮询 `GET ./{codeNo}/status` 得到,最终以上游异步回调落库的状态为准(回调链路在批次一之前就有,不在本轮改动范围)。
|
|
|
|
|
+
|
|
|
|
|
+### 4.5 二维码生成
|
|
|
|
|
|
|
|
- 依赖:**新增 `com.google.zxing:core`**。已实测它不在依赖树里,Hutool 的 `QrCodeUtil` 没有它会在运行时炸。
|
|
- 依赖:**新增 `com.google.zxing:core`**。已实测它不在依赖树里,Hutool 的 `QrCodeUtil` 没有它会在运行时炸。
|
|
|
-- 生成点:**只有一处** —— `qr.png` 端点。
|
|
|
|
|
-- 二维码内容:`code_url` 原文。
|
|
|
|
|
|
|
+- 生成点:**只有一处** —— `/code/{id}/qr.png`。
|
|
|
|
|
+- 二维码内容:`https://<收银台域名>/payment/cashier/{codeNo}`(**我们自己的地址,不是上游链接**)。
|
|
|
- 尺寸/容错级别:选一个能印能扫的默认值即可,不作配置项(YAGNI)。
|
|
- 尺寸/容错级别:选一个能印能扫的默认值即可,不作配置项(YAGNI)。
|
|
|
|
|
|
|
|
前端不生成二维码:新工程因此**不需要引入任何二维码 npm 包**,`<img src=".../qr.png">` 即可。
|
|
前端不生成二维码:新工程因此**不需要引入任何二维码 npm 包**,`<img src=".../qr.png">` 即可。
|
|
|
|
|
|
|
|
-### 4.4 管理端收款接口
|
|
|
|
|
|
|
+### 4.6 管理端收款接口
|
|
|
|
|
|
|
|
路径前缀 `/payment/collection/receive`,鉴权走既有 JWT 过滤器。
|
|
路径前缀 `/payment/collection/receive`,鉴权走既有 JWT 过滤器。
|
|
|
|
|
|
|
|
| 端点 | 用途 | 权限串 |
|
|
| 端点 | 用途 | 权限串 |
|
|
|
|---|---|---|
|
|
|---|---|---|
|
|
|
-| `POST /qrcode` | 主扫下单,输入金额 | `module_payment:receive:qrcode` |
|
|
|
|
|
|
|
+| `POST /code` | 建收款码 | `module_payment:receive:code` |
|
|
|
|
|
+| `PUT /code/{id}` | 改名 / 停用 / 启用 | `module_payment:receive:code` |
|
|
|
|
|
+| `GET /code/page` | 收款码列表 | `module_payment:receive:code:list` |
|
|
|
|
|
+| `GET /code/{id}/qr.png` | 二维码 PNG | `module_payment:receive:code:list` |
|
|
|
| `POST /barcode` | 被扫下单,输入付款码 | `module_payment:receive:barcode` |
|
|
| `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` |
|
|
|
|
|
|
|
+| `GET /order/page` | 订单明细分页 | `module_payment:receive:order:list` |
|
|
|
|
|
+| `GET /order/{outTradeNo}` | 订单详情 | `module_payment:receive:order:list` |
|
|
|
|
|
+| `POST /order/{outTradeNo}/query` | 主动向上游查一次 | `module_payment:receive:order:list` |
|
|
|
|
|
|
|
|
**不复用** `module_payment:leshouying:order:list`。理由:能查订单不该自动等于能发起收款。
|
|
**不复用** `module_payment:leshouying:order:list`。理由:能查订单不该自动等于能发起收款。
|
|
|
|
|
|
|
|
-主扫下单应答:`outTradeNo`、`payTraceNo`、`cashierUrl`、`qrImageUrl`、`amount`、`expireTime`。
|
|
|
|
|
-**不回 `codeUrl`**——理由与 §4.5 一致。
|
|
|
|
|
|
|
+**旧版的 `POST /qrcode`(管理端先输金额再下单)去掉了**:新模型下订单由顾客在收银台点「确认支付」时触发,管理端不再有「先生成码」这一步。管理端只管理码本身。
|
|
|
|
|
|
|
|
被扫下单:入参 `authCode`(付款码)+ `amount`。走既有 `LsyOrderService.createBarcodeOrder`。
|
|
被扫下单:入参 `authCode`(付款码)+ `amount`。走既有 `LsyOrderService.createBarcodeOrder`。
|
|
|
|
|
|
|
|
-### 4.5 开放接口变更
|
|
|
|
|
|
|
+### 4.7 开放接口变更(**与 §0.6 冲突,待用户裁决,默认不动**)
|
|
|
|
|
|
|
|
`POST /payment/openapi/pay/qrcode/create` 的应答:`codeUrl` 字段**换成 `cashierUrl`**。
|
|
`POST /payment/openapi/pay/qrcode/create` 的应答:`codeUrl` 字段**换成 `cashierUrl`**。
|
|
|
|
|
|
|
|
理由有两条,都需要:
|
|
理由有两条,都需要:
|
|
|
|
|
|
|
|
1. 商户拿到的应该是**我们的**链接,点开是**我们的**收银台页。直接把 `code_url` 发出去,商户点开看到的是上游域名,等于暴露上游。
|
|
1. 商户拿到的应该是**我们的**链接,点开是**我们的**收银台页。直接把 `code_url` 发出去,商户点开看到的是上游域名,等于暴露上游。
|
|
|
-2. 顺带收窄暴露面:`code_url` 是上游可枚举短链(§4.2),不再外发。
|
|
|
|
|
|
|
+2. 顺带收窄暴露面:`code_url` 是上游可枚举短链,不再外发。
|
|
|
|
|
|
|
|
这是**破坏性变更**。成立的前提是用户确认过「乐收赢所有代码都没有上过生产」。落地前要再核一次 `LsyPayResultVO.codeUrl` 有没有别的消费方。
|
|
这是**破坏性变更**。成立的前提是用户确认过「乐收赢所有代码都没有上过生产」。落地前要再核一次 `LsyPayResultVO.codeUrl` 有没有别的消费方。
|
|
|
|
|
|
|
|
-### 4.6 独立前端工程 `frontend-receive/`
|
|
|
|
|
|
|
+⚠ **冲突**:用户口径 §0.6 是「原有的开放接口**不变**」,本节的提议是**改**它。两条都是用户说的,我不能替用户选。**默认取值:不动**(遵守 §0.6),**代价**是走老开放接口的商户仍会拿到上游链接。等用户裁决。
|
|
|
|
|
+
|
|
|
|
|
+### 4.8 独立前端工程 `frontend-receive/`
|
|
|
|
|
|
|
|
- 栈与现有 `frontend/` 一致(Vue 3 + Vite + Element Plus + Pinia + vue-router + axios + pnpm),不引入新框架。
|
|
- 栈与现有 `frontend/` 一致(Vue 3 + Vite + Element Plus + Pinia + vue-router + axios + pnpm),不引入新框架。
|
|
|
- 登录:调 `POST /api/v1/system/auth/login`,复用现有平台账号,不新建账号体系。
|
|
- 登录:调 `POST /api/v1/system/auth/login`,复用现有平台账号,不新建账号体系。
|
|
|
- 令牌:`Authorization: Bearer`,与现有 `frontend/src/utils/request.ts` 的拦截器同构。
|
|
- 令牌:`Authorization: Bearer`,与现有 `frontend/src/utils/request.ts` 的拦截器同构。
|
|
|
-- 四页:
|
|
|
|
|
|
|
+- **四页**(旧版第 2 页的形态变了:不再「输金额 → 生成码」,而是「**管理收款码**」):
|
|
|
1. **登录**
|
|
1. **登录**
|
|
|
- 2. **二维码收款(主扫)**:输入金额 → 生成 → 页面展示二维码 + 金额 + 倒计时,支持保存/打印
|
|
|
|
|
|
|
+ 2. **收款码管理**:列表(名称 / 渠道 / 绑定商户 / 状态)+ 新建 + 改名 + 停用 + **下载二维码图片**
|
|
|
3. **扫描二维码收款(被扫)**:输入框接扫码枪(键盘模拟输入 + 回车触发)→ 提交 → 展示结果
|
|
3. **扫描二维码收款(被扫)**:输入框接扫码枪(键盘模拟输入 + 回车触发)→ 提交 → 展示结果
|
|
|
4. **订单明细**:分页列表 + 详情
|
|
4. **订单明细**:分页列表 + 详情
|
|
|
|
|
+- **收银台页不在这个工程里**——它是后端直出的免鉴权静态页(§4.3),不经过前端构建产物。
|
|
|
- 构建:`base: '/receive/'`,产物独立;nginx 新增 `location /receive`。
|
|
- 构建:`base: '/receive/'`,产物独立;nginx 新增 `location /receive`。
|
|
|
- 不作多租户切换 UI:租户从 JWT 里来。
|
|
- 不作多租户切换 UI:租户从 JWT 里来。
|
|
|
|
|
|
|
|
-### 4.7 主扫有效期与状态收敛
|
|
|
|
|
-
|
|
|
|
|
-**改**:`leshouying.default-qr-expire-minutes` 由 `15` 改为 `4320`(三天,上游文档默认值,也是用户要的),`QR_MAX_EXPIRE_MINUTES` 保持 4320。
|
|
|
|
|
|
|
+### 4.9 旧「主扫有效期」方案**作废**(原 §4.7)
|
|
|
|
|
|
|
|
-**随之必须改的**:`LsyReconcileScheduler.expireStaleOrders` 当前按 `absolute-expire-hours = 24` 把所有 `PENDING`(= `{CREATED, PAYING}`,**含 PAYING**)且创建超 24 小时的订单置 `EXPIRED`。主扫单在拿到 `code_url` 后停在 `PAYING`,**正好落在网里**。
|
|
|
|
|
|
|
+旧设计要改:`leshouying.default-qr-expire-minutes` 由 `15` 改为 `4320`,并把主扫单精确排除出 24 小时绝对兜底网(排除条件 `pay_scene='QRCODE' AND code_url_expire_time IS NOT NULL`)。
|
|
|
|
|
|
|
|
-今天的 15 分钟有效期让两者永远碰不上;改成三天后,第 24 小时本地记 `EXPIRED`、而上游链接活到第 72 小时。**第 25 到 72 小时之间付的款会变成「钱收了、本地账是过期」**,而现有的两条补救通道都是死的,属于永久性错账。
|
|
|
|
|
|
|
+**新模型下这两项改动都不需要了**:
|
|
|
|
|
|
|
|
-**规则**:主扫单**自带收敛链**(`code_url_expire_time` → `/v3/qrclose` → `CLOSED`,由 `LsyReconcileScheduler.closeExpiredQrOrders` 负责),因此把它排除出 24 小时绝对兜底网。
|
|
|
|
|
|
|
+- 旧方案要解决的是「码活三天、本地账在第 24 小时被兜底网置 EXPIRED」的错账。新模型里**订单是顾客点确认那一刻才创建的**,不存在「码的生命周期」与「订单的生命周期」错位这回事。
|
|
|
|
|
+- 顾客确认后下的 JSAPI 单,其未支付窗口以微信侧 `prepay_id` 为准(远短于 24 小时),**落回 24 小时兜底网是合适的**,不需要排除。
|
|
|
|
|
|
|
|
-排除条件要**精确**,不能一刀切掉整个 `QRCODE`:`pay_scene='QRCODE'` **且 `code_url_expire_time IS NOT NULL`** 才排除。理由:`QRCODE` 卡在 `CREATED`(上游没回 `code_url`,例如下单直接失败)时**没有任何网**,它仍然需要绝对兜底。
|
|
|
|
|
-
|
|
|
|
|
-H5 与被扫的 24 小时不动。
|
|
|
|
|
|
|
+**但保留一句**:`/payment/openapi/pay/qrcode/create` 走的老 `qrpay` 链路**保持原样**(§0.6),`default-qr-expire-minutes` 与 `closeExpiredQrOrders` 都不动。若将来老链路的有效期也要拉长到三天,本节的两条改动**必须重新捡起来**——那时的错账风险同旧文所述,不会因为新模型出现而消失。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
@@ -229,11 +336,22 @@ H5 与被扫的 24 小时不动。
|
|
|
|
|
|
|
|
### 5.1 会不会有 DDL
|
|
### 5.1 会不会有 DDL
|
|
|
|
|
|
|
|
-批次一有(§7 一条)。批次二**没有**:收银台复用 `pay_trace_no`,订单表列已够用。
|
|
|
|
|
|
|
+批次一有(§7 一条)。批次二**原本没有,现在有了**:§0.9 引入的长期收款码需要一张新表 `pay_receive_code`(§4.2),见 §7。
|
|
|
|
|
+
|
|
|
|
|
+收银台的 `pay` 端点仍复用既有订单表,不新增订单相关列。
|
|
|
|
|
|
|
|
### 5.2 权限与菜单
|
|
### 5.2 权限与菜单
|
|
|
|
|
|
|
|
-新增权限串(§4.4 三个)。**菜单与权限的 SQL 由我写好、由用户执行**。
|
|
|
|
|
|
|
+新增权限串(§4.6 四个):
|
|
|
|
|
+
|
|
|
|
|
+| 权限串 | 覆盖端点 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `module_payment:receive:code` | `POST /code`、`PUT /code/{id}` |
|
|
|
|
|
+| `module_payment:receive:code:list` | `GET /code/page`、`GET /code/{id}/qr.png` |
|
|
|
|
|
+| `module_payment:receive:barcode` | `POST /barcode` |
|
|
|
|
|
+| `module_payment:receive:order:list` | `GET /order/page`、`GET /order/{outTradeNo}`、`POST /order/{outTradeNo}/query` |
|
|
|
|
|
+
|
|
|
|
|
+**菜单与权限的 SQL 由我写好、由用户执行**。权限串必须与 `sys_menu.permission` 逐字一致(本仓既有教训:差一个字符就是 403)。
|
|
|
|
|
|
|
|
⚠ **SQL 未执行前,收款接口一律 403**。这不是缺陷,是预期;验收顺序上要排在 SQL 之后。
|
|
⚠ **SQL 未执行前,收款接口一律 403**。这不是缺陷,是预期;验收顺序上要排在 SQL 之后。
|
|
|
|
|
|
|
@@ -241,7 +359,9 @@ H5 与被扫的 24 小时不动。
|
|
|
|
|
|
|
|
| 键 | 用途 |
|
|
| 键 | 用途 |
|
|
|
|---|---|
|
|
|---|---|
|
|
|
-| `leshouying.cashier-base-url` | 拼 `cashierUrl` 用。默认取现有对外域名,与 `notify-url` 同源 |
|
|
|
|
|
|
|
+| `leshouying.cashier-base-url` | 拼收银台地址(二维码内容 + 授权回跳)用。默认取现有对外域名,与 `notify-url` 同源 |
|
|
|
|
|
+| `receive.amount-min` | 顾客可输的最小金额,默认 `1`(分,即 0.01 元) |
|
|
|
|
|
+| `receive.amount-max` | 顾客可输的最大金额,默认待用户给数(见 §11.1 第 6 条) |
|
|
|
|
|
|
|
|
不新增:二维码尺寸、颜色、容错级别。
|
|
不新增:二维码尺寸、颜色、容错级别。
|
|
|
|
|
|
|
@@ -353,7 +473,31 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_lsy_merchant_tenant_level
|
|
|
ON pay_lsy_merchant (tenant_id) WHERE enterprise_id IS NULL;
|
|
ON pay_lsy_merchant (tenant_id) WHERE enterprise_id IS NULL;
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-订单表、门店相关表**无 DDL**。
|
|
|
|
|
|
|
+### 7.2 批次二:收款码表(§0.9 引入)
|
|
|
|
|
+
|
|
|
|
|
+列约定照 `pay_lsy_merchant`(`V1.8__create_pay_lsy_tables.sql`):`id BIGINT PRIMARY KEY` 由应用侧雪花生成、`status` 用**字符串枚举**不用 '0'/'1'、时间列 `TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`、每表每列都带 `COMMENT`。
|
|
|
|
|
+
|
|
|
|
|
+```sql
|
|
|
|
|
+CREATE TABLE IF NOT EXISTS pay_receive_code (
|
|
|
|
|
+ id BIGINT PRIMARY KEY,
|
|
|
|
|
+ tenant_id BIGINT NOT NULL,
|
|
|
|
|
+ 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(),
|
|
|
|
|
+ updated_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
|
|
|
+);
|
|
|
|
|
+CREATE UNIQUE INDEX IF NOT EXISTS uk_receive_code_no ON pay_receive_code (code_no);
|
|
|
|
|
+CREATE UNIQUE INDEX IF NOT EXISTS uk_receive_code_name ON pay_receive_code (tenant_id, name);
|
|
|
|
|
+CREATE INDEX IF NOT EXISTS idx_receive_code_mch ON pay_receive_code (merchant_id);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+`code_no` 唯一索引是**全局**的(不带 `tenant_id`)——它出现在公开 URL 上,必须全局唯一,否则两个租户撞码就是串户。
|
|
|
|
|
+
|
|
|
|
|
+比对旧文:**订单表、门店相关表仍无 DDL**。
|
|
|
|
|
|
|
|
按本仓规则:SQL 我写,**只有用户执行**。
|
|
按本仓规则:SQL 我写,**只有用户执行**。
|
|
|
|
|
|
|
@@ -375,11 +519,14 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_lsy_merchant_tenant_level
|
|
|
|
|
|
|
|
- **批次一**:`requireByTenant` / `requireByEnterprise` 分流各一组;`enterpriseId` 为空与非空两条下单路径。现有 418 个测试必须继续全绿。
|
|
- **批次一**:`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。这条要**两向**测:该红的红,不该红的绿。
|
|
|
|
|
|
|
+ - 收款码:`code_no` 全局唯一(**跨租户撞码要红**);`(tenant_id, name)` 唯一;停用后收银台返回「链接无效」页。
|
|
|
|
|
+ - 收银台:`codeNo` 不存在 → 无效页(**不是 404 白页**);存在 → `/info` 的商户名/码名称/渠道正确;页面 HTML **与 `codeNo` 无关**(同一段静态内容,挡的是「以后有人加回服务端替换」)。
|
|
|
|
|
+ - `qr.png`:返回 PNG 魔数,且内容能被**解码回** `https://<收银台域名>/payment/cashier/{codeNo}`(拿 zxing 自己解一遍,别只断言「非空」)。
|
|
|
|
|
+ - **唤起分流**(§4.4):微信 UA → JSAPI 分支;支付宝 UA → JSAPI 分支;其它 UA → H5 分支。**两向测**:把分类器改坏,该红的那条必须红、不该红的那条必须仍绿。
|
|
|
|
|
+ - **授权回跳**:`/auth` 出参是带签名的授权地址;回跳带 `openid` 时页面把它存进 `sessionStorage` 并用 `history.replaceState` 抹掉 URL 上的值。
|
|
|
|
|
+ - **不泄露上游**:`/pay` 应答里**不含**上游订单号、上游域名、上游字段名。用**字段名白名单**断言,不要用「没看到就算过」。
|
|
|
- 免鉴权:`/payment/cashier/**` 无 token 可达;`/payment/collection/receive/**` 无 token **不可达**。
|
|
- 免鉴权:`/payment/cashier/**` 无 token 可达;`/payment/collection/receive/**` 无 token **不可达**。
|
|
|
|
|
+ - §4.7 若裁决为「改」:`/qrcode/create` 应答**不含** `codeUrl`、**含** `cashierUrl`;裁决为「不改」则**保留**现有断言。**别按未裁决的版本写测试。**
|
|
|
- **批次三**:三-A 的六个接口按报文契约单测 + 签名单测;三-B 的字段组装与必填校验单测,**其联网行为不做自动化测试**(会被外部环境卡住),改为拿到凭据后手工跑一次并留证据。
|
|
- **批次三**:三-A 的六个接口按报文契约单测 + 签名单测;三-B 的字段组装与必填校验单测,**其联网行为不做自动化测试**(会被外部环境卡住),改为拿到凭据后手工跑一次并留证据。
|
|
|
- `mvn test` 需要库隧道在位(`PaymentApplicationContextTest` 的已知依赖)。
|
|
- `mvn test` 需要库隧道在位(`PaymentApplicationContextTest` 的已知依赖)。
|
|
|
|
|
|
|
@@ -402,4 +549,11 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_lsy_merchant_tenant_level
|
|
|
1. **批次顺序**:一 → 二 → 三 是否认可?三-A 能否单独先做(它不依赖任何外部输入)?
|
|
1. **批次顺序**:一 → 二 → 三 是否认可?三-A 能否单独先做(它不依赖任何外部输入)?
|
|
|
2. **§6.3 的 1–6 谁来要**:`channel_id` / `bd_id` / `rate_code` / `account_bank_code` / `channel_type` 需要找客户经理;进件审核通知地址需要上游配置。
|
|
2. **§6.3 的 1–6 谁来要**:`channel_id` / `bd_id` / `rate_code` / `account_bank_code` / `channel_type` 需要找客户经理;进件审核通知地址需要上游配置。
|
|
|
3. **§6.2 (a) 我裁定 `1=企业, 2=个体工商户, 3=小微商户`**,是否认可(依据是三处互相印证 vs 一处有复制痕迹)?
|
|
3. **§6.2 (a) 我裁定 `1=企业, 2=个体工商户, 3=小微商户`**,是否认可(依据是三处互相印证 vs 一处有复制痕迹)?
|
|
|
-4. **§4.5 开放接口应答 `codeUrl` → `cashierUrl` 是破坏性变更**,确认可以改(前提是乐收赢确实没上过生产)。
|
|
|
|
|
|
|
+4. **§4.7 开放接口应答 `codeUrl` → `cashierUrl` 是破坏性变更**,确认可以改(前提是乐收赢确实没上过生产)。**这条与 §0.6「原有的开放接口不变」直接冲突,默认按 §0.6 不动,请裁决。**
|
|
|
|
|
+
|
|
|
|
|
+### 11.1 §0.9 新需求带来的待确认(2026-09-22 新增)
|
|
|
|
|
+
|
|
|
|
|
+5. **§4.2 渠道不符时软处理**:微信码被支付宝扫到,我默认**不拦、按 UA 放行**,只在页面上给一句提示。若要改成硬拦,请说一声(改动只在收银台页一处)。
|
|
|
|
|
+6. **金额输入范围**:我默认「最低 0.01 元、上限做成配置项」,未定上限值——**商户单笔能收多少,你给个数**(上游 `total_amount` 是 `string(8)`,理论上限 999,999.99 元)。
|
|
|
|
|
+7. **收银台对外域名**:§5.3 的 `leshouying.cashier-base-url` 要指向哪个域名?现有 `notify-url` 用的是 `https://qcsj88888.com/api/v1/...`,收银台是否用同一个域名。
|
|
|
|
|
+8. **上游侧的两件事要不要现在就去问**(不阻塞本地开发,但阻塞上线):① 正式环境的 `redirect_uri` 是否需要预先登记白名单;② 支付宝回跳时携带用户标识的字段名(文档只写了微信侧示例)。
|