diff --git a/internal/service/asset/service.go b/internal/service/asset/service.go index d9145af..62dbc5f 100644 --- a/internal/service/asset/service.go +++ b/internal/service/asset/service.go @@ -346,7 +346,7 @@ func (s *Service) buildCardResolveResponse(ctx context.Context, card *model.IotC // fillPackageInfo 填充当前主套餐信息到响应中 func (s *Service) fillPackageInfo(ctx context.Context, resp *dto.AssetResolveResponse, carrierType string, carrierID uint) { - usage, err := s.packageUsageStore.GetActiveMainPackage(ctx, carrierType, carrierID) + usage, err := s.packageUsageStore.GetCurrentMainPackage(ctx, carrierType, carrierID) if err != nil || usage == nil { return } @@ -892,7 +892,7 @@ func buildAssetPackageHistoryNode(node *assetquery.PackageHistoryNode, packageMa return item } -// GetCurrentPackage 获取资产当前生效的主套餐 +// GetCurrentPackage 获取资产当前可展示的主套餐 // callerAccountType: 调用方账号类型,"platform" 时返回成本价(paid_amount),其他类型不返回 func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id uint, callerAccountType string) (*dto.AssetPackageResponse, error) { carrierType := assetType @@ -900,14 +900,14 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui carrierType = "iot_card" } - usage, err := s.packageUsageStore.GetActiveMainPackage(ctx, carrierType, id) + usage, err := s.packageUsageStore.GetCurrentMainPackage(ctx, carrierType, id) if err != nil { - // 记录不存在表示当前无生效套餐,属于正常业务状态,返回 nil 而非错误 + // 记录不存在表示当前无套餐,属于正常业务状态,返回 nil 而非错误 // 其他 DB 错误才作为服务端错误处理 if stderrors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } - return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询当前生效套餐失败") + return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询当前套餐失败") } return s.buildAssetPackageResponse(ctx, usage, callerAccountType), nil diff --git a/internal/store/postgres/package_usage_store.go b/internal/store/postgres/package_usage_store.go index e82c349..79ee00e 100644 --- a/internal/store/postgres/package_usage_store.go +++ b/internal/store/postgres/package_usage_store.go @@ -58,13 +58,12 @@ func (s *PackageUsageStore) Update(ctx context.Context, usage *model.PackageUsag return s.db.WithContext(ctx).Save(usage).Error } -// GetActiveMainPackage 查询载体的生效中主套餐 -// carrierType: "iot_card" 或 "device" -// carrierID: iot_card_id 或 device_id -func (s *PackageUsageStore) GetActiveMainPackage(ctx context.Context, carrierType string, carrierID uint) (*model.PackageUsage, error) { +// GetCurrentMainPackage 查询载体当前可展示的主套餐。 +// 当前套餐包含生效中和已用完状态;carrierType 为 "iot_card" 或 "device"。 +func (s *PackageUsageStore) GetCurrentMainPackage(ctx context.Context, carrierType string, carrierID uint) (*model.PackageUsage, error) { var usage model.PackageUsage query := s.db.WithContext(ctx). - Where("status = ?", constants.PackageUsageStatusActive). + Where("status IN (?, ?)", constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted). Where("master_usage_id IS NULL"). // 主套餐的 master_usage_id 为 NULL Order("priority ASC, activated_at ASC") diff --git a/openspec/changes/fix-depleted-package-display/.openspec.yaml b/openspec/changes/fix-depleted-package-display/.openspec.yaml new file mode 100644 index 0000000..7a8e2be --- /dev/null +++ b/openspec/changes/fix-depleted-package-display/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-08 diff --git a/openspec/changes/fix-depleted-package-display/design.md b/openspec/changes/fix-depleted-package-display/design.md new file mode 100644 index 0000000..47362b5 --- /dev/null +++ b/openspec/changes/fix-depleted-package-display/design.md @@ -0,0 +1,27 @@ +## Context + +现有资产信息组装只查询 `status=1` 的主套餐。套餐达到有效业务阈值会变为 `status=2`,但仍未到期;此时 C 端资产信息把套餐摘要投影为无套餐零值。 + +## Goals / Non-Goals + +**Goals:** + +- 让 C 端资产信息对 `status IN (1,2)` 的当前主套餐使用同一套摘要投影。 +- 维持既有主套餐选择顺序与空套餐响应形状。 + +**Non-Goals:** + +- 不改变虚流量耗尽阈值或任何套餐状态流转。 +- 不改变待生效、已过期、已失效套餐的展示规则。 +- 不修改套餐历史接口、价格、续费资格、Schema 或迁移。 + +## Decisions + +- 将资产信息使用的主套餐查询语义扩展为 `status IN (1,2)`,并仅限 `master_usage_id IS NULL`。两种状态都是当前生命周期内占位的主套餐;已有生命周期与到期扫描也以这两个状态共同处理。 +- 保留既有 `priority ASC, activated_at ASC` 排序,避免多记录异常时改变当前选择结果。 +- 套餐摘要继续直接使用选中的 `PackageUsage` 快照及统一流量指标计算,避免针对已用完状态复制或重算数据。 + +## Risks / Trade-offs + +- B1 的 `current_package_*` 字段会从零值变为已用完套餐数据;这是用户要求的兼容性行为修正,前端需按已用完状态展示而非将有使用记录视为可继续使用。 +- 若存量数据同时存在多条 `status IN (1,2)` 主套餐,接口沿用既有排序选第一条;本变更不修复该数据不变量问题。 diff --git a/openspec/changes/fix-depleted-package-display/proposal.md b/openspec/changes/fix-depleted-package-display/proposal.md new file mode 100644 index 0000000..c9833a0 --- /dev/null +++ b/openspec/changes/fix-depleted-package-display/proposal.md @@ -0,0 +1,24 @@ +## Why + +C 端资产信息接口将已用完的主套餐当作“无套餐”返回,导致用户无法查看已用量、套餐名称和到期时间。已用完仍是当前世代中可展示且未到期的套餐状态,必须与生效中套餐一致投影其使用信息。 + +## What Changes + +- 调整 `GET /api/c/v1/asset/info` 的当前主套餐选择范围:除生效中主套餐外,也选择已用完主套餐。 +- 已用完主套餐命中时,接口返回套餐名称、使用记录 ID、开始/到期时间及真/虚流量指标,不再返回套餐字段零值。 +- 保持待生效、已过期、已失效主套餐不作为当前套餐展示;不改变套餐耗尽阈值、状态流转、续费价格或套餐历史接口。 + +## Capabilities + +### New Capabilities + +- 无。 + +### Modified Capabilities + +- `personal-customer`: C 端资产信息必须展示当前已用完主套餐的使用信息。 + +## Impact + +- 影响 `internal/service/asset` 的当前主套餐读取,以及 `internal/store/postgres` 的主套餐状态筛选。 +- `GET /api/c/v1/asset/info` 对已用完套餐由零值摘要改为完整套餐摘要;不涉及 Schema、迁移、外部调用或写操作。 diff --git a/openspec/changes/fix-depleted-package-display/specs/personal-customer/spec.md b/openspec/changes/fix-depleted-package-display/specs/personal-customer/spec.md new file mode 100644 index 0000000..ee71e4d --- /dev/null +++ b/openspec/changes/fix-depleted-package-display/specs/personal-customer/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: 资产信息展示当前可用或已用完主套餐 + +系统 SHALL 在个人客户查询其已绑定资产的 `GET /api/c/v1/asset/info` 时,将当前世代中状态为生效中(`status=1`)或已用完(`status=2`)的主套餐作为当前套餐摘要返回。返回的套餐摘要 SHALL 包含套餐名称、套餐使用记录 ID、开始时间、到期时间、真流量与虚流量指标。待生效、已过期和已失效的主套餐不得作为当前套餐摘要返回。 + +#### Scenario: 已用完主套餐仍展示使用情况 + +- **GIVEN** 已认证个人客户拥有当前世代唯一的已用完主套餐,且该主套餐状态为 `status=2` +- **WHEN** 客户请求 `GET /api/c/v1/asset/info` +- **THEN** 响应返回该主套餐的名称、使用记录 ID、开始时间、到期时间及流量指标 +- **AND** 响应不得将该套餐摘要字段返回为无套餐零值 + +#### Scenario: 生效中主套餐保持既有展示 + +- **GIVEN** 已认证个人客户拥有当前世代唯一的生效中主套餐,且该主套餐状态为 `status=1` +- **WHEN** 客户请求 `GET /api/c/v1/asset/info` +- **THEN** 响应返回该主套餐的既有套餐摘要和流量指标 + +#### Scenario: 不可展示状态不作为当前套餐 + +- **GIVEN** 已认证个人客户没有状态为 `status=1` 或 `status=2` 的主套餐 +- **WHEN** 客户请求 `GET /api/c/v1/asset/info` +- **THEN** 响应的当前套餐摘要保持无套餐零值 diff --git a/openspec/changes/fix-depleted-package-display/tasks.md b/openspec/changes/fix-depleted-package-display/tasks.md new file mode 100644 index 0000000..75e5a81 --- /dev/null +++ b/openspec/changes/fix-depleted-package-display/tasks.md @@ -0,0 +1,9 @@ +## 1. 当前套餐读取 + +- [x] 1.1 将资产信息的当前主套餐读取范围扩展为生效中和已用完状态,同时保留主套餐与既有排序限制。 +- [x] 1.2 确认资产信息响应复用既有套餐快照和流量指标映射,不改变其他状态、价格或续费逻辑。 + +## 2. 验证与交付 + +- [x] 2.1 用临时可删除的调用或隔离数据验证已用完主套餐在资产信息中返回名称、使用记录、时间和流量指标;不新增自动化测试。 +- [x] 2.2 运行受影响的格式化、构建、OpenAPI 生成与 OpenSpec 校验,并删除临时验证产物。