feat: 资产套餐历史增加主子层级查询
This commit is contained in:
304
internal/query/asset/package_history.go
Normal file
304
internal/query/asset/package_history.go
Normal file
@@ -0,0 +1,304 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user