package postgres import ( "context" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" ) // effectivePackageStatuses 是"采集时刻有效套餐"认可的套餐状态:状态可能在采集之后才变为已用完或已过期, // 是否在采集时刻有效由激活时间与到期时间判断。 var effectivePackageStatuses = []int{ constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted, constants.PackageUsageStatusExpired, } // EffectivePackages 是一次"采集时刻有效套餐"查询的结果。 type EffectivePackages struct { // CarrierType 与 CarrierID 是实际扣减载体:卡自身没有有效套餐时回退到卡当前绑定的设备。 CarrierType string CarrierID uint // Packages 按扣减优先级排序:加油包优先,再按 priority、id。 Packages []*model.PackageUsage } // FindEffectivePackagesAt 按"有套餐"共用口径查询卡在采集时刻的有效套餐:状态为生效中、已用完或已过期, // 激活时间不晚于采集时间(为空按已生效),到期时间为空或晚于采集时间,未退款; // 卡自身一个都没有时看卡当前绑定设备的套餐。lock 为 true 时对查到的套餐行加 FOR UPDATE。 // 采集侧(写入范围、是否发事件、采集记录、日用量 0 行)与扣费侧共用本函数,避免两边口径不一致。 func FindEffectivePackagesAt(ctx context.Context, db *gorm.DB, cardID uint, observedAt time.Time, lock bool) (EffectivePackages, error) { own, err := queryEffectivePackages(ctx, db, "iot_card_id = ?", cardID, observedAt, lock) if err != nil || len(own) > 0 { return EffectivePackages{CarrierType: constants.AssetTypeIotCard, CarrierID: cardID, Packages: own}, err } var deviceIDs []uint if err := db.WithContext(ctx).Model(&model.DeviceSimBinding{}). Where("iot_card_id = ? AND bind_status = ?", cardID, constants.BindStatusBound). Limit(1).Pluck("device_id", &deviceIDs).Error; err != nil { return EffectivePackages{}, err } if len(deviceIDs) == 0 { return EffectivePackages{CarrierType: constants.AssetTypeIotCard, CarrierID: cardID}, nil } devicePackages, err := queryEffectivePackages(ctx, db, "device_id = ?", deviceIDs[0], observedAt, lock) if err != nil { return EffectivePackages{}, err } if len(devicePackages) == 0 { return EffectivePackages{CarrierType: constants.AssetTypeIotCard, CarrierID: cardID}, nil } return EffectivePackages{CarrierType: constants.AssetTypeDevice, CarrierID: deviceIDs[0], Packages: devicePackages}, nil } func queryEffectivePackages(ctx context.Context, db *gorm.DB, carrierCondition string, carrierID uint, observedAt time.Time, lock bool) ([]*model.PackageUsage, error) { query := db.WithContext(ctx).Model(&model.PackageUsage{}). Where(carrierCondition, carrierID). Where("status IN ?", effectivePackageStatuses). Where("activated_at IS NULL OR activated_at <= ?", observedAt). Where("expires_at IS NULL OR expires_at > ?", observedAt). Where("refund_id IS NULL") if lock { query = query.Clauses(clause.Locking{Strength: "UPDATE"}) } var packages []*model.PackageUsage err := query.Order("CASE WHEN master_usage_id IS NOT NULL THEN 0 ELSE 1 END, priority ASC, id ASC").Find(&packages).Error return packages, err } // EnsureDailyUsageRows 为每个套餐确保 date 当天一行套餐日用量(没有则写 0,已有不动)。 func EnsureDailyUsageRows(ctx context.Context, db *gorm.DB, packageUsageIDs []uint, date time.Time, cumulative map[uint]int64) error { for _, id := range packageUsageIDs { if err := db.WithContext(ctx).Exec(` INSERT INTO tb_package_usage_daily_record (package_usage_id, date, daily_usage_mb, cumulative_usage_mb, created_at, updated_at) VALUES (?, ?::date, 0, ?, NOW(), NOW()) ON CONFLICT (package_usage_id, date) DO NOTHING`, id, date.Format(cardFlowDateLayout), cumulative[id]).Error; err != nil { return err } } return nil } // AddDailyUsage 原子累加套餐 date 当天用量并覆盖累计用量;当天没有行时新建。 func AddDailyUsage(ctx context.Context, db *gorm.DB, packageUsageID uint, date time.Time, dailyMB int64, cumulativeMB int64) error { return db.WithContext(ctx).Exec(` INSERT INTO tb_package_usage_daily_record (package_usage_id, date, daily_usage_mb, cumulative_usage_mb, created_at, updated_at) VALUES (?, ?::date, ?, ?, NOW(), NOW()) ON CONFLICT (package_usage_id, date) DO UPDATE SET daily_usage_mb = tb_package_usage_daily_record.daily_usage_mb + EXCLUDED.daily_usage_mb, cumulative_usage_mb = EXCLUDED.cumulative_usage_mb, updated_at = NOW()`, packageUsageID, date.Format(cardFlowDateLayout), dailyMB, cumulativeMB).Error }