feat: 资产套餐历史增加主子层级查询

This commit is contained in:
2026-09-07 17:17:15 +08:00
parent c7c2b17d78
commit 696120ab38
18 changed files with 1030 additions and 236 deletions

View File

@@ -6,7 +6,6 @@ package asset
import (
"context"
stderrors "errors"
"sort"
"strconv"
"time"
@@ -14,6 +13,7 @@ import (
infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
assetquery "github.com/break/junhong_cmp_fiber/internal/query/asset"
packageexpiry "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
@@ -760,11 +760,9 @@ func parseGatewayTime(raw gateway.FlexString) *time.Time {
return &t
}
// GetPackages 获取资产的所有套餐列表(支持分页和状态筛选)
// callerAccountType: 调用方账号类型,"platform" 时返回成本价paid_amount其他类型不返回
// page 默认 1pageSize 默认 50pageSize 最大 100
// GetPackages 获取资产套餐历史层级(支持顶层关系组分页和状态筛选)
// callerAccountType: 调用方账号类型,"platform" 时返回成本价paid_amount其他类型不返回
func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, page, pageSize int, status *int, callerAccountType string) (*dto.AssetPackagesResult, error) {
// 分页参数边界处理
if page < 1 {
page = 1
}
@@ -778,109 +776,122 @@ func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, pa
return nil, errors.New(errors.CodeInvalidParam, "套餐状态非法")
}
// assetType 对应 Store 中的 carrierTypecard→iot_card, device→device
carrierType := assetType
if assetType == "card" {
carrierType = "iot_card"
}
usages, err := s.packageUsageStore.ListByCarrier(ctx, carrierType, id, status)
history, err := assetquery.NewPackageHistoryQuery(s.db).List(ctx, assetquery.PackageHistoryInput{
AssetType: assetType,
AssetID: id,
Status: status,
Page: page,
PageSize: pageSize,
})
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询套餐使用记录失败")
return nil, err
}
// 收集所有 PackageID 并批量查询
pkgIDSet := make(map[uint]struct{}, len(usages))
for _, u := range usages {
pkgIDSet[u.PackageID] = struct{}{}
}
pkgIDs := make([]uint, 0, len(pkgIDSet))
for id := range pkgIDSet {
pkgIDs = append(pkgIDs, id)
}
packages, pkgErr := s.packageStore.GetByIDsUnscoped(ctx, pkgIDs)
packageIDs := collectPackageHistoryPackageIDs(history.Items)
packages, pkgErr := s.packageStore.GetByIDsUnscoped(ctx, packageIDs)
if pkgErr != nil {
logger.GetAppLogger().Warn("批量查询套餐信息失败,套餐名称可能缺失",
zap.Uints("package_ids", pkgIDs),
zap.Uints("package_ids", packageIDs),
zap.Error(pkgErr))
}
pkgMap := make(map[uint]*model.Package, len(packages))
for _, p := range packages {
pkgMap[p.ID] = p
packageMap := make(map[uint]*model.Package, len(packages))
for _, pkg := range packages {
packageMap[pkg.ID] = pkg
}
all := make([]*dto.AssetPackageResponse, 0, len(usages))
for _, u := range usages {
pkg := pkgMap[u.PackageID]
metrics := u.BuildTrafficMetrics()
pkgName := u.PackageName
pkgType := ""
expiryBase := ""
if pkg != nil {
if pkgName == "" {
pkgName = pkg.PackageName
}
pkgType = pkg.PackageType
expiryBase = pkg.ExpiryBase
}
var paidAmount *int64
if callerAccountType == constants.OwnerTypePlatform {
paidAmount = u.PaidAmount
}
item := &dto.AssetPackageResponse{
PackageUsageID: u.ID,
PackageID: u.PackageID,
PackageName: pkgName,
PackageType: pkgType,
ExpiryBase: expiryBase,
OrderID: u.OrderID,
OrderNo: u.OrderNo,
RefundID: u.RefundID,
RefundNo: u.RefundNo,
UsageType: u.UsageType,
Status: u.Status,
StatusName: packageStatusName(u.Status),
RealTotalMB: metrics.RealTotalMB,
RealUsedMB: metrics.RealUsedMB,
VirtualTotalMB: metrics.VirtualTotalMB,
VirtualUsedMB: metrics.VirtualUsedMB,
ReductionPct: metrics.ReductionPct,
EnableVirtualData: u.EnableVirtualDataSnapshot,
ActivatedAt: u.ActivatedAt,
ExpiresAt: u.ExpiresAt,
MasterUsageID: u.MasterUsageID,
Priority: u.Priority,
PaidAmount: paidAmount,
RetailAmount: u.RetailAmount,
CreatedAt: u.CreatedAt,
}
all = append(all, item)
items := make([]*dto.AssetPackageHistoryNode, 0, len(history.Items))
for _, node := range history.Items {
items = append(items, buildAssetPackageHistoryNode(node, packageMap, callerAccountType))
}
// 按 created_at DESC 排序
sort.Slice(all, func(i, j int) bool {
return all[i].CreatedAt.After(all[j].CreatedAt)
})
total := int64(len(all))
offset := (page - 1) * pageSize
end := offset + pageSize
if offset >= len(all) {
offset = len(all)
}
if end > len(all) {
end = len(all)
}
items := all[offset:end]
return &dto.AssetPackagesResult{
Total: total,
Total: history.Total,
Page: page,
PageSize: pageSize,
Items: items,
}, nil
}
func collectPackageHistoryPackageIDs(items []*assetquery.PackageHistoryNode) []uint {
ids := make([]uint, 0)
seen := make(map[uint]struct{})
var collect func(*assetquery.PackageHistoryNode)
collect = func(node *assetquery.PackageHistoryNode) {
if node == nil || node.Usage == nil {
return
}
if _, exists := seen[node.Usage.PackageID]; !exists {
seen[node.Usage.PackageID] = struct{}{}
ids = append(ids, node.Usage.PackageID)
}
for _, child := range node.Children {
collect(child)
}
}
for _, item := range items {
collect(item)
}
return ids
}
func buildAssetPackageHistoryNode(node *assetquery.PackageHistoryNode, packageMap map[uint]*model.Package, callerAccountType string) *dto.AssetPackageHistoryNode {
usage := node.Usage
pkg := packageMap[usage.PackageID]
metrics := usage.BuildTrafficMetrics()
packageName := usage.PackageName
packageType := ""
expiryBase := ""
if pkg != nil {
if packageName == "" {
packageName = pkg.PackageName
}
packageType = pkg.PackageType
expiryBase = pkg.ExpiryBase
}
var paidAmount *int64
if callerAccountType == constants.OwnerTypePlatform {
paidAmount = usage.PaidAmount
}
item := &dto.AssetPackageHistoryNode{
PackageUsageID: usage.ID,
PackageID: usage.PackageID,
PackageName: packageName,
PackageType: packageType,
ExpiryBase: expiryBase,
OrderID: usage.OrderID,
OrderNo: usage.OrderNo,
RefundID: usage.RefundID,
RefundNo: usage.RefundNo,
UsageType: usage.UsageType,
Status: usage.Status,
StatusName: packageStatusName(usage.Status),
RealTotalMB: metrics.RealTotalMB,
RealUsedMB: metrics.RealUsedMB,
VirtualTotalMB: metrics.VirtualTotalMB,
VirtualUsedMB: metrics.VirtualUsedMB,
ReductionPct: metrics.ReductionPct,
EnableVirtualData: usage.EnableVirtualDataSnapshot,
ActivatedAt: usage.ActivatedAt,
ExpiresAt: usage.ExpiresAt,
MasterUsageID: usage.MasterUsageID,
Priority: usage.Priority,
PaidAmount: paidAmount,
RetailAmount: usage.RetailAmount,
CreatedAt: usage.CreatedAt,
Children: make([]*dto.AssetPackageHistoryNode, 0, len(node.Children)),
}
if node.RelationshipStatus != "" {
item.RelationshipStatus = node.RelationshipStatus
item.RelationshipStatusName = "关联主套餐缺失"
}
for _, child := range node.Children {
item.Children = append(item.Children, buildAssetPackageHistoryNode(child, packageMap, callerAccountType))
}
item.ExpandByDefault = len(item.Children) > 0
return item
}
// GetCurrentPackage 获取资产当前生效的主套餐
// callerAccountType: 调用方账号类型,"platform" 时返回成本价paid_amount其他类型不返回
func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id uint, callerAccountType string) (*dto.AssetPackageResponse, error) {