---

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<PosStoreEzpay>`，声明分页查询方法 `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` 由开发者手动跑
