package asset import ( "context" "sort" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "gorm.io/gorm" ) const relationshipStatusMasterMissing = "master_missing" // PackageHistoryQuery 读取资产范围内的套餐使用关系。 type PackageHistoryQuery struct { db *gorm.DB } // PackageHistoryInput 定义已完成入口授权后的套餐历史读取范围。 type PackageHistoryInput struct { AssetType string AssetID uint Generation *int Status *int PackageType *string Page int PageSize int } // PackageHistoryResult 保存筛选后的顶层关系组总数和分页后的关系组。 type PackageHistoryResult struct { Total int64 Items []*PackageHistoryNode } // PackageHistoryNode 保存一个套餐使用记录及其可展示关联子项。 type PackageHistoryNode struct { Usage *model.PackageUsage Children []*PackageHistoryNode RelationshipStatus string } type packageUsagePresence struct { ID uint IotCardID uint DeviceID uint Generation int DeletedAt gorm.DeletedAt } // NewPackageHistoryQuery 创建套餐历史关系查询。 func NewPackageHistoryQuery(db *gorm.DB) *PackageHistoryQuery { return &PackageHistoryQuery{db: db} } // List 在既有资产和世代范围内读取完整关系,再按整组应用筛选、排序和分页。 func (q *PackageHistoryQuery) List(ctx context.Context, input PackageHistoryInput) (*PackageHistoryResult, error) { query, err := q.baseUsageQuery(ctx, input) if err != nil { return nil, err } var usages []*model.PackageUsage if err := query.Find(&usages).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史失败") } usageByID := make(map[uint]*model.PackageUsage, len(usages)) for _, usage := range usages { usageByID[usage.ID] = usage } missingMasterIDs := unresolvedMasterIDs(usages, usageByID) presentMasters, err := q.lookupMasterUsagePresence(ctx, missingMasterIDs) if err != nil { return nil, err } items := make([]*PackageHistoryNode, 0, len(usages)) nodes := make(map[uint]*PackageHistoryNode, len(usages)) for _, usage := range usages { nodes[usage.ID] = &PackageHistoryNode{Usage: usage, Children: make([]*PackageHistoryNode, 0)} } for _, usage := range usages { node := nodes[usage.ID] if usage.MasterUsageID == nil { items = append(items, node) continue } master, inRange := nodes[*usage.MasterUsageID] if inRange { master.Children = append(master.Children, node) continue } if _, exists := presentMasters[*usage.MasterUsageID]; exists { return nil, errors.New(errors.CodeDatabaseError, "读取套餐历史关联失败") } node.RelationshipStatus = relationshipStatusMasterMissing items = append(items, node) } matchingPackageIDs, err := q.matchingPackageIDs(ctx, input.PackageType, usages) if err != nil { return nil, err } items = filterPackageHistoryGroups(items, input.Status, matchingPackageIDs) sortPackageHistoryGroups(items) total := int64(len(items)) return &PackageHistoryResult{ Total: total, Items: paginatePackageHistoryGroups(items, input.Page, input.PageSize), }, nil } func (q *PackageHistoryQuery) baseUsageQuery(ctx context.Context, input PackageHistoryInput) (*gorm.DB, error) { query := q.db.WithContext(ctx).Model(&model.PackageUsage{}) switch input.AssetType { case "card": query = query.Where("iot_card_id = ?", input.AssetID) case "device": query = query.Where("device_id = ?", input.AssetID) default: return nil, errors.New(errors.CodeInvalidParam, "资产类型非法") } if input.Generation != nil { query = query.Where("generation = ?", *input.Generation) } return query, nil } func (q *PackageHistoryQuery) matchingPackageIDs(ctx context.Context, packageType *string, usages []*model.PackageUsage) (map[uint]struct{}, error) { if packageType == nil { return nil, nil } usagePackageIDs := collectPackageHistoryUsagePackageIDs(usages) result := make(map[uint]struct{}) if len(usagePackageIDs) == 0 { return result, nil } var ids []uint if err := q.db.WithContext(ctx).Model(&model.Package{}). Where("id IN ? AND package_type = ?", usagePackageIDs, *packageType). Pluck("id", &ids).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐类型资格失败") } for _, id := range ids { result[id] = struct{}{} } return result, nil } func collectPackageHistoryUsagePackageIDs(usages []*model.PackageUsage) []uint { ids := make([]uint, 0, len(usages)) seen := make(map[uint]struct{}, len(usages)) for _, usage := range usages { if usage == nil || usage.PackageID == 0 { continue } if _, exists := seen[usage.PackageID]; exists { continue } seen[usage.PackageID] = struct{}{} ids = append(ids, usage.PackageID) } return ids } func filterPackageHistoryGroups(items []*PackageHistoryNode, status *int, packageIDs map[uint]struct{}) []*PackageHistoryNode { filtered := make([]*PackageHistoryNode, 0, len(items)) for _, item := range items { if matchesPackageHistoryUsage(item.Usage, status, packageIDs) { filtered = append(filtered, item) continue } for _, child := range item.Children { if matchesPackageHistoryUsage(child.Usage, status, packageIDs) { filtered = append(filtered, item) break } } } return filtered } func matchesPackageHistoryUsage(usage *model.PackageUsage, status *int, packageIDs map[uint]struct{}) bool { if status != nil && usage.Status != *status { return false } if packageIDs == nil { return true } _, ok := packageIDs[usage.PackageID] return ok } func sortPackageHistoryGroups(items []*PackageHistoryNode) { for _, item := range items { sort.Slice(item.Children, func(i, j int) bool { return packageHistoryChildLess(item.Children[i].Usage, item.Children[j].Usage) }) } sort.Slice(items, func(i, j int) bool { left := items[i].Usage right := items[j].Usage if left.CreatedAt.Equal(right.CreatedAt) { return left.ID > right.ID } return left.CreatedAt.After(right.CreatedAt) }) } func packageHistoryChildLess(left, right *model.PackageUsage) bool { leftBucket := packageHistoryChildBucket(left) rightBucket := packageHistoryChildBucket(right) if leftBucket != rightBucket { return leftBucket < rightBucket } leftTime := left.CreatedAt rightTime := right.CreatedAt if leftBucket == 0 { leftTime = *left.ActivatedAt rightTime = *right.ActivatedAt } if leftTime.Equal(rightTime) { return left.ID < right.ID } return leftTime.Before(rightTime) } func packageHistoryChildBucket(usage *model.PackageUsage) int { if usage.Status == constants.PackageUsageStatusPending { return 2 } if usage.ActivatedAt != nil { return 0 } return 1 } func paginatePackageHistoryGroups(items []*PackageHistoryNode, page, pageSize int) []*PackageHistoryNode { if len(items) == 0 { return make([]*PackageHistoryNode, 0) } if page < 1 { page = 1 } if pageSize < 1 { pageSize = 1 } if page > (len(items)-1)/pageSize+1 { return make([]*PackageHistoryNode, 0) } start := (page - 1) * pageSize end := start + pageSize if end > len(items) { end = len(items) } return items[start:end] } func unresolvedMasterIDs(usages []*model.PackageUsage, usageByID map[uint]*model.PackageUsage) []uint { ids := make([]uint, 0) seen := make(map[uint]struct{}) for _, usage := range usages { if usage.MasterUsageID == nil { continue } masterID := *usage.MasterUsageID if _, found := usageByID[masterID]; found { continue } if _, alreadySeen := seen[masterID]; alreadySeen { continue } seen[masterID] = struct{}{} ids = append(ids, masterID) } return ids } func (q *PackageHistoryQuery) lookupMasterUsagePresence(ctx context.Context, ids []uint) (map[uint]packageUsagePresence, error) { found := make(map[uint]packageUsagePresence, len(ids)) if len(ids) == 0 { return found, nil } var records []packageUsagePresence if err := q.db.WithContext(ctx).Unscoped().Model(&model.PackageUsage{}). Select("id, iot_card_id, device_id, generation, deleted_at"). Where("id IN ?", ids). Find(&records).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "核对套餐主记录失败") } for _, record := range records { found[record.ID] = record } return found, nil }