---

description: "Task list for 订单 ezPay 电子发票开立"
---

# Tasks: 订单 ezPay 电子发票开立

**Input**: Design documents from `/specs/010-order-invoice/`

**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/api.md ✅, quickstart.md ✅

**Tests**: 轻量。仅对「开票金额拆分 + ezPay 回应判读」加可选单测（mock `EzPay.issueInvoice`），其余按 quickstart.md 手测。项目无强制 TDD。

**范围说明**: **客户端前端 UI 不在本期**——本期交付后端开票 API（`applyInvoice`/`getInvoice`，供客户端团队后续对接）+ 管理端（商家端 + 平台后台）查看/重试/作废。客户端 UI 与其 i18n 由客户端团队另行实现，不在本 tasks 内。

**Organization**: 按 spec.md 五个 user story 组织；US1–US3 为后端开票链路，US4–US5 为管理端。

## Format: `[ID] [P?] [Story] Description`

- **[P]**: 可并行（不同文件、无未完成依赖）
- **[Story]**: 所属 user story（US1~US5）
- 描述含精确文件路径

## Path Conventions

- **后端**（本仓库 `foodie_server`）：
  - 实体/DTO/VO：`ruoyi-system/src/main/java/com/ruoyi/system/domain/`（含 `dto/`、`vo/`）
  - Mapper 接口：`ruoyi-system/src/main/java/com/ruoyi/system/mapper/`
  - Mapper XML：`ruoyi-system/src/main/resources/mapper/chanting/`
  - 开票 Service：`ruoyi-admin/src/main/java/com/ruoyi/app/order/`（本期放 admin，因需调 EzPay）
  - 客户端订单 Controller：`ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java`（已有，新增接口）
  - 管理端 Controller：`ruoyi-admin/src/main/java/com/ruoyi/app/mendian/`（新增 `PosOrderInvoiceController`）
  - ezPay 工具类（复用不改）：`ruoyi-admin/src/main/java/com/ruoyi/app/utils/ezPay/`
  - 009 凭证（复用）：`PosStoreEzpay` / `PosStoreEzpayMapper`
  - SQL：`updatesql/sql.md`
- **平台后台前端**（兄弟仓库 `E:\QtwCode\foodie\foodie-admin-vue`）：`src/api/chanting/`、`src/views/mendian/`、`src/api/language/`（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-16 段落：① 按 data-model.md 创建 `pos_order_invoice` 表（含 `uk_order_id` 唯一键、`idx_status`、`idx_store` 索引）；② `sys_menu` 插入「订单发票管理」(C, `chanting:orderInvoice:list`) + 按钮权限 `chanting:orderInvoice:query/retry/invalid`（参考 009 菜单 SQL 写法，挂在门店/订单相关父菜单下）

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: 数据模型地基，所有 user story 都依赖

**⚠️ CRITICAL**: 本阶段完成前不得开始任何 user story

- [x] T002 [P] 新建 `PosOrderInvoice` 实体于 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderInvoice.java`（MyBatis-Plus `@TableName("pos_order_invoice")`、`@TableId(type=IdType.AUTO)`，字段见 data-model.md：id/orderId/orderNo/storeId/invoiceCategory/buyerName/buyerUbn/buyerEmail/carrierType/carrierNum/invoiceNumber/randomNum/invoiceStatus/totalAmt/salesAmt/taxAmt/failReason/invoiceUrl/applyTime/issueTime/invalidTime/createTime/updateTime/createBy/updateBy，lombok `@Data`）
- [x] T003 [P] 新建 `ApplyInvoiceDto` 于 `ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ApplyInvoiceDto.java`（字段：orderId/category/buyerName/buyerUbn/buyerEmail/carrierType/carrierNum；校验注解：orderId 必填、category 必填∈{B2C,B2B}、buyerName 必填、buyerEmail 邮箱格式、buyerUbn 长度 8 数字）
- [x] T004 [P] 新建 `PosOrderInvoiceVo` 于 `ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PosOrderInvoiceVo.java`（列表/详情字段：订单号/门店名/发票类型/买方/发票号/状态/金额/各时间 + 查询筛选字段 invoiceStatus/orderNo/storeId/invoiceCategory/日期范围）
- [x] T005 [P] 新建 `PosOrderInvoiceMapper` 接口于 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderInvoiceMapper.java`，继承 MyBatis-Plus `BaseMapper<PosOrderInvoice>`，声明 `selectInvoiceList(PosOrderInvoiceVo query)` 分页查询 + `selectInvoiceDetail(@Param("orderId") Long)` 单行
- [x] T006 [P] 新建 `PosOrderInvoiceMapper.xml` 于 `ruoyi-system/src/main/resources/mapper/chanting/PosOrderInvoiceMapper.xml`：`selectInvoiceList`（`pos_order_invoice i LEFT JOIN pos_order o LEFT JOIN pos_store s`，取订单号/门店名/发票字段，支持 invoiceStatus/orderNo/storeId/invoiceCategory/日期范围筛选）、`selectInvoiceDetail`（单行，含买方/载具/金额/凭证详情）
- [x] T007 新建 `OrderInvoiceService` 于 `ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderInvoiceService.java`（`@Service`，注入 `EzPay`、`PosOrderMapper`（或 PosOrderService）、`PosStoreEzpayMapper`、`PosStoreMapper`、`PosOrderInvoiceMapper`），声明占位方法 `applyInvoice/ getInvoice/ list/ detail/ retry/ invalid`，具体业务在后续 story 实现。**说明**：本期 service 放 admin（需调 EzPay），与 009 不同

**Checkpoint**: 表结构 + 实体 + DTO/VO + mapper + service 骨架就绪，user story 可开始

---

## Phase 3: User Story 1 - 客户端开票 API（B2C 个人发票）(Priority: P1) 🎯 MVP

**Goal**: 提供客户端可调用的开票 API——客户对已完成订单申请 B2C 个人发票（邮箱），系统调 ezPay 即时开立、记录发票号，并可查询

**Independent Test**: 已完成订单（所属门店 ezPay 已开通启用）→ 调 `POST /system/userOrder/applyInvoice`（B2C+邮箱）→ 开立成功 → 调 `GET /getInvoice` 返回发票号

### Implementation for User Story 1

- [x] T008 [US1] 实现 `OrderInvoiceService.applyInvoice(ApplyInvoiceDto dto, Long currentUserId)`（核心开票链路）：① 校验订单归属（`pos_order.user_id == currentUserId`）、`state==3`（已完成）、`pay_status==1`（已支付）；② 校验门店可开票：`pos_store_ezpay.ezpay_status==2 && is_enabled==1` 且 `pos_store.invoice_exempt==0`，否则抛「门店暂不支持开票」；③ 校验订单未开票（`invoice_status != 1`）；④ 金额拆分（research D1）：`invoiceTotal = amount - freight`、`sales = round(invoiceTotal/1.05)`、`tax = invoiceTotal - sales`；⑤ 组装 ezPay issue 参数：`Category=B2C`、`PrintFlag=Y`、`BuyerName`、`BuyerEmail`、`TaxType=1`、`TaxRate=5`、`Amt/TaxAmt/TotalAmt`、`MerchantOrderNo=ddId`，逐商品明细从 `food` JSON 解析（`ItemName`/`ItemPrice=price+otherPrice` 含税/`ItemCount=number`/`ItemAmt`），优惠按比例分摊到各商品（research D3 方案 B）；⑥ `EzPayConfig` 由门店 `pos_store_ezpay` 凭证构造 → `ezPay.issueInvoice(baseUrl, cfg, postData)`；⑦ 判读（research D9）：`Status==SUCCESS && InvoiceNumber 非空` → 落库 `invoice_number/random_num/invoice_url/total/sales/tax/invoice_status=1/issue_time`；否则 `invoice_status=2 + fail_reason`；⑧ 网络异常 → `invoice_status=2 + "ezPay 服务暂不可用"`、订单状态不变；⑨ `uk_order_id` 防重复（catch DuplicateKeyException）。依赖 T002、T005、T007
- [x] T009 [US1] 实现 `OrderInvoiceService.getInvoice(Long orderId, Long currentUserId)`：校验订单归属，返回发票 VO（`invoiceStatus/invoiceNumber/invoiceUrl/category`）；门店不可开票时 `invoiceStatus=null`（客户端据此隐藏入口）。依赖 T008
- [x] T010 [US1] 在 `UserOrderController`（`/system/userOrder`）新增：`POST /applyInvoice`（`@RequestBody ApplyInvoiceDto`，从 JWT/当前用户取 userId 调 `applyInvoice`）、`GET /getInvoice?orderId=`（调 `getInvoice`）。依赖 T008、T009

**Checkpoint**: US1 可独立验收——开票/查询 API 可用，curl/Postman 跑通 B2C 开票

---

## Phase 4: User Story 2 - 客户端开票 API（B2B 公司发票）(Priority: P1)

**Goal**: 开票 API 支持 B2B 公司发票（统编），提交前校验统编格式

**Independent Test**: `applyInvoice` B2B + 合法统编 → 开立含买方统编；非法统编 → 调 ezPay 前被拦截

### Implementation for User Story 2

- [x] T011 [US2] 在 `applyInvoice` 增加 B2B 分支：`Category=B2B`、`BuyerUBN=统编(8码)`、`PrintFlag=Y`、`BuyerName=公司名`；B2B 时 `buyerUbn` 必填且 8 位数字校验（service 内强校验，非法抛「统编格式不正确」不调 ezPay）。依赖 T008

**Checkpoint**: US2 可独立验收——B2B 开票 API + 统编校验双分支

---

## Phase 5: User Story 3 - 客户端开票 API（电子发票载具）(Priority: P2)

**Goal**: 开票 API 支持选择载具（手机条码/自然人凭证/会员载具），发票存入载具

**Independent Test**: `applyInvoice` 选手机条码载具 → 校验后开立、PrintFlag=N、发票存入载具

### Implementation for User Story 3

- [x] T012 [US3] 在 `applyInvoice` 增加载具分支：`carrierType` 非空时传 `CarrierType`(0/1/2)+`CarrierNum`、`PrintFlag=N`、`LoveCode` 空；校验载具号非空（手机条码格式 `/XXXX+/` 可选校验）。依赖 T008

**Checkpoint**: US3 可独立验收——载具开票 API

---

## Phase 6: User Story 4 - 商家与平台查看订单发票、失败重试 (Priority: P2)

**Goal**: 商家端/平台后台查看订单开票状态与发票号；运营对失败单重新开票

**Independent Test**: 一笔开票失败订单 → 平台后台点「重新开票」 → 成功、状态变已开

### Implementation for User Story 4

- [x] T013 [US4] 实现 `OrderInvoiceService` 的管理端方法：`list(query)`（分页，带门店/状态筛选）、`detail(orderId)`、`retry(orderId)`（校验 `invoice_status ∈ {2 失败, 3 作废}` → 重置为未开 → 复用 T008 开票逻辑重开，运营身份不校验客户归属）。依赖 T008
- [x] T014 [US4] 新建 `PosOrderInvoiceController` 于 `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosOrderInvoiceController.java`，`@RequestMapping("/system/orderInvoice")`：`GET /list`（`@PreAuthorize("@ss.hasPermi('chanting:orderInvoice:list')")` + `startPage()` + `TableDataInfo`）、`GET /{orderId}`（`chanting:orderInvoice:query`）、`PUT /retry/{orderId}`（`chanting:orderInvoice:retry` + `@Log`）。依赖 T013
- [x] T015 [P] [US4] 平台后台前端：新建 `E:\QtwCode\foodie\foodie-admin-vue\src\api\chanting\orderInvoice.js`（list/detail/retry 封装，走 `/system/orderInvoice/*`）与页面 `src\views\mendian\orderInvoice\index.vue`（Element 表格：订单号/门店/类型/买方/发票号/状态/金额/开立时间 + 顶部筛选 + 失败/作废行的「重新开票」按钮，`v-hasPermi` 权限控制）。依赖 T014
- [x] T016 [P] [US4] 平台后台 i18n：在 `foodie-admin-vue\src\api\language\` 的 zh_CN/zh_TW/en_US/vi.js 新增 `orderInvoice:{}` 对象层级（标题/表头/状态文案/按钮，四文件 key 一致）。依赖 T015
- [x] T017 [P] [US4] 商家端前端 `foodie-store`：订单详情显示发票状态/发票号（复用现有商家订单详情视图，加展示）。依赖 T014（接口）
- [x] T018 [P] [US4] 商家端 i18n：`foodie-store\src\lang\` 的 zh/tw/en/vi.js 加发票状态相关 key（四语言一致，CRLF 用 Python 脚本改）。依赖 T017

**Checkpoint**: US4 可独立验收——三端查看 + 失败重试

---

## Phase 7: User Story 5 - 商家作废已开立的发票 (Priority: P3)

**Goal**: 已开立发票可作废（ezPay invalid），作废后订单可重开

**Independent Test**: 作废一张本月开具的发票 → ezPay invalid 成功 → 状态作废 → 重新开票生成新号

### Implementation for User Story 5

- [x] T019 [US5] 实现 `OrderInvoiceService.invalid(Long orderId, String reason)`：校验 `invoice_status==1`（已开），构造 `{InvoiceNumber, InvalidReason, RespondType:JSON, Version:1.0}` 调 `ezPay.doPost(baseUrl + EzPay.URL_INVALID, cfg, postData)`；判读成功 → `invoice_status=3/invalid_time`；失败 → 记 `fail_reason`；不可作废以 ezPay 返回为准（不做本地时限计算）。依赖 T014
- [x] T020 [US5] 在 `PosOrderInvoiceController` 加 `PUT /invalid/{orderId}`（入参可选 `{invalidReason}`，`@PreAuthorize('chanting:orderInvoice:invalid')` + `@Log`）。依赖 T019
- [x] T021 [P] [US5] 平台后台前端 `index.vue` 对「已开」行加「作废」按钮 + 作废原因输入弹窗（调 invalid，成功刷新）。依赖 T015、T020
- [x] T022 [P] [US5] 平台后台 i18n：在 `orderInvoice:{}` 下补充作废相关 key（作废/作废原因/不可作废提示）。依赖 T021

**Checkpoint**: US5 可独立验收——作废 + 重开

---

## Phase 8: Polish & Cross-Cutting Concerns

- [ ] T023 [P] 按 `quickstart.md` 跑通手测（通过 curl/Postman 调开票 API + 平台后台 UI）：执行 SQL → ezPay 测试环境真凭证 → B2C 邮箱 / B2B 统编 / 载具开票 → 失败重试 → 作废重开 → 优惠对账（方案 B 分摊），逐条核对 spec.md 的 SC-001~SC-006
- [ ] T024 [P] 单元测试（可选）：`OrderInvoiceServiceTest`（mock `EzPay.issueInvoice`）验证 ① 金额拆分（`invoiceTotal=amount-freight`、`sales+tax=total`）；② 回应判读两条分支（SUCCESS→已开 / 其它→失败 status 不变）。按项目测试习惯放 `ruoyi-admin/src/test/...`
- [x] T025 [P] 更新记忆索引：在 `C:\Users\qmj\.claude\projects\E--QtwCode-foodie-foodie-server\memory\MEMORY.md` 加一行指向 `specs/010-order-invoice/spec.md`（参考 008/009 的写法）

---

## Phase 9: Enhancement - 发票凭证补全与平台后台展示 (2026-06-22)

**Goal**: 修正 invoice_url 字段语义、补落库发票码值凭证、平台后台详情可看/下载发票

**Independent Test**: 开一张 B2C 邮箱发票 → 详情弹窗显示二维码/条码图 + 可下载 PNG；开一张载具发票 → 详情显示「已存入载具」无码图

- [x] T026 [Enh] SQL：在 `updatesql/sql.md` 追加 2026-06-22 段——① `ALTER TABLE pos_order_invoice CHANGE invoice_url invoice_trans_no VARCHAR(20)` 改名；② `ADD COLUMN invoice_bar_code VARCHAR(255)`/`invoice_qrcode_l VARCHAR(255)`/`invoice_qrcode_r VARCHAR(255)`（码值字符串，均可空，仅 PrintFlag=Y 有值）
- [x] T027 [Enh] 实体 `PosOrderInvoice.java`：`invoiceUrl` → `invoiceTransNo`（改字段名 + 注释「ezPay 交易流水号 InvoiceTransNo」），新增 `invoiceBarCode`/`invoiceQrcodeL`/`invoiceQrcodeR`（注释「发票码值，PrintFlag=Y 才有」）
- [x] T028 [Enh] VO `PosOrderInvoiceVo.java`：同步加 `invoiceTransNo`/`invoiceBarCode`/`invoiceQrcodeL`/`invoiceQrcodeR` 字段
- [x] T029 [Enh] Mapper XML `PosOrderInvoiceMapper.xml`：`selectInvoiceDetail` 改 `invoice_trans_no AS invoiceTransNo` + 加 3 码值字段；`selectInvoiceList` 不带码值（列表无需）
- [x] T030 [Enh] `OrderInvoiceService.java`：开票成功落库时从 ezPay issue 回应取 `BarCode`/`QRcodeL`/`QRcodeR` 存入（applyInvoice + reIssueAndSave 两处；PrintFlag=N 时为空）；`setInvoiceUrl`→`setInvoiceTransNo`
- [x] T031 [Enh] 平台后台前端 `orderInvoice/index.vue` 详情弹窗：① 有码值时用 qrcode + jsbarcode 渲染左右二维码 + 条码，加「下载 PNG / 打印」按钮（html2canvas / window.print）；② 载具发票（码值空）显示「已存入买受人载具，无纸本凭证」；③ npm 引入 qrcode/jsbarcode/html2canvas 依赖（package.json）
- [x] T032 [P] [Enh] 平台后台 i18n：`orderInvoice:{}` 下补 key（交易流水号/发票凭证/下载发票/打印/已存入载具提示），四语言一致

---

## Phase 10: User Story 6 - 客户捐赠发票给社福机构 (Priority: P2)

**Goal**: 客户开票可选「捐赠发票」——从机构列表选机构或手输捐赠码，系统校验后调 ezPay 以 LoveCode 开立（PrintFlag=N）捐给所选机构；客户端可拉取机构列表渲染选择器

**Independent Test**: 已完成订单（门店可开票）→ `GET /system/userOrder/loveOrg/list?keyword=創世` 返回机构 → `POST /applyInvoice`（category=B2C+loveCode=919）→ 开立成功 → `getInvoice` 返回发票号+捐赠机构名、码图为空

### Implementation for User Story 6

- [x] T033 [US6] SQL：在 `updatesql/sql.md` 追加 2026-07-20 段——`ALTER TABLE pos_order_invoice ADD COLUMN love_code VARCHAR(10) DEFAULT NULL COMMENT '捐赠码' , ADD COLUMN love_org_name VARCHAR(255) DEFAULT NULL COMMENT '捐赠机构名'`；注明捐赠机构字典 `pos_love_org` 表+2016行数据已在 `updatesql/pos_love_org.sql`（开发者另执行）
- [x] T034 [P] [US6] 新建 `PosLoveOrg` 实体于 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosLoveOrg.java`（@TableName("pos_love_org")、@TableId(type=IdType.AUTO)，字段 id/loveCode/orgName/orgShortName/ubn/city/enabled/syncTime/createTime，lombok @Data）
- [x] T035 [P] [US6] `PosOrderInvoice.java` 加 `loveCode`/`loveOrgName` 两字段（注释「捐赠码 / 捐赠机构名，捐赠发票时填」）
- [x] T036 [P] [US6] `ApplyInvoiceDto.java` 加 `loveCode` 字段（注释「捐赠码 3-7 位；category=B2C 且非空 → 捐赠分支」）
- [x] T037 [P] [US6] `PosOrderInvoiceVo.java` 加 `loveCode`/`loveOrgName`（列表/详情携带）
- [x] T038 [P] [US6] 新建 `PosLoveOrgMapper` 接口（继承 BaseMapper<PosLoveOrg>）+ `ruoyi-system/src/main/resources/mapper/chanting/PosLoveOrgMapper.xml`：`selectListByKeyword(@Param keyword)` 按 org_name/org_short_name 模糊搜 + enabled=1
- [x] T039 [US6] `PosOrderInvoiceMapper.xml`：resultMap + `selectInvoiceDetail`/`selectInvoiceList` 加 `love_code AS loveCode`、`love_org_name AS loveOrgName` 两列
- [x] T040 [P] [US6] `ruoyi-admin/src/main/java/com/ruoyi/app/utils/ezPay/EzPay.java` 加 `checkLoveCode(baseUrl, cfg, loveCode)`：封装 BDV `/Api_inv_application/checkLoveCode`（PostData_ 加密 + CheckValue，回 IsExist Y/N）。**已知坑**：CheckValue 按手册「HashKey 在前」实现，测试环境若 ezPay 回参数错误→改「HashIV 在前」重试（附件三勘误，见 memory reference-ezpay-invoice-api）
- [x] T041 [US6] `OrderInvoiceService.applyInvoice` 加捐赠分支（category=B2C 且 loveCode 非空）：① `validateInvoiceInput` 增校验（loveCode `^\d{3,7}$`、carrierType/carrierNum/buyerUbn 必须空=互斥）；② `resolveLoveOrg`（先查 pos_love_org enabled=1 命中取 orgName；未命中调 `EzPay.checkLoveCode`，IsExist=N 抛「捐赠码无效」、Y 则 orgName="未知"）；③ `buildIssueData` 捐赠分支（Category=B2C、LoveCode=码、CarrierType=""、PrintFlag=N、BuyerName=orgName 或「愛心捐贈」、不传 BuyerEmail）；④ 落库 love_code/love_org_name（码图三列空）。同步 `reIssueAndSave`/`retry` 从 `row.loveCode` 回填 dto.loveCode。依赖 T034–T040
- [x] T042 [US6] `OrderInvoiceService` 加 `listLoveOrg(keyword, pageNum, pageSize)`（PageHelper + PosLoveOrgMapper.selectListByKeyword，仅 enabled=1）；`resolveLoveOrg` 提为可复用私有方法（T041 调用）。依赖 T038、T040、T041
- [x] T043 [US6] `UserOrderController`（/system/userOrder）加 `GET /loveOrg/list`（入参 keyword/pageNum/pageSize，调 `listLoveOrg` 返回 TableDataInfo）。依赖 T042
- [x] T044 [P] [US6] 平台后台 `foodie-admin-vue/src/views/mendian/orderInvoice/index.vue` 详情：捐赠发票（loveCode 非空）走「无码图」分支——展示「已捐赠给 {loveOrgName}」+ 捐赠码，不渲染条码/二维码（复用载具发票无码图分支逻辑）。依赖 T039
- [x] T045 [P] [US6] 平台后台 i18n：`src/api/language/{zh_CN,zh_TW,en_US,vi}.js` 的 `orderInvoice:{}` 下补捐赠 key（已捐赠给/捐赠码/捐赠发票），四语言 key 一致
- [ ] T046 (商家端前端不做,跳过) [P] [US6] 商家端 `foodie-store` 订单详情：捐赠发票显示「已捐赠给 {loveOrgName}」标识。依赖 T039
- [ ] T047 (商家端前端不做,跳过) [P] [US6] 商家端 i18n：`src/lang/{zh,tw,en,vi}.js` 加捐赠相关 key（四语言一致，CRLF 用 Python 脚本改）

**Checkpoint**: US6 可独立验收——机构列表接口可搜、捐赠开票 API 跑通（B2C+loveCode）、捐赠发票无码图展示「已捐赠给 X」

---

## 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)**：依赖 US1（复用 applyInvoice，加 B2B 分支）
- **Phase 5 US3 (P2)**：依赖 US1（复用 applyInvoice，加载具分支）
- **Phase 6 US4 (P2)**：依赖 US1（retry 复用开票逻辑）；与 US2/US3 独立
- **Phase 7 US5 (P3)**：依赖 US4（复用管理端 controller，加 invalid）
- **Phase 8 Polish**：依赖所有欲交付的 story 完成
- **Phase 10 US6 (捐赠 P2)**：依赖 US1（复用 applyInvoice 链路）+ Phase 9（码图展示分支可复用）；管理端展示依赖 Phase 6 US4 的 orderInvoice 页面。客户端选择器 UI 由客户端团队消费 `/loveOrg/list` 接口，不在本 tasks

### Within Each User Story

- 后端 service 先于 controller
- 平台后台 `orderInvoice/index.vue` 跨 US4/US5 同文件 → **顺序追加**
- 四语言 i18n 文件跨 story 同文件 → **顺序追加**，不并行

### Parallel Opportunities

- Phase 2：T002/T003/T004/T005/T006 不同文件可并行（T007 依赖前述）
- US4 平台后台（foodie-admin-vue）与商家端（foodie-store）不同仓库 → 可并行
- 单测 T024 与前端不同文件可并行

---

## Implementation Strategy

### MVP First (User Story 1 Only)

1. Phase 1 Setup：写 SQL（开发者执行）
2. Phase 2 Foundational：建实体/DTO/VO/mapper/service 骨架
3. Phase 3 US1：B2C 开票 API 主链路（后端）
4. **STOP 验收**：curl/Postman 对已完成订单调 `applyInvoice`（B2C 邮箱）→ 开票成功、`getInvoice` 返回发票号
5. 此时已交付核心价值——开票能力就绪，客户端团队可对接

### Incremental Delivery

1. Setup + Foundational → 地基就绪
2. +US1 → B2C 开票 API（MVP，可演示）
3. +US2 → B2B 开票 API + 统编校验
4. +US3 → 载具开票 API
5. +US4 → 管理端三端查看 + 失败重试
6. +US5 → 作废 + 重开
7. Polish：quickstart 手测 + 记忆索引

---

## Notes

- **客户端前端 UI 不在本期**：本期交付开票/查询后端 API（`/system/userOrder/applyInvoice`、`/getInvoice`），客户端团队后续对接 UI；US1–US3 验收以 API 手测（curl/Postman）为准
- ezPay 工具类 `EzPay`/`EzPayConfig`/`EzPayEncryptUtil` 已实现并经官方数据验证，**本期不改其内部**，仅业务层调用（issue 开票 / doPost 作废）
- 开票金额**不含运费**：`invoiceTotal = amount - freight`（research D1），各类优惠已含在 amount 内无需逐项区分
- 优惠在商品明细的体现用方案 B（按比例分摊，research D3），实现时若需切方案 A（折扣负项）需在测试环境验证 ezPay 是否接受负金额
- 开票 service 放 `ruoyi-admin.app.order`（非 ruoyi-system），因需调 EzPay；domain/mapper 放 ruoyi-system
- 前端两个仓库均为 CRLF，编辑 lang 文件用 Python 脚本（见 CLAUDE.md「前端文件编辑注意事项」）
- 所有前端新增文字必须四语言 i18n，key 加到正确对象层级、驼峰命名
- SQL 不直接执行，写 `updatesql/sql.md` 由开发者手动跑

---

## Phase 11: 全联式自动开票 (2026-07-27)

**Goal**: 下单捕获发票意图 + 出餐自动开 + 零信息纸本兜底 + 会员发票夹 + 载具校验 + 默认载具
**Design**: [`auto-invoice-design.md`](./auto-invoice-design.md)

### Implementation

- [x] T048 [P] SQL: `updatesql/sql.md` 追加 2026-07-28 段(方案B)——① `pos_order_invoice` ADD COLUMN `invoice_choice` + `issue_triggered`(下单意图 + 是否已触发开票);② `pos_order` 6 发票意图列废弃(若初版已执行则 DROP 回滚);③ `info_invoice` ADD COLUMN `is_default`
- [ ] T049 [P] `OrderCreateInput.java` 加发票意图字段(invoiceChoice/carrierType/carrierNum/buyerUbn/buyerName/loveCode/saveAsDefault)
- [x] T050 [P] 方案B:`PosOrderInvoice.java` 加 `invoiceChoice` + `issueTriggered`(+ VO + mapper SELECT/WHERE);`PosOrder.java` **不加** 发票字段(意图落 pos_order_invoice)
- [ ] T051 [P] `InfoInvoice.java`(014 实体)加 `isDefault` 字段 + mapper
- [ ] T052 [P] `EzPay.checkBarCode(baseUrl, cfg, barCode)`:平行 `checkLoveCode`,封装 BDV `/Api_inv_application/checkBarCode`,回 IsExist。依赖 `EzPayEncryptUtil`(CheckValue 已就绪)、`URL_CHECK_BARCODE` 常量
- [x] T053 `UserOrderController.createOrder`:存发票意图到 `pos_order_invoice` 意图行(`issue_triggered=0`,经 `OrderInvoiceService.captureInvoiceIntent`);门店守卫(`canInvoice=false` 不插意图);`saveAsDefault=true` 写 info_invoice 标记默认;载具/捐赠验真前置、不通过拦截下单。依赖 T049/T050/T052
- [ ] T054 `OrderInvoiceService.validateInvoiceInput`/`buildIssueData`:放宽 B2C(允许无载具)、补「B2C 无载具→PrintFlag=Y 纸本」分支、个人 BuyerName 取 `InfoUser.phone`(`infoUserService` 已注入)
- [x] T055 `OrderInvoiceService.autoIssue(orderId)`:新增——读 `pos_order_invoice` 发票意图行 + `assertInvoiceable`;不可开票/无意图跳过;组装 `ApplyInvoiceDto`;复用开票核心(buildIssueData+issue+落库),开票后置 `issue_triggered=1`;放宽归属/payStatus 校验。依赖 T054
- [ ] T056 `PosOrderShOprateController.dispatchOrder`:`setState(2)` 后调 `orderInvoiceService.autoIssue(orderId)`。依赖 T055
- [x] T057 货到付款/取消兜底:**核查后无需改代码**——商家取消仅允许 state∈{0,1}、用户取消仅 state=0，都在出餐(state→2)之前；而发票出餐时才开，故标准取消路径下发票尚未开、无发票可作废，挂 invalid 是死代码。货到付款"送达被拒收"走售后/退款流程(不经这两个 cancel 入口)，作废 hook 留待该流程接入时再补，本期不做。
- [x] T058 [P] `UserOrderController` 加 `GET /myInvoices`:JWT,按 userId 分页,`pos_order_invoice` JOIN `pos_order`,字段裁剪(不返回码图)。依赖 —
- [x] T059 [P] 默认载具:**014 list 接口已返回 `isDefault`(T051 加的字段),客户端据此选常用载具/识别默认,无需新增后端接口**。
- [x] T060 [P] i18n:结账页发票选项文案属客户端 UI(客户端团队);**后端无新增面向用户文字**,本期后端 i18n no-op。

**Checkpoint**: 已完成订单(开发票门店)→ 出餐自动开出个人纸本(零信息)→ 码值落库 → 商家可打印;选手機條碜下单 → checkBarCode 验真 → 出餐自动开载具发票;会员 `/myInvoices` 看到发票。
