59eb34976a
iotlcm-admin-svr:管理后台服务。 - Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件) - 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、 MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位; 私网 IP → 文档地址段;库名 xiujie_admin → iotlcm - 真实手机号/邮箱 → 合法 11 位编造号 / example.com - 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、 私有依赖 go get 行、明文 registry 登录口令 - 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名 - Dockerfile 改用公共基础镜像 - 拼错的 RADEME.md → README.md,README 重写 - 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用 - 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
749 lines
24 KiB
Go
749 lines
24 KiB
Go
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
|
|
}
|