# 发票列表 page 结构调整(破坏性变更) > 变更类型:修改接口(破坏性) > 端类型:管理后台 > 日期:2026-06-23 | Issue:#4265 / #4271 | PR:#4267 / #4271 / #4272 | 服务:hl-order-service-v3 > **破坏性变更**:`tabCounts` 和 `stats` 两个字段的数据结构均已从扁平对象改为数组,前端 tab 角标读取、统计卡渲染、列表行新增字段**全部需要改动**,上线时前后端需同步发布。 --- > **勘误(2026-06-23,#4292 #4302)**:`invoiceType` 枚举已删除 `ELECTRONIC`(电子发票),当前系统只有 `VAT_NORMAL`(增值税普通发票)和 `VAT_SPECIAL`(增值税专用发票)两种类型。列表行 `invoiceType` 字段不会出现 `ELECTRONIC` 值,前端发票类型筛选下拉也只保留普票/专票两项。 > > **同步修正(截至 PR [#4311](https://git.1814.love:8443/wx/HL/pulls/4311) 收口)**:records 行 `amount` 及 stats `amount` 均改为 JSON number(单位**元**,如 `986.00`),原「字符串/单位分」已全部作废。关联 Issue [#4290](https://git.1814.love:8443/wx/HL/issues/4290) [#4310](https://git.1814.love:8443/wx/HL/issues/4310)。 ## 接口背景 财务发票管理列表本轮进行了三项结构升级: 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>`,分页对象顶层附带 `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` | number / null | 开票金额,JSON number,单位**元**(如 `986.00`);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` | number | 该维度金额,JSON number,单位**元**(如 `986.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` | 增值税专用发票 | --- ## 错误码 | 错误码 | 含义 | 触发场景 | |--------|------|---------| | `401` | 未授权 | 未携带有效 JWT | | `403` | 无权限 | 当前账号无发票管理权限 | | `400` | 参数错误 | `tab` 传入不存在的枚举值 | --- ## 示例 ### 典型成功(ALL tab,返回数组结构) 请求: ``` GET /v3/admin/order/invoice/page?tab=ALL&pageNo=1&pageSize=20 Authorization: Bearer ``` 响应: ```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": 986.00, "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 ``` 响应: ```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 ``` 响应: ```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` 为 JSON number,单位**元**(如 `986.00`),前端直接渲染,无需除以 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` 为 JSON number,单位**元**(如 `986.00`),前端直接渲染,无需除以 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 |