api.md 9.6 KB

Phase 1 API Contracts: 订单 ezPay 电子发票开立

Date: 2026-06-16

后端 REST 接口契约。客户端开票挂在 UserOrderController;管理端(商家+平台)查看/重试/作废用独立 PosOrderInvoiceController

一、客户端(客户为自己的订单开票)

挂在 UserOrderController@RequestMapping("/system/userOrder"),已有 JWT 鉴权)。

POST /system/userOrder/applyInvoice

客户对已完成订单申请开票。

请求体 ApplyInvoiceDto

① 字段含义

字段 类型 含义 格式 / 示例
orderId Long 要开票的订单 id 必须是当前登录客户自己的订单,且已完成 state=3、已支付 payStatus=1
category String 发票类型 B2C=个人发票(载具);B2B=公司发票(统编报账);DONATION=捐赠发票(带 loveCode 捐给社福机构,服务端翻译成 ezPay B2C+LoveCode)
buyerName String 买方名称 B2C 填个人姓名;B2B 填公司全名
buyerUbn String 买方统一编号(统编) 8 位数字,如 12345678仅 B2B 用
buyerEmail String 接收发票的邮箱 可选。B2C 填了则发邮件通知,不填则客户在系统查看
carrierType String 电子载具类型 0=手机条码 / 1=自然人凭证 / 2=ezPay 会员载具;留空 = 不用载具
carrierNum String 载具号码 随 carrierType:手机条码以 / 开头(/ABC1234);自然人凭证 2字母+14数字;ezPay 会员载具填会员账号

② 什么时候必填(按场景,对齐 ezPay 开票模型)

场景 buyerUbn buyerEmail carrierType + carrierNum
B2B 公司发票 ✅ 必填(8 位数字) ✅ 必填(ezPay 发开立通知给买方,凭此查看) ❌ 不填(B2B 不走载具,传了报错)
B2C / 手机条码载具(0) ❌ 不填 ⚪ 可不填(票进载具) ✅ 必填(类型 0 + 号码)
B2C / 自然人凭证载具(1) ❌ 不填 ⚪ 可不填(票进载具) ✅ 必填(类型 1 + 号码)
B2C / ezPay会员载具(2) ❌ 不填 ✅ 必填(ezPay 规则:会员载具须带邮箱) ✅ 必填(类型 2 + 号码)

记忆口诀:

  • B2B统编 + 邮箱必填,不传载具
  • B2C载具必填(0/1/2 三选一,不再支持"都不填");其中 ezPay会员载具(2) 还须带邮箱,其余两种载具邮箱留空。
  • 只要 carrierType 有值,carrierNum 就必须跟着填。
  • 已取消原「B2C 都不填 / 系统查看」模式(票无送达渠道、客户拿不到);原"ezPay 是否接受空 BuyerEmail"待办随之取消。

③ 三种场景请求示例

B2B 公司发票(统编 + 邮箱):

{ "orderId": 1001, "category": "B2B", "buyerName": "美食達有限公司", "buyerUbn": "12345678", "buyerEmail": "ms@example.com" }

B2C / 手机条码载具(载具 0,邮箱留空):

{ "orderId": 1001, "category": "B2C", "buyerName": "王小明", "carrierType": "0", "carrierNum": "/ABC1234" }

B2C / ezPay会员载具(载具 2 + 邮箱):

{ "orderId": 1001, "category": "B2C", "buyerName": "王小明", "carrierType": "2", "carrierNum": "C1597485444", "buyerEmail": "xm@example.com" }

处理:校验订单归属与可开票(门店 ezPay 已开通启用、非免用发票)→ 金额拆分 → 组装明细 → 调 EzPay.issueInvoice → 落库发票号/随机码/凭证。

响应 AjaxResult

  • 成功 {code:200, msg:"开票成功", data:{invoiceNumber, randomNum, invoiceTransNo}}
  • 失败 {code:500, msg:"<可理解原因>"}(统编格式非法 / 门店不可开票 / 已开票 / ezPay 失败原因)

GET /system/userOrder/getInvoice?orderId=

客户查看自己订单的发票状态。

响应 AjaxResult data:{invoiceStatus, invoiceNumber, invoiceUrl, category, ...}(不可开票门店返回 invoiceStatus=null 前端隐藏入口)。


二、管理端(商家端 + 平台后台)

PosOrderInvoiceController@RequestMapping("/system/orderInvoice"),权限键 chanting:orderInvoice:*)。

GET /system/orderInvoice/list

订单发票列表(分页 + 筛选)。

入参(query):orderId/orderNo/storeId/invoiceStatus/invoiceCategory/日期范围 + startPage()

响应 TableDataInfo,行 PosOrderInvoiceVo:订单号、门店、发票类型、买方、发票号、状态、金额、开立/作废时间。

GET /system/orderInvoice/{orderId}

订单发票详情。

PUT /system/orderInvoice/retry/{orderId}

重试开票(仅 invoiceStatus ∈ {2 失败, 3 作废})。权限 chanting:orderInvoice:retry

PUT /system/orderInvoice/invalid/{orderId}

作废发票(仅 invoiceStatus = 1 已开)。权限 chanting:orderInvoice:invalid。入参可选 {invalidReason}


三、ezPay 外部调用(复用工具类,非新接口)

业务 工具类方法 ezPay 端点
开立 EzPay.issueInvoice(baseUrl, cfg, postData) /Api/invoice_issue (v1.5)
作废 EzPay.doPost(baseUrl + URL_INVALID, cfg, postData) /Api/invoice_invalid (v1.0)

EzPayConfig 由门店 pos_store_ezpaymerchantId/hashKey/hashIv 构造;baseUrl 测试 BASE_TEST、正式 BASE_PROD(按环境配置)。

四、菜单 / 权限(写入 updatesql/sql.md)

sys_menu 新增「订单发票管理」(C) + 按钮权限 chanting:orderInvoice:list/query/retry/invalid,挂在平台后台门店/订单相关父菜单下(参考 009 菜单写法)。


五、捐赠发票 US6(LoveCode)

GET /system/userOrder/loveOrg/list(客户端,挂 UserOrderController)

捐赠机构列表,供客户端渲染选择器、选中回填 loveCode。

入参(query):keyword(按 org_name/org_short_name 模糊搜索,可空)、pageNum/pageSize(分页,默认 1/20)。

响应 TableDataInfo,行 {loveCode, orgName, orgShortName},仅返回 enabled=1

POST /system/userOrder/applyInvoice(捐赠场景)

DTO ApplyInvoiceDto 新增字段:

字段 类型 含义
loveCode String 捐赠码(3-7 位数字)。category=DONATION 时必填

捐赠场景必填/互斥

场景 loveCode carrierType/carrierNum buyerUbn buyerEmail
捐赠 ✅ 必填(3-7 位) ❌ 必须空(与载具互斥) ❌ 空 ❌ 空

请求示例:

{ "orderId": 1001, "category": "DONATION", "loveCode": "919" }

处理:校验 loveCode 格式 + 互斥 → 机构解析(local pos_love_org 命中取 orgName;未命中调 EzPay.checkLoveCode,IsExist=N 拒「捐赠码无效」)→ 组装 issue(ezPay Category=B2C、LoveCode、CarrierType 空、PrintFlag=N、BuyerName=orgName)→ 调 EzPay.issueInvoice → 落库 love_code/love_org_name + 发票号(码图三列为空)。

响应:成功 {code:200, data:{invoiceNumber, loveOrgName}};失败 {code:500, msg:"捐赠码无效/门店不可开票/ezPay 失败"}

ezPay checkLoveCode(US6 新增封装,复用工具类)

业务 工具类方法 ezPay 端点
捐赠码验真 EzPay.checkLoveCode(baseUrl, cfg, loveCode) /Api_inv_application/checkLoveCode (BDV v1.0)

仅 local 未命中时调用;回 IsExist Y/N(不回机构名)。BDV 请求需 CheckValue(已知手册勘误,见 plan.md US6 设计决策 5)。


六、客户端选择器对接说明(给客户端团队,US6)

客户在订单详情发起开票、选「捐赠发票」时的客户端对接流程:

1. 拉机构列表渲染选择器

用户进入捐赠开票 → 客户端调列表接口(带搜索关键词,远程分页):

GET /system/userOrder/loveOrg/list?keyword=創世&pageNum=1&pageSize=20

响应 TableDataInfo

{
  "code": 200,
  "rows": [
    { "loveCode": "919", "orgName": "財團法人創世社會福利基金會", "orgShortName": "創世基金會" }
  ],
  "total": 3
}

客户端据此渲染一个可搜索的下拉/列表(输入关键词 → 调接口 → 显示 orgShortNameorgName);用户选中某机构 → 自动回填 loveCode

2. 提交捐赠开票

用户选好机构(或手输捐赠码)后,提交开票,只带 loveCode,不带姓名/邮箱/载具/统编

POST /system/userOrder/applyInvoice
{ "orderId": 1001, "category": "DONATION", "loveCode": "919" }

成功响应 data.loveOrgName 为捐赠机构名(本地命中取简称/全名,未命中且 ezPay 验真通过则为「未知机构」)。

3. 交互要点

  • 字段互斥:选捐赠时载具/邮箱/统编输入框隐藏;后端校验若同时带 carrierTypeloveCode 会拒。
  • 捐赠码长度:3-7 位数字(ezPay 限制);前端输入框 maxlength=7,格式即时校验 ^\d{3,7}$
  • 手输兜底:用户手输一个清单里没有的码,后端会调 ezPay checkLoveCode 验真(Y 才放行),机构名显示「未知机构」。
  • 查询入口:提供一个「查机构捐赠码 ↗」链接跳财政部平台 https://www.einvoice.nat.gov.tw/portal/btc/btc603w,让用户找不在列表里的机构。
  • 开票后展示:捐赠发票无条码/二维码(PrintFlag=N),客户端详情页显示「已捐赠给 {loveOrgName}」+ 捐赠码,不要渲染码图。
  • 机构清单维护:清单由后端 pos_love_org 表提供(财政部 CSV 导入),客户端无需关心同步。