Input: Design documents from /specs/031-pay-method-config/
Prerequisites: plan.md ✅ spec.md ✅ research.md ✅ data-model.md ✅ contracts/api.md ✅ quickstart.md ✅
Tests: 按项目惯例含单测任务(JUnit5+Mockito,参照 InfoUserControllerTest 手动 mock 模式)
Organization: 按 spec 六个用户故事分组;分支直接在 test-202609v2,不建 feature 分支
全局规范(每个任务都适用,不再逐条重复):
@RequestHeader token / @RequestBody DTO / @RequestParam,DTO 无 Bean Validation,错误走 MessageUtils.messagemessages*.properties 全部 6 文件;前端 key 进 4 语言文件(有意义驼峰命名)updatesql/sql.md 不执行;前端文件 CRLF 用 Python 脚本编辑;Java 注释禁 */Purpose: 一次性落全部数据库变更文档
updatesql/sql.md(2026-09-11 标注):新表 payment_method_config(uk(method_code,scope),缺行=开)、新表 info_bank_card、info_user 加 pay_methods/flash_pay_methods 两列、flash_delivery_order 加 payment_status、027 三字段迁移 INSERT...SELECT(结构见 data-model.md 第 1-5 节,逐条含用途注释)Purpose: 闸门服务与共享实体——所有故事的前置
⚠️ CRITICAL: 本阶段完成前不得开始任何用户故事
PaymentMethodConfig + PaymentMethodConfigMapper(ruoyi-system/src/main/java/com/ruoyi/system/domain/ 与 mapper/,MyBatis-Plus,全字段注释,字段见 data-model.md 第 1 节)InfoBankCard + InfoBankCardMapper(同上目录,字段见 data-model.md 第 3 节;user_id 索引在 T001 DDL 已含)InfoUser 加 payMethods/flashPayMethods 字段(ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUser.java,注释注明 NULL=全部;同步其 mapper XML 的 resultMap/列清单若存在)PaymentMethodGateService(ruoyi-system/src/main/java/com/ruoyi/system/service/PaymentMethodGateService.java,依赖 T002-T004):assertUsable(payType, scope, payeeUserId) 三层闸门(平台开关缺行=开 → 收款方选择 NULL=全部 → 就绪度:CARD_OMG 查 pos_store_omg、LINE_PAY 查 pos_store_line_pay、OFFLINE_TRANSFER 查 info_bank_card is_active=1、COD 恒满足;现金 4 不校验直接放行);listAvailable(scope, payeeUserId) 供可选项;开关读取加缓存(DictUtils 同款模式,保存后失效);payType↔组代码映射(2+5→CARD_OMG 联动);i18n key pay.method.not.available 写入 6 个 ruoyi-admin/src/main/resources/i18n/messages*.propertiesruoyi-system/src/test/java/com/ruoyi/system/service/PaymentMethodGateServiceTest.java):三种拒绝原因各自抛错且消息为国际化 key 文案、缺行=开、NULL 选择=全部、CARD_OMG 组 2 与 5 同开同关、COD 恒过、现金放行、缓存失效后新值生效Checkpoint: 闸门可用,各故事可并行开始
Goal: 平台管理员可按方式×维度开关,即时生效,默认全开=现状
Independent Test: 关闭某方式×维度 → 对应下单入口该方式立即不可用(SC-001),表为空时行为与现状一致
PayMethodConfigController(ruoyi-admin/src/main/java/com/ruoyi/app/pay/):GET /system/payMethodConfig/list 返回全矩阵(缺行按开补齐)、PUT /system/payMethodConfig 批量 upsert;@PreAuthorize("@ss.hasPermi('pay:method:config')") + @Log(title="支付方式设置");保存 DTO 走 Controller 规范;单测 PayMethodConfigControllerTest(矩阵补齐、保存后缓存失效、越权 403)E:\QtwCode\foodie\foodie-admin-vue\src\views\pay\methodConfig\index.vue(方式×维度开关矩阵,参照现有 mendian/storePayment 页面风格)+ 路由 + 菜单按钮权限 SQL(追加 updatesql/sql.md)+ i18n 四语言(admin-vue 语言文件,按命名空间定位插入)Checkpoint: MVP 达成——平台已可一键下线/恢复任意支付方式
Goal: 商家主账号级勾选接受的方式,未设置=全部;用户结算页按可用集渲染
Independent Test: 商家只勾"到付"后用户结算页仅剩到付;清空勾选回落全部(SC-003 存量零变化)
MerchantPayMethodController(ruoyi-admin/src/main/java/com/ruoyi/app/pay/):GET /system/merchantPayMethods(available+就绪度+selected,NULL 展开为全部)、PUT /system/merchantPayMethods(写主账号行,子账号经 022 MerchantStoreAccessService/主账号体系定位,空数组=清空回落);值域校验组代码合法;单测(子账号写主账号、清空语义、非法组代码拒绝)GET /system/storePayMethods(@Anonymous + @RequestParam storeId):pos_store → user_id → listAvailable(MERCHANT, 商家主账号),返回 [{methodCode, payType, ready}] 供 App 结算页渲染(契约已补入 contracts/api.md 第 7 节);单测E:\QtwCode\foodie\foodie-store\src\views\PayMethodSettings.vue(参照 SelfDeliverySettings.vue 结构;本任务只做方式勾选区,银行卡区在 T020)+ 路由菜单 + i18n 四语言(src/lang/ zh/tw/en/vi 同 key)Checkpoint: 餐饮单维度三层闸门完整闭环(平台∩商家∩就绪度 + 结算页渲染)
Goal: 餐饮两下单入口接统一闸门,绕过界面提交被禁方式 100% 拒绝(SC-002)
Independent Test: curl 直提交被禁 paymentMethod → 国际化错误;老 App 同样被拒
UserOrderController.createOrder 接入闸门(ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java:261 setPayType 前调 assertUsable(paymentMethod, MERCHANT, 商家主账号);商家主账号经 pos_store 反查,与 T011 同路径抽取复用);单测(被禁拒绝/放行路径/现金跳过)PosOrderController /addorder 接入闸门(payType 固定 "1" COD,对称调用 assertUsable,平台关到付时商家建单被拒并提示);单测Checkpoint: SC-002 达成;闸门调用点=contracts 白名单第 1、2 处
Goal: 骑手设置接受方式(NULL=全部),闪送下单选方式并记录,抢单列表过滤(SC-005)
Independent Test: 骑手只接受现金 → 转账单不进其列表;未设置骑手全可见
RiderFlashPayMethodController(ruoyi-admin/.../pay/ 或骑手包):GET/PUT /system/riderFlashPayMethods(token 校验 userType=2,scope=RIDER_FLASH,NULL=全部);单测ruoyi-admin/.../flashdelivery/):FlashDeliveryQuoteRequest/FlashDeliveryCreateRequest 加可选 payType 字段(注释:缺省现金 4);normalizePayType 从透传改为 assertUsable(payType, RIDER_FLASH, null)(仅平台开关层,抢单前无收款方);home/quote 响应增可选方式列表 listAvailable(RIDER_FLASH, null);相关 DTO 字段注释补齐;单测(非法方式拒绝、缺省现金、可选项返回)FlashDeliveryApplicationService.riderOrdersQuery newTask 分支):追加 pay_type ∈ 骑手 flash_pay_methods 展开集合(NULL=平台开放全部集合)条件;FlashDeliveryRiderOrderListViewTest 补过滤用例Checkpoint: 闪送维度闸门闭环(US3 白名单第 3 处)
Goal: 银行卡多卡单启用管理;027 存量迁移零回退(SC-004);bankInfo 结构不变改数据源
Independent Test: 迁移后存量商家转账信息照常展示;启用新卡付款方只见新卡
BankCardController(ruoyi-admin/.../pay/):CRUD + PUT /system/bankCard/activate/{id}(同事务停旧卡);校验 bankName∈字典 taiwan_bank_list(读取先例 InfoUserController.java:815)、上限 10 张、仅本人卡;i18n key pay.bankcard.*;单测(启用停旧、越权、上限、字典校验)ChantingStoreController.bankInfo,契约 specs/027-offline-transfer-payment/contracts/api.md):数据源改 info_bank_card is_active=1,返回三字段名结构完全不变;前置套闸门 assertUsable("6", MERCHANT, 商家userId) 不满足返回 data=null;单测(迁移卡透出、无启用卡 null、闸门关闭 null、字段名不变)PayMethodSettings.vue 第二块:卡列表/新增/编辑/删除/启用,银行名下拉取 taiwan_bank_list 字典接口)+ i18n 补 keyCheckpoint: US3 白名单第 4 处(bankInfo);027 平滑迁移
Goal: 闪送订单收款状态记录,不阻塞履约(FR-013)
Independent Test: 未确认不影响送达/完成;确认后 0→1 + 日志;重复确认幂等
FlashDeliveryOrder 加 paymentStatus(注释:0未收1已确认)+ 同步其 mapper XML;FlashDeliveryRiderController 增 POST /orders/{id}/confirmPayment(本人中单+状态≥已送达;幂等;写 flash_delivery_order_log operator_type=RIDER);用户/骑手订单视图返回 paymentStatus;单测(条件校验、幂等、日志、不阻塞状态流转)Checkpoint: 全部用户故事完成
messages*.properties 的 031 新 key 集合一致(pay.method.not.available、pay.bankcard.*、闪送支付相关),无遗漏无硬编码中文mvn compile + mvn test -pl ruoyi-admin -am(含既有 60 个 OAuth/登录用例不回归)+ mapper XML python ET.parse 校验 + 行尾检查(新改文件与原风格一致)assertUsable)、15 项决策与 spec 符合性、注释规范、存量零变化路径FlashDelivery*Test)不可回归;027 商家确认收款 confirmTransferPayment 与 US6 骑手确认收款并存,互不影响