tasks.md 23 KB


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

  • T001 在 updatesql/sql.md 追加 2026-06-16 段落:① 按 data-model.md 创建 pos_order_invoice 表(含 uk_order_id 唯一键、idx_statusidx_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

  • 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
  • T003 [P] 新建 ApplyInvoiceDtoruoyi-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 数字)
  • T004 [P] 新建 PosOrderInvoiceVoruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PosOrderInvoiceVo.java(列表/详情字段:订单号/门店名/发票类型/买方/发票号/状态/金额/各时间 + 查询筛选字段 invoiceStatus/orderNo/storeId/invoiceCategory/日期范围)
  • 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) 单行
  • T006 [P] 新建 PosOrderInvoiceMapper.xmlruoyi-system/src/main/resources/mapper/chanting/PosOrderInvoiceMapper.xmlselectInvoiceListpos_order_invoice i LEFT JOIN pos_order o LEFT JOIN pos_store s,取订单号/门店名/发票字段,支持 invoiceStatus/orderNo/storeId/invoiceCategory/日期范围筛选)、selectInvoiceDetail(单行,含买方/载具/金额/凭证详情)
  • T007 新建 OrderInvoiceServiceruoyi-admin/src/main/java/com/ruoyi/app/order/OrderInvoiceService.java@Service,注入 EzPayPosOrderMapper(或 PosOrderService)、PosStoreEzpayMapperPosStoreMapperPosOrderInvoiceMapper),声明占位方法 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

  • 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==1pos_store.invoice_exempt==0,否则抛「门店暂不支持开票」;③ 校验订单未开票(invoice_status != 1);④ 金额拆分(research D1):invoiceTotal = amount - freightsales = round(invoiceTotal/1.05)tax = invoiceTotal - sales;⑤ 组装 ezPay issue 参数:Category=B2CPrintFlag=YBuyerNameBuyerEmailTaxType=1TaxRate=5Amt/TaxAmt/TotalAmtMerchantOrderNo=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
  • T009 [US1] 实现 OrderInvoiceService.getInvoice(Long orderId, Long currentUserId):校验订单归属,返回发票 VO(invoiceStatus/invoiceNumber/invoiceUrl/category);门店不可开票时 invoiceStatus=null(客户端据此隐藏入口)。依赖 T008
  • 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

  • T011 [US2] 在 applyInvoice 增加 B2B 分支:Category=B2BBuyerUBN=统编(8码)PrintFlag=YBuyerName=公司名;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

  • T012 [US3] 在 applyInvoice 增加载具分支:carrierType 非空时传 CarrierType(0/1/2)+CarrierNumPrintFlag=NLoveCode 空;校验载具号非空(手机条码格式 /XXXX+/ 可选校验)。依赖 T008

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


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

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

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

Implementation for User Story 4

  • T013 [US4] 实现 OrderInvoiceService 的管理端方法:list(query)(分页,带门店/状态筛选)、detail(orderId)retry(orderId)(校验 invoice_status ∈ {2 失败, 3 作废} → 重置为未开 → 复用 T008 开票逻辑重开,运营身份不校验客户归属)。依赖 T008
  • T014 [US4] 新建 PosOrderInvoiceControllerruoyi-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
  • 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
  • T016 [P] [US4] 平台后台 i18n:在 foodie-admin-vue\src\api\language\ 的 zh_CN/zh_TW/en_US/vi.js 新增 orderInvoice:{} 对象层级(标题/表头/状态文案/按钮,四文件 key 一致)。依赖 T015
  • T017 [P] [US4] 商家端前端 foodie-store:订单详情显示发票状态/发票号(复用现有商家订单详情视图,加展示)。依赖 T014(接口)
  • 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

  • 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
  • T020 [US5] 在 PosOrderInvoiceControllerPUT /invalid/{orderId}(入参可选 {invalidReason}@PreAuthorize('chanting:orderInvoice:invalid') + @Log)。依赖 T019
  • T021 [P] [US5] 平台后台前端 index.vue 对「已开」行加「作废」按钮 + 作废原因输入弹窗(调 invalid,成功刷新)。依赖 T015、T020
  • 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-freightsales+tax=total);② 回应判读两条分支(SUCCESS→已开 / 其它→失败 status 不变)。按项目测试习惯放 ruoyi-admin/src/test/...
  • 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;开一张载具发票 → 详情显示「已存入载具」无码图

  • 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 有值)
  • T027 [Enh] 实体 PosOrderInvoice.javainvoiceUrlinvoiceTransNo(改字段名 + 注释「ezPay 交易流水号 InvoiceTransNo」),新增 invoiceBarCode/invoiceQrcodeL/invoiceQrcodeR(注释「发票码值,PrintFlag=Y 才有」)
  • T028 [Enh] VO PosOrderInvoiceVo.java:同步加 invoiceTransNo/invoiceBarCode/invoiceQrcodeL/invoiceQrcodeR 字段
  • T029 [Enh] Mapper XML PosOrderInvoiceMapper.xmlselectInvoiceDetailinvoice_trans_no AS invoiceTransNo + 加 3 码值字段;selectInvoiceList 不带码值(列表无需)
  • T030 [Enh] OrderInvoiceService.java:开票成功落库时从 ezPay issue 回应取 BarCode/QRcodeL/QRcodeR 存入(applyInvoice + reIssueAndSave 两处;PrintFlag=N 时为空);setInvoiceUrlsetInvoiceTransNo
  • T031 [Enh] 平台后台前端 orderInvoice/index.vue 详情弹窗:① 有码值时用 qrcode + jsbarcode 渲染左右二维码 + 条码,加「下载 PNG / 打印」按钮(html2canvas / window.print);② 载具发票(码值空)显示「已存入买受人载具,无纸本凭证」;③ npm 引入 qrcode/jsbarcode/html2canvas 依赖(package.json)
  • 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

  • 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(开发者另执行)
  • 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
  • T035 [P] [US6] PosOrderInvoice.javaloveCode/loveOrgName 两字段(注释「捐赠码 / 捐赠机构名,捐赠发票时填」)
  • T036 [P] [US6] ApplyInvoiceDto.javaloveCode 字段(注释「捐赠码 3-7 位;category=B2C 且非空 → 捐赠分支」)
  • T037 [P] [US6] PosOrderInvoiceVo.javaloveCode/loveOrgName(列表/详情携带)
  • T038 [P] [US6] 新建 PosLoveOrgMapper 接口(继承 BaseMapper)+ ruoyi-system/src/main/resources/mapper/chanting/PosLoveOrgMapper.xmlselectListByKeyword(@Param keyword) 按 org_name/org_short_name 模糊搜 + enabled=1
  • T039 [US6] PosOrderInvoiceMapper.xml:resultMap + selectInvoiceDetail/selectInvoiceListlove_code AS loveCodelove_org_name AS loveOrgName 两列
  • T040 [P] [US6] ruoyi-admin/src/main/java/com/ruoyi/app/utils/ezPay/EzPay.javacheckLoveCode(baseUrl, cfg, loveCode):封装 BDV /Api_inv_application/checkLoveCode(PostData_ 加密 + CheckValue,回 IsExist Y/N)。已知坑:CheckValue 按手册「HashKey 在前」实现,测试环境若 ezPay 回参数错误→改「HashIV 在前」重试(附件三勘误,见 memory reference-ezpay-invoice-api)
  • 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/retryrow.loveCode 回填 dto.loveCode。依赖 T034–T040
  • T042 [US6] OrderInvoiceServicelistLoveOrg(keyword, pageNum, pageSize)(PageHelper + PosLoveOrgMapper.selectListByKeyword,仅 enabled=1);resolveLoveOrg 提为可复用私有方法(T041 调用)。依赖 T038、T040、T041
  • T043 [US6] UserOrderController(/system/userOrder)加 GET /loveOrg/list(入参 keyword/pageNum/pageSize,调 listLoveOrg 返回 TableDataInfo)。依赖 T042
  • T044 [P] [US6] 平台后台 foodie-admin-vue/src/views/mendian/orderInvoice/index.vue 详情:捐赠发票(loveCode 非空)走「无码图」分支——展示「已捐赠给 {loveOrgName}」+ 捐赠码,不渲染条码/二维码(复用载具发票无码图分支逻辑)。依赖 T039
  • T045 [P] [US6] 平台后台 i18n:src/api/language/{zh_CN,zh_TW,en_US,vi}.jsorderInvoice:{} 下补捐赠 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 由开发者手动跑