Files
junhong_cmp_fiber/internal/store/postgres/package_usage_effective.go
邹进 9c3c258f16
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m27s
feat: 卡流量读数先过采集队列再入账,扣减按采集时刻确定套餐
- 每卡 Redis 采集队列(默认 N=3、跨天清空、回退容差 1 MB),攒满且不下降才写最后一个点
- 测试环境全量切换日快照计量,下线灰度名单、切换水位与卡表基线扣费;全部卡置待建起点
- 读数区分没给值与 0;有套餐的卡读数 0 也写快照与日用量 0 行;重置日比较基准为 0 照常防旧值
- 新增采集记录(按月分区保留 90 天)、人工冲正流水与后台接口、采集告警
- 扣减按采集时刻有效套餐查找,只对生效中套餐做状态迁移,查不到套餐按成功跳过
- 换货新卡(新设备下的卡)打待建起点;采集间隔上限只校验启用中的轮询配置

Change: add-card-traffic-queue-and-observed-deduction

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:18:47 +09:00

95 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}