package trafficusage import ( "context" "database/sql" "fmt" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/cmd/inithttp" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/tdengine" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "hash/fnv" "math" "regexp" "strconv" "strings" "time" "unicode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" ) // identifierPattern 限制 database/stable 只能是普通标识符,防止拼 SQL 时传入非法名称。 var identifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) type sTrafficUsage struct { } func NewTrafficUsage() *sTrafficUsage { return &sTrafficUsage{} } func init() { service.RegisterTrafficUsage(NewTrafficUsage()) // 只注册连接释放逻辑,不在应用启动时自动创建 TDengine 库表。 inithttp.RegisterHttpCleanupFunc("trafficUsage", func(ctx context.Context) error { return tdengine.Close() }) } // WriteDaily 批量写入日流量数据。 // 每个 ICCID 会落到独立子表,子表通过 t_sim_card_usage_daily 超级表自动创建。 func (s *sTrafficUsage) WriteDaily(ctx context.Context, items []model.TrafficUsageDailyItem) error { if len(items) == 0 { return nil } if !tdengine.Enabled(ctx) { g.Log().Debug(ctx, "sTrafficUsage.WriteDaily skipped, tdengine disabled") return gerror.New("TDengine disabled; daily usage was not persisted") } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(cfg.DailyStable) if err := validateTableConfig(dbName, cfg.DailyStable); err != nil { return err } batchSize := normalizeBatchSize(cfg.BatchSize) for start := 0; start < len(items); start += batchSize { end := start + batchSize if end > len(items) { end = len(items) } batch := items[start:end] var builder strings.Builder builder.WriteString("INSERT INTO ") for i, item := range batch { if item.Iccid == "" { return gerror.New("traffic usage daily item iccid is empty") } if err := validateUsageMb("traffic usage daily item usage mb", item.UsageMb); err != nil { return err } if i > 0 { builder.WriteByte(' ') } builder.WriteString(qualify(dbName, childTrafficTableName(cfg.DailyStable, item.Iccid))) builder.WriteString(" USING ") builder.WriteString(qualify(dbName, cfg.DailyStable)) builder.WriteString(" TAGS (") builder.WriteString(sqlString(item.Iccid)) builder.WriteString(") VALUES (") builder.WriteString(strings.Join([]string{ sqlTime(dayStart(item.Date)), sqlFloat(item.UsageMb), sqlString(item.PackageCode), }, ", ")) builder.WriteByte(')') } if _, err := tdengine.Exec(ctx, builder.String()); err != nil { return gerror.Wrap(err, "tdengine write daily traffic usage failed") } } return nil } // WriteMonthly 批量写入月流量数据。 // 月流量和日流量共用同一个 TDengine database,但使用不同超级表。 func (s *sTrafficUsage) WriteMonthly(ctx context.Context, items []model.TrafficUsageMonthlyItem) error { if len(items) == 0 { return nil } if !tdengine.Enabled(ctx) { g.Log().Debug(ctx, "sTrafficUsage.WriteMonthly skipped, tdengine disabled") return gerror.New("TDengine disabled; monthly usage was not persisted") } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(cfg.MonthlyStable) if err := validateTableConfig(dbName, cfg.MonthlyStable); err != nil { return err } batchSize := normalizeBatchSize(cfg.BatchSize) for start := 0; start < len(items); start += batchSize { end := start + batchSize if end > len(items) { end = len(items) } batch := items[start:end] var builder strings.Builder builder.WriteString("INSERT INTO ") for i, item := range batch { if item.Iccid == "" { return gerror.New("traffic usage monthly item iccid is empty") } if err := validateUsageMb("traffic usage monthly item usage mb", item.UsageMb); err != nil { return err } if i > 0 { builder.WriteByte(' ') } builder.WriteString(qualify(dbName, childTrafficTableName(cfg.MonthlyStable, item.Iccid))) builder.WriteString(" USING ") builder.WriteString(qualify(dbName, cfg.MonthlyStable)) builder.WriteString(" TAGS (") builder.WriteString(sqlString(item.Iccid)) builder.WriteString(") VALUES (") builder.WriteString(strings.Join([]string{ sqlTime(monthStart(item.Month)), sqlFloat(item.UsageMb), sqlString(item.PackageCode), }, ", ")) builder.WriteByte(')') } if _, err := tdengine.Exec(ctx, builder.String()); err != nil { return gerror.Wrap(err, "tdengine write monthly traffic usage failed") } } return nil } // WritePoolUsageDaily 批量写入流量池的流量使用记录。 // 每个 FlowPoolCode 会落到独立子表,子表通过 t_flow_pool_usage_daily 超级表自动创建。 func (s *sTrafficUsage) WritePoolUsageDaily(ctx context.Context, items []model.TrafficPoolUsageItem) error { return s.writePoolUsage(ctx, items, tdengine.GetConfig(ctx).PoolDailyStable, "flow_pool_usage_d_", dayStart, "daily") } // WritePoolUsageMonthly 批量写入流量池的流量使用记录。 // 每个 FlowPoolCode 会落到独立子表,子表通过 t_flow_pool_usage_monthly 超级表自动创建。 func (s *sTrafficUsage) WritePoolUsageMonthly(ctx context.Context, items []model.TrafficPoolUsageItem) error { return s.writePoolUsage(ctx, items, tdengine.GetConfig(ctx).PoolMonthlyStable, "flow_pool_usage_m_", monthStart, "monthly") } // WritePoolCardCountDaily 批量写入流量池的卡数量记录。 // 每个 FlowPoolCode 会落到独立子表,子表通过 t_flow_pool_card_count_daily 超级表自动创建。 func (s *sTrafficUsage) WritePoolCardCountDaily(ctx context.Context, items []model.TrafficPoolCardCountItem) error { return s.writePoolCardCount(ctx, items, tdengine.GetConfig(ctx).PoolCardCountDailyStable, "flow_pool_card_count_d_", dayStart, "daily") } // WritePoolCardCountMonthly 批量写入流量池的卡数量记录。 // 每个 FlowPoolCode 会落到独立子表,子表通过 t_flow_pool_card_count_monthly 超级表自动创建。 func (s *sTrafficUsage) WritePoolCardCountMonthly(ctx context.Context, items []model.TrafficPoolCardCountItem) error { return s.writePoolCardCount(ctx, items, tdengine.GetConfig(ctx).PoolCardCountMonthlyStable, "flow_pool_card_count_m_", monthStart, "monthly") } // UpdateDaily 更新日流量数据。 func (s *sTrafficUsage) UpdateDaily(ctx context.Context, items []model.TrafficUsageDailyItem) error { return s.WriteDaily(ctx, items) } // UpdateMonthly 更新月流量数据。 func (s *sTrafficUsage) UpdateMonthly(ctx context.Context, items []model.TrafficUsageMonthlyItem) error { return s.WriteMonthly(ctx, items) } // DeleteDaily 删除日流量数据。 func (s *sTrafficUsage) DeleteDaily(ctx context.Context, input model.TrafficUsageDeleteInput) error { if !tdengine.Enabled(ctx) { g.Log().Debug(ctx, "sTrafficUsage.DeleteDaily skipped, tdengine disabled") return nil } if input.Iccid == "" { return gerror.New("traffic usage daily delete iccid is empty") } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(cfg.DailyStable) if err := validateTableConfig(dbName, cfg.DailyStable); err != nil { return err } return deleteRange(ctx, qualify(dbName, childTrafficTableName(cfg.DailyStable, input.Iccid)), input.StartTime, input.EndTime) } // DeleteMonthly 删除月流量数据。 func (s *sTrafficUsage) DeleteMonthly(ctx context.Context, input model.TrafficUsageDeleteInput) error { if !tdengine.Enabled(ctx) { g.Log().Debug(ctx, "sTrafficUsage.DeleteMonthly skipped, tdengine disabled") return nil } if input.Iccid == "" { return gerror.New("traffic usage monthly delete iccid is empty") } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(cfg.MonthlyStable) if err := validateTableConfig(dbName, cfg.MonthlyStable); err != nil { return err } return deleteRange(ctx, qualify(dbName, childTrafficTableName(cfg.MonthlyStable, input.Iccid)), input.StartTime, input.EndTime) } // QueryDaily 查询日流量数据。 func (s *sTrafficUsage) QueryDaily(ctx context.Context, input model.TrafficUsageQueryInput) ([]model.TrafficUsageDailyRecord, int, error) { if !tdengine.Enabled(ctx) { return nil, 0, nil } if input.Iccid == "" { return nil, 0, gerror.New("traffic usage daily query iccid is empty") } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(cfg.DailyStable) if err := validateTableConfig(dbName, cfg.DailyStable); err != nil { return nil, 0, err } table := qualify(dbName, childTrafficTableName(cfg.DailyStable, input.Iccid)) total, err := countRows(ctx, table, input.StartTime, input.EndTime) if err != nil { if isTableNotExist(err) { return []model.TrafficUsageDailyRecord{}, 0, nil } return nil, 0, err } rows, err := queryRows(ctx, table, input) if err != nil { if isTableNotExist(err) { return []model.TrafficUsageDailyRecord{}, 0, nil } return nil, 0, err } defer rows.Close() list := make([]model.TrafficUsageDailyRecord, 0) for rows.Next() { var item model.TrafficUsageDailyRecord if err = rows.Scan(&item.Ts, &item.UsageMb, &item.PackageCode, &item.Iccid); err != nil { return nil, 0, err } list = append(list, item) } if err = rows.Err(); err != nil { return nil, 0, err } return list, total, nil } // QueryMonthly 查询月流量数据。 func (s *sTrafficUsage) QueryMonthly(ctx context.Context, input model.TrafficUsageQueryInput) ([]model.TrafficUsageMonthlyRecord, int, error) { if !tdengine.Enabled(ctx) { return nil, 0, nil } if input.Iccid == "" { return nil, 0, gerror.New("traffic usage monthly query iccid is empty") } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(cfg.MonthlyStable) if err := validateTableConfig(dbName, cfg.MonthlyStable); err != nil { return nil, 0, err } table := qualify(dbName, childTrafficTableName(cfg.MonthlyStable, input.Iccid)) total, err := countRows(ctx, table, input.StartTime, input.EndTime) if err != nil { if isTableNotExist(err) { return []model.TrafficUsageMonthlyRecord{}, 0, nil } return nil, 0, err } rows, err := queryRows(ctx, table, input) if err != nil { if isTableNotExist(err) { return []model.TrafficUsageMonthlyRecord{}, 0, nil } return nil, 0, err } defer rows.Close() list := make([]model.TrafficUsageMonthlyRecord, 0) for rows.Next() { var item model.TrafficUsageMonthlyRecord if err = rows.Scan(&item.Ts, &item.UsageMb, &item.PackageCode, &item.Iccid); err != nil { return nil, 0, err } list = append(list, item) } if err = rows.Err(); err != nil { return nil, 0, err } return list, total, nil } // AggregatePoolUsageDaily 汇总日流量数据。 func (s *sTrafficUsage) AggregatePoolUsageDaily(ctx context.Context, date time.Time, cards []model.TrafficPoolCard) ([]model.TrafficPoolUsageAggregate, error) { return s.aggregatePoolUsage(ctx, dayStart(date), cards, tdengine.GetConfig(ctx).DailyStable, "daily") } // AggregatePoolUsageMonthly 汇总月流量数据。 func (s *sTrafficUsage) AggregatePoolUsageMonthly(ctx context.Context, month time.Time, cards []model.TrafficPoolCard) ([]model.TrafficPoolUsageAggregate, error) { return s.aggregatePoolUsage(ctx, monthStart(month), cards, tdengine.GetConfig(ctx).MonthlyStable, "monthly") } // aggregatePoolUsage 汇总流量数据。 func (s *sTrafficUsage) aggregatePoolUsage(ctx context.Context, ts time.Time, cards []model.TrafficPoolCard, stable string, name string) ([]model.TrafficPoolUsageAggregate, error) { if len(cards) == 0 { return []model.TrafficPoolUsageAggregate{}, nil } if !tdengine.Enabled(ctx) { g.Log().Debugf(ctx, "sTrafficUsage.aggregatePoolUsage %s skipped, tdengine disabled", name) return []model.TrafficPoolUsageAggregate{}, nil } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(stable) if err := validateTableConfig(dbName, stable); err != nil { return nil, err } grouped := make(map[string][]string) for _, card := range cards { if card.FlowPoolCode == "" || card.Iccid == "" { continue } grouped[card.FlowPoolCode] = append(grouped[card.FlowPoolCode], card.Iccid) } result := make([]model.TrafficPoolUsageAggregate, 0, len(grouped)) for flowPoolCode, iccids := range grouped { usageMb, recordCount, err := aggregatePoolUsageByIccids(ctx, dbName, stable, ts, iccids) if err != nil { return nil, gerror.Wrapf(err, "tdengine aggregate %s traffic pool usage failed, flowPoolCode:%s", name, flowPoolCode) } if recordCount != int64(len(iccids)) { return nil, gerror.Newf( "tdengine aggregate %s traffic pool usage incomplete, flowPoolCode:%s, expected:%d, actual:%d", name, flowPoolCode, len(iccids), recordCount, ) } result = append(result, model.TrafficPoolUsageAggregate{ FlowPoolCode: flowPoolCode, UsageMb: usageMb, RecordCount: recordCount, }) } return result, nil } // writePoolUsage 批量写入流量池使用量数据。 // 每个 FlowPoolCode 会落到独立子表,子表通过对应的超级表自动创建。 func (s *sTrafficUsage) writePoolUsage(ctx context.Context, items []model.TrafficPoolUsageItem, stable string, prefix string, normalizeTime func(time.Time) time.Time, name string) error { if len(items) == 0 { return nil } if !tdengine.Enabled(ctx) { g.Log().Debugf(ctx, "sTrafficUsage.writePoolUsage %s skipped, tdengine disabled", name) return nil } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(stable) if err := validateTableConfig(dbName, stable); err != nil { return err } batchSize := normalizeBatchSize(cfg.BatchSize) for start := 0; start < len(items); start += batchSize { end := start + batchSize if end > len(items) { end = len(items) } batch := items[start:end] var builder strings.Builder builder.WriteString("INSERT INTO ") for i, item := range batch { if item.FlowPoolCode == "" { return gerror.New("traffic pool usage item flow pool code is empty") } if err := validateUsageMb("traffic pool usage item usage mb", item.UsageMb); err != nil { return err } if i > 0 { builder.WriteByte(' ') } builder.WriteString(qualify(dbName, childTableNameWithPrefix(prefix, item.FlowPoolCode))) builder.WriteString(" USING ") builder.WriteString(qualify(dbName, stable)) builder.WriteString(" TAGS (") builder.WriteString(sqlString(item.FlowPoolCode)) builder.WriteString(") VALUES (") builder.WriteString(strings.Join([]string{ sqlTime(normalizeTime(item.Date)), sqlFloat(item.UsageMb), }, ", ")) builder.WriteByte(')') } if _, err := tdengine.Exec(ctx, builder.String()); err != nil { return gerror.Wrapf(err, "tdengine write %s traffic pool usage failed", name) } } return nil } // writePoolCardCount 批量写入流量池卡数量数据。 // 每个 FlowPoolCode 会落到独立子表,子表通过对应的超级表自动创建。 func (s *sTrafficUsage) writePoolCardCount(ctx context.Context, items []model.TrafficPoolCardCountItem, stable string, prefix string, normalizeTime func(time.Time) time.Time, name string) error { if len(items) == 0 { return nil } if !tdengine.Enabled(ctx) { g.Log().Debugf(ctx, "sTrafficUsage.writePoolCardCount %s skipped, tdengine disabled", name) return nil } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(stable) if err := validateTableConfig(dbName, stable); err != nil { return err } batchSize := normalizeBatchSize(cfg.BatchSize) for start := 0; start < len(items); start += batchSize { end := start + batchSize if end > len(items) { end = len(items) } batch := items[start:end] var builder strings.Builder builder.WriteString("INSERT INTO ") for i, item := range batch { if item.FlowPoolCode == "" { return gerror.New("traffic pool card count item flow pool code is empty") } if item.CardCount < 0 { return gerror.Newf("traffic pool card count item card count cannot be negative: %d", item.CardCount) } if i > 0 { builder.WriteByte(' ') } builder.WriteString(qualify(dbName, childTableNameWithPrefix(prefix, item.FlowPoolCode))) builder.WriteString(" USING ") builder.WriteString(qualify(dbName, stable)) builder.WriteString(" TAGS (") builder.WriteString(sqlString(item.FlowPoolCode)) builder.WriteString(") VALUES (") builder.WriteString(strings.Join([]string{ sqlTime(normalizeTime(item.Date)), strconv.FormatInt(item.CardCount, 10), }, ", ")) builder.WriteByte(')') } if _, err := tdengine.Exec(ctx, builder.String()); err != nil { return gerror.Wrapf(err, "tdengine write %s traffic pool card count failed", name) } } return nil } // aggregatePoolUsageByIccids 汇总流量数据。 // 每个 ICCID 会落到独立子表,子表通过 t_sim_card_usage_daily 或 t_sim_card_usage_monthly 超级表自动创建。 func aggregatePoolUsageByIccids(ctx context.Context, database string, stable string, ts time.Time, iccids []string) (float64, int64, error) { var usageMb float64 var recordCount int64 for _, iccid := range iccids { table := qualify(database, childTrafficTableName(stable, iccid)) row, err := tdengine.QueryRow(ctx, fmt.Sprintf("SELECT usage_mb FROM %s WHERE ts = %s", table, sqlTime(ts))) if err != nil { return 0, 0, gerror.Wrap(err, "tdengine query traffic usage row failed") } var value float64 if err = row.Scan(&value); err != nil { if err == sql.ErrNoRows || isTableNotExist(err) { continue } return 0, 0, gerror.Wrap(err, "tdengine scan traffic usage row failed") } usageMb += value recordCount++ } return usageMb, recordCount, nil } // countRows 统计流量数据记录数。 func countRows(ctx context.Context, table string, startTime, endTime time.Time) (int, error) { where := whereRangeSQL(startTime, endTime) row, err := tdengine.QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %s%s", table, where)) if err != nil { return 0, gerror.Wrap(err, "tdengine count traffic usage failed") } total := 0 if err = row.Scan(&total); err != nil { return 0, gerror.Wrap(err, "tdengine count traffic usage failed") } return total, nil } // queryRows 查询流量数据记录。 func queryRows(ctx context.Context, table string, input model.TrafficUsageQueryInput) (*sql.Rows, error) { fields := "ts, usage_mb, package_code, iccid" where := whereRangeSQL(input.StartTime, input.EndTime) order := "DESC" if input.Asc { order = "ASC" } limit := "" page, pageSize := normalizePage(input.Page, input.PageSize) if pageSize > 0 { limit = fmt.Sprintf(" LIMIT %d OFFSET %d", pageSize, (page-1)*pageSize) } rows, err := tdengine.Query(ctx, fmt.Sprintf("SELECT %s FROM %s%s ORDER BY ts %s%s", fields, table, where, order, limit)) if err != nil { return nil, gerror.Wrap(err, "tdengine query traffic usage failed") } return rows, nil } // deleteRange 删除流量数据记录。 func deleteRange(ctx context.Context, table string, startTime, endTime time.Time) error { where := whereRangeSQL(startTime, endTime) if where == "" { return gerror.New("traffic usage delete time range is empty") } if _, err := tdengine.Exec(ctx, fmt.Sprintf("DELETE FROM %s%s", table, where)); err != nil { if isTableNotExist(err) { return nil } return gerror.Wrap(err, "tdengine delete traffic usage failed") } return nil } // whereRangeSQL 构建时间范围查询条件。 func whereRangeSQL(startTime, endTime time.Time) string { clauses := make([]string, 0, 2) if !startTime.IsZero() { clauses = append(clauses, "ts >= "+sqlTime(startTime)) } if !endTime.IsZero() { clauses = append(clauses, "ts <= "+sqlTime(endTime)) } if len(clauses) == 0 { return "" } return " WHERE " + strings.Join(clauses, " AND ") } // validateTableConfig 验证表配置是否有效。 func validateTableConfig(database, stable string) error { if !identifierPattern.MatchString(database) { return gerror.Newf("invalid tdengine database name: %s", database) } if !identifierPattern.MatchString(stable) { return gerror.Newf("invalid tdengine stable name: %s", stable) } return nil } // qualify 格式化表名。 func qualify(database, table string) string { return fmt.Sprintf("%s.%s", database, table) } // childTrafficTableName 构建子表名。 // 每个 ICCID 会落到独立子表,子表通过 t_sim_card_usage_daily 或 t_sim_card_usage_monthly 超级表自动创建。 func childTrafficTableName(stable string, iccid string) string { prefix := "sim_card_usage_m_" if stable == "t_sim_card_usage_daily" { prefix = "sim_card_usage_d_" } return childTableNameWithPrefix(prefix, iccid) } // childTableNameWithPrefix 构建子表名。 func childTableNameWithPrefix(prefix string, value string) string { var builder strings.Builder builder.WriteString(prefix) for _, r := range strings.ToLower(value) { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' { builder.WriteRune(r) } } name := builder.String() if name == prefix || len(name) > 190 { return fmt.Sprintf("%s%x", prefix, hashString(value)) } return name } // hashString 对字符串进行哈希处理。 // 用于生成子表名。 func hashString(value string) uint32 { hash := fnv.New32a() _, _ = hash.Write([]byte(value)) return hash.Sum32() } // normalizeBatchSize 归一化批量大小。 func normalizeBatchSize(size int) int { if size <= 0 { return 200 } return size } // normalizePage 归一化分页参数。 func normalizePage(page, pageSize int) (int, int) { if page <= 0 { page = 1 } if pageSize < 0 { pageSize = 0 } return page, pageSize } // dayStart 获取指定日期的快照时间。 // 用于查询指定日期的流量数据记录。 func dayStart(t time.Time) time.Time { if t.IsZero() { t = time.Now() } return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location()) } // monthStart 获取指定月份的快照时间。 // 用于查询指定月份的流量数据记录。 func monthStart(t time.Time) time.Time { if t.IsZero() { t = time.Now() } lastDay := time.Date(t.Year(), t.Month()+1, 0, 23, 59, 59, 0, t.Location()) return lastDay } // sqlString 格式化字符串为SQL字符串。 func sqlString(value string) string { return "'" + strings.ReplaceAll(value, "'", "''") + "'" } // sqlTime 格式化时间时间为SQL时间。 func sqlTime(value time.Time) string { return sqlString(value.Format("2006-01-02 15:04:05.000")) } // sqlFloat 格式化浮点数为SQL浮点数。 func sqlFloat(value float64) string { return strconv.FormatFloat(value, 'f', -1, 64) } // validateUsageMb 验证流量使用记录是否有效。 // 用于检查流量使用记录是否为有限的浮点数,且是否为负数。 func validateUsageMb(name string, value float64) error { if math.IsNaN(value) || math.IsInf(value, 0) { return gerror.Newf("%s must be finite: %v", name, value) } if value < 0 { return gerror.Newf("%s cannot be negative: %v", name, value) } return nil } // isTableNotExist 检查表是否存在。 // 用于在删除流量数据记录时,检查表是否存在。 func isTableNotExist(err error) bool { if err == nil { return false } text := strings.ToLower(err.Error()) return strings.Contains(text, "table does not exist") || strings.Contains(text, "table not exist") || strings.Contains(text, "does not exist") } // GetTopUsageCardsYesterday 查询前一天流量使用前五名的卡片 func (s *sTrafficUsage) GetTopUsageCardsYesterday(ctx context.Context) ([]model.TopUsageCard, error) { if !tdengine.Enabled(ctx) { return nil, nil } cfg := tdengine.GetConfig(ctx) dbName := cfg.DbNameFor(cfg.DailyStable) if err := validateTableConfig(dbName, cfg.DailyStable); err != nil { return nil, err } // 获取前一天日期 yesterday := time.Now().AddDate(0, 0, -1) startStr := yesterday.Format("2006-01-02") endStr := dayStart(yesterday).Format("2006-01-02 15:04:05") table := qualify(dbName, cfg.DailyStable) sql := fmt.Sprintf( "SELECT iccid, FIRST(usage_mb), LAST(usage_mb) FROM %s WHERE ts >= '%s' AND ts <= '%s' GROUP BY iccid ORDER BY LAST(usage_mb) DESC LIMIT 5", table, startStr, endStr) rows, err := tdengine.Query(ctx, sql) if err != nil { if isTableNotExist(err) { return []model.TopUsageCard{}, nil } return nil, err } defer rows.Close() result := make([]model.TopUsageCard, 0) for rows.Next() { var iccid string var firstUsage, lastUsage float64 if err = rows.Scan(&iccid, &firstUsage, &lastUsage); err != nil { return nil, err } result = append(result, model.TopUsageCard{ Iccid: iccid, UsageMb: lastUsage, StatDate: yesterday.Format("2006-01-02"), }) } if err = rows.Err(); err != nil { return nil, err } return result, nil }