# 发票详情接口（财务开票弹窗 / 详情页）

> 变更类型：新增接口
> 端类型：管理后台
> 日期：2026-06-23 | Issue：#4258 | PR：#4262 | 服务：hl-order-service-v3

---

> **勘误（2026-06-23，#4292 #4302）**：本文件初版（#4262）包含 3 处与最终实现不符的内容，已在本次修正：
> 1. `invoiceType` 枚举已删除 `ELECTRONIC`（电子发票），只保留 `VAT_NORMAL`（增值税普通发票）和 `VAT_SPECIAL`（增值税专用发票）
> 2. 出参已删除 `mailAddress` 字段（无纸质邮寄，全电子交付）
> 3. `email` 字段始终有值（全电子交付），不再是"电子发票为 null"
> 4. `amount` 字段改为 JSON number（单位**元**，如 `986.00`），原「字符串/单位分」均已作废；边界示例专票 `email` 由 null 改为始终有值（全电子交付）。关联 PR [#4311](https://git.1814.love:8443/wx/HL/pulls/4311)，Issue [#4290](https://git.1814.love:8443/wx/HL/issues/4290) [#4310](https://git.1814.love:8443/wx/HL/issues/4310)。

## 接口背景

财务在「上传发票 PDF」弹窗或发票详情页中，需要读取完整的开票申请明细：抬头信息、开票金额、专票四项（银行账号、注册地址、注册电话）、开票后的发票号 / PDF / 推送记录。本接口返回单张发票的全量字段，供弹窗回显和详情页展示使用。

---

## 变更清单

| # | 变更类型 | 说明 |
|---|---------|------|
| 1 | 新增接口 | `GET /v3/admin/order/invoice/{id}` 发票详情 |

---

## 接口详情

| 项 | 说明 |
|---|------|
| **方法 + 路径** | `GET /v3/admin/order/invoice/{id}` |
| **接口名** | 发票详情（财务开票弹窗 / 详情页） |
| **描述** | 返回单张发票的完整申请明细、状态、开票痕迹、推送痕迹及订单冗余信息 |
| **认证** | 管理后台 JWT（Bearer Token） |
| **幂等性** | 只读，天然幂等 |
| **限流** | 无特殊限流 |

---

## 接口入参

### 路径参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `id` | string（Long 雪花 ID） | 是 | 发票 ID，前端以字符串传输防精度丢失 |

### 请求体

无。

---

## 出参字段

返回结构：`Result<AdminInvoiceDetailRespVO>`

### 标识字段

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `id` | string | 发票 ID（雪花 ID，字符串） |
| `orderId` | string | 所属订单 ID |
| `orderNo` | string | 订单编号，如 `ORD2026062300001` |

### 开票申请明细

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `invoiceType` | string | 发票类型枚举值，见枚举节 |
| `invoiceTypeText` | string | 发票类型中文名，如「增值税普通发票」 |
| `titleType` | string | 抬头类型，`COMPANY` 或 `PERSONAL` |
| `titleName` | string | 发票抬头（企业名称或个人姓名） |
| `taxNo` | string / null | 税号，个人抬头为 null |
| `bankName` | string / null | 开户银行，**仅专票**有值，其余 null |
| `bankAccount` | string / null | 银行账号，**仅专票**有值，其余 null |
| `registAddress` | string / null | 注册地址，**仅专票**有值，其余 null |
| `registPhone` | string / null | 注册电话，**仅专票**有值，其余 null |
| `amount` | number | 开票金额，JSON number，单位**元**（如 `986.00`） |
| `email` | string | 收件邮箱（始终有值，全电子交付） |
| `applyReason` | string / null | 备注（申请原因） |

### 状态字段

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `status` | string | 发票状态枚举值，见枚举节 |
| `statusText` | string | 发票状态中文名，如「待开票」 |
| `requestedBy` | string / null | 申请人姓名 |
| `requestedAt` | string / null | 申请时间，ISO 8601，如 `"2026-06-20T14:30:00"` |

### 开票痕迹（ISSUED 后有值）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `invoiceNo` | string / null | 发票号码，开票后回填 |
| `fileUrl` | string / null | 发票 PDF 下载 URL（OSS 直链），开票后回填 |
| `pdfName` | string / null | PDF 文件名，如 `"invoice_202606230001.pdf"` |
| `pdfSize` | string / null | PDF 大小（字节数），字符串，如 `"204800"` |
| `issuedBy` | string / null | 开票操作人姓名 |
| `issuedAt` | string / null | 开票时间，ISO 8601 |

### 推送痕迹（PUSHED 后有值）

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `pushedAt` | string / null | 推送时间，ISO 8601 |
| `pushedChannels` | array(string) / null | 推送渠道列表，如 `["EMAIL","WECHAT"]`；未推送为 null |

### 订单冗余字段

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `productName` | string | 产品名称 |
| `tierName` | string / null | 产品档次名称 |
| `contactName` | string | 客户联系人姓名 |
| `customizerName` | string / null | 定制师姓名 |
| `departureDate` | string | 出发日期，格式 `YYYY-MM-DD` |

---

## 枚举 / 数据字典

### 发票类型（invoiceType）

| 枚举值 | 中文名 | 说明 |
|--------|--------|------|
| `VAT_NORMAL` | 增值税普通发票 | 普票，专票四项字段均为 null |
| `VAT_SPECIAL` | 增值税专用发票 | 专票，bankName / bankAccount / registAddress / registPhone 有值 |

### 抬头类型（titleType）

| 枚举值 | 中文名 |
|--------|--------|
| `COMPANY` | 企业 |
| `PERSONAL` | 个人 |

### 发票状态（status）

| 枚举值 | 中文名 | 说明 |
|--------|--------|------|
| `REQUESTED` | 待开票 | 申请已提交，财务未处理 |
| `ISSUED` | 已开票（待推送） | 财务已上传 PDF，尚未推送给客户 |
| `PUSHED` | 已推送 | 已推送给客户 |
| `VOIDED` | 已作废 | 该发票已作废 |

---

## 错误码

| 错误码 | 含义 | 触发场景 |
|--------|------|---------|
| `581500` | 发票不存在 | 传入的 `id` 在库中不存在或已软删除 |
| `401` | 未授权 | 未携带有效 JWT |
| `403` | 无权限 | 当前账号无发票管理权限 |

---

## 示例

### 典型成功（REQUESTED 状态，普票，全字段）

请求：
```
GET /v3/admin/order/invoice/1934567890123456789
Authorization: Bearer <token>
```

响应：
```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "id": "1934567890123456789",
    "orderId": "1920000000000000001",
    "orderNo": "ORD2026062300001",
    "invoiceType": "VAT_NORMAL",
    "invoiceTypeText": "增值税普通发票",
    "titleType": "COMPANY",
    "titleName": "呼籁科技有限公司",
    "taxNo": "91310000XXXXXXXXXX",
    "bankName": null,
    "bankAccount": null,
    "registAddress": null,
    "registPhone": null,
    "amount": 986.00,
    "email": "finance@hulalv.com",
    "applyReason": "报销使用",
    "status": "REQUESTED",
    "statusText": "待开票",
    "requestedBy": "张三",
    "requestedAt": "2026-06-20T14:30:00",
    "invoiceNo": null,
    "fileUrl": null,
    "pdfName": null,
    "pdfSize": null,
    "issuedBy": null,
    "issuedAt": null,
    "pushedAt": null,
    "pushedChannels": null,
    "productName": "云南香格里拉深度游5日",
    "tierName": "标准档",
    "contactName": "李四",
    "customizerName": "王五",
    "departureDate": "2026-07-10"
  }
}
```

### 边界情况（ISSUED 状态，含 fileUrl / invoiceNo，专票四项有值）

请求：
```
GET /v3/admin/order/invoice/1934567890123456790
Authorization: Bearer <token>
```

响应：
```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "id": "1934567890123456790",
    "orderId": "1920000000000000002",
    "orderNo": "ORD2026062200001",
    "invoiceType": "VAT_SPECIAL",
    "invoiceTypeText": "增值税专用发票",
    "titleType": "COMPANY",
    "titleName": "某某贸易有限公司",
    "taxNo": "91310000YYYYYYYYYY",
    "bankName": "工商银行上海支行",
    "bankAccount": "6222 0000 0000 0001",
    "registAddress": "上海市浦东新区XX路XX号",
    "registPhone": "021-12345678",
    "amount": 1500.00,
    "email": "finance@company.com",
    "applyReason": null,
    "status": "ISSUED",
    "statusText": "已开票（待推送）",
    "requestedBy": "赵六",
    "requestedAt": "2026-06-21T09:00:00",
    "invoiceNo": "20260623000001",
    "fileUrl": "https://oss.hulalv.com/invoice/202606/invoice_20260623000001.pdf",
    "pdfName": "invoice_20260623000001.pdf",
    "pdfSize": "204800",
    "issuedBy": "财务小陈",
    "issuedAt": "2026-06-22T16:00:00",
    "pushedAt": null,
    "pushedChannels": null,
    "productName": "西藏拉萨朝圣7日",
    "tierName": "尊享档",
    "contactName": "赵六",
    "customizerName": "孙七",
    "departureDate": "2026-08-01"
  }
}
```

### 业务失败（发票不存在）

请求：
```
GET /v3/admin/order/invoice/9999999999999999999
Authorization: Bearer <token>
```

响应：
```json
{
  "code": 581500,
  "msg": "发票不存在",
  "data": null
}
```

---

## 业务边界

**适用场景**
- 财务在「上传发票 PDF」弹窗打开时调用，回显完整申请明细
- 发票详情页查看全量开票及推送信息

**不适用场景**
- 批量查询多张发票（用列表接口 `GET /v3/admin/order/invoice/page`）
- 小程序端查询发票（本接口仅管理后台）

**特殊边界**
- 专票五项字段（`taxNo` / `bankName` / `bankAccount` / `registAddress` / `registPhone`）：taxNo 普票公司抬头也需填，专票四项仅 `invoiceType=VAT_SPECIAL` 时有值，前端按 invoiceType 决定是否展示
- 开票痕迹字段（`invoiceNo` / `fileUrl` / `pdfName` / `pdfSize` / `issuedBy` / `issuedAt`）：状态为 `ISSUED` 或 `PUSHED` 时有值，`REQUESTED` 和 `VOIDED` 时为 null
- 推送痕迹字段（`pushedAt` / `pushedChannels`）：仅 `PUSHED` 时有值
- `amount` 为 JSON number，单位**元**（如 `986.00`），前端直接渲染，无需除以 100

---

## 注意事项

1. `id` / `orderId` 为 Long 雪花 ID，前端必须以**字符串**接收，不可用 JS number 类型（会丢失精度）
2. `amount` 为 JSON number，单位**元**（如 `986.00`），前端直接渲染，无需除以 100
3. `pdfSize` 单位为**字节**，字符串格式，展示时自行换算为 KB / MB
4. `pushedChannels` 未推送时为 null，不是空数组 `[]`，前端判断时注意区分

---

## 关联 / 联系人

| 项 | 内容 |
|----|------|
| **Issue** | [#4258 发票详情接口](https://git.1814.love:8443/wx/HL/issues/4258) |
| **PR** | [#4262](https://git.1814.love:8443/wx/HL/pulls/4262) |
| **后端负责人** | yst |
