chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,748 @@
|
||||
package trafficusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"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"
|
||||
"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 批量写入月流量数据。
|
||||
// 月流量按配置写入月库,未配置分库时回退默认库。
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user