feat: 给 PackageUsage 添加 package_name 快照字段,修复企业卡列表 carrier_name/package_name 为空问题
- 迁移 000099:tb_package_usage 新增 package_name 列,回填历史数据 - PackageUsage model 添加 PackageName 字段(与 DataResetCycle 保持一致的快照设计) - 4 处 PackageUsage 创建点补充 PackageName 快照(order/service.go + auto_purchase.go) - EnterpriseCard ListCards:carrier_name 直接取 card.CarrierName,package_name 通过 DISTINCT ON 批量查当前生效主套餐,单次查询覆盖整页,无 N+1
This commit is contained in:
@@ -77,6 +77,7 @@ type PackageUsage struct {
|
||||
MasterUsageID *uint `gorm:"column:master_usage_id;type:bigint;index:idx_package_usage_master_usage_id;comment:主套餐使用记录ID(加油包关联主套餐,主套餐此字段为NULL)" json:"master_usage_id"`
|
||||
HasIndependentExpiry bool `gorm:"column:has_independent_expiry;type:boolean;default:false;comment:加油包是否有独立有效期(true-有独立到期时间 false-跟随主套餐)" json:"has_independent_expiry"`
|
||||
PendingRealnameActivation bool `gorm:"column:pending_realname_activation;type:boolean;default:false;comment:是否等待实名激活(true-待实名后激活 false-已激活或不需实名)" json:"pending_realname_activation"`
|
||||
PackageName string `gorm:"column:package_name;type:varchar(255);not null;default:'';comment:套餐名称快照(从Package复制,用于历史记录展示)" json:"package_name"`
|
||||
DataResetCycle string `gorm:"column:data_reset_cycle;type:varchar(20);comment:流量重置周期(从Package复制,用于历史记录)" json:"data_reset_cycle"`
|
||||
LastResetAt *time.Time `gorm:"column:last_reset_at;comment:最后一次流量重置时间" json:"last_reset_at"`
|
||||
NextResetAt *time.Time `gorm:"column:next_reset_at;index:idx_package_usage_next_reset_at;comment:下次流量重置时间(用于定时任务查询)" json:"next_reset_at"`
|
||||
|
||||
@@ -288,6 +288,13 @@ func (s *Service) ListCards(ctx context.Context, enterpriseID uint, req *dto.Ent
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡列表失败")
|
||||
}
|
||||
|
||||
// 批量查询各卡当前生效的主套餐名称(取 package_name 快照,避免 JOIN tb_package)
|
||||
pageCardIDs := make([]uint, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
pageCardIDs = append(pageCardIDs, card.ID)
|
||||
}
|
||||
packageNameMap := s.buildPackageNameMap(ctx, pageCardIDs)
|
||||
|
||||
items := make([]dto.EnterpriseCardItem, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
items = append(items, dto.EnterpriseCardItem{
|
||||
@@ -295,6 +302,8 @@ func (s *Service) ListCards(ctx context.Context, enterpriseID uint, req *dto.Ent
|
||||
ICCID: card.ICCID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierID: card.CarrierID,
|
||||
CarrierName: card.CarrierName,
|
||||
PackageName: packageNameMap[card.ID],
|
||||
Status: card.Status,
|
||||
StatusName: getCardStatusName(card.Status),
|
||||
NetworkStatus: card.NetworkStatus,
|
||||
@@ -310,6 +319,36 @@ func (s *Service) ListCards(ctx context.Context, enterpriseID uint, req *dto.Ent
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildPackageNameMap 批量查询卡列表当前生效主套餐名称,返回 map[iotCardID]packageName
|
||||
func (s *Service) buildPackageNameMap(ctx context.Context, cardIDs []uint) map[uint]string {
|
||||
result := make(map[uint]string, len(cardIDs))
|
||||
if len(cardIDs) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
type usageRow struct {
|
||||
IotCardID uint `gorm:"column:iot_card_id"`
|
||||
PackageName string `gorm:"column:package_name"`
|
||||
}
|
||||
|
||||
// 每张卡取优先级最低(数字最小)的生效中主套餐,使用 DISTINCT ON 保证每卡仅一条
|
||||
var rows []usageRow
|
||||
s.db.WithContext(ctx).Raw(`
|
||||
SELECT DISTINCT ON (iot_card_id) iot_card_id, package_name
|
||||
FROM tb_package_usage
|
||||
WHERE iot_card_id IN ?
|
||||
AND status = 1
|
||||
AND master_usage_id IS NULL
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY iot_card_id, priority ASC
|
||||
`, cardIDs).Scan(&rows)
|
||||
|
||||
for _, row := range rows {
|
||||
result[row.IotCardID] = row.PackageName
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) SuspendCard(ctx context.Context, enterpriseID, cardID uint) error {
|
||||
return s.updateCardNetworkStatus(ctx, enterpriseID, cardID, 0)
|
||||
}
|
||||
|
||||
@@ -1910,6 +1910,7 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
},
|
||||
OrderID: order.ID,
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.RealDataMB,
|
||||
Status: status,
|
||||
@@ -1988,6 +1989,7 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
},
|
||||
OrderID: order.ID,
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.RealDataMB,
|
||||
Status: status,
|
||||
|
||||
@@ -440,6 +440,7 @@ func (h *AutoPurchaseHandler) activateMainPackage(
|
||||
},
|
||||
OrderID: order.ID,
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.RealDataMB,
|
||||
Status: status,
|
||||
@@ -510,6 +511,7 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
|
||||
},
|
||||
OrderID: order.ID,
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.RealDataMB,
|
||||
Status: constants.PackageUsageStatusActive,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE tb_package_usage
|
||||
DROP COLUMN IF EXISTS package_name;
|
||||
15
migrations/000099_add_package_name_to_package_usage.up.sql
Normal file
15
migrations/000099_add_package_name_to_package_usage.up.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- 给 tb_package_usage 添加 package_name 快照字段
|
||||
-- 用于展示套餐名称,避免每次展示时都需要 JOIN tb_package 表
|
||||
-- 与 data_reset_cycle 字段保持一致的快照设计
|
||||
|
||||
ALTER TABLE tb_package_usage
|
||||
ADD COLUMN IF NOT EXISTS package_name VARCHAR(255) NOT NULL DEFAULT '';
|
||||
|
||||
COMMENT ON COLUMN tb_package_usage.package_name IS '套餐名称快照(创建时从 tb_package 复制,用于历史记录展示)';
|
||||
|
||||
-- 回填历史数据:从 tb_package 表取套餐名称
|
||||
UPDATE tb_package_usage pu
|
||||
SET package_name = COALESCE(p.package_name, '')
|
||||
FROM tb_package p
|
||||
WHERE pu.package_id = p.id
|
||||
AND pu.package_name = '';
|
||||
Reference in New Issue
Block a user