tasks.md 14 KB


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 变更与权限菜单(交开发者手动执行)

  • 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

  • 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
  • T003 [P] 在 ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStore.java 新增 invoiceExempt(Integer)字段 + getter/setter(遵循该文件既有手写 getter 风格)
  • T004 新建 PosStoreEzpayMapper 接口于 ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreEzpayMapper.java,继承 MyBatis-Plus BaseMapper<PosStoreEzpay>,声明分页查询方法 selectEzpayStoreList(...)(参数见 contracts/api.md 列表入参)
  • T005 [P] 新建 PosStoreEzpayMapper.xmlruoyi-system/src/main/resources/mapper/chanting/PosStoreEzpayMapper.xml:resultMap(门店字段 + ezPay 字段)+ selectEzpayStoreListpos_store LEFT JOIN pos_store_ezpay,过滤实际卖货门店 isStall,支持 ezpayStatus/invoiceExempt/posName/isStall + quickFilter=needApply|notEnabled,LEFT JOIN 无行时 ezpayStatus 按 0 处理)
  • T006 [P] 修改 ruoyi-system/src/main/resources/mapper/chanting/PosStoreMapper.xml:resultMap 加 invoice_exempt、相关 select 列加 invoice_exempt(供门店基础接口携带该字段)
  • T007 新建 IPosStoreEzpayService 接口与 PosStoreEzpayServiceImpl 实现于 ruoyi-system/src/main/java/com/ruoyi/system/service/.../impl/,注入 PosStoreEzpayMapperPosStoreMapperEzPay,声明占位方法(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

  • T008 [US1] 实现 PosStoreEzpayServiceImpl.listEzpayStore(...)(组装查询参数含 quickFilter→ezpayStatus 映射,调用 mapper 分页,无 ezPay 行的门店补 status=0/invoiceExempt 取自 pos_store)依赖 T005、T007
  • T009 [US1] 新建 PosStoreEzpayControllerruoyi-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
  • 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
  • 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

  • T012 [P] [US2] 新建 StoreEzpayCredentialDtoruoyi-admin/src/main/java/com/ruoyi/app/mendian/dto/StoreEzpayCredentialDto.java(字段 storeId/merchantId/hashKey/hashIv/companyId,校验注解:merchantId 必填、hashKey 长度 32、hashIv 长度 16)
  • 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=2isEnabled=1approvedTime=now、存凭证、记通过。网络异常→抛可重试错误、不改状态 依赖 T007、复用 EzPay/EzPayConfig
  • T014 [US2] 在 PosStoreEzpayControllerPUT /apply/{storeId}(0→1,建议校验 ubn 已填,记 applyTime)、PUT /saveCredentials(调 T013)、PUT /reset/{storeId},均带 @PreAuthorize@Log 依赖 T012、T013
  • T015 [US2] 平台前端 index.vue 加:行内"发起申请"按钮;"录入凭证"弹窗(表单:商店代号/HashKey/HashIV/会员编号 + "验证并开通"按钮,调用 saveCredentials,失败展示 ezPay 错误文案)依赖 T010、T014
  • 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

  • T018 [US3] 实现 PosStoreEzpayServiceImpl.markExempt(storeId, invoiceExempt)(更新 pos_store.invoice_exempt)+ PosStoreEzpayController PUT /markExempt/{storeId}(入参 {invoiceExempt}@PreAuthorize('chanting:storeEzpay:markExempt'))依赖 T007
  • 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

  • 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
  • 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

  • T022 [US5] 实现 PosStoreEzpayServiceImpl.toggleEnable(storeId)(前置 status=2,翻转 isEnabled 1↔0,否则抛"当前状态不允许此操作")+ PosStoreEzpayController PUT /toggleEnable/{storeId}@PreAuthorize('chanting:storeEzpay:toggleEnable'))依赖 T014(需 status=2 数据)
  • 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
  • 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 由开发者手动跑