# 发票补齐 - 小程序端申请 + 可见性过滤 + 签名下载（小程序端）

> Issue: [#4174](https://git.1814.love:8443/wx/HL/issues/4174) / [#4182](https://git.1814.love:8443/wx/HL/issues/4182)
> PR: [#4181](https://git.1814.love:8443/wx/HL/pulls/4181)（mp 端申请+可见性）/ [#4184](https://git.1814.love:8443/wx/HL/pulls/4184)（签名下载）
> 日期: 2026-06-21
> 服务: hl-order-service-v3（invoice 域）
> 端类型: 小程序端

---

## 1. 接口背景

小程序发票模块本次补齐三项改动：

- **申请门槛收紧**（PR4 / Issue #4174）：原 collab 域 POST /v3/internal/mp/order/{orderId}/invoice/apply 路径不变，但实现从 collab 迁入 invoice 域，门槛由宽松（可叠加多张）改为严格（一单一票 + 订单必须 COMPLETED + 金额不超订单总价）。
- **出参可见性过滤**（PR4）：GET /v3/internal/mp/order/{orderId}/invoice/list 和 GET /v3/internal/mp/order/invoice/{invoiceId}/detail 出参中，fileUrl 字段现在仅在 ISSUED / PUSHED 状态时返回；REQUESTED 状态返回 null（处理中，前端勿展示 PDF 链接）。
- **新增签名下载端点**（#4184 / Issue #4182）：GET /v3/internal/mp/order/invoice/{invoiceId}/download，返回 1 小时有效期 OSS 签名 URL，供前端调 wx.openDocument 预览或下载发票 PDF。

---

## 2. 变更清单

| # | 接口 | 变更类型 | 说明 |
|---|------|----------|------|
| 1 | POST /v3/internal/mp/order/{orderId}/invoice/apply | 修改接口（行为收紧） | 申请门槛由宽松改严格：一单一票 + 订单 COMPLETED + 金额不超总价；resp status 字段值从 APPLIED 改为 REQUESTED |
| 2 | GET /v3/internal/mp/order/{orderId}/invoice/list | 修改接口（字段行为变化） | fileUrl 字段：REQUESTED 状态由有值改为 null；ISSUED / PUSHED 保持有值 |
| 3 | GET /v3/internal/mp/order/invoice/{invoiceId}/detail | 修改接口（字段行为变化） | fileUrl 字段同上：REQUESTED 返 null，ISSUED / PUSHED 有值 |
| 4 | GET /v3/internal/mp/order/invoice/{invoiceId}/download | 新增接口 | 返回发票 PDF 的 OSS 签名下载 URL（1 小时有效期），含 IDOR 鉴权 |

---

## 3. 接口详情

认证：所有接口走小程序 JWT 认证，Header 携带 Authorization: Bearer token（小程序 openId → userId）。
幂等性：apply 非幂等（一单一票，重复提交报 581511）；download 幂等（每次生成新签名 URL）。
限流：网关全局限流。

---

## 4. 接口入参

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

| 接口 | 参数名 | 类型 | 必填 | 说明 |
|------|--------|------|------|------|
| POST /{orderId}/apply | orderId | Long | 是 | 路径参数，订单 ID |
| GET /{orderId}/invoice/list | orderId | Long | 是 | 路径参数，订单 ID |
| GET /invoice/{invoiceId}/detail | invoiceId | Long | 是 | 路径参数，发票 ID |
| GET /invoice/{invoiceId}/download | invoiceId | Long | 是 | 路径参数，发票 ID |

### 4.2 请求体字段

**POST /v3/internal/mp/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 | 否 | 备注 |

GET 接口无请求体。

---

## 5. 出参字段

**POST /v3/internal/mp/order/{orderId}/invoice/apply**

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

注意：status 值此前旧实现返回 APPLIED（collab 域），新实现返回 REQUESTED（invoice 域）。请前端对齐新值。

**GET /v3/internal/mp/order/{orderId}/invoice/list（单条字段，变化部分）**

| 字段 | 原来行为 | 现在行为 |
|------|---------|---------|
| fileUrl | REQUESTED / ISSUED / PUSHED 均有值 | REQUESTED 返 null；ISSUED / PUSHED 有值 |

其余字段不变（id / orderId / status / statusName / amount / invoiceType / title / taxNo / createTime 等）。

**GET /v3/internal/mp/order/invoice/{invoiceId}/detail（变化部分）**

| 字段 | 原来行为 | 现在行为 |
|------|---------|---------|
| fileUrl | 同上，恒有值 | REQUESTED 返 null；ISSUED / PUSHED 有值 |

**GET /v3/internal/mp/order/invoice/{invoiceId}/download（新增接口）**

| 字段 | 类型 | 说明 |
|------|------|------|
| fileUrl | String | OSS 签名下载 URL（1 小时有效期，格式为 HTTPS 带签名参数的 URL） |
| pdfName | String | PDF 文件名（可选，用于 wx.openDocument 的 name 参数） |

---

## 6. 枚举 / 数据字典

**InvoiceStatus - 发票状态（影响 fileUrl 可见性）**

| 枚举值 | 中文名 | fileUrl 是否可见 |
|--------|--------|----------------|
| REQUESTED | 待开票 | null（处理中，前端勿展示 PDF 链接） |
| ISSUED | 已开票 | 有值 |
| PUSHED | 已推送 | 有值 |
| VOIDED | 已作废 | null |

**InvoiceType - 发票类型**

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

**TitleType - 抬头类型**

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

---

## 7. 错误码

| 错误码 | 常量 | 触发场景 |
|--------|------|----------|
| 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 | 电子发票时邮箱为必填 |
| 581517 | INVOICE_NOT_DOWNLOADABLE | 发票尚未开具（REQUESTED / VOIDED 状态），暂时无法下载 |
| 581518 | INVOICE_FORBIDDEN | 无权访问该发票（IDOR 防护，含发票不存在场景） |
| 581519 | INVOICE_SIGNED_URL_FAILED | 获取签名下载 URL 失败，请稍后重试（hl-user-service 暂时不可用） |

---

## 8. 示例

### 8.1 典型成功：申请开票

POST /v3/internal/mp/order/1234567890123456789/invoice/apply

请求体：


响应：


### 8.2 边界情况：列表中 REQUESTED 状态 fileUrl 为 null

GET /v3/internal/mp/order/1234567890123456789/invoice/list 响应示例（某张待开票发票）：


### 8.3 业务失败：REQUESTED 状态发票不可下载

GET /v3/internal/mp/order/invoice/111/download

响应：


---

## 9. 业务边界

**适用：**
- 申请：订单状态必须为 COMPLETED；无有效发票（一单一票）；金额不超订单总价
- 下载：发票状态为 ISSUED 或 PUSHED；且该发票属于当前登录用户的订单

**不适用：**
- 订单 COMPLETED 之前不可申请
- 已有有效发票，不可重复申请
- REQUESTED / VOIDED 状态发票不可下载（报 581517）
- 访问他人订单的发票报 581518（IDOR 防护，不返回 404 以避免泄露存在性）

**特殊边界：**
- 签名 URL 有效期 1 小时。前端如果缓存了 fileUrl，每次打开下载前须重新调 download 接口获取最新签名 URL，不可长期复用。
- wx.openDocument 下载后端直接返回签名 URL，前端无需中转。

---

## 10. 修改前后对比

### POST apply 申请门槛变化

| 项目 | 变更前（旧 collab 实现） | 变更后（invoice 域） |
|------|----------------------|-------------------|
| 一单一票限制 | 无（可叠加多张） | 有（existsActiveByOrderId 拦截） |
| 金额校验 | 已开+本次 <= 总价 | 本次金额 <= 总价（一单一票前提下等价） |
| 响应 status 值 | APPLIED | REQUESTED |

### GET list / detail fileUrl 可见性变化

| 发票状态 | 变更前 fileUrl | 变更后 fileUrl |
|---------|--------------|--------------|
| REQUESTED | 有值（OSS 公链） | null（前端显示处理中） |
| ISSUED | 有值 | 有值（不变） |
| PUSHED | 有值 | 有值（不变） |
| VOIDED | 有值 | null |

---

## 11. 影响评估 / 回滚

- 破坏兼容性（重要）：fileUrl 字段在 REQUESTED 状态下由有值变为 null。前端如果之前有展示 REQUESTED 状态下 PDF 链接的逻辑（点击打开 OSS 文件），现在会读到 null，需要做 null 判断，展示「处理中」状态文案。
- apply status 值变化：从 APPLIED 改为 REQUESTED。前端如果有对 status 值做 hardcode 判断（if status == APPLIED），需要更新。
- 前端同步上线建议：fileUrl null 判断 + apply 响应 status 对齐必须在本次版本同步更新。
- 回滚方案：回滚旧版本，fileUrl 可见性恢复，download 端点返回 404。apply 的 status 回退为 APPLIED（如旧版本有此值）。

---

## 12. 注意事项

1. fileUrl null 判断：list / detail 接口读到 fileUrl 为 null 时，前端应显示「处理中」或「待开票」而不是空链接。
2. 签名 URL 勿缓存超过 1 小时：download 返回的签名 URL 有效期 1 小时，前端每次用前须重新调接口，不可全局缓存复用。
3. apply 响应 status 值变更：旧值 APPLIED 已废弃，现在返回 REQUESTED。如前端有 switch/if 判断该值，须对齐。
4. download IDOR 防护：发票不存在或不属于当前用户均返回 581518（INVOICE_FORBIDDEN），不返回 404，避免存在性枚举攻击。

---

## 13. 关联 / 联系人

- Issue（小程序端 mp）: https://git.1814.love:8443/wx/HL/issues/4174
- Issue（签名下载）: https://git.1814.love:8443/wx/HL/issues/4182
- PR（mp 端 #4181）: https://git.1814.love:8443/wx/HL/pulls/4181
- PR（签名下载 #4184）: https://git.1814.love:8443/wx/HL/pulls/4184
- 后端负责人: yaosutu
