diff --git a/internal/model/package.go b/internal/model/package.go index c3693a3..457a18b 100644 --- a/internal/model/package.go +++ b/internal/model/package.go @@ -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"` diff --git a/internal/service/enterprise_card/service.go b/internal/service/enterprise_card/service.go index eabb7dc..80aaad9 100644 --- a/internal/service/enterprise_card/service.go +++ b/internal/service/enterprise_card/service.go @@ -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) } diff --git a/internal/service/order/service.go b/internal/service/order/service.go index 5ba89ab..f5a1ba1 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -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, diff --git a/internal/task/auto_purchase.go b/internal/task/auto_purchase.go index c75dbde..1012abc 100644 --- a/internal/task/auto_purchase.go +++ b/internal/task/auto_purchase.go @@ -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, diff --git a/migrations/000099_add_package_name_to_package_usage.down.sql b/migrations/000099_add_package_name_to_package_usage.down.sql new file mode 100644 index 0000000..12f550a --- /dev/null +++ b/migrations/000099_add_package_name_to_package_usage.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE tb_package_usage +DROP COLUMN IF EXISTS package_name; diff --git a/migrations/000099_add_package_name_to_package_usage.up.sql b/migrations/000099_add_package_name_to_package_usage.up.sql new file mode 100644 index 0000000..7f29258 --- /dev/null +++ b/migrations/000099_add_package_name_to_package_usage.up.sql @@ -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 = '';