# 财务发票管理列表 - ALL tab 改订单视角（破坏性变更）

- **接口**：GET /v3/admin/order/invoice/page
- **变更类型**：修改接口 破坏性变更（tab=ALL 列表语义 + tabCounts.ALL 计数口径均变）
- **端类型**：管理后台
- **日期**：2026-06-24
- **Issue**：[#4337](https://git.1814.love:8443/wx/HL/issues/4337)
- **PR**：[#4338](https://git.1814.love:8443/wx/HL/pulls/4338)

---

## 1 接口背景

财务发票管理列表原先以发票记录为视角。ALL tab 只返回已有发票记录的订单行，tabCounts.ALL 计数只统计发票记录总数，导致：

- ALL 数量 < NONE（未申请）数量，前端展示出现矛盾
- ALL 列表无法让财务在一个页面纵览所有已完成订单的发票状态

本次将 ALL tab 切换为**订单视角**：以全部 COMPLETED（已完成）订单为数据源，有发票的行展示发票字段，无发票的行 status=NONE，对齐前端原型设计。

---

## 2 变更清单

| # | 变更项 | 变更前 | 变更后 |
|---|--------|--------|--------|
| 1 | tabCounts.ALL 计数口径 | 仅统计发票记录数（REQUESTED+ISSUED+PUSHED），不含未申请 | 统计全部 COMPLETED 订单数（含未申请），永远 >= 任何单个 tab |
| 2 | tab=ALL 列表数据源 | 只返回有发票记录的订单行 | 返回全部 COMPLETED 订单，无发票行 status=NONE |
| 3 | titleName/requestedBy 在 ALL/NONE tab 的行为 | 未定义（可能干扰结果） | 静默忽略（两字段仅在 REQUESTED/ISSUED/PUSHED 三个发票视角 tab 生效） |

---

## 3 接口详情

| 属性 | 值 |
|------|----|
| 方法 | GET |
| 路径 | /v3/admin/order/invoice/page |
| 描述 | 财务发票管理列表（分页），含 tab 计数与汇总统计 |
| 认证 | Bearer JWT（管理员） |
| 幂等性 | 只读，天然幂等 |
| 限流 | 无特殊限制 |

---

## 4 接口入参

### 4.1 Query 参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| tab | string | 否 | 当前 tab，枚举值见第 6 节；默认 ALL |
| pageNo | integer | 否 | 页码，默认 1 |
| pageSize | integer | 否 | 每页条数，默认 20 |
| keyword | string | 否 | 关键词搜索（订单号 / 产品名 / 联系人），所有 tab 均生效 |
| titleName | string | 否 | 发票抬头名称，**仅 REQUESTED/ISSUED/PUSHED tab 生效，ALL/NONE tab 静默忽略** |
| requestedBy | string | 否 | 申请人（定制师），**仅 REQUESTED/ISSUED/PUSHED tab 生效，ALL/NONE tab 静默忽略** |
| departureStartDate | string | 否 | 出发日期起，格式 YYYY-MM-DD |
| departureEndDate | string | 否 | 出发日期止，格式 YYYY-MM-DD |

### 4.2 请求体

无（GET 接口）

---

## 5 出参字段

响应外层结构：

```json
{
  "code": 200,
  "data": {
    "tabCounts": [...],
    "stats": [...],
    "records": [...],
    "total": 0,
    "pageNo": 1,
    "pageSize": 20
  }
}
```

### 5.1 tabCounts（5 项固定顺序）

| 字段 | 类型 | 说明 |
|------|------|------|
| code | string | tab 枚举值（见第 6 节） |
| name | string | tab 中文标签 |
| count | integer | 该 tab 对应的记录数 |

固定顺序：REQUESTED → ISSUED → PUSHED → NONE → ALL

> **ALL.count 语义已变**：现为全部 COMPLETED 订单数（含未申请）；之前仅含发票记录数。

### 5.2 stats（4 项固定顺序）

| 字段 | 类型 | 说明 |
|------|------|------|
| code | string | 枚举值（见第 6 节） |
| name | string | 统计项中文标签 |
| count | integer | 数量 |
| amount | number | 金额（元，JSON number） |

固定顺序：REQUESTED → ISSUED → PUSHED → CURRENT_MONTH_ISSUED

### 5.3 records 列表行（InvoicePageItemRespVO）

| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 发票记录 ID（雪花）；**无发票行为 null** |
| orderId | string | 订单 ID（雪花） |
| orderNo | string | 订单号 |
| productName | string | 产品名称 |
| tierName | string | 档期名称 |
| contactName | string | 联系人姓名 |
| customizerName | string | 定制师姓名 |
| departureDate | string | 出发日期，格式 YYYY-MM-DD |
| adultCount | integer | 成人数 |
| childCount | integer | 儿童数 |
| youngChildCount | integer | 婴幼儿数 |
| invoiceType | string | 发票类型枚举（见第 6 节）；无发票行为 null |
| invoiceTypeText | string | 发票类型中文；无发票行为 null |
| titleType | string | 抬头类型枚举（见第 6 节）；无发票行为 null |
| titleName | string | 抬头名称；无发票行为 null |
| taxNo | string | 税号；无发票行为 null |
| amount | number | 发票金额（元）；无发票行为 null |
| email | string | 接收邮箱；无发票行为 null |
| status | string | 发票状态枚举（见第 6 节）；**无发票行固定为 NONE** |
| statusText | string | 发票状态中文；**无发票行固定为 未申请** |
| requestedBy | string | 申请人（定制师）；无发票行为 null |
| requestedAt | string | 申请时间 ISO 8601；无发票行为 null |
| issuedAt | string | 开票时间 ISO 8601；无发票行为 null |
| issuedBy | string | 开票人；无发票行为 null |
| invoiceNo | string | 发票号；无发票行为 null |
| fileUrl | string | 发票文件 URL；无发票行为 null |
| pdfName | string | PDF 文件名；无发票行为 null |
| pdfSize | string | PDF 文件大小（字符串，如 128KB）；无发票行为 null |

---


## 6 枚举 / 数据字典

### 6.1 tab 枚举（查询参数 & tabCounts.code）

| code | name | 说明 |
|------|------|------|
| REQUESTED | 待开票 | 已申请、待财务开票 |
| ISSUED | 待推送 | 已开票、待推送给客户 |
| PUSHED | 已推送 | 已推送给客户 |
| NONE | 未申请 | 已完成订单但尚未申请发票 |
| ALL | 全部 | 订单视角：全部 COMPLETED 订单（含无发票行） |

### 6.2 stats.code 枚举

| code | name |
|------|------|
| REQUESTED | 待开票 |
| ISSUED | 已开票（待推送） |
| PUSHED | 已推送 |
| CURRENT_MONTH_ISSUED | 本月已开票 |

### 6.3 发票状态枚举（records.status）

| code | statusText | 说明 |
|------|------------|------|
| REQUESTED | 待开票 | 已申请 |
| ISSUED | 待推送 | 已开票 |
| PUSHED | 已推送 | 已推送 |
| NONE | 未申请 | tab=ALL/NONE 时无发票行专用 |

### 6.4 发票类型枚举（invoiceType）

| code | 说明 |
|------|------|
| ELECTRONIC | 电子发票 |
| PAPER | 纸质发票 |

### 6.5 抬头类型枚举（titleType）

| code | 说明 |
|------|------|
| PERSONAL | 个人 |
| COMPANY | 企业 |

---

## 7 错误码

| 错误码 | 含义 | 前端处理建议 |
|--------|------|-------------|
| 200 | 成功 | 正常渲染 |
| 401 | 未认证 | 跳登录 |
| 403 | 无权限 | 提示无访问权限 |
| 500 | 服务端异常 | 通用错误提示 |

---



## 8 示例

### 8.1 典型成功——tab=ALL，混合行（有发票 + 无发票）

**请求**
```
GET /v3/admin/order/invoice/page?tab=ALL&pageNo=1&pageSize=20
```

**响应**
```json
{
  "code": 200,
  "data": {
    "tabCounts": [
      {"code": "REQUESTED", "name": "待开票", "count": 3,},
      {"code": "ISSUED",    "name": "待推送", "count": 1,},
      {"code": "PUSHED",    "name": "已推送", "count": 5,},
      {"code": "NONE",      "name": "未申请", "count": 12,},
      {"code": "ALL",       "name": "全部",   "count": 21}
    ],
    "stats": [
      {"code": "REQUESTED",            "name": "待开票",     "count": 3,  "amount": 8800.00},
      {"code": "ISSUED",               "name": "已开票",     "count": 1,  "amount": 3200.00},
      {"code": "PUSHED",               "name": "已推送",     "count": 5,  "amount": 15600.00},
      {"code": "CURRENT_MONTH_ISSUED", "name": "本月已开票", "count": 4,  "amount": 12000.00}
    ],
    "records": [
      {
        "id": "1923456789012345678",
        "orderId": "1823456789012345678",
        "orderNo": "HL20260620001",
        "productName": "云南深度游7日",
        "tierName": "2026-07-01班期",
        "contactName": "张三",
        "customizerName": "李定制",
        "departureDate": "2026-07-01",
        "adultCount": 2,
        "childCount": 1,
        "youngChildCount": 0,
        "invoiceType": "ELECTRONIC",
        "invoiceTypeText": "电子发票",
        "titleType": "COMPANY",
        "titleName": "北京科技有限公司",
        "taxNo": "91110000123456789X",
        "amount": 6800.00,
        "email": "finance@example.com",
        "status": "REQUESTED",
        "statusText": "待开票",
        "requestedBy": "李定制",
        "requestedAt": "2026-06-20T10:30:00+08:00",
        "issuedAt": null,
        "issuedBy": null,
        "invoiceNo": null,
        "fileUrl": null,
        "pdfName": null,
        "pdfSize": null
      },
      {
        "id": null,
        "orderId": "1823456789012345679",
        "orderNo": "HL20260619002",
        "productName": "西藏圣地朝圣8日",
        "tierName": "2026-06-25班期",
        "contactName": "王五",
        "customizerName": "赵定制",
        "departureDate": "2026-06-25",
        "adultCount": 4,
        "childCount": 0,
        "youngChildCount": 0,
        "invoiceType": null,
        "invoiceTypeText": null,
        "titleType": null,
        "titleName": null,
        "taxNo": null,
        "amount": null,
        "email": null,
        "status": "NONE",
        "statusText": "未申请",
        "requestedBy": null,
        "requestedAt": null,
        "issuedAt": null,
        "issuedBy": null,
        "invoiceNo": null,
        "fileUrl": null,
        "pdfName": null,
        "pdfSize": null
      }
    ],
    "total": 21,
    "pageNo": 1,
    "pageSize": 20
  }
}
```

### 8.2 边界情况——tab=ALL 传了 titleName（被静默忽略）

**请求**
```
GET /v3/admin/order/invoice/page?tab=ALL&titleName=北京科技&pageNo=1&pageSize=20
```

**说明**：titleName 参数不参与过滤，接口正常返回全量 COMPLETED 订单分页。records 中仍会出现 status=NONE 的无发票行，响应结构与 8.1 相同，此处不重复。

### 8.3 业务失败——无已完成订单时 tab=ALL 返回空列表

**请求**
```
GET /v3/admin/order/invoice/page?tab=ALL&pageNo=1&pageSize=20
```

**响应**（无 COMPLETED 订单时）
```json
{
  "code": 200,
  "data": {
    "tabCounts": [
      {"code": "REQUESTED", "name": "待开票", "count": 0},
      {"code": "ISSUED",    "name": "待推送", "count": 0},
      {"code": "PUSHED",    "name": "已推送", "count": 0},
      {"code": "NONE",      "name": "未申请", "count": 0},
      {"code": "ALL",       "name": "全部",   "count": 0}
    ],
    "stats": [
      {"code": "REQUESTED",            "name": "待开票",     "count": 0, "amount": 0},
      {"code": "ISSUED",               "name": "已开票",     "count": 0, "amount": 0},
      {"code": "PUSHED",               "name": "已推送",     "count": 0, "amount": 0},
      {"code": "CURRENT_MONTH_ISSUED", "name": "本月已开票", "count": 0, "amount": 0}
    ],
    "records": [],
    "total": 0,
    "pageNo": 1,
    "pageSize": 20
  }
}
```

---

## 9 业务边界

**适用**：
- 订单状态为 COMPLETED（已完成）的订单才出现在 ALL/NONE tab
- REQUESTED/ISSUED/PUSHED 三个 tab 仍以发票记录为视角，只返回有对应状态发票的订单行

**不适用**：
- 进行中（未完成）订单不出现在任何 tab
- 已取消订单不出现

**特殊边界**：
- ALL tab 下：tabCounts.ALL.count = REQUESTED + ISSUED + PUSHED + NONE，四者无重叠
- 同一订单只有一张有效发票记录，不会重复出现
- titleName/requestedBy 在 ALL/NONE tab 传入后静默忽略，不报错、不影响结果

---

## 10 修改前后对比

### 字段级对比

| 字段 | 变更前 | 变更后 |
|------|--------|--------|
| tabCounts.ALL.count | 仅 REQUESTED+ISSUED+PUSHED 发票记录数之和 | 全部 COMPLETED 订单数（含无发票订单） |
| records（tab=ALL 时） | 仅返回有发票记录的订单行，无 status=NONE 行 | 返回全部 COMPLETED 订单，无发票行 status=NONE |
| records[].id（tab=ALL 时） | 所有行均有值 | 无发票行为 null |
| records[].amount（tab=ALL 时） | 所有行均有值 | 无发票行为 null |

### tab=ALL 计数对比（假设 2 张发票 + 12 个未申请）

变更前：
```
ALL.count = 2   （仅发票记录，ALL < NONE，矛盾）
NONE.count = 12
```

变更后：
```
ALL.count = 14  （= 2 + 12，ALL >= NONE，正确）
NONE.count = 12
```

---

## 11 影响评估 / 回滚

**破坏兼容性**：是

- 前端若假设 tab=ALL 列表行的 id 一定不为 null，需修改：无发票行 id 为 null
- 前端若用 id !== null 判断是否显示开票按钮，需改为判断 status 是否为 NONE
- tabCounts.ALL.count 现在总是 >= 之前的值，若前端有基于此的断言需同步更新
- stats 块不受影响，无需改动

**前端同步上线**：建议与本次后端部署同期上线，避免显示数据矛盾窗口期。

**回滚方案**：回滚后端至上一个版本 jar 即可恢复旧行为；若前端已适配新结构则需同步回滚前端。

---

## 12 注意事项

1. **无发票行的 id 字段为 null**，前端不可用 id 做有无发票判断，应改用 status 字段值是否为 NONE。
2. **amount 字段类型为 JSON number（元）**，不是字符串，渲染时直接用数值格式化。
3. **titleName/requestedBy 在 ALL/NONE tab 下静默忽略**，搜索框可保留，后端不过滤，行为符合预期。
4. **stats 块不受 tab 切换影响**，始终返回全局四项汇总统计，与当前 tab 无关。
5. **tabCounts 固定 5 项、顺序不变**，前端可按 code 匹配或按索引渲染。

---

## 13 关联 / 联系人

- **Issue**：[#4337 发票管理列表 ALL tab 改订单视角](https://git.1814.love:8443/wx/HL/issues/4337)
- **PR**：[#4338](https://git.1814.love:8443/wx/HL/pulls/4338)
- **后端负责人**：腰苏图（yaosutu）
