Input: Design documents from /specs/009-ezpay-invoice-onboarding/
Prerequisites: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/api.md ✅, quickstart.md ✅
Tests: 轻量。仅对最核心的「凭证验证判读逻辑」加单测(mock EzPay.doPost),其余按 quickstart.md 手测。项目无强制 TDD。
Organization: 按 spec.md 五个 user story 组织,每个 story 可独立实现与测试。
[ID] [P?] [Story] Descriptionfoodie_server):
ruoyi-system/src/main/java/com/ruoyi/system/domain/ruoyi-system/src/main/java/com/ruoyi/system/mapper/ruoyi-system/src/main/resources/mapper/chanting/ruoyi-system/src/main/java/com/ruoyi/system/service/(接口)与 .../service/impl/(实现)ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ruoyi-admin/src/main/java/com/ruoyi/app/utils/ezPay/updatesql/sql.mdE:\QtwCode\foodie\foodie-admin-vue):src/api/、src/views/、src/lang/(CRLF 换行,用 Python 脚本改)E:\QtwCode\foodie\foodie-store):src/lang/、门店设置视图(CRLF 换行,用 Python 脚本改)Purpose: SQL 变更与权限菜单(交开发者手动执行)
updatesql/sql.md 追加 2026-06-15 段落:① ALTER TABLE pos_store ADD COLUMN invoice_exempt TINYINT(1) DEFAULT 0 COMMENT '是否免用发票:0需开票/1免用发票';② 按 data-model.md 创建 pos_store_ezpay 表(含 uk_store_id 唯一键、idx_ezpay_status 索引);③ sys_menu 插入 chanting:storeEzpay:list/query/apply/saveCredentials/toggleEnable/markExempt/reset 权限项(参考既有门店菜单 SQL 写法)Purpose: 数据模型地基,所有 user story 都依赖
⚠️ CRITICAL: 本阶段完成前不得开始任何 user story
PosStoreEzpay 实体于 ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStoreEzpay.java(MyBatis-Plus @TableName("pos_store_ezpay")、@TableId(type=IdType.AUTO),字段见 data-model.md:id/storeId/ezpayStatus/isEnabled/merchantId/hashKey/hashIv/companyId/ubn/applyTime/approvedTime/lastVerifyResult/remark/createTime/updateTime/createBy/updateBy,用 lombok @Data)ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStore.java 新增 invoiceExempt(Integer)字段 + getter/setter(遵循该文件既有手写 getter 风格)PosStoreEzpayMapper 接口于 ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreEzpayMapper.java,继承 MyBatis-Plus BaseMapper<PosStoreEzpay>,声明分页查询方法 selectEzpayStoreList(...)(参数见 contracts/api.md 列表入参)PosStoreEzpayMapper.xml 于 ruoyi-system/src/main/resources/mapper/chanting/PosStoreEzpayMapper.xml:resultMap(门店字段 + ezPay 字段)+ selectEzpayStoreList(pos_store LEFT JOIN pos_store_ezpay,过滤实际卖货门店 isStall,支持 ezpayStatus/invoiceExempt/posName/isStall + quickFilter=needApply|notEnabled,LEFT JOIN 无行时 ezpayStatus 按 0 处理)ruoyi-system/src/main/resources/mapper/chanting/PosStoreMapper.xml:resultMap 加 invoice_exempt、相关 select 列加 invoice_exempt(供门店基础接口携带该字段)IPosStoreEzpayService 接口与 PosStoreEzpayServiceImpl 实现于 ruoyi-system/src/main/java/com/ruoyi/system/service/ 与 .../impl/,注入 PosStoreEzpayMapper、PosStoreMapper、EzPay,声明占位方法(list/detail/apply/saveCredentials/toggleEnable/markExempt/reset/uploadUbn),具体业务在后续 story 实现Checkpoint: 表结构 + 实体 + mapper + service 骨架就绪,user story 可开始
Goal: 平台后台列表展示所有需开票门店的 ezPay 状态,支持按状态筛选与"还没开通/还要去开通"快捷过滤(均排除免用发票门店)
Independent Test: 打开"ezPay 发票开通管理" → 点"还要去开通" → 仅显示需开票且未申请门店(免用发票门店不在内)
PosStoreEzpayServiceImpl.listEzpayStore(...)(组装查询参数含 quickFilter→ezpayStatus 映射,调用 mapper 分页,无 ezPay 行的门店补 status=0/invoiceExempt 取自 pos_store)依赖 T005、T007PosStoreEzpayController 于 ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreEzpayController.java,@RequestMapping("/system/storeEzpay"),实现 GET /list(@PreAuthorize("@ss.hasPermi('chanting:storeEzpay:list')") + startPage() + 返回 TableDataInfo)与 GET /{storeId} 详情 依赖 T008E:\QtwCode\foodie\foodie-admin-vue\src\api\mendian\storeEzpay.js(list/detail/apply/saveCredentials/toggleEnable/markExempt 接口封装)与页面 src\views\mendian\storeEzpay\index.vue(Element 表格:门店名/类型/ezPay状态/启用/统编是否已填/操作;顶部筛选:状态下拉 + 门店名搜索 + "还没开通/还要去开通"快捷按钮)依赖 T009foodie-admin-vue\src\lang\ 的 zh.js/tw.js/en.js/vi.js 新增 storeEzpay:{} 对象层级(标题/表头/状态文案/筛选按钮等 key,驼峰命名,四个文件 key 一致)Checkpoint: US1 可独立验收——列表 + 筛选 + 快捷过滤生效
Goal: 未申请→发起申请→申请中;运营录入 ezPay 凭证(商店代号/HashKey/HashIV)后系统调 ezPay 验证,通过才标记已开通并启用
Independent Test: 对测试门店录入 ezPay 测试环境真凭证→验证通过→状态变已开通;故意填错 HashKey→提示凭证无效、状态保持申请中
StoreEzpayCredentialDto 于 ruoyi-admin/src/main/java/com/ruoyi/app/mendian/dto/StoreEzpayCredentialDto.java(字段 storeId/merchantId/hashKey/hashIv/companyId,校验注解:merchantId 必填、hashKey 长度 32、hashIv 长度 16)PosStoreEzpayServiceImpl.verifyAndSaveCredentials(dto):构造 EzPayConfig(merchantId,hashKey,hashIv) → ezPay.doPost(EzPay.BASE_TEST + EzPay.URL_SEARCH, cfg, {RespondType:JSON,Version:1.3,SearchType:0,InvoiceNumber:测试号,RandomNum:测试码}) → 判读:回应含 KEY1 → 凭证无效,记 last_verify_result、保持 status、抛 ServiceException;回应业务错误或成功 → ezpayStatus=2、isEnabled=1、approvedTime=now、存凭证、记通过。网络异常→抛可重试错误、不改状态 依赖 T007、复用 EzPay/EzPayConfigPosStoreEzpayController 加 PUT /apply/{storeId}(0→1,建议校验 ubn 已填,记 applyTime)、PUT /saveCredentials(调 T013)、PUT /reset/{storeId},均带 @PreAuthorize 与 @Log 依赖 T012、T013index.vue 加:行内"发起申请"按钮;"录入凭证"弹窗(表单:商店代号/HashKey/HashIV/会员编号 + "验证并开通"按钮,调用 saveCredentials,失败展示 ezPay 错误文案)依赖 T010、T014storeEzpay:{} 下补充 US2 相关 key(发起申请/录入凭证/凭证无效提示/各字段名),四语言一致PosStoreEzpayServiceImplTest(mock EzPay.doPost)验证两条分支——回应含 KEY1→判无效且 status 不变;回应业务错误/成功→判通过且 status=2。可选,按项目测试习惯放 ruoyi-system/src/test/...Checkpoint: US2 可独立验收——申请推进 + 凭证录入验证开通/拒绝双分支
Goal: 运营把门店标记为免用发票(或恢复需开票),标记后退出待办过滤
Independent Test: 把一个需开票门店标记免用发票 → 它从"还要去开通/还没开通"过滤消失;恢复后回归
PosStoreEzpayServiceImpl.markExempt(storeId, invoiceExempt)(更新 pos_store.invoice_exempt)+ PosStoreEzpayController PUT /markExempt/{storeId}(入参 {invoiceExempt},@PreAuthorize('chanting:storeEzpay:markExempt'))依赖 T007index.vue 加"标记免用发票/恢复需开票"操作按钮(调 markExempt,成功后刷新列表)+ 对应 i18n key 进 storeEzpay:{}(四语言一致)依赖 T018Checkpoint: US3 可独立验收——免用标记影响待办过滤
Goal: 商家在商家端门店设置填统一编号并保存,平台后台可见
Independent Test: 商家填统编保存 → 平台后台该门店显示统编已填
PosStoreController(/chanting/store)扩展——addmendian/edit 保存门店时 upsert pos_store_ezpay.ubn(按 storeId,无行则建行 status=0);或新增 POST /saveUbn {storeId,ubn} 带 @Auth+JWT 校验 storeId 归属当前商家。商家端仅能写 ubn,不可改状态/凭证/免用 依赖 T007E:\QtwCode\foodie\foodie-store 门店设置表单加"统一编号"字段(保存随门店提交);src/lang/ 的 zh.js/tw.js/en.js/vi.js 加统编相关 key(四语言一致,CRLF 文件用 Python 脚本改)依赖 T020Checkpoint: US4 可独立验收——商家上传统编、后台可见
Goal: 已开通门店可临时停用/恢复,不影响申请状态、无需重新验证
Independent Test: 已开通门店点停用→启用关→点恢复→启用开,状态仍已开通
PosStoreEzpayServiceImpl.toggleEnable(storeId)(前置 status=2,翻转 isEnabled 1↔0,否则抛"当前状态不允许此操作")+ PosStoreEzpayController PUT /toggleEnable/{storeId}(@PreAuthorize('chanting:storeEzpay:toggleEnable'))依赖 T014(需 status=2 数据)index.vue 对已开通行显示"启用/停用"开关(调 toggleEnable)+ 对应 i18n key 进 storeEzpay:{}(四语言一致)依赖 T022Checkpoint: US5 可独立验收——已开通门店停用/恢复
quickstart.md 跑通手测:执行 SQL → ezPay 测试环境拿真凭证 → 发起申请 → 录入正确/错误凭证验证双分支 → 停用/恢复 → 标记免用 → 商家上传统编,逐条核对 spec.md 的 SC-001~SC-006C:\Users\qmj\.claude\projects\E--QtwCode-foodie-foodie-server\memory\MEMORY.md 加一行指向 specs/009-ezpay-invoice-onboarding/spec.md(参考 008 的写法)EzPay/EzPayConfig/EzPayEncryptUtil 已实现并经官方数据验证,本期不改其内部,仅业务层调用invoice_search(不走 CheckValue),KEY1xxxx=无效、业务错误/成功=有效updatesql/sql.md 由开发者手动跑