Date: 2026-06-16
后端 REST 接口契约。客户端开票挂在 UserOrderController;管理端(商家+平台)查看/重试/作废用独立 PosOrderInvoiceController。
挂在 UserOrderController(@RequestMapping("/system/userOrder"),已有 JWT 鉴权)。
/system/userOrder/applyInvoice客户对已完成订单申请开票。
请求体 ApplyInvoiceDto:
① 字段含义
| 字段 | 类型 | 含义 | 格式 / 示例 |
|---|---|---|---|
| orderId | Long | 要开票的订单 id | 必须是当前登录客户自己的订单,且已完成 state=3、已支付 payStatus=1 |
| category | String | 发票类型 | B2C=个人发票(开给个人);B2B=公司发票(开给营业人,凭统编报账) |
| 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 失败原因)/system/userOrder/getInvoice?orderId=客户查看自己订单的发票状态。
响应 AjaxResult data:{invoiceStatus, invoiceNumber, invoiceUrl, category, ...}(不可开票门店返回 invoiceStatus=null 前端隐藏入口)。
PosOrderInvoiceController(@RequestMapping("/system/orderInvoice"),权限键 chanting:orderInvoice:*)。
/system/orderInvoice/list订单发票列表(分页 + 筛选)。
入参(query):orderId/orderNo/storeId/invoiceStatus/invoiceCategory/日期范围 + startPage()。
响应 TableDataInfo,行 PosOrderInvoiceVo:订单号、门店、发票类型、买方、发票号、状态、金额、开立/作废时间。
/system/orderInvoice/{orderId}订单发票详情。
/system/orderInvoice/retry/{orderId}重试开票(仅 invoiceStatus ∈ {2 失败, 3 作废})。权限 chanting:orderInvoice:retry。
/system/orderInvoice/invalid/{orderId}作废发票(仅 invoiceStatus = 1 已开)。权限 chanting:orderInvoice:invalid。入参可选 {invalidReason}。
| 业务 | 工具类方法 | 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_ezpay 的 merchantId/hashKey/hashIv 构造;baseUrl 测试 BASE_TEST、正式 BASE_PROD(按环境配置)。
sys_menu 新增「订单发票管理」(C) + 按钮权限 chanting:orderInvoice:list/query/retry/invalid,挂在平台后台门店/订单相关父菜单下(参考 009 菜单写法)。