chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。 - app/:uni-app 小程序前端(基于 unibest,MIT) - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID - 内网后端地址 → admin.iotlcm.example.com - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明) - server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0) - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件) - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化 - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 依赖号码尾部的断言同步更新 - 补 server/LICENSE(Apache-2.0) - 移除内部资产:CI 触发器 - 补根 README,LF 行尾 + .gitattributes 已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
_ "github.com/taosdata/driver-go/v3/taosRestful"
|
||||
_ "github.com/taosdata/driver-go/v3/taosWS"
|
||||
)
|
||||
|
||||
const (
|
||||
appDiagnosisDailyStable = "t_sim_card_usage_daily"
|
||||
appDiagnosisMonthlyStable = "t_sim_card_usage_monthly"
|
||||
)
|
||||
|
||||
var appDiagnosisIdentifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
type appDiagnosisTrafficConfig struct {
|
||||
Enabled bool
|
||||
DriverName string
|
||||
Dsn string
|
||||
DbName string
|
||||
DailyDbName string
|
||||
MonthlyDbName string
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
}
|
||||
|
||||
// dbNameFor 返回指定超级表所在 database。
|
||||
//
|
||||
// 日流量与月流量分库存储(两库 duration 不同,而 duration 只能建库时确定);
|
||||
// 未单独配置 dailyDbName/monthlyDbName 时回退到 DbName,与改造前行为一致。
|
||||
func (c appDiagnosisTrafficConfig) dbNameFor(stable string) string {
|
||||
if stable == appDiagnosisDailyStable && c.DailyDbName != "" {
|
||||
return c.DailyDbName
|
||||
}
|
||||
if stable == appDiagnosisMonthlyStable && c.MonthlyDbName != "" {
|
||||
return c.MonthlyDbName
|
||||
}
|
||||
return c.DbName
|
||||
}
|
||||
|
||||
type appDiagnosisTrafficRecord struct {
|
||||
Ts time.Time
|
||||
UsageMb float64
|
||||
PackageCode string
|
||||
Iccid string
|
||||
}
|
||||
|
||||
func (s *sAppDiagnosis) queryDiagnosisDailyFlow(ctx context.Context, iccid string, days int) (*model.AppDiagnosisFlowModel, error) {
|
||||
if days <= 0 {
|
||||
days = 7
|
||||
}
|
||||
now := time.Now()
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -(days - 1))
|
||||
endTime := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
|
||||
|
||||
records, err := appDiagnosisQueryTrafficRows(ctx, iccid, appDiagnosisDailyStable, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询SIM卡使用流量失败,请稍后重试")
|
||||
}
|
||||
usageMap := make(map[string]float64, len(records))
|
||||
for _, item := range records {
|
||||
usageMap[item.Ts.Format("2006-01-02")] = item.UsageMb
|
||||
}
|
||||
|
||||
res := &model.AppDiagnosisFlowModel{
|
||||
Times: make([]string, 0, days),
|
||||
Flow: make([]float64, 0, days),
|
||||
Unit: "MB",
|
||||
Granularity: "day",
|
||||
HasData: len(records) > 0,
|
||||
}
|
||||
for i := 0; i < days; i++ {
|
||||
current := startTime.AddDate(0, 0, i)
|
||||
res.Times = append(res.Times, current.Format("01-02"))
|
||||
res.Flow = append(res.Flow, usageMap[current.Format("2006-01-02")])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *sAppDiagnosis) queryDiagnosisMonthlyFlow(ctx context.Context, iccid string, months int) (*model.AppDiagnosisFlowModel, error) {
|
||||
if months <= 0 {
|
||||
months = 6
|
||||
}
|
||||
now := time.Now()
|
||||
currentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
startMonth := currentMonth.AddDate(0, -(months - 1), 0)
|
||||
endTime := time.Date(now.Year(), now.Month()+1, 0, 23, 59, 59, 0, now.Location())
|
||||
|
||||
records, err := appDiagnosisQueryTrafficRows(ctx, iccid, appDiagnosisMonthlyStable, startMonth, endTime)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询SIM卡使用流量失败,请稍后重试")
|
||||
}
|
||||
usageMap := make(map[string]float64, len(records))
|
||||
for _, item := range records {
|
||||
usageMap[item.Ts.Format("2006-01")] = item.UsageMb
|
||||
}
|
||||
|
||||
res := &model.AppDiagnosisFlowModel{
|
||||
Times: make([]string, 0, months),
|
||||
Flow: make([]float64, 0, months),
|
||||
Unit: "MB",
|
||||
Granularity: "month",
|
||||
HasData: len(records) > 0,
|
||||
}
|
||||
for i := 0; i < months; i++ {
|
||||
current := startMonth.AddDate(0, i, 0)
|
||||
key := current.Format("2006-01")
|
||||
res.Times = append(res.Times, key)
|
||||
res.Flow = append(res.Flow, usageMap[key])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func appDiagnosisQueryTrafficRows(ctx context.Context, iccid string, stable string, startTime, endTime time.Time) ([]appDiagnosisTrafficRecord, error) {
|
||||
iccid = strings.TrimSpace(iccid)
|
||||
if iccid == "" {
|
||||
return nil, gerror.New("traffic usage query iccid is empty")
|
||||
}
|
||||
cfg := appDiagnosisGetTrafficConfig(ctx)
|
||||
if !cfg.Enabled || strings.TrimSpace(cfg.Dsn) == "" {
|
||||
g.Log().Debug(ctx, "app diagnosis traffic query skipped, tdengine disabled")
|
||||
return []appDiagnosisTrafficRecord{}, nil
|
||||
}
|
||||
dbName := cfg.dbNameFor(stable)
|
||||
if err := appDiagnosisValidateTableConfig(dbName, stable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driverName := strings.TrimSpace(cfg.DriverName)
|
||||
if driverName == "" {
|
||||
driverName = "taosWS"
|
||||
}
|
||||
db, err := sql.Open(driverName, cfg.Dsn)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "tdengine open failed")
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
db.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||
|
||||
table := appDiagnosisQualify(dbName, appDiagnosisChildTrafficTableName(stable, iccid))
|
||||
rows, err := db.QueryContext(ctx, fmt.Sprintf(
|
||||
"SELECT ts, usage_mb, package_code, iccid FROM %s%s ORDER BY ts ASC",
|
||||
table,
|
||||
appDiagnosisWhereRangeSQL(startTime, endTime),
|
||||
))
|
||||
if err != nil {
|
||||
if appDiagnosisIsTableNotExist(err) {
|
||||
return []appDiagnosisTrafficRecord{}, nil
|
||||
}
|
||||
return nil, gerror.Wrap(err, "tdengine query traffic usage failed")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
list := make([]appDiagnosisTrafficRecord, 0)
|
||||
for rows.Next() {
|
||||
var item appDiagnosisTrafficRecord
|
||||
if err = rows.Scan(&item.Ts, &item.UsageMb, &item.PackageCode, &item.Iccid); err != nil {
|
||||
return nil, gerror.Wrap(err, "tdengine scan traffic usage failed")
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, gerror.Wrap(err, "tdengine rows traffic usage failed")
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func appDiagnosisGetTrafficConfig(ctx context.Context) appDiagnosisTrafficConfig {
|
||||
// dbName 只是兑底键名,不决定实际访问哪个库;默认取日库,避免兑底到不存在的库名。
|
||||
dbName := strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dbName", "iotlcm_daily").String())
|
||||
return appDiagnosisTrafficConfig{
|
||||
Enabled: strings.EqualFold(g.Cfg().MustGet(ctx, "tsdb.database", "TdEngine").String(), "TdEngine"),
|
||||
DriverName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.type", "taosWS").String()),
|
||||
Dsn: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dsn", "").String()),
|
||||
DbName: dbName,
|
||||
DailyDbName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.dailyDbName", dbName).String()),
|
||||
MonthlyDbName: strings.TrimSpace(g.Cfg().MustGet(ctx, "tsdb.tdengine.monthlyDbName", dbName).String()),
|
||||
MaxOpenConns: g.Cfg().MustGet(ctx, "tsdb.tdengine.maxOpenConns", 100).Int(),
|
||||
MaxIdleConns: g.Cfg().MustGet(ctx, "tsdb.tdengine.maxIdleConns", 10).Int(),
|
||||
}
|
||||
}
|
||||
|
||||
func appDiagnosisValidateTableConfig(database string, stable string) error {
|
||||
if !appDiagnosisIdentifierPattern.MatchString(database) {
|
||||
return gerror.Newf("invalid tdengine database name: %s", database)
|
||||
}
|
||||
if !appDiagnosisIdentifierPattern.MatchString(stable) {
|
||||
return gerror.Newf("invalid tdengine stable name: %s", stable)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appDiagnosisQualify(database, table string) string {
|
||||
return fmt.Sprintf("%s.%s", database, table)
|
||||
}
|
||||
|
||||
func appDiagnosisChildTrafficTableName(stable string, iccid string) string {
|
||||
prefix := "sim_card_usage_m_"
|
||||
if stable == appDiagnosisDailyStable {
|
||||
prefix = "sim_card_usage_d_"
|
||||
}
|
||||
return appDiagnosisChildTableNameWithPrefix(prefix, iccid)
|
||||
}
|
||||
|
||||
func appDiagnosisChildTableNameWithPrefix(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, appDiagnosisHashString(value))
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func appDiagnosisHashString(value string) uint32 {
|
||||
hash := fnv.New32a()
|
||||
_, _ = hash.Write([]byte(value))
|
||||
return hash.Sum32()
|
||||
}
|
||||
|
||||
func appDiagnosisWhereRangeSQL(startTime, endTime time.Time) string {
|
||||
clauses := make([]string, 0, 2)
|
||||
if !startTime.IsZero() {
|
||||
clauses = append(clauses, "ts >= "+appDiagnosisSQLTime(startTime))
|
||||
}
|
||||
if !endTime.IsZero() {
|
||||
clauses = append(clauses, "ts <= "+appDiagnosisSQLTime(endTime))
|
||||
}
|
||||
if len(clauses) == 0 {
|
||||
return ""
|
||||
}
|
||||
return " WHERE " + strings.Join(clauses, " AND ")
|
||||
}
|
||||
|
||||
func appDiagnosisSQLTime(value time.Time) string {
|
||||
return appDiagnosisSQLString(value.Format("2006-01-02 15:04:05.000"))
|
||||
}
|
||||
|
||||
func appDiagnosisSQLString(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
||||
}
|
||||
|
||||
func appDiagnosisIsTableNotExist(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")
|
||||
}
|
||||
Reference in New Issue
Block a user