From 6a1543f9f68f2e4925aa703e1efafab036868fd9 Mon Sep 17 00:00:00 2001 From: luo Date: Tue, 8 Sep 2026 16:12:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A5=97=E9=A4=90=E5=88=86=E5=B1=82?= =?UTF-8?q?=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/modules/asset.js | 20 +- components/PackageHistoryNode.vue | 286 ++++++++++++++++++ docs/API.md | 97 +++++- .../asset-package-history.vue | 282 +++++------------ 4 files changed, 479 insertions(+), 206 deletions(-) create mode 100644 components/PackageHistoryNode.vue diff --git a/api/modules/asset.js b/api/modules/asset.js index 0dbe581..3ee3ad1 100644 --- a/api/modules/asset.js +++ b/api/modules/asset.js @@ -19,6 +19,24 @@ export const normalizeAssetInfo = (data = {}) => ({ : data.is_expiring === true || data.is_expiring === 1 }); +const normalizePackageHistoryNode = (node = {}) => ({ + ...node, + children: Array.isArray(node.children) + ? node.children.map(normalizePackageHistoryNode) + : [], + expand_by_default: node.expand_by_default === true || node.expand_by_default === 1 +}); + +export const normalizeAssetPackageHistory = (data = {}) => ({ + ...data, + items: Array.isArray(data.items) + ? data.items.map(normalizePackageHistoryNode) + : [], + page: Number(data.page), + size: Number(data.size), + total: Number(data.total) +}); + export const isAssetRealNameCompleted = (assetInfo = {}) => { if (assetInfo.asset_type === 'device') { return (assetInfo.cards || []).some((card) => Number(card?.real_name_status) === 1); @@ -50,7 +68,7 @@ export const assetApi = { url: '/api/c/v1/asset/package-history', method: 'GET', data: { identifier, page, page_size, ...params } - }); + }).then(normalizeAssetPackageHistory); }, getPackages(identifier, packageType = '') { diff --git a/components/PackageHistoryNode.vue b/components/PackageHistoryNode.vue new file mode 100644 index 0000000..2de3dfe --- /dev/null +++ b/components/PackageHistoryNode.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/docs/API.md b/docs/API.md index 7e8f8a1..b36e3ec 100644 --- a/docs/API.md +++ b/docs/API.md @@ -376,7 +376,102 @@ data: - wallet_balance:钱包余额(分) -## 3.2 资产套餐历史 +## 3.2 资产套餐历史(当前契约) + +URL: +GET /api/c/v1/asset/package-history + +更新说明(2026-09-08): +* 套餐历史已改为主套餐—加油包关系组。`items` 只包含顶层关系组;关联加油包位于所属主项的 `children` 中,子项绝不会跨页返回。 +* `total` 为筛选后的顶层关系组数量,分页壳使用 `page`、`size`、`total`;其中 `size` 表示每页顶层关系组数量。 +* 传入 `status` 和 `package_type` 时,两项必须由同一条使用记录联合命中;主项或任一子项命中时,接口均返回完整关系组,客户端不得对组内节点二次过滤。 +* 加油包通过 `master_usage_id` 标识关联主套餐使用记录。关联主套餐物理缺失时,节点以独立顶层项返回并设置 `relationship_status: "master_missing"`;关联主套餐存在但读取或展示失败时,接口按统一错误响应返回失败。 + +Query 参数: +- identifier(string,必填)- 资产标识符(SN/IMEI/虚拟号/ICCID/MSISDN),长度 1–50 +- package_type(string,可选)- 套餐类型(formal:正式套餐;addon:加油包) +- status(integer,可选)- 套餐状态(0:待生效;1:生效中;2:已用完;3:已过期;4:已失效) +- page(integer,必填)- 页码,最小为 1 +- page_size(integer,必填)- 每页顶层关系组数量,范围 1–100 + +成功响应: +```json +{ + "code": 0, + "msg": "success", + "timestamp": "2026-09-08T00:00:00Z", + "data": { + "items": [ + { + "activated_at": "2026-09-01T00:00:00Z", + "children": [ + { + "children": [], + "expand_by_default": false, + "master_usage_id": 5001, + "package_id": 1002, + "package_name": "5GB加油包", + "package_type": "addon", + "package_usage_id": 5002, + "status": 1, + "status_name": "生效中" + } + ], + "created_at": "2026-09-01T00:00:00Z", + "enable_virtual_data": false, + "expand_by_default": true, + "expires_at": "2026-09-30T23:59:59Z", + "master_usage_id": null, + "order_id": 0, + "package_id": 1001, + "package_name": "10GB月套餐", + "package_type": "formal", + "package_usage_id": 5001, + "priority": 1, + "real_total_mb": 10240, + "real_used_mb": 2048, + "reduction_pct": 0, + "status": 1, + "status_name": "生效中", + "usage_type": "single_card", + "virtual_total_mb": 10240, + "virtual_used_mb": 2048 + } + ], + "page": 1, + "size": 10, + "total": 1 + } +} +``` + +`data.items[]` 为递归节点,所有节点均可包含以下字段: +- activated_at(date-time,可空):激活时间 +- children(array):关联加油包或下级关联节点 +- created_at(date-time):购买创建时间 +- enable_virtual_data(boolean):是否启用虚流量 +- expand_by_default(boolean):是否默认展开 `children` +- expires_at(date-time,可空):到期时间 +- master_usage_id(integer,可空):关联主套餐使用记录 ID;普通主项为 `null` +- order_id(integer):历史兼容字段,始终输出零值 `0`,不填充真实订单 ID +- package_id、package_name、package_type、package_usage_id:套餐及使用记录标识;`package_type` 为 `formal` 或 `addon` +- priority:优先级 +- real_total_mb、real_used_mb:真实总量和真实已用量(MB) +- virtual_total_mb、virtual_used_mb:业务停机阈值和展示已用量(MB) +- reduction_pct:展示增幅比例 +- status、status_name:套餐状态及名称 +- usage_type:使用类型(single_card/device) +- relationship_status、relationship_status_name:关系异常状态及名称;仅在主套餐物理缺失时返回 `master_missing` + +错误响应: +- HTTP 400:请求参数错误 +- HTTP 401:未认证或认证已过期 +- HTTP 403:无权访问 +- HTTP 500:服务器内部错误,包括关联主套餐存在但读取或展示失败 + +以上失败场景均使用统一 `ErrorResponse`:`code`、`msg`、`timestamp`,可选 `data`。H5 通过共享请求层展示后端返回的 `msg`;401 同时清除登录状态并跳转登录页。 + +## 3.2 资产套餐历史(旧版,已废弃) URL: GET /api/c/v1/asset/package-history diff --git a/pages/asset-package-history/asset-package-history.vue b/pages/asset-package-history/asset-package-history.vue index 4647b4f..cd0b73c 100644 --- a/pages/asset-package-history/asset-package-history.vue +++ b/pages/asset-package-history/asset-package-history.vue @@ -1,6 +1,5 @@