# 发票列表 page 结构调整（破坏性变更）

> 变更类型：修改接口（破坏性）
> 端类型：管理后台
> 日期：2026-06-23 | Issue：#4265 / #4271 | PR：#4267 / #4271 / #4272 | 服务：hl-order-service-v3

> **破坏性变更**：`tabCounts` 和 `stats` 两个字段的数据结构均已从扁平对象改为数组，前端 tab 角标读取、统计卡渲染、列表行新增字段**全部需要改动**，上线时前后端需同步发布。

---

## 接口背景

财务发票管理列表本轮进行了三项结构升级：

1. **列表行**新增订单冗余字段（产品、出行人数、定制师等），让财务在列表页即可看到关键订单信息
2. **新增 NONE tab**（未申请），支持代客申请场景——展示已完成但尚未申请发票的订单
3. **`tabCounts` 和 `stats` 从扁平对象改为数组**，便于前端遍历渲染、后端动态扩展 tab 和统计维度

---

## 变更清单

| # | 变更类型 | 具体内容 |
|---|---------|---------|
| 1 | 出参新增字段 | `records[]` 行新增 `orderNo`（补回填）、`productName`、`tierName`、`contactName`、`customizerName`、`departureDate`、`adultCount`、`childCount`、`youngChildCount` 共 9 个字段 |
| 2 | 入参新增枚举值 | `tab` 参数新增 `NONE`（未申请），返回「已完成且无有效发票」的订单行 |
| 3 | 出参结构破坏性变更 | `tabCounts` 从扁平对象改为数组 TabCountItem[] |
| 4 | 出参结构破坏性变更 | `stats` 从扁平对象改为数组 StatItem[] |

---

## 接口详情

| 项 | 说明 |
|---|------|
| **方法 + 路径** | `GET /v3/admin/order/invoice/page` |
| **接口名** | 财务发票管理列表（分页） |
| **描述** | 分页查询发票列表，支持多 tab 过滤、统计卡汇总、tab 计数角标 |
| **认证** | 管理后台 JWT（Bearer Token） |
| **幂等性** | 只读，天然幂等 |
| **限流** | 无特殊限流 |

---

## 接口入参

### Query 参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `tab` | string | 否 | Tab 过滤，见枚举节；默认 `ALL`；**本次新增 `NONE`** |
| `pageNo` | integer | 否 | 页码，默认 1 |
| `pageSize` | integer | 否 | 每页条数，默认 20，最大 100 |
| `keyword` | string | 否 | 关键词搜索（订单号、客户名、定制师名） |
| `invoiceType` | string | 否 | 发票类型过滤，见枚举 |
| `startDate` | string | 否 | 申请日期起，格式 `YYYY-MM-DD` |
| `endDate` | string | 否 | 申请日期止，格式 `YYYY-MM-DD` |

### 请求体

无。

---

## 出参字段

返回结构：`Result<PageResult<AdminInvoicePageRespVO>>`，分页对象顶层附带 `tabCounts`（数组）和 `stats`（数组）。

### 分页包装层

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `total` | integer | 总条数 |
| `pageNo` | integer | 当前页码 |
| `pageSize` | integer | 每页大小 |
| `records` | array | 当前页数据行，见下表 |
| `tabCounts` | array | 各 tab 计数数组（**已改为数组**），见 TabCountItem |
| `stats` | array | 统计卡汇总数组（**已改为数组**），见 StatItem |

### records[] 行字段

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `id` | string / null | 发票 ID；**NONE tab 行为 null** |
| `orderId` | string | 订单 ID |
| `orderNo` | string | 订单编号（本次补充回填，原定义但无值） |
| `invoiceType` | string / null | 发票类型；NONE tab 行为 null |
| `invoiceTypeText` | string / null | 发票类型中文名；NONE tab 行为 null |
| `titleName` | string / null | 发票抬头；NONE tab 行为 null |
| `amount` | string / null | 开票金额（分），字符串；NONE tab 行为 null |
| `status` | string | 发票状态；NONE tab 行固定为 `"NONE"` |
| `statusText` | string | 发票状态中文名；NONE tab 行固定为 `"未申请"` |
| `requestedAt` | string / null | 申请时间；NONE tab 行为 null |
| `productName` | string | 产品名称（新增） |
| `tierName` | string / null | 产品档次名称（新增） |
| `contactName` | string | 客户联系人姓名（新增） |
| `customizerName` | string / null | 定制师姓名（新增） |
| `departureDate` | string | 出发日期，格式 `YYYY-MM-DD`（新增） |
| `adultCount` | integer | 成人数（新增） |
| `childCount` | integer | 儿童数（新增） |
| `youngChildCount` | integer | 婴儿数（新增） |

> 出行人数展示：将 `adultCount` / `childCount` / `youngChildCount` 三值组合为「2成人1儿童」等文案，数量为 0 的维度可省略。

### TabCountItem 字段

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `code` | string | Tab 枚举值 |
| `name` | string | Tab 中文名 |
| `count` | integer | 该 tab 下的条数 |

### StatItem 字段

| 字段名 | 类型 | 说明 |
|--------|------|------|
| `code` | string | 统计维度枚举值 |
| `name` | string | 统计维度中文名（直接渲染，无需前端自行拼接） |
| `count` | integer | 该维度数量 |
| `amount` | string | 该维度金额，单位分，字符串，保留两位小数，如 `"98600.00"` |

---
## 枚举 / 数据字典

### tab 入参枚举（含新增 NONE）

| 枚举值 | 说明 | records 行特征 |
|--------|------|--------------|
| `REQUESTED` | 待开票 | 正常发票行，发票字段有值 |
| `ISSUED` | 待推送（已开票） | 正常发票行，fileUrl 有值 |
| `PUSHED` | 已推送 | 正常发票行，pushedAt 有值 |
| `NONE` | 未申请（新增） | 订单行，id / invoiceType 等发票字段均为 null，status="NONE" |
| `ALL` | 全部 | 混合行，包含 NONE 行和发票行 |

### tabCounts 数组 code 枚举

| code | name |
|------|------|
| `REQUESTED` | 待开票 |
| `ISSUED` | 待推送 |
| `PUSHED` | 已推送 |
| `NONE` | 未申请 |
| `ALL` | 全部 |

### stats 数组 code 枚举

| code | name | 备注 |
|------|------|------|
| `REQUESTED` | 待开票 | 待处理发票的金额汇总 |
| `ISSUED` | 已开票·待推送 | 含义与 tabCounts 中 ISSUED 相同，语境不同 |
| `PUSHED` | 已推送 | 已推送给客户的金额汇总 |
| `CURRENT_MONTH_ISSUED` | 本月已开票 | 当月开票汇总（跨 ISSUED + PUSHED） |

> `ISSUED` 在 tabCounts 中文名是「待推送」，在 stats 中文名是「已开票·待推送」，含义相同但语境不同。后端按语境在 `name` 字段直接返回正确文案，前端**直接渲染 `name`，不要按 code 自行拼文案**。

### 发票类型（invoiceType）

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

---

## 错误码

| 错误码 | 含义 | 触发场景 |
|--------|------|---------|
| `401` | 未授权 | 未携带有效 JWT |
| `403` | 无权限 | 当前账号无发票管理权限 |
| `400` | 参数错误 | `tab` 传入不存在的枚举值 |

---
## 示例

### 典型成功（ALL tab，返回数组结构）

请求：
```
GET /v3/admin/order/invoice/page?tab=ALL&pageNo=1&pageSize=20
Authorization: Bearer <token>
```

响应：
```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "total": 12,
    "pageNo": 1,
    "pageSize": 20,
    "records": [
      {
        "id": "1934567890123456789",
        "orderId": "1920000000000000001",
        "orderNo": "ORD2026062300001",
        "invoiceType": "VAT_NORMAL",
        "invoiceTypeText": "增值税普通发票",
        "titleName": "呼籁科技有限公司",
        "amount": "98600",
        "status": "REQUESTED",
        "statusText": "待开票",
        "requestedAt": "2026-06-20T14:30:00",
        "productName": "云南香格里拉深度游5日",
        "tierName": "标准档",
        "contactName": "李四",
        "customizerName": "王五",
        "departureDate": "2026-07-10",
        "adultCount": 2,
        "childCount": 1,
        "youngChildCount": 0
      }
    ],
    "tabCounts": [
      { "code": "REQUESTED", "name": "待开票", "count": 3 },
      { "code": "ISSUED", "name": "待推送", "count": 2 },
      { "code": "PUSHED", "name": "已推送", "count": 2 },
      { "code": "NONE", "name": "未申请", "count": 5 },
      { "code": "ALL", "name": "全部", "count": 12 }
    ],
    "stats": [
      { "code": "REQUESTED", "name": "待开票", "count": 3, "amount": "1000.00" },
      { "code": "ISSUED", "name": "已开票·待推送", "count": 2, "amount": "98600.00" },
      { "code": "PUSHED", "name": "已推送", "count": 2, "amount": "50000.00" },
      { "code": "CURRENT_MONTH_ISSUED", "name": "本月已开票", "count": 4, "amount": "148600.00" }
    ]
  }
}
```

### 边界情况（NONE tab，发票字段为 null 的订单行）

请求：
```
GET /v3/admin/order/invoice/page?tab=NONE&pageNo=1&pageSize=20
Authorization: Bearer <token>
```

响应：
```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "total": 5,
    "pageNo": 1,
    "pageSize": 20,
    "records": [
      {
        "id": null,
        "orderId": "1920000000000000099",
        "orderNo": "ORD2026062200099",
        "invoiceType": null,
        "invoiceTypeText": null,
        "titleName": null,
        "amount": null,
        "status": "NONE",
        "statusText": "未申请",
        "requestedAt": null,
        "productName": "西藏拉萨朝圣7日",
        "tierName": null,
        "contactName": "钱八",
        "customizerName": "孙九",
        "departureDate": "2026-06-15",
        "adultCount": 4,
        "childCount": 0,
        "youngChildCount": 1
      }
    ],
    "tabCounts": [
      { "code": "REQUESTED", "name": "待开票", "count": 3 },
      { "code": "ISSUED", "name": "待推送", "count": 2 },
      { "code": "PUSHED", "name": "已推送", "count": 2 },
      { "code": "NONE", "name": "未申请", "count": 5 },
      { "code": "ALL", "name": "全部", "count": 12 }
    ],
    "stats": [
      { "code": "REQUESTED", "name": "待开票", "count": 3, "amount": "1000.00" },
      { "code": "ISSUED", "name": "已开票·待推送", "count": 2, "amount": "98600.00" },
      { "code": "PUSHED", "name": "已推送", "count": 2, "amount": "50000.00" },
      { "code": "CURRENT_MONTH_ISSUED", "name": "本月已开票", "count": 4, "amount": "148600.00" }
    ]
  }
}
```

### 业务失败（tab 枚举值不存在）

请求：
```
GET /v3/admin/order/invoice/page?tab=INVALID
Authorization: Bearer <token>
```

响应：
```json
{
  "code": 400,
  "msg": "参数错误：tab 枚举值不合法",
  "data": null
}
```

---
## 业务边界

**适用场景**
- 财务管理后台「发票管理」列表页
- 代客申请场景：切换到 NONE tab 查看「已完成但未申请发票」的订单，点击进入代客申请流程

**不适用场景**
- 小程序端查看发票状态（走小程序端专属接口）
- 查询单张发票全量明细（走 `GET /v3/admin/order/invoice/{id}` 详情接口）

**特殊边界**
- NONE tab 行是订单行，不是发票行：`id` 为 null，发票字段均为 null，`status` 固定为字符串 `"NONE"`，前端需按 status 判断是否展示发票操作按钮
- ALL tab 混合 NONE 行和发票行：遍历 records 时需按 `status === "NONE"` 区分行类型
- `amount` 单位为分，字符串格式，前端展示时除以 100 转为元

---

## 修改前后对比

### tabCounts 字段

旧（扁平对象）：
```json
{
  "tabCounts": {
    "requestedCount": 3,
    "issuedCount": 2,
    "pushedCount": 2,
    "allCount": 12,
    "noneCount": 5
  }
}
```

新（数组，现行）：
```json
{
  "tabCounts": [
    { "code": "REQUESTED", "name": "待开票", "count": 3 },
    { "code": "ISSUED", "name": "待推送", "count": 2 },
    { "code": "PUSHED", "name": "已推送", "count": 2 },
    { "code": "NONE", "name": "未申请", "count": 5 },
    { "code": "ALL", "name": "全部", "count": 12 }
  ]
}
```

### stats 字段

旧（扁平对象）：
```json
{
  "stats": {
    "requestedCount": 3,
    "requestedAmount": 1000,
    "issuedCount": 2,
    "issuedAmount": 98600,
    "pushedCount": 2,
    "pushedAmount": 50000,
    "currentMonthIssuedCount": 4,
    "currentMonthIssuedAmount": 148600
  }
}
```

新（数组，现行）：
```json
{
  "stats": [
    { "code": "REQUESTED", "name": "待开票", "count": 3, "amount": "1000.00" },
    { "code": "ISSUED", "name": "已开票·待推送", "count": 2, "amount": "98600.00" },
    { "code": "PUSHED", "name": "已推送", "count": 2, "amount": "50000.00" },
    { "code": "CURRENT_MONTH_ISSUED", "name": "本月已开票", "count": 4, "amount": "148600.00" }
  ]
}
```

### tab 入参新增值

| 旧可选值 | 新增值 |
|---------|--------|
| REQUESTED / ISSUED / PUSHED / ALL | 新增 `NONE`（未申请） |

### records[] 行新增字段

| 字段 | 变化 |
|------|------|
| `orderNo` | 原已定义，本次补充回填（实际有值） |
| `productName` | 新增 |
| `tierName` | 新增 |
| `contactName` | 新增 |
| `customizerName` | 新增 |
| `departureDate` | 新增 |
| `adultCount` | 新增 |
| `childCount` | 新增 |
| `youngChildCount` | 新增 |

---

## 影响评估 / 回滚

### 破坏兼容性评估

| 模块 | 是否影响 | 必须改动 |
|------|---------|---------|
| Tab 角标数字渲染 | 是 | 旧代码读 `tabCounts.requestedCount` 等扁平字段会 undefined，需改为遍历数组按 `code` 取值 |
| 统计卡渲染 | 是 | 旧代码读 `stats.requestedCount` / `stats.requestedAmount` 等扁平字段会 undefined，需改为遍历数组按 `code` 取 `count` / `amount` |
| 列表行渲染 | 需新增 | 新增 9 个字段需接线渲染（产品名、出行人数组合、定制师、出发日期等） |
| NONE tab 处理 | 需新增 | NONE tab 下行无发票 `id`，代客申请按钮需按 `status==="NONE"` 判断 |
| ALL tab 混合行 | 需新增 | ALL tab 下需区分 NONE 行和正常发票行 |

### 前端同步上线要求

本次变更前后端需同步发布。旧前端读新后端的数组结构，tab 角标和统计卡均会渲染为空。

### 回滚方案

如需回滚后端，通知前端同步回滚对应代码；两端数据结构不匹配会导致 tab 角标消失和统计卡白屏。

---

## 注意事项

1. `tabCounts` 和 `stats` **均已变为数组**，旧的扁平对象键名已全部废弃，前端读取方式必须更新
2. NONE tab 行中 `status` 是字符串 `"NONE"`，而不是 null，前端用 `status === "NONE"` 判断行类型
3. `stats` 中的 `amount` 单位为**分**，字符串格式，保留两位小数，前端展示时除以 100 转为元
4. `ISSUED` 在 tabCounts 和 stats 中 `name` 不同，直接渲染 `name` 字段即可，不要自行拼接
5. `adultCount` / `childCount` / `youngChildCount` 为整数，0 的维度建议展示时省略

---

## 关联 / 联系人

| 项 | 内容 |
|----|------|
| **Issue** | [#4265 发票列表 NONE tab](https://git.1814.love:8443/wx/HL/issues/4265) / [#4271 列表订单冗余字段](https://git.1814.love:8443/wx/HL/issues/4271) |
| **PR** | [#4267](https://git.1814.love:8443/wx/HL/pulls/4267) / [#4271](https://git.1814.love:8443/wx/HL/pulls/4271) / [#4272 stats 拍平](https://git.1814.love:8443/wx/HL/pulls/4272) |
| **后端负责人** | yst |
