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 @@
+
+
+
+
+
+ {{ node.relationship_status_name || node.relationship_status }}
+
+
+ 关联主套餐使用记录:{{ node.master_usage_id }}
+
+
+
+
+ 购买时间
+ {{ formatDate(node.created_at) }}
+
+
+ 激活时间
+ {{ formatDate(node.activated_at) }}
+
+
+ 到期时间
+ {{ formatDate(node.expires_at) }}
+
+
+
+
+
+
+ 流量信息
+
+
+ 已使用
+ {{ getUsedFlow(node) }}
+
+
+ 总流量
+ {{ getTotalFlow(node) }}
+
+
+ 剩余
+ {{ getRemainFlow(node) }}
+
+
+
+
+
+
+
+
+
+
+ {{ expanded ? '收起关联加油包' : '展开关联加油包' }}
+ ({{ children.length }})
+
+
+
+
+
+
+
+
+
+
+
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/openspec/changes/update-asset-package-history-hierarchy/proposal.md b/openspec/changes/update-asset-package-history-hierarchy/proposal.md
new file mode 100644
index 0000000..67900b8
--- /dev/null
+++ b/openspec/changes/update-asset-package-history-hierarchy/proposal.md
@@ -0,0 +1,21 @@
+# Change: Update asset package history hierarchy
+
+## Why
+
+资产套餐历史接口已从平铺使用记录升级为主套餐与关联加油包的关系组。当前 H5 页面把每一条记录当作独立卡片,并以返回条数推断是否还有下一页,无法正确展示层级、默认展开状态和按关系组分页的结果。
+
+## What Changes
+
+- Consume `children` recursively and render a main-package card with its associated addon-package cards.
+- Honor `expand_by_default` for the initial expanded state and allow users to expand or collapse a group without losing any child records.
+- Preserve and render relationship metadata needed by the contract, including `master_usage_id` for addons and `relationship_status` / `relationship_status_name` for a `master_missing` standalone item.
+- Use the response pagination shell (`page`, `size`, `total`) as top-level relationship-group pagination; never infer completion from the number of returned nodes.
+- Keep the complete returned group when the API filter matches either its main package or one of its addons. The client will pass filters through unchanged and will not re-filter individual nodes.
+- Update the local API documentation to match the hierarchical response fields and pagination shell.
+
+## Impact
+
+- Affected capability: `asset-package-history-display` (new)
+- Affected code: `pages/asset-package-history/asset-package-history.vue`, `api/modules/asset.js`, `docs/API.md`
+- Affected API: `GET /api/c/v1/asset/package-history`
+- No backend filtering, relationship recovery, or error-code mapping is implemented in this H5 repository; the client continues to surface the API's unified error response through the shared request layer.
diff --git a/openspec/changes/update-asset-package-history-hierarchy/specs/asset-package-history-display/spec.md b/openspec/changes/update-asset-package-history-hierarchy/specs/asset-package-history-display/spec.md
new file mode 100644
index 0000000..4c2e5a9
--- /dev/null
+++ b/openspec/changes/update-asset-package-history-hierarchy/specs/asset-package-history-display/spec.md
@@ -0,0 +1,56 @@
+## ADDED Requirements
+
+### Requirement: Hierarchical package history display
+
+The H5 client SHALL render each item returned by `GET /api/c/v1/asset/package-history` as a top-level relationship group and SHALL render its recursively supplied `children` as associated addon-package entries without flattening, omitting, or moving them to another page.
+
+#### Scenario: Main package with addon children
+
+- **WHEN** a returned top-level package has one or more `children`
+- **THEN** the client displays the main package and all supplied children within the same relationship group
+- **AND** each node retains its own package, usage, status, and traffic-display fields
+
+#### Scenario: Relationship exception item
+
+- **WHEN** a top-level item has `relationship_status` equal to `master_missing`
+- **THEN** the client displays it as a standalone relationship-exception item
+- **AND** displays `relationship_status_name` when supplied
+
+### Requirement: Group expansion behavior
+
+The H5 client SHALL initialize a package group's visibility from `expand_by_default` and SHALL allow the customer to toggle groups that have children. Addon nodes SHALL retain their supplied `master_usage_id` metadata.
+
+#### Scenario: Default-expanded relationship group
+
+- **WHEN** a top-level group is returned with `expand_by_default` equal to true
+- **THEN** its children are visible on initial render
+
+#### Scenario: Customer expands a collapsed group
+
+- **WHEN** a top-level group with children is returned with `expand_by_default` equal to false and the customer activates its expand control
+- **THEN** every returned child in that group becomes visible
+- **AND** no additional API request is made to retrieve those children
+
+### Requirement: Relationship-group pagination
+
+The H5 client SHALL treat `page`, `size`, and `total` from the package-history response as pagination metadata for top-level relationship groups. It SHALL not use a child count or the number of returned nodes to decide whether more pages are available.
+
+#### Scenario: Last page has a full number of groups
+
+- **WHEN** the response contains `size` top-level groups and `page * size` is greater than or equal to `total`
+- **THEN** the client indicates that no more relationship groups are available
+
+#### Scenario: Filtered group response
+
+- **WHEN** the API returns a complete relationship group because either its main node or an addon node jointly matches the supplied `status` and `package_type` filter
+- **THEN** the client displays the complete returned group without applying node-level filtering
+
+### Requirement: Package history failure handling
+
+The H5 client SHALL rely on the shared request layer for non-success package-history responses, preserving the API-provided unified error message and authentication handling.
+
+#### Scenario: Related master cannot be read
+
+- **WHEN** the API returns a non-success response because a related master record exists but cannot be displayed
+- **THEN** the client does not synthesize a relationship item
+- **AND** the shared request layer displays the unified API error response
diff --git a/openspec/changes/update-asset-package-history-hierarchy/tasks.md b/openspec/changes/update-asset-package-history-hierarchy/tasks.md
new file mode 100644
index 0000000..7ad00b0
--- /dev/null
+++ b/openspec/changes/update-asset-package-history-hierarchy/tasks.md
@@ -0,0 +1,16 @@
+## 1. API contract and data handling
+
+- [x] 1.1 Document the hierarchical node fields, relationship exceptions, and top-level pagination shell for package history.
+- [x] 1.2 Add a client-side response normalizer that preserves recursively returned `children` and all relationship metadata.
+- [x] 1.3 Use `total` and returned top-level groups to drive pagination, so children never consume page capacity or cross pages.
+
+## 2. Package history UI
+
+- [x] 2.1 Render returned top-level relationship groups and their addon children with the existing package metrics and status styling.
+- [x] 2.2 Initialize groups from `expand_by_default` and add an accessible expand/collapse control for groups with children.
+- [x] 2.3 Render `master_missing` as a standalone relationship-exception item and preserve normal shared-request error handling for all other API failures.
+
+## 3. Verification
+
+- [x] 3.1 Verify nested, collapsed, expanded, filtered-group, `master_missing`, and multi-page response fixtures.
+- [x] 3.2 Run the H5 production build and inspect the changed files for contract consistency.
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 @@
-
@@ -10,169 +9,81 @@
当前账号下暂无套餐历史信息
-
-
-
-
-
-
- 激活时间
- {{ item.activated_at || '-' }}
-
-
- 到期时间
- {{ item.expires_at || '-' }}
-
-
-
-
-
-
- 流量信息
-
-
- 已使用
- {{ getUsedFlow(item) }}
-
-
- 总流量
- {{ getTotalFlow(item) }}
-
-
- 剩余
- {{ getRemainFlow(item) }}
-
-
-
-
-
-
-
-
+
+
-
-
+
加载中...
- 没有更多了
- 点击加载更多
+ 没有更多套餐
+ 点击加载更多