# 发票补齐 - 财务开票闭环 + 定制师代申请（管理后台）

> Issue: [#4172](https://git.1814.love:8443/wx/HL/issues/4172) / [#4173](https://git.1814.love:8443/wx/HL/issues/4173)
> PR: [#4179](https://git.1814.love:8443/wx/HL/pulls/4179) / [#4180](https://git.1814.love:8443/wx/HL/pulls/4180)
> 日期: 2026-06-21
> 服务: hl-order-service-v3（invoice 域）
> 端类型: 管理后台

---

## 1. 接口背景

order-v3 发票模块此前缺少财务侧开票操作和定制师代客申请入口。本次补齐：

- 财务开票闭环（PR2 / Issue #4172）：财务人员上传发票 PDF 至 OSS，完成开票（REQUESTED 变 ISSUED），支持重新上传覆盖；以及发票管理列表（分页 + Tab 统计 + 当月金额统计 + 关键词搜索）。
- 定制师代客申请（PR3 / Issue #4173）：定制师代用户提交开票申请，门槛与小程序端统一（订单 COMPLETED + 一单一票 + 申请金额不超订单总价）。
- 已有接口字段补全：GET /v3/admin/order/{id}/invoices 出参补充 8 个原来恒为 null 的字段。

---

## 2. 变更清单

| # | 接口 | 变更类型 | 说明 |
|---|------|----------|------|
| 1 | POST /v3/admin/order/invoice/upload | 新增接口 | 财务上传发票文件至 OSS，返回 fileUrl（不绑定发票记录） |
| 2 | PUT /v3/admin/order/invoice/{id}/issue | 新增接口 | 完成开票，状态 REQUESTED 变 ISSUED |
| 3 | PUT /v3/admin/order/invoice/{id}/reupload | 新增接口 | 重新上传覆盖文件（ISSUED / PUSHED 均可） |
| 4 | GET /v3/admin/order/invoice/page | 新增接口 | 发票管理列表（分页 + Tab 计数 + 当月统计 + 搜索） |
| 5 | POST /v3/admin/order/{orderId}/invoice/apply | 新增接口 | 定制师代客申请开票 |
| 6 | GET /v3/admin/order/{id}/invoices（已有） | 修改接口（字段补全） | 出参新增：fileUrl / pdfName / pdfSize / invoiceNo / issuedAt / issuedBy / requestedAt / requestedBy |

---

## 3. 接口详情

认证：所有接口走管理后台 JWT，Header 携带 Authorization: Bearer token。upload 使用 multipart/form-data，其余 application/json。
幂等性：upload 每次生成新 OSS 文件，无幂等。issue / reupload 依赖发票状态守卫，状态不符报业务错误。
限流：网关全局限流，无接口级特殊限流。

---

## 4. 接口入参

### 4.1 路径参数 / Query 参数

| 接口 | 参数名 | 类型 | 必填 | 说明 |
|------|--------|------|------|------|
| PUT /issue/{id} | id | Long | 是 | 发票记录 ID |
| PUT /reupload/{id} | id | Long | 是 | 发票记录 ID |
| GET /page | page | Integer | 否 | 页码，默认 1 |
| GET /page | pageSize | Integer | 否 | 每页条数，默认 20 |
| GET /page | tab | String | 否 | Tab 过滤，见 6 节，默认 ALL |
| GET /page | keyword | String | 否 | 关键词搜索（发票号 / 申请人） |
| GET /page | startDate | String | 否 | 申请开始日期 yyyy-MM-dd |
| GET /page | endDate | String | 否 | 申请结束日期 yyyy-MM-dd |
| POST /{orderId}/apply | orderId | Long | 是 | 路径参数，订单 ID |

### 4.2 请求体字段

**POST /v3/admin/order/invoice/upload**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | MultipartFile | 是 | 发票 PDF 文件（multipart/form-data） |

**PUT /v3/admin/order/invoice/{id}/issue**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| fileUrl | String | 是 | upload 接口返回的 OSS 地址 |
| pdfName | String | 否 | PDF 文件名（展示用） |
| pdfSize | Long | 否 | PDF 文件大小（字节） |
| invoiceNo | String | 否 | 发票号码 |

**PUT /v3/admin/order/invoice/{id}/reupload**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| fileUrl | String | 是 | 新发票 PDF 的 OSS 地址 |
| pdfName | String | 否 | 新 PDF 文件名 |
| pdfSize | Long | 否 | 新 PDF 文件大小（字节） |

**POST /v3/admin/order/{orderId}/invoice/apply**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| amount | String | 是 | 开票金额（字符串，单位元），不超过订单总价 |
| invoiceType | String | 是 | 发票类型，枚举见 6 节 |
| titleType | String | 是 | 抬头类型：PERSONAL / COMPANY |
| title | String | 是 | 发票抬头（姓名或公司名） |
| taxNo | String | 条件必填 | 税号（titleType=COMPANY 时必填） |
| bankName | String | 条件必填 | 开户行（invoiceType=VAT_SPECIAL 时必填） |
| bankAccount | String | 条件必填 | 银行账号（invoiceType=VAT_SPECIAL 时必填） |
| registAddress | String | 条件必填 | 注册地址（invoiceType=VAT_SPECIAL 时必填） |
| registPhone | String | 条件必填 | 注册电话（invoiceType=VAT_SPECIAL 时必填） |
| email | String | 条件必填 | 收票邮箱（invoiceType=ELECTRONIC 时必填） |
| remark | String | 否 | 备注 |

---

## 5. 出参字段

**POST /v3/admin/order/invoice/upload**

| 字段 | 类型 | 说明 |
|------|------|------|
| data | String | 上传成功后的 OSS 文件 URL，用于后续 issue / reupload 接口 |

**PUT issue 和 reupload：** 操作成功返回 HTTP 200，data 为 null。

**GET /v3/admin/order/invoice/page**

| 字段 | 类型 | 说明 |
|------|------|------|
| records | Array | 发票列表（见下表） |
| total | Long | 总记录数 |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
| tabCounts | Object | 各 Tab 计数（requestedCount / issuedCount / pushedCount） |
| stats | Object | 当月统计（currentMonthIssuedCount / currentMonthIssuedAmount） |

records 单条字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 发票 ID（Long 序列化） |
| orderId | String | 所属订单 ID（Long 序列化） |
| status | String | 发票状态枚举，见 6 节 |
| statusName | String | 状态中文名 |
| amount | String | 开票金额（字符串，单位元） |
| invoiceType | String | 发票类型枚举 |
| invoiceTypeName | String | 发票类型中文名 |
| titleType | String | 抬头类型 |
| title | String | 发票抬头 |
| taxNo | String | 税号 |
| fileUrl | String | 发票 PDF 地址（ISSUED / PUSHED 有值，其余 null） |
| pdfName | String | PDF 文件名 |
| pdfSize | Long | PDF 文件大小（字节） |
| invoiceNo | String | 发票号码 |
| issuedAt | String | 开票时间（ISO 8601） |
| issuedBy | String | 开票人姓名（财务真实姓名） |
| requestedAt | String | 申请时间（ISO 8601） |
| requestedBy | String | 申请人（定制师姓名；mp 端为 userId 字符串） |
| createTime | String | 记录创建时间 |

**POST /v3/admin/order/{orderId}/invoice/apply**

| 字段 | 类型 | 说明 |
|------|------|------|
| id | String | 新建发票 ID（Long 序列化） |
| status | String | 固定为 REQUESTED（申请后待开票） |

**GET /v3/admin/order/{id}/invoices（已有接口，字段补全）**

以下字段由恒 null 变为有值：fileUrl / pdfName / pdfSize / invoiceNo / issuedAt / issuedBy / requestedAt / requestedBy。字段含义同 /page 出参表。

---

## 6. 枚举 / 数据字典

**InvoiceStatus - 发票状态**

| 枚举值 | 中文名 | 说明 |
|--------|--------|------|
| REQUESTED | 待开票 | 用户 / 定制师已申请，等待财务处理 |
| ISSUED | 已开票 | 财务已完成开票并上传 PDF |
| PUSHED | 已推送 | 发票已推送给客户 |
| VOIDED | 已作废 | 发票已作废 |

**Tab 过滤值（GET /page 入参 tab 字段）**

| 值 | 说明 |
|----|------|
| ALL | 全部（默认） |
| REQUESTED | 仅待开票 |
| ISSUED | 仅已开票 |
| PUSHED | 仅已推送 |

**InvoiceType - 发票类型**

| 枚举值 | 中文名 |
|--------|--------|
| VAT_NORMAL | 增值税普通发票 |
| VAT_SPECIAL | 增值税专用发票 |
| ELECTRONIC | 电子发票 |

**TitleType - 抬头类型**

| 枚举值 | 中文名 |
|--------|--------|
| PERSONAL | 个人 |
| COMPANY | 公司 |

---

## 7. 错误码

| 错误码 | 常量 | 触发场景 |
|--------|------|----------|
| 581502 | INVOICE_CANNOT_ISSUE | 发票状态非 REQUESTED，不允许开票 |
| 581503 | INVOICE_CANNOT_REUPLOAD | 发票状态非 ISSUED / PUSHED，不允许重新上传 |
| 581504 | INVOICE_FILE_UPLOAD_FAILED | 文件上传失败（OSS 或 hl-user-service 不可用） |
| 581510 | INVOICE_ORDER_NOT_COMPLETED | 订单状态非 COMPLETED，不可申请开票 |
| 581511 | INVOICE_ALREADY_EXISTS | 该订单已有有效发票，不可重复申请（一单一票） |
| 581512 | INVOICE_AMOUNT_EXCEED | 开票金额超过订单总价 |
| 581513 | INVOICE_TYPE_INVALID | 发票类型枚举值非法 |
| 581514 | INVOICE_TAX_NO_REQUIRED | 公司抬头 / 专票时税号为必填 |
| 581515 | INVOICE_VAT_SPECIAL_FIELDS_REQUIRED | 专票时开户行 / 银行账号 / 注册地址 / 注册电话为必填 |
| 581516 | INVOICE_EMAIL_REQUIRED | 电子发票时邮箱为必填 |

---

## 8. 示例

### 8.1 典型成功：财务两步完成开票

Step 1 上传文件：

POST /v3/admin/order/invoice/upload，Content-Type: multipart/form-data，file 字段传 PDF 文件。

响应：



Step 2 完成开票：

PUT /v3/admin/order/invoice/1234567890123456789/issue，请求体：



响应：



### 8.2 边界情况：发票列表 tab=PUSHED 无记录

GET /v3/admin/order/invoice/page?tab=PUSHED&page=1&pageSize=20

响应：



### 8.3 业务失败：订单未完成，定制师代申请被拒

POST /v3/admin/order/9876543210987654321/invoice/apply，请求体：



订单状态为 CUSTOMIZING（定制中）时，响应：



---

## 9. 业务边界

**适用：**
- 财务开票：发票状态必须为 REQUESTED
- 重新上传：发票状态为 ISSUED 或 PUSHED
- 定制师代申请：订单状态必须为 COMPLETED（出行结束后）

**不适用：**
- 订单处于 PAID、CUSTOMIZING、TRAVELLING 等非 COMPLETED 状态
- 已存在有效发票（一单一票），不可重复申请
- 开票金额超过订单 orderAmount（产品原售价）

**特殊边界：**
- PUSHED 状态重新上传，状态自动降级为 ISSUED，需重新推送给客户
- 发票列表 NONE Tab（未申请的 COMPLETED 订单）当前未实现，传 tab=NONE 返回空集合

---

## 10. 修改前后对比

**GET /v3/admin/order/{id}/invoices 出参字段补全：**

| 字段名 | 变更前 | 变更后 |
|--------|--------|--------|
| fileUrl | 恒 null | ISSUED / PUSHED 时有值（OSS URL） |
| pdfName | 恒 null | 有值 |
| pdfSize | 恒 null | 有值（字节数） |
| invoiceNo | 恒 null | 有值（发票号码） |
| issuedAt | 恒 null | 有值（ISO 8601 时间戳） |
| issuedBy | 恒 null | 有值（开票人姓名） |
| requestedAt | 恒 null | 有值（申请时间） |
| requestedBy | 恒 null | 有值（申请人姓名） |

---

## 11. 影响评估 / 回滚

- 破坏兼容性：GET /{id}/invoices 字段由 null 变为有值，前端对 null 的防护代码可安全保留。新增 5 个接口对旧版本无影响。
- 前端同步上线：无强制要求，新接口按需接线，/invoices 字段补全向后兼容。
- 回滚方案：回滚旧版本，新接口返回 404，/invoices 出参回退为 null，已写入 DB 的开票信息保留不丢失。

---

## 12. 注意事项

1. upload 接口仅上传，不绑定发票：返回的 fileUrl 须在调用 issue / reupload 时传入，否则 OSS 文件闲置。
2. PUSHED 状态重新上传后降级为 ISSUED，前端应提示操作者需要重新推送给客户。
3. 专票五项（taxNo / bankName / bankAccount / registAddress / registPhone）缺任一即报 581515。
4. requestedBy 记录的是登录管理员真实姓名（来自网关注入的 X-Admin-RealName），不是用户姓名。
5. amount、currentMonthIssuedAmount 均为 String 类型，前端须按字符串接收，避免 JS 大数精度丢失。

---

## 13. 关联 / 联系人

- Issue（财务开票）: https://git.1814.love:8443/wx/HL/issues/4172
- Issue（定制师代申请）: https://git.1814.love:8443/wx/HL/issues/4173
- PR（财务开票 #4179）: https://git.1814.love:8443/wx/HL/pulls/4179
- PR（定制师代申请 #4180）: https://git.1814.love:8443/wx/HL/pulls/4180
- 后端负责人: yaosutu
