For agentic workers: REQUIRED SUB-SKILL: 用 superpowers:executing-plans 或 subagent-driven-development 按任务实现。步骤用
- [ ]复选框。
Goal: 一个操作员从零到收款,全程不离开界面、不需要懂任何内部概念(「进件」「映射」「mchNo」这些词他不该看见)。
Architecture: 后端补两处缺件(进件「已提交」记号、管理端收款端点),前端把「商户进件」页从"一张 41 字段的大表单"改成"列表打头 + 每行一个下一步按钮",新增「扫码收款」页。
Tech Stack: Spring Boot 3 + MyBatis-Plus + PostgreSQL / Vue3 + Element Plus
Spec: docs/superpowers/specs/2026-09-22-receive-workbench-design.md(批次二收款工作台;本计划是它的最小可用子集)
LESHOUYING_*)也算上游名号的罗马字。mvn 用绝对路径,绝不 -proc:none / -q(GC9);mvn test 需要 DB 隧道(GC11)。| 控制器 | 路径 | 端点 |
|---|---|---|
LeshouyingOnboardingController |
/payment/collection/onboarding |
page / create / commit / update / upload-image / areas / mcc |
LeshouyingMerchantController |
/payment/collection/merchant |
page / save / delete |
LeshouyingOrderController |
/payment/collection/order |
page / detail / notify-logs(只读) |
LeshouyingRefundController |
/payment/collection/refund |
page / detail(只读) |
LeshouyingOpenController |
/payment/openapi/pay |
barcode/pay、qrcode/create、h5/create、order/query、order/close、refund、refund/query |
POST /onboarding/commit 前端零调用 —— 正对照:同命令搜 onboarding/create 命中 1 处。「提交入网」四字在 frontend/src/ 零命中。菜单 SQL lsy-onboarding-menu.sql:187 配了按钮权限,但没有页面用它。GET /onboarding/page 前端零调用 —— 操作员看不到自己建过哪些商户。入网状态查询没有 HTTP 端点是有意的:LeshouyingOnboardingController.java:50-52 写着「审核结果怎么回到我们这边」走的是上游主动推的免登录通知端点,不从管理端发起,回写到 pay_lsy_merchant.review_status。
createOnboardingRow 写 review_status = PENDING(LsyMerchantService.java:391),而回调也写这一列。⇒「建了号还没提交入网」与「提交了在等审核」在库里长得一模一样,系统答不出"下一步该干嘛"。
Files:
java/src/main/java/com/payment/platform/module/payment/leshouying/enums/LsyMerchantReviewStatus.javajava/src/main/java/com/payment/platform/module/payment/leshouying/service/LsyMerchantService.javajava/src/main/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingOnboardingController.javaInterfaces:
Produces: LsyMerchantReviewStatus.SUBMITTED(值 "SUBMITTED");LsyMerchantService.markSubmitted(String mchNo, Long tenantId)
[x] Step 1: 写失败测试 —— LsyMerchantReviewStatusTest 断言四个字面值;LsyMerchantServiceTest 断言 markSubmitted 定向只写 review_status + updated_time,且 WHERE 带租户谓词。
[x] Step 2: 跑测试确认红 —— 7 处「找不到符号」(枚举常量 1 + markSubmitted 调用点 6),再一轮加上控制器 3 条(多一处 when(void) 编译错,已修)。
[x] Step 3: 实现 —— 枚举加 SUBMITTED;服务加定向更新方法(不复用 save 的编辑分支,那条写不了这两列);控制器 commit 成功返回前调用。
[x] Step 4: 跑测试确认绿 —— mvn test 668 绿(基线 658,+10 = 服务 7 + 控制器 3),BUILD SUCCESS。
[x] Step 5: 提交 —— 279aae0。
落地时定下的三处(计划里没写、实现时才发现):
markSubmitted 自己再闸一次,不收调用方刚定位到的那一行 —— requireByMchNoForTenant 的契约就是「闸是本方法自己的事」。代价:/commit 上多一次 SELECT。
★ 副作用:LeshouyingOnboardingControllerTest.merchantQuery_carriesExplicitTenantPredicate_evenWhenInterceptorWouldSkip 的 InOrder 断言表达不了新形状(两次 selectList 在 mapper 调用表里连成一块 ⇒ times(1) 报 "Wanted 1 time but was 2",atLeastOnce 又整块消费、把游标推过 commitMerchantInfo)。改成直接比 Mockito 全局序号,并把两次查询逐条断言带租户谓词(比原来只看第一条更强)。/commit 加一条出口护栏(markSubmittedOrSayWhy):记号写在上游调用之后 ⇒ 上游已受理是既成事实 ⇒ 失败不得原样上抛(闸的 404 会说「商户号不存在」;持久层异常落 catch-all 500 = 「请重试」)。照 /create 那条 409 先例。updateReviewStatusByMchNo 那样「值已相同就不写」):走到这方法意味着上游刚又收到一次提交,SUBMITTED 就是如实状态,短路只会让 updated_time 落后于事实。两向变异读数:M-A2(丢租户谓词、查询形状不变)⇒ 4 红,杀它的是那条谓词断言本身(报出 [(mch_no = …)] expected: <2> but was: <1>);M-B(记号挪到上游之前)⇒ 新加 3 条控制器用例全红、服务侧不受影响;M-C(SET 两列换序,无害)⇒ 全绿。
判定规则(前端进度列用):
review_status = PENDING ⇒ 待提交入网review_status = SUBMITTED ⇒ 审核中review_status = APPROVED ⇒ 可以收款了review_status = REJECTED ⇒ 被驳回(按钮回到"改资料/重新提交")Files:
frontend/src/api/module_payment/onboarding.tsfrontend/src/api/module_payment/collection.tsInterfaces:
Produces: OnboardingMerchantRow(接口)、getOnboardingPage({pageNo, pageSize, keyword?})、commitOnboardingMerchant(data)
[x] Step 1: 加函数 —— 只落了两个:getOnboardingPage → GET /payment/collection/onboarding/page;commitOnboardingMerchant(data) → POST /payment/collection/onboarding/commit。外加 OnboardingMerchantRow 接口(Task 3 的列表要用)。
[x] Step 2: pnpm ts:check 前后差分 —— 基线(改之前)exit 2 / 325 行;改之后 exit 2 / 325 行、diff 逐字节空。本仓 ts:check 有非零历史基线(命令恒 exit 2),不能说"跑绿了",只能说"零新增"。
裁定:closeCollectionOrder 不在 Task 2 落,挪进 Task 4 —— 理由:Task 3 一个字都不用它(唯一调用方是 Task 4 的收款页),而它的 javadoc 要写的东西(应答形状、关单的副作用、失败语义)此刻一条都没量过 —— 只能照计划表转写,那正是「约束文档自己会失真」的入口。Task 4 端点落地时它有东西可量。代价:Task 4 多改一个文件(它本来就列着要改 collection.ts)。
两向读数(正控):diff 为空本身也可能是量具没读这个文件 ⇒ 往刚改的文件尾部塞一行真类型错(const _t2probe: number = getOnboardingPage;)⇒ 输出多且只多 1 行:onboarding.ts(264,7): error TS2322: Type '(params: { pageNo: number; pageSize: number; keyword?: string | undefined; }) => Promise<AxiosResponse<…>>' is not assignable to type 'number'.(325 → 326 行)。撤掉后回到 325 行 / IDENTICAL,grep -c _t2probe = 0。⇒ 那个「无差异」是真读数。
(★ 探针落点 (264,7) 顺带证明 getOnboardingPage 真的存在且形状就是这个——比"编译通过"硬。)
Files:
frontend/src/views/module_payment/collection/onboarding/index.vue交互契约(傻瓜式)
我的商户 [+ 新建商户]
──────────────────────────────────────────────────────────
商户号 商户名 进度 操作
8xxxxxx XX店 ① 待提交入网 [继续:提交入网] ← 主按钮
8yyyyyy YY店 ② 审核中 [改资料]
8zzzzzz ZZ店 ③ 可以收款了 [改资料]
不出现「进件」「映射」「mchNo」「商户号由服务端产出」这类词。
[x] Step 1 列表 + 空态引导
[x] Step 2 进度列 + 单主按钮逻辑
[x] Step 3 提交审核小表单(6 字段)
[x] Step 4 建号/改资料表单接进来(el-drawer + destroy-on-close)
[x] Step 5 验证:见下
落地时定下的两处(计划里没写、实现时才发现):
review_status 不猜哪条是下一步,主按钮给保守的「改资料」,但保留送审入口免得变死路。openUpdate 先 prepareForm())。原形状是「切模式不清表单」,
于是从建号结果那条链接跳过去时,建号表单里那 41 个值会原样进更新报文
—— 那正是既有的 X6 标签警告的「等于按建号值再覆盖一遍」。旧台子的 X6 标签这么写、
断言却查相反的事(=== "建号时填的"),本次一并订正为标签的说法。读数(2026-09-23,git status 干净的工作区上):
| 量具 | 读数 |
|---|---|
pnpm ts:check |
exit 2 / 325 行,与改前逐字节相同(本仓该命令有非零历史基线,故只能同机差分) |
| 同谓词正控 | 往页面尾部塞 const _t3probe: number = actionsFor; ⇒ 多且只多 1 行:index.vue(1325,7): error TS2322: Type '(row: OnboardingMerchantRow) => RowAction[]' is not assignable to type 'number'(325→326);撤掉回 325 行 / 逐字节相同 / grep -c _t3probe = 0。★ 这条同时证明这页真在 ts:check 的扫描范围内(否则「无差异」是量具没读这个文件) |
pnpm build |
exit 0(59.26s) |
| 产物串(同谓词正控) | 8 个候选里 7 个满足严格谓词(src 里只出现在本页 且 dist 里查得到,全在同一个 chunk);第 8 个「审核未通过」被脚本自己判成「不是本页独有,本读数不成立」(它在 AccountOverview.vue 里也有)—— 这正是该谓词在起作用的样子;对照串 zzz_t3_absent_control_zzz 0/0 |
| keep-alive 组件名 | CollectionOnboarding 在产物里;对照名 AccountOverview 命中 3 个文件 ⇒ 量具会区分(★ 丢了它页面不被缓存且不报错,所以必须单独量) |
| GC1 文案 | 模板段 11 个禁词(含上游名号两种写法与 LESHOUYING_* 罗马字)全 0;正控:模板段 1395 字符、含主标题「我的商户」 |
| 执行级台子 | task-9-fix1-harness 25/0(未改动);fe-update-harness 89/0(原 43 条 → 本轮扩到 89);fe-update-keycheck 全绿 |
| 两向变异 | fe-task3-mutate.mjs 4 轮全绿(3 个真变异各打中预期用例、1 个无害变异仍绿、4 轮还原 sha 全部逐字节回基线) |
| 台子 README | .claude/plan/2026-09-23-lsy-onboarding-B-harness/README.md 随之更新:pass=43 → 89、登记 fe-task3-mutate.mjs(新增 3a 节)、指纹表补上本轮两个文件并加入 CRLF 侧 sha256(原先只指去交付说明)。顺手订正三处随本轮改动而失真的读数:stdout 行数 27 → 91(实测,随用例数走)、「四支」→ 五支、以及第 0 节那句「下面每支都不写生产文件」(对两台变异驱动是假的) |
变异暴露出的两条我自己的问题(都已修,留在台子里当记录):
submitCommit
在调用之前就把 committedMchNo 置空 ⇒ 一切失败路径上它都是空串 ⇒ 该断言分辨不出
「走了 409 分支」与「根本没走」。它对任何变异恒真。已删,不留它冒充钉子。
(→ 我的 expectRed 也按实测改,不是按我当初的预期改。)b.list.value[0].mch_no 在 L1 一红时抛 TypeError ⇒ 整轮崩掉,
SUMMARY 与后面所有用例全丢 —— 恰好在最需要读数的时候量具失效。改成 ?.。这是最大的缺口。 管理端今天没有任何主动收款入口。
Files:
java/src/main/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingCashierController.javajava/src/test/java/com/payment/platform/module/payment/leshouying/controller/LeshouyingCashierControllerTest.javafrontend/src/views/module_payment/collection/cashier/index.vuefrontend/src/api/module_payment/collection.tsjava/src/main/resources/db/lsy-cashier-menu.sql端点(JWT 认证,带租户闸)
| 方法 | 路径 | 作用 |
|---|---|---|
| POST | /payment/collection/cashier/qrcode |
入参 {amount, remark?} → 返回 {outTradeNo, codeUrl, status} |
| GET | /payment/collection/cashier/{outTradeNo} |
轮询用:返回 {status, receiptAmount} |
| POST | /payment/collection/cashier/{outTradeNo}/close |
撤销(关单) |
复用:LsyMerchantService.requireByTenant(找本租户默认商户)、LsyOrderService(下单/关单),不抄 LeshouyingOpenController 的落库装配。
dist/ 产物含收款文档(前面被打断的检查):grep -rl collection_qrcode dist/js/ + 同谓词正控reviewed_status 映射成人话 —— 上游同一页里两套互相冲突的 0–6 码表(LsyOnboardingService.java javadoc 逐字抄了),谁映射谁是在猜。等联调实测后一次落下映射 + 改 javadoc。:69-70 已裁「本批次不实现」)。review_status 列。