--- description: "Task list for 商家 ezPay 发票开通管理" --- # Tasks: 商家 ezPay 发票开通管理 **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 可独立实现与测试。 ## Format: `[ID] [P?] [Story] Description` - **[P]**: 可并行(不同文件、无未完成依赖) - **[Story]**: 所属 user story(US1~US5) - 描述含精确文件路径 ## Path Conventions - **后端**(本仓库 `foodie_server`): - 实体:`ruoyi-system/src/main/java/com/ruoyi/system/domain/` - Mapper 接口:`ruoyi-system/src/main/java/com/ruoyi/system/mapper/` - Mapper XML:`ruoyi-system/src/main/resources/mapper/chanting/` - Service:`ruoyi-system/src/main/java/com/ruoyi/system/service/`(接口)与 `.../service/impl/`(实现) - Controller:`ruoyi-admin/src/main/java/com/ruoyi/app/mendian/` - ezPay 工具类(复用不改):`ruoyi-admin/src/main/java/com/ruoyi/app/utils/ezPay/` - SQL:`updatesql/sql.md` - **平台后台前端**(兄弟仓库 `E:\QtwCode\foodie\foodie-admin-vue`):`src/api/`、`src/views/`、`src/lang/`(CRLF 换行,用 Python 脚本改) - **商家端前端**(兄弟仓库 `E:\QtwCode\foodie\foodie-store`):`src/lang/`、门店设置视图(CRLF 换行,用 Python 脚本改) --- ## Phase 1: Setup (Shared Infrastructure) **Purpose**: SQL 变更与权限菜单(交开发者手动执行) - [x] T001 在 `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 写法) --- ## Phase 2: Foundational (Blocking Prerequisites) **Purpose**: 数据模型地基,所有 user story 都依赖 **⚠️ CRITICAL**: 本阶段完成前不得开始任何 user story - [x] T002 [P] 新建 `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`) - [x] T003 [P] 在 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStore.java` 新增 `invoiceExempt`(Integer)字段 + getter/setter(遵循该文件既有手写 getter 风格) - [x] T004 新建 `PosStoreEzpayMapper` 接口于 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreEzpayMapper.java`,继承 MyBatis-Plus `BaseMapper`,声明分页查询方法 `selectEzpayStoreList(...)`(参数见 contracts/api.md 列表入参) - [x] T005 [P] 新建 `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 处理) - [x] T006 [P] 修改 `ruoyi-system/src/main/resources/mapper/chanting/PosStoreMapper.xml`:resultMap 加 `invoice_exempt`、相关 select 列加 `invoice_exempt`(供门店基础接口携带该字段) - [x] T007 新建 `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 可开始 --- ## Phase 3: User Story 1 - 运营查看门店 ezPay 开通全景并定位待办 (Priority: P1) 🎯 MVP **Goal**: 平台后台列表展示所有需开票门店的 ezPay 状态,支持按状态筛选与"还没开通/还要去开通"快捷过滤(均排除免用发票门店) **Independent Test**: 打开"ezPay 发票开通管理" → 点"还要去开通" → 仅显示需开票且未申请门店(免用发票门店不在内) ### Implementation for User Story 1 - [x] T008 [US1] 实现 `PosStoreEzpayServiceImpl.listEzpayStore(...)`(组装查询参数含 quickFilter→ezpayStatus 映射,调用 mapper 分页,无 ezPay 行的门店补 status=0/invoiceExempt 取自 pos_store)依赖 T005、T007 - [x] T009 [US1] 新建 `PosStoreEzpayController` 于 `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}` 详情 依赖 T008 - [x] T010 [P] [US1] 平台前端新建 `E:\QtwCode\foodie\foodie-admin-vue\src\api\mendian\storeEzpay.js`(list/detail/apply/saveCredentials/toggleEnable/markExempt 接口封装)与页面 `src\views\mendian\storeEzpay\index.vue`(Element 表格:门店名/类型/ezPay状态/启用/统编是否已填/操作;顶部筛选:状态下拉 + 门店名搜索 + "还没开通/还要去开通"快捷按钮)依赖 T009 - [x] T011 [P] [US1] 平台前端 i18n:在 `foodie-admin-vue\src\lang\` 的 zh.js/tw.js/en.js/vi.js 新增 `storeEzpay:{}` 对象层级(标题/表头/状态文案/筛选按钮等 key,驼峰命名,四个文件 key 一致) **Checkpoint**: US1 可独立验收——列表 + 筛选 + 快捷过滤生效 --- ## Phase 4: User Story 2 - 运营发起申请并录入凭证验证开通 (Priority: P1) **Goal**: 未申请→发起申请→申请中;运营录入 ezPay 凭证(商店代号/HashKey/HashIV)后系统调 ezPay 验证,通过才标记已开通并启用 **Independent Test**: 对测试门店录入 ezPay 测试环境真凭证→验证通过→状态变已开通;故意填错 HashKey→提示凭证无效、状态保持申请中 ### Implementation for User Story 2 - [x] T012 [P] [US2] 新建 `StoreEzpayCredentialDto` 于 `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/dto/StoreEzpayCredentialDto.java`(字段 storeId/merchantId/hashKey/hashIv/companyId,校验注解:merchantId 必填、hashKey 长度 32、hashIv 长度 16) - [x] T013 [US2] 实现 `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`/`EzPayConfig` - [x] T014 [US2] 在 `PosStoreEzpayController` 加 `PUT /apply/{storeId}`(0→1,建议校验 ubn 已填,记 applyTime)、`PUT /saveCredentials`(调 T013)、`PUT /reset/{storeId}`,均带 `@PreAuthorize` 与 `@Log` 依赖 T012、T013 - [x] T015 [US2] 平台前端 `index.vue` 加:行内"发起申请"按钮;"录入凭证"弹窗(表单:商店代号/HashKey/HashIV/会员编号 + "验证并开通"按钮,调用 saveCredentials,失败展示 ezPay 错误文案)依赖 T010、T014 - [x] T016 [US2] 平台前端 i18n:在 `storeEzpay:{}` 下补充 US2 相关 key(发起申请/录入凭证/凭证无效提示/各字段名),四语言一致 - [ ] T017 [P] [US2] 单元测试:`PosStoreEzpayServiceImplTest`(mock `EzPay.doPost`)验证两条分支——回应含 KEY1→判无效且 status 不变;回应业务错误/成功→判通过且 status=2。可选,按项目测试习惯放 `ruoyi-system/src/test/...` **Checkpoint**: US2 可独立验收——申请推进 + 凭证录入验证开通/拒绝双分支 --- ## Phase 5: User Story 3 - 运营标记免用发票门店 (Priority: P2) **Goal**: 运营把门店标记为免用发票(或恢复需开票),标记后退出待办过滤 **Independent Test**: 把一个需开票门店标记免用发票 → 它从"还要去开通/还没开通"过滤消失;恢复后回归 ### Implementation for User Story 3 - [x] T018 [US3] 实现 `PosStoreEzpayServiceImpl.markExempt(storeId, invoiceExempt)`(更新 `pos_store.invoice_exempt`)+ `PosStoreEzpayController` `PUT /markExempt/{storeId}`(入参 `{invoiceExempt}`,`@PreAuthorize('chanting:storeEzpay:markExempt')`)依赖 T007 - [x] T019 [US3] 平台前端 `index.vue` 加"标记免用发票/恢复需开票"操作按钮(调 markExempt,成功后刷新列表)+ 对应 i18n key 进 `storeEzpay:{}`(四语言一致)依赖 T018 **Checkpoint**: US3 可独立验收——免用标记影响待办过滤 --- ## Phase 6: User Story 4 - 商家在商家端上传门店统一编号 (Priority: P2) **Goal**: 商家在商家端门店设置填统一编号并保存,平台后台可见 **Independent Test**: 商家填统编保存 → 平台后台该门店显示统编已填 ### Implementation for User Story 4 - [x] T020 [US4] 后端 UBN 上传:在 `PosStoreController`(`/chanting/store`)扩展——`addmendian`/edit 保存门店时 upsert `pos_store_ezpay.ubn`(按 storeId,无行则建行 status=0);或新增 `POST /saveUbn {storeId,ubn}` 带 `@Auth`+JWT 校验 storeId 归属当前商家。商家端仅能写 ubn,不可改状态/凭证/免用 依赖 T007 - [x] T021 [P] [US4] 商家端前端:`E:\QtwCode\foodie\foodie-store` 门店设置表单加"统一编号"字段(保存随门店提交);`src/lang/` 的 zh.js/tw.js/en.js/vi.js 加统编相关 key(四语言一致,CRLF 文件用 Python 脚本改)依赖 T020 **Checkpoint**: US4 可独立验收——商家上传统编、后台可见 --- ## Phase 7: User Story 5 - 运营停用/恢复已开通门店 (Priority: P3) **Goal**: 已开通门店可临时停用/恢复,不影响申请状态、无需重新验证 **Independent Test**: 已开通门店点停用→启用关→点恢复→启用开,状态仍已开通 ### Implementation for User Story 5 - [x] T022 [US5] 实现 `PosStoreEzpayServiceImpl.toggleEnable(storeId)`(前置 status=2,翻转 isEnabled 1↔0,否则抛"当前状态不允许此操作")+ `PosStoreEzpayController` `PUT /toggleEnable/{storeId}`(`@PreAuthorize('chanting:storeEzpay:toggleEnable')`)依赖 T014(需 status=2 数据) - [x] T023 [US5] 平台前端 `index.vue` 对已开通行显示"启用/停用"开关(调 toggleEnable)+ 对应 i18n key 进 `storeEzpay:{}`(四语言一致)依赖 T022 **Checkpoint**: US5 可独立验收——已开通门店停用/恢复 --- ## Phase 8: Polish & Cross-Cutting Concerns - [ ] T024 [P] 按 `quickstart.md` 跑通手测:执行 SQL → ezPay 测试环境拿真凭证 → 发起申请 → 录入正确/错误凭证验证双分支 → 停用/恢复 → 标记免用 → 商家上传统编,逐条核对 spec.md 的 SC-001~SC-006 - [x] T025 [P] 更新记忆索引:在 `C:\Users\qmj\.claude\projects\E--QtwCode-foodie-foodie-server\memory\MEMORY.md` 加一行指向 `specs/009-ezpay-invoice-onboarding/spec.md`(参考 008 的写法) --- ## Dependencies & Execution Order ### Phase Dependencies - **Phase 1 Setup**:无依赖,立即开始(SQL 交开发者手动执行) - **Phase 2 Foundational**:依赖 Phase 1(表先建好);**阻塞所有 user story** - **Phase 3 US1 (P1)**:依赖 Foundational - **Phase 4 US2 (P1)**:依赖 Foundational;US2 的前端复用 US1 的页面 - **Phase 5 US3 (P2)**:依赖 Foundational(需 invoice_exempt 字段);与 US1/US2 独立 - **Phase 6 US4 (P2)**:依赖 Foundational;与其它 story 独立 - **Phase 7 US5 (P3)**:依赖 US2(需"已开通"门店数据) - **Phase 8 Polish**:依赖所有欲交付的 story 完成 ### Within Each User Story - DTO/实体(不同文件)可并行 - Service 先于 Controller,Controller 先于前端 - 前端页面 index.vue 跨 story 是同一文件 → **顺序进行**(US1 建页,US2/US3/US5 在其上增量),不并行 - 四语言 i18n 文件跨 story 同文件(zh.js 等)→ **顺序追加**,不并行 ### Parallel Opportunities - Phase 2:T002/T003/T005/T006 不同文件可并行(T004 依赖 T002,T007 依赖 T004/T005) - 后端 track 与前端 track 可并行(US1 后端 T008/T009 与前端 T010/T011) - US4 商家端(foodie-store)与 US1~US3/US5 平台端(foodie-admin-vue)不同仓库 → 可并行 - 单测 T017 与前端 T015/T016 不同文件可并行 --- ## Implementation Strategy ### MVP First (User Story 1 Only) 1. Phase 1 Setup:写 SQL(开发者执行) 2. Phase 2 Foundational:建实体/mapper/service 骨架 3. Phase 3 US1:列表 + 筛选 + 快捷过滤 4. **STOP 验收**:运营能在列表看到全部门店状态、"还要去开通"过滤正确 5. 此时已交付核心价值——运营知道谁要推动开通 ### Incremental Delivery 1. Setup + Foundational → 地基就绪 2. +US1 → 列表/筛选(MVP,可演示) 3. +US2 → 申请推进 + 凭证验证开通(核心开通链路打通) 4. +US3 → 免用发票标记 5. +US4 → 商家上传统编 6. +US5 → 停用/恢复 7. Polish:quickstart 手测 + 记忆索引 --- ## Notes - ezPay 工具类 `EzPay`/`EzPayConfig`/`EzPayEncryptUtil` 已实现并经官方数据验证,**本期不改其内部**,仅业务层调用 - 凭证验证务必用 `invoice_search`(不走 CheckValue),KEY1xxxx=无效、业务错误/成功=有效 - 前端两个仓库均为 CRLF,编辑 lang 文件用 Python 脚本(见 CLAUDE.md「前端文件编辑注意事项」) - 所有前端新增文字必须四语言 i18n,key 加到正确对象层级、驼峰命名 - SQL 不直接执行,写 `updatesql/sql.md` 由开发者手动跑