alphah
odeslal do větve java v repositáři gatsby/payment-platform
ed24480f4a fix(乐收赢): 收款链接 token 换 CSPRNG —— 与 HAR 同一处订正
用户令:「lsy 那个也改成一样的」(指上一轮 HAR 收款链接的 token 生成器订正,见 b97a0e93)。
乐收赢侧同一处问题:LsyCheckoutLinkService:86 用 IdUtil.fastSimpleUUID(),
而 LeshouyingCheckoutController 的类 javadoc 写着同一个失实的「122 bit 熵」论证。
五个文件。
改了什么
- LsyCheckoutLinkService.issue:IdUtil.fastSimpleUUID() → IdUtil.simpleUUID(),就地写清理由
(fastSimpleUUID 走 RandomUtil.getRandom() = ThreadLocalRandom,48 位 LCG;simpleUUID 走
SecureRandom;两者产出的字符串**形态一模一样**,这正是当初写错还看不出来的原因)。
★ 已发出的 token 不受影响:库里旧值不动、照用;这里只影响新插入的行。
- LeshouyingCheckoutController 类 javadoc:订正「IdUtil.fastSimpleUUID,122 bit 熵」→
simpleUUID/SecureRandom,并留一段「2026-10-06 订正」说明 122 是 UUIDv4 的格式位、
本身不是不可预测性论证。
- LsyCheckoutLinkEntity.token 的字段注释:同步成 IdUtil.simpleUUID()(CSPRNG)。
- LsyCheckoutLinkServiceTest:新增守卫用例 + 订正形状断言的措辞。
- frontend/src/plugins/permission.ts 注释:那里原写「不要把前端钉死在后端 IdUtil.fastSimpleUUID()
这一种形状上」—— 换生成器后这句话点名的函数两边都不存在了。改成
「令牌生成器**当下**的形状(现在是 IdUtil.simpleUUID() 的 32 位十六进制)」,
既不点名到会腐烂的具体函数,也保留了它原本的论点(别按形状收紧)。
测试
- 新增 issue_tokenComesFromSimpleUuid_notFastSimpleUuid(形态同 HAR 那条):
形状断言对「换错生成器」永远是绿的(两者都是 32 位 hex),所以不测形状,
用 mockStatic 直接钉住**调的是哪一个方法**。
验证读数(本机实跑)
- RED(修复前):mvn -Dtest=LsyCheckoutLinkServiceTest test → Tests run: 17, Failures: 1
(只有新守卫那条红;这正是它该有的样子)logs/_t-lsy-token-1-red.txt
- GREEN(修复后):同命令 → Tests run: 17, Failures: 0,BUILD SUCCESS logs/_t-lsy-token-2-green.txt
- 全量:mvn test → Tests run: 1152, Failures: 0, Errors: 0,BUILD SUCCESS
(上一轮 HAR 修复后是 1151 ⇒ +1,正好这条守卫)logs/_t-lsy-token-3-full.txt
- jar 已重打:target/payment-platform-1.0.0.jar,mtime 2026-10-06 13:41:27,
sha256 90d2cb65bdb3ec31c5d7a5801971b4be3ad59f76ff63e931939990a9b8eeb5af,143117153 字节。
★ 正对照做在**产物本身**上:从 jar 里取出 class 反汇编,LsyCheckoutLinkService 与
HarCheckoutLinkService 都显示 invokestatic IdUtil.simpleUUID(不是只信源码)。
(上一版 942d3be9… 作废。)
- frontend/dist.zip **不变**:改了 permission.ts 的注释后重跑 pnpm build(✓ built in 58.27s,
产物 mtime 13:41:46 = 本次构建),277 个文件的 sha256 与改前逐个相同(0 差异)
⇒ 注释不进产物,dist.zip 仍是当前源码的正确构建,sha256 1e9716e3…。
※ 第一次比对得到「0 差异」是**假读数**:那次跑在构建仍在编译时(dist 还是旧树,等于拿基线跟
自己比)。上面这组是在构建结束后复量的 —— 记在这里,免得下次又把它当成结论。
有意不动的
- 乐收赢其它 fastSimpleUUID 用处(pay_trace_no / trace_no / refund_trace_no / close_trace_no /
cancel_trace_no / query_trace_no,遍布 checkout/cashier/open/refund/query/cancel/onboarding 各腿)
**不改**:这些是我们发给上游的对账号,随请求体出网、由上游原样回传,不是任何免登录页面的定位键。
TenantInnerInterceptor:185 已钉住「要按业务键做未认证读必须先补唯一约束(trade_no / pay_trace_no 均无)」
⇒ 它们不构成匿名定位原语。换成 SecureRandom 不买任何东西,只是更慢。
- .claude/plan/2026-09-30-lsy-checkout-link.md:73 仍写着「token = fastSimpleUUID」:那是**当日**的
设计文档(历史记录,且本就与代码有出入 —— 它写的是 resolve(),代码里是 requireByToken()),
不改历史文档,故只在此说明。
未验证(如实记)
- .claude/plan/ops-manual.md 读取被权限闸拒 ⇒ **没有**检查它是否也点名了这个生成器;
若点名了,那一处仍是旧的。
b97a0e9311 fix(汇安融): 收款链接评审修复 —— 并发幂等、顾客语 404、token 换 CSPRNG
2026-10-06 独立评审(agent har-checkout-review,台账 logs/_ledger-append-har-checkout-review.md)
提了 F1..F5;用户授权「你决策就行哈」后:修 F2/F3/F4,不修 F1/F5(理由见台账「修复轮」)。
四个文件,全部是本特性自己的新代码。
F2 并发双击 ⇒ 500(HarCheckoutLinkService.issue)
- 先读后插之间的窗口:两个请求都读到「没有这一行」⇒ 都 insert ⇒ 后到的撞
uk_har_checkout_tenant_mch,异常穿成 500,而链接其实已经建好 ——
「重复取链接返回同一条」这个承诺在并发下不成立。
- 改为 insert 包 catch DataIntegrityViolationException → 按同一 (租户, 商户编号) 重读:
命中 1 行 ⇒ 返回赢家那一行(log.info,同一条链接同一个 token);
仍为空(撞的是别的索引,例如 token 撞车)⇒ 原样抛出,绝不吞 ——
「链接没建出来却报了成功」比一次 500 坏得多。
- 两处读抽出 selectByTenantAndCode:一份谓词,免得两处 WHERE 慢慢长歪(少一个条件就是跨租户)。
- ★ 能捕到靠 mybatis-spring 的异常翻译(SQLIntegrityConstraintViolation → DuplicateKeyException);
裸 PersistenceException 会穿过去 —— 那是与修复前完全相同的行为,不是新增盲区。注释里写明了。
F3 管理端文案上屏给顾客(HarCheckoutController.create)
- 本地闸 requireByTenantAndCode 抛的是「商户编号不存在或不属于当前租户: X」。
这句话写给我们自己的操作员是恰当的,取链接、收银台几条腿共用同一条消息、**不改**;
但这条腿的出口是免登录顾客页,前端 checkout/har/index.vue:395 把 res.msg 原样显示
⇒ 顾客会读到「租户」「商户编号」这些内部词汇加一串内部编号。
- 改为就地 catch BusinessException → log.warn 留痕 + 换顾客语常量再抛。
状态码照旧 404(它是有意的,不变)。
- 文案「收款方暂时无法收款,请稍后重试或联系收款方」,与 MSG_PAY_FAILED 的差别是刻意的:
那条重按有用(再按生成新单号,重试安全);这条重按没用(商户配置不会自愈)⇒ 指引带「联系收款方」。
F4 token 随机源不是 CSPRNG + 注释失实(HarCheckoutLinkService / HarCheckoutController)
- javap 复现(量具 hutool-all-5.8.34):IdUtil.fastSimpleUUID() → randomUUID(false)
→ RandomUtil.getRandom() = ThreadLocalRandom(48 位 LCG);只有无参 randomUUID()
(= UUID.randomUUID().toString(true))走 SecureRandom。
- 两者产出的字符串**形态一模一样**(32 位 hex)—— 这正是当初写错还看不出来的原因,
而免登录路径的安全性全押在「这串字符猜不到」上(类 javadoc「(甲)·预防」第 2 条)。
- 类注释原文「IdUtil.fastSimpleUUID,122 bit 熵」两头都不对(122 是 UUIDv4 的格式位,
本身不是不可预测性论证)。改为 IdUtil.simpleUUID() + 注释订正,并留一段「评审订正」
说明为什么两者形态相同。未部署 ⇒ 无兼容包袱;已发出的 token 不受影响(库里旧值不动、照用)。
- ★ 2109cb6c 的**提交信息**里仍有那句失实断言 —— 不改历史提交,订正在本条正文里。
测试(四条新用例)
- issue_whenConcurrentInsertLosesRace_reReadsAndReturnsTheWinnersRow(F2 正)
- issue_whenInsertFails_andReReadStillEmpty_rethrows(F2 负:钉「不许吞」;前后都绿,是用例守卫不是 RED→GREEN)
- create_missingMerchantRow_messageIsCustomerFacing_notTheAdminWording(F3)
- issue_tokenComesFromSimpleUuid_notFastSimpleUuid(F4)
· 这是 F4 **唯一可行**的守卫形态:形状断言对「换错生成器」永远是绿的(两者形态相同),
所以用 mockStatic 直接钉住调的是哪一个方法。本仓首次用 mockStatic(Mockito 5 inline
mock maker,实测可用)。
验证读数(本机实跑)
- RED(修复前):mvn -Dtest=HarCheckoutLinkServiceTest,HarCheckoutControllerTest test
→ Tests run: 32, Failures: 1, Errors: 1
· F3 用例:expected <收款方暂时无法收款…> but was <商户编号不存在或不属于当前租户: M001>
· F2 用例:DuplicateKeyException uk_har_checkout_tenant_mch 穿出(ERROR)
(logs/_t-harcheckout-fix-1-red.txt)
- GREEN(修复后):同命令 → Tests run: 33, Failures: 0, Errors: 0,BUILD SUCCESS
(logs/_t-harcheckout-fix-2-green.txt)
- F4 变异两向:实现换回 fastSimpleUUID() → 恰好那条守卫 RED(19 run / 1 failure),
而**形状用例仍绿**(证明形状断言确实抓不到这一类);换回 simpleUUID() → 33/33 绿。
(logs/_t-harcheckout-fix-3-mutant.txt、_t-harcheckout-fix-4-restored.txt)
- 全量:mvn test → Tests run: 1151, Failures: 0, Errors: 0,BUILD SUCCESS
(修复前 1147 ⇒ +4,正好是这四条;logs/_t-harcheckout-fix-5-full.txt)
- 交付件:jar 已重打 —— target/payment-platform-1.0.0.jar,mtime 2026-10-06 13:37:05,
sha256 942d3be956884131cc8ced282499d71b0624618e76018c08d0cc4b06ff11fbcf,
143117157 字节,1180 条目;正对照 HarCheckoutLinkService.class / HarCheckoutController.class
都在 BOOT-INF(读回 true)。旧 jar 8e9ba10a… 作废。
frontend/dist.zip **不变**(本轮零前端改动),sha256 1e9716e3…
未验证(如实记)
- F2 那条 catch 的**真实**触发路径未在真库复现(并发时序);且 pay_har_checkout_link 的建表 SQL
至今没在任何真库跑过 ⇒ 该表上的唯一索引冲突无法实测。单元测试抛的是异常翻译后的形态(应然)。
- pay_url 从未被真机扫过;顾客页从未用真实 token 在浏览器打开过(沿用首次交付未验证项)。
- F1(无限流/金额无上界)与 F5(订单号单前缀 HAR)**未改**,报用户拍板。
3ae6bf8a9d feat(汇安融): 收款链接(四)—— 前端:顾客页 + 商户页收款二维码入口
后端三刀已齐(2109cb6c 存储与顾客页端点 / b00a8348 管理端取链接 / 8b4619ba 装配接线),
本刀把页面接上。五个文件,全部是前端。
路由与免登录声明
- router/index.ts 新增 /checkout/har/:token(name: HarCheckout),与乐收赢那条同段。
★ 为什么是**三段**路径:后端 HarCheckoutLinkService#buildUrl 的 PATH_PREFIX 是
/#/checkout/har/,har 是路径里的**字面段**不是参数。若照抄乐收赢的
checkout/:token,"har" 会被当成 token ⇒ 顾客页拿着 "har" 去查链接,一律「链接无效」。
- plugins/permission.ts:PUBLIC_ROUTE_NAMES 加入 "HarCheckout"。
按名字放行 ⇒ 新增公开页必须有人在这里**显式**写一行(这正是当初不按路径前缀的用意)。
两处 name 必须一致,否则顾客页静默变回「跳登录页」—— 页面本身没有任何报错。
- main.ts 无需改:标题净化用的是 hash.startsWith("#/checkout/"),已覆盖 #/checkout/har/。
顾客页(新)views/checkout/har/index.vue
- 与乐收赢顾客页同构(同模板、同键盘几何、同四相:加载/无效/可付/跳转),差异都在数据面:
· GET /payment/har/checkout/<token> → 读 merchant_name
· POST /payment/har/checkout/<token>/orders → body { price: Number(fenToYuan(fen)) }
· 取回 pay_url / order_no / price
- ★ 金额单位是**元**(HAR 全通道如此;乐收赢那份是分),下限 1 元 ——
与后端 HarAmountFloor.MIN 配对(2026-10-06 裁定:下限还是 1)。
内部仍以分做整数运算(MIN_FEN = 100),只在出网/回显时转元;
回显价走 echoedYuanToFen(字符串→分,不做浮点乘),转不出来才回退本地分。
- 自己的 axios 实例(不过全局拦截器):管理端那支会附 Authorization 并弹 ElMessage,
顾客页不能要(顾客页连登录态都没有)。
- defineOptions({ name: "HarCheckout" }) —— 与路由 name 一致(keep-alive 契约)。
商户页 views/module_payment/har/merchant/index.vue
- 操作列 150 → 220,新增「收款二维码」按钮,排在超管专属的 编辑/删除 之前:
它**对两种身份都显示**(租户取自己名下那行、超管可代取,后端两道数据闸)。
- 弹窗:长期有效提示 + 商户(merchant_code / merchant_name 快照)+ 只读链接输入框 +
二维码画布(QRCode.toCanvas 240px),底部 复制链接 / 下载二维码 / 关闭。
- ★ 二维码内容与可复制内容都取后端返回的**整条 url**,前端不拼:
拼第二个副本的后果是「复制的链接」与「二维码里的链接」不是同一条。
- api/module_payment/har.ts:新增 HarCheckoutLinkVO(三格)与 HarAPI.checkoutLink(id),
POST(首次调用会写一行,幂等返回同一条——这是刻意的,见那里 javadoc)。
验证(本机实跑读数)
- pnpm ts:check:289 错 = 基线 289(logs/_tscheck-har-cashier.txt,12:36 实跑)。
唯一差分:har/merchant/index.vue 一条既有 TS2322 从 162 行挪到 167 行 ——
逐行打印两版都是 :value="t.id",同一句被新增行推移。**新增错误 0**。
- pnpm build:✓ built in 57.71s,exit 0(logs/_build-har-checkout.txt)。
- dist 产物核对(不止步于「构建成功」,按路由映射逐条钉):
· checkout/har/index.vue → dist/js/index.DvK02O4b.js(内含 payment/har/checkout、
最低、请输入金额、重新获取链接);
· module_payment/har/merchant/index.vue → dist/js/index.D3PpoOoz.js(内含 收款码_、长期有效);
· 路由字面量 checkout/har/:token 在 dist/js/index.DKGjoO3J.js。
chunk mtime 均为 2026-10-06 13:16:33 = 本次构建产物(非旧残留)。
未验证(如实记)
- pay_url 从未被真机扫过;顾客页从未用真实 token 在浏览器里打开过。
这条要等部署后拿真实商户取链接实测。
8b4619ba3a feat(汇安融): 收款链接(三)—— 装配接线:白名单 + 条件表 + 守卫测试
顾客页两个端点(2109cb6c)与管理端取链接端点(b00a8348)都到了,但缺了这两处
装配它们就是打不开的。本刀只接线与守卫,不改业务代码。
SecurityConfig.WHITE_LIST
- 新增 "/payment/har/checkout/**"(免登录,顾客页)。
- ★ 不能指望乐收赢那条 "/payment/checkout/**" 兜住:前缀不同(/payment/har/... 而非
/payment/checkout/...),Ant 模式匹配不到 —— 守卫测试里把这条差异写成了前提复核
(matcher.match("/payment/checkout/**", "/payment/har/checkout/tok") == false)。
- 漏了它 ⇒ 顾客打开链接被 Spring Security 挡成 401/403,而这条路径本来就不该有登录态。
TenantInnerInterceptor.CONDITIONAL_TABLES
- 新增 "pay_har_checkout_link"(pay_har_order 补尾逗号)。
- ★ 漏登记 ⇒ 未认证查询被追加 AND tenant_id = 0 ⇒ **每条链接都查不到**,
表现是顾客页一律「链接无效」,而日志里一个字都没有。
- 论证沿用同形条目 pay_lsy_checkout_link 那段(通则 (甲)·预防):定位键是 token,
而 uk_har_checkout_token **单独**建在 token 上 ⇒ 同一 token 命中两行在 schema 层
建不出来 ⇒ 定位用 selectOne 是允许的。注释里写明与 LSY 条目的唯一差异(索引名)。
守卫测试(先看到红:2/4 失败于「未加入白名单 / 未加入条件表」)
- HarCheckoutWiringTest 4 条,与 LeshouyingWiringTest 同形:
· 白名单用真 Ant 匹配器撞**模式**(存的是 /** 形态,contains 字面量恒 false)
+ 前提复核(LSY 条目覆盖不到本前缀,所以本登记不可省);
· 反面对照:管理端 /payment/har/merchant(含 checkout-link)**不得**被白名单覆盖
—— 否定式读数配正对照(先用确实放行的 /payment/openapi/har/** 证明量具是好的);
· 条件表含四张 har 表(漏登记症状见上);
· shouldNotFilter(顾客页) == true:顾客页不是下游系统,不该经 ApiKey 认证面
(反向守卫:哪天有人把它挪进 /payment/openapi/** 立刻红)。
- 邻居复跑:HarCheckoutWiringTest 4 + LeshouyingWiringTest 8 = 12/12 绿
(两处常量都是它们读的同一份)。
b00a83485c feat(汇安融): 收款链接(二)—— 管理端「取收款链接」端点
对标乐收赢 LeshouyingMerchantController#checkoutLink:商户页操作列取一条永久收款
链接(顾客免登录打开、自输金额;顾客页见上一刀 2109cb6c)。本刀是管理端入口的
**后端半边**;前端按钮/二维码对话框与装配接线在后续提交。
新增端点
- POST /payment/har/merchant/{id}/checkout-link,权限串复用
module_payment:har:merchant:list(与 list 同一条:能看见商户页的人本来就读得到
该行的 merchantCode,本端点没有多给出任何一项**读**的能力)⇒ 不需要新菜单 SQL。
- 幂等且会写库(首次调用建链接行)⇒ POST 不是 GET。
- 应答三格:merchantCode / merchantName / url。merchantName 取**链接行的快照**
(操作员看到的名字必须与顾客打开链接时看到的一样),url 只从
HarCheckoutLinkService#buildUrl 出(控制器不自己拼 —— 那是第二个副本,
两处漂移的后果是「复制的链接」与「二维码里的链接」不是同一条)。
两种调用方,两条腿(逐条对标 LSY)
- 租户用户:requireByIdForTenant(id, 自己的 tenantId);超管:requireById(id),
租户号取自**那一行** —— 超管的 LoginUser.tenantId 可以是 null(它跨租户看),
若照「取不到租户就 403」写,超管将完全无法使用这个按钮,而商户编号的录入
恰恰只有超管能做。
- 分岔判据 LoginUser#getIsSuperuser,与 TenantInnerInterceptor 用的是同一个开关
(用别的判据会出现「一边以为放行、一边仍在过滤」或更坏的反向组合)。
- 取不到主体 ⇒ 403(默认拒绝,先于任何库操作);非超管且 tenantId==null 仍走
带谓词的那条腿(这种主体在条件表上会被跳过过滤,落进超管腿就能按 id 读任意
租户的行)。404 不区分「不存在」与「不属于你」(不构成越权探测面),且
verifyNoInteractions(linkService) 钉住「拒绝发生在建链接之前」。
测试与验证
- 新增 HarMerchantControllerTest 6 条(先看到编译红:找不到符号 HarCheckoutLinkVO)。
- 定向变异两向验证:
· 腿分岔取反(Boolean.TRUE.equals → !Boolean.TRUE.equals):4/6 红
(三条腿用例 + 404 那条);
· 快照来源改成商户行 m.getMerchantName():精确 1/6 红(tenantUser 那条);
· 两处还原后 6/6 绿(还原动作本身也验证过)。
- 含一处接口形状反射:权限串与 list 逐字相等(两处一起改也抓得住)+ POST +
/payment/har/merchant/{id}/checkout-link 拼接路径(前端契约)。
- Zobrazit porovnání pro tyto 6 revize »
před 1 týdnem