feat(platform): query tdengine history data

This commit is contained in:
lingniu
2026-07-03 21:22:21 +08:00
parent 2de569f104
commit f9b8182949
11 changed files with 306 additions and 55 deletions

View File

@@ -5,6 +5,7 @@ import (
"net/http"
_ "github.com/go-sql-driver/mysql"
_ "github.com/taosdata/driver-go/v3/taosWS"
"lingniu/vehicle-data-platform/apps/api/internal/app"
"lingniu/vehicle-data-platform/apps/api/internal/config"

View File

@@ -2,6 +2,16 @@ module lingniu/vehicle-data-platform/apps/api
go 1.26
require github.com/go-sql-driver/mysql v1.9.3
require (
github.com/go-sql-driver/mysql v1.9.3
github.com/taosdata/driver-go/v3 v3.8.1
)
require filippo.io/edwards25519 v1.1.0 // indirect
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
)

View File

@@ -1,4 +1,35 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/taosdata/driver-go/v3 v3.8.1 h1:kkd4ABsGiU+oXDbsw/sic985LKAvnpF9Gb/TEunTnLE=
github.com/taosdata/driver-go/v3 v3.8.1/go.mod h1:S6OGOinfR0xxxaMGsvBi9cLkYxEIW1p6qqr8QJATTlg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,6 +2,7 @@ package app
import (
"context"
"database/sql"
"log"
"net/http"
"time"
@@ -16,11 +17,20 @@ func NewServer(cfg config.Config) http.Handler {
if cfg.MySQLDSN != "" {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
db, err := platform.OpenMySQL(ctx, cfg.MySQLDSN)
db, err := platform.OpenSQL(ctx, "mysql", cfg.MySQLDSN)
if err != nil {
log.Printf("production mysql store disabled: %v", err)
} else {
store = platform.NewProductionStore(db, cfg.TDengineDatabase)
var tdengine *sql.DB
if cfg.TDengineDSN != "" {
tdengine, err = platform.OpenSQL(ctx, cfg.TDengineDriver, cfg.TDengineDSN)
if err != nil {
log.Printf("production tdengine store disabled: %v", err)
} else {
log.Printf("production tdengine store enabled")
}
}
store = platform.NewProductionStore(db, tdengine, cfg.TDengineDatabase)
log.Printf("production mysql store enabled")
}
}

View File

@@ -13,6 +13,7 @@ type Config struct {
RedisUsername string
RedisPassword string
RedisDB int
TDengineDriver string
TDengineDSN string
TDengineDatabase string
CapacityCheckBin string
@@ -28,6 +29,7 @@ func Load() Config {
RedisUsername: os.Getenv("REDIS_USERNAME"),
RedisPassword: os.Getenv("REDIS_PASSWORD"),
RedisDB: envInt("REDIS_DB", 50),
TDengineDriver: env("TDENGINE_DRIVER", "taosWS"),
TDengineDSN: os.Getenv("TDENGINE_DSN"),
TDengineDatabase: env("TDENGINE_DATABASE", "lingniu_vehicle_ts"),
CapacityCheckBin: os.Getenv("CAPACITY_CHECK_BIN"),

View File

@@ -67,6 +67,10 @@ func (m *MockStore) RealtimeLocations(_ context.Context, query url.Values) (Page
}
func (m *MockStore) HistoryLocations(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
return m.HistoryLocationsFromTDengine(ctx, query)
}
func (m *MockStore) HistoryLocationsFromTDengine(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
realtime, _ := m.RealtimeLocations(ctx, query)
rows := make([]HistoryLocationRow, 0, len(realtime.Items))
for _, row := range realtime.Items {

View File

@@ -6,24 +6,26 @@ import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
type ProductionStore struct {
db *sql.DB
database string
db *sql.DB
tdengine *sql.DB
tdDatabase string
}
func NewProductionStore(db *sql.DB, tdengineDatabase string) *ProductionStore {
func NewProductionStore(db *sql.DB, tdengine *sql.DB, tdengineDatabase string) *ProductionStore {
if db == nil {
panic("production db must not be nil")
}
return &ProductionStore{db: db, database: tdengineDatabase}
return &ProductionStore{db: db, tdengine: tdengine, tdDatabase: tdengineDatabase}
}
func OpenMySQL(ctx context.Context, dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
func OpenSQL(ctx context.Context, driver, dsn string) (*sql.DB, error) {
db, err := sql.Open(driver, dsn)
if err != nil {
return nil, err
}
@@ -139,10 +141,86 @@ func (s *ProductionStore) HistoryLocations(ctx context.Context, query url.Values
return Page[HistoryLocationRow]{Items: items, Total: realtime.Total, Limit: realtime.Limit, Offset: realtime.Offset}, nil
}
func (s *ProductionStore) HistoryLocationsFromTDengine(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
if s.tdengine == nil {
return s.HistoryLocations(ctx, query)
}
limit, offset := buildLimitOffset(query)
tdQuery := map[string]string{
"protocol": query.Get("protocol"),
"vin": query.Get("vin"),
"limit": strconv.Itoa(limit),
"offset": strconv.Itoa(offset),
}
if value := strings.TrimSpace(query.Get("dateFrom")); value != "" {
tdQuery["dateFrom"] = value
}
if value := strings.TrimSpace(query.Get("dateTo")); value != "" {
tdQuery["dateTo"] = value
}
built := buildHistoryLocationSQL(s.tdDatabase, tdQuery)
rows, err := s.tdengine.QueryContext(ctx, built.Text, built.Args...)
if err != nil {
return Page[HistoryLocationRow]{}, err
}
defer rows.Close()
items := make([]HistoryLocationRow, 0)
for rows.Next() {
var row HistoryLocationRow
var ts, receivedAt string
var longitude, latitude, speed, mileage sql.NullFloat64
if err := rows.Scan(&ts, &row.VIN, &row.Protocol, &longitude, &latitude, &speed, &mileage, &receivedAt); err != nil {
return Page[HistoryLocationRow]{}, err
}
row.Longitude = nullFloat64(longitude)
row.Latitude = nullFloat64(latitude)
row.SpeedKmh = nullFloat64(speed)
row.TotalMileageKm = nullFloat64(mileage)
row.DeviceTime = ts
row.ServerTime = firstNonEmpty(receivedAt, ts)
items = append(items, row)
}
if err := rows.Err(); err != nil {
return Page[HistoryLocationRow]{}, err
}
return Page[HistoryLocationRow]{Items: items, Total: len(items), Limit: limit, Offset: offset}, nil
}
func (s *ProductionStore) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {
_ = ctx
_ = buildRawFrameSQL(s.database, query)
return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
if s.tdengine == nil {
return Page[RawFrameRow]{Items: []RawFrameRow{}, Total: 0, Limit: query.Limit, Offset: query.Offset}, nil
}
built := buildRawFrameSQL(s.tdDatabase, query)
rows, err := s.tdengine.QueryContext(ctx, built.Text, built.Args...)
if err != nil {
return Page[RawFrameRow]{}, err
}
defer rows.Close()
items := make([]RawFrameRow, 0)
for rows.Next() {
var row RawFrameRow
var ts, frameID, eventTime, receivedAt, parsedFields, parseStatus, parseError, sourceEndpoint, protocol, vehicleKey, vin, phone string
var rawSizeBytes int
if err := rows.Scan(&ts, &frameID, &eventTime, &receivedAt, &rawSizeBytes, &parsedFields, &parseStatus, &parseError, &sourceEndpoint, &protocol, &vehicleKey, &vin, &phone); err != nil {
return Page[RawFrameRow]{}, err
}
row.ID = frameID
row.VIN = vin
row.Protocol = protocol
row.FrameType = vehicleKey
row.DeviceTime = firstNonEmpty(eventTime, ts)
row.ServerTime = firstNonEmpty(receivedAt, ts)
row.RawSizeBytes = rawSizeBytes
row.ParsedFields = parsedFieldsFromString(parsedFields)
if len(query.Fields) > 0 {
row.ParsedFields = filterParsedFieldsMap(row.ParsedFields, query.Fields)
}
items = append(items, row)
}
if err := rows.Err(); err != nil {
return Page[RawFrameRow]{}, err
}
return Page[RawFrameRow]{Items: items, Total: len(items), Limit: query.Limit, Offset: query.Offset}, nil
}
func (s *ProductionStore) DailyMileage(ctx context.Context, query url.Values) (Page[DailyMileageRow], error) {
@@ -206,11 +284,11 @@ func (s *ProductionStore) OpsHealth(ctx context.Context) (OpsHealth, error) {
{Name: "MySQL realtime", Status: mysqlStatus, Detail: mysqlDetail},
{Name: "vehicle_realtime_snapshot", Status: "ok", Detail: "读取实时快照表"},
{Name: "vehicle_realtime_location", Status: "ok", Detail: "读取实时位置表"},
{Name: "TDengine raw_frames", Status: "warning", Detail: "中台 RAW 查询接口待接 TDengine 驱动"},
{Name: "TDengine raw_frames", Status: tdengineStatus(s.tdengine), Detail: tdengineDetail(s.tdengine)},
},
KafkaLag: 0,
RedisOnlineKeys: redisKeys,
TDengineWritable: false,
TDengineWritable: s.tdengine != nil,
MySQLWritable: mysqlStatus == "ok",
}, nil
}
@@ -242,3 +320,50 @@ func parsedFieldsFromString(value string) map[string]any {
}
return fields
}
func filterParsedFieldsMap(fields map[string]any, names []string) map[string]any {
if len(names) == 0 || len(fields) == 0 {
return fields
}
out := make(map[string]any, len(names))
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if value, ok := fields[name]; ok {
out[name] = value
}
}
return out
}
func nullFloat64(value sql.NullFloat64) float64 {
if value.Valid {
return value.Float64
}
return 0
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func tdengineStatus(db *sql.DB) string {
if db == nil {
return "warning"
}
return "ok"
}
func tdengineDetail(db *sql.DB) string {
if db == nil {
return "TDengine 未配置或连接失败,历史查询降级"
}
return "读取 TDengine raw_frames / vehicle_locations"
}

View File

@@ -36,10 +36,10 @@ func TestBuildRawFrameSQL(t *testing.T) {
VIN: "VIN001",
Limit: 1,
})
if !strings.Contains(built.Text, "lingniu_vehicle_ts.raw_frames") || !strings.Contains(built.Text, "parsed_fields") {
if !strings.Contains(built.Text, "lingniu_vehicle_ts.raw_gb32960_") || !strings.Contains(built.Text, "parsed_fields") {
t.Fatalf("SQL = %s", built.Text)
}
if len(built.Args) != 4 || built.Args[0] != "GB32960" || built.Args[1] != "VIN001" || built.Args[2] != 1 {
if len(built.Args) != 0 || !strings.Contains(built.Text, "protocol = 'GB32960'") || !strings.Contains(built.Text, "vin = 'VIN001'") || !strings.Contains(built.Text, "LIMIT 1 OFFSET 0") {
t.Fatalf("args = %#v", built.Args)
}
}

View File

@@ -10,6 +10,7 @@ type Store interface {
Vehicles(context.Context, url.Values) (Page[VehicleRow], error)
RealtimeLocations(context.Context, url.Values) (Page[RealtimeLocationRow], error)
HistoryLocations(context.Context, url.Values) (Page[HistoryLocationRow], error)
HistoryLocationsFromTDengine(context.Context, url.Values) (Page[HistoryLocationRow], error)
RawFrames(context.Context, RawFrameQuery) (Page[RawFrameRow], error)
DailyMileage(context.Context, url.Values) (Page[DailyMileageRow], error)
QualityIssues(context.Context, url.Values) (Page[QualityIssueRow], error)
@@ -48,7 +49,7 @@ func (s *Service) RealtimeLocations(ctx context.Context, query url.Values) (Page
}
func (s *Service) HistoryLocations(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
return s.store.HistoryLocations(ctx, query)
return s.store.HistoryLocationsFromTDengine(ctx, query)
}
func (s *Service) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) {

View File

@@ -1,59 +1,78 @@
package platform
import "strings"
import (
"crypto/sha1"
"encoding/hex"
"strconv"
"strings"
"time"
)
func buildRawFrameSQL(database string, query RawFrameQuery) SQLQuery {
table := qualifyTDengine(database, "raw_frames")
where := []string{"1 = 1"}
table := rawFrameTable(database, query)
where := rawFrameWhere(query)
parsedFieldsSelect := "'' AS parsed_fields"
if query.IncludeFields || len(query.Fields) > 0 {
parsedFieldsSelect = "parsed_json AS parsed_fields"
}
args := []any{}
if query.Protocol != "" {
where = append(where, "protocol = ?")
args = append(args, query.Protocol)
}
if query.VIN != "" {
where = append(where, "vin = ?")
args = append(args, query.VIN)
}
if query.DateFrom != "" {
where = append(where, "ts >= ?")
args = append(args, query.DateFrom)
}
if query.DateTo != "" {
where = append(where, "ts <= ?")
args = append(args, query.DateTo)
}
limit := query.Limit
if limit <= 0 {
limit = 100
}
args = append(args, limit, query.Offset)
return SQLQuery{
Text: `SELECT ts, frame_id, event_time, received_at, raw_size_bytes, parsed_fields, parse_status, ` +
`parse_error, source_endpoint, protocol, vehicle_key, vin, phone FROM ` + table +
` WHERE ` + strings.Join(where, " AND ") + ` ORDER BY ts DESC LIMIT ? OFFSET ?`,
Args: args,
offset := query.Offset
if offset < 0 {
offset = 0
}
text := `SELECT ts, frame_id, event_time, received_at, raw_size_bytes, ` + parsedFieldsSelect +
`, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone FROM ` + table
if len(where) > 0 {
text += ` WHERE ` + strings.Join(where, " AND ")
}
text += ` ORDER BY ts DESC LIMIT ` + strconv.Itoa(limit) + ` OFFSET ` + strconv.Itoa(offset)
return SQLQuery{Text: text, Args: args}
}
func rawFrameWhere(query RawFrameQuery) []string {
where := make([]string, 0, 4)
if query.Protocol != "" {
where = append(where, "protocol = '"+quoteTDengine(strings.ToUpper(strings.TrimSpace(query.Protocol)))+"'")
}
if query.VIN != "" {
where = append(where, "vin = '"+quoteTDengine(strings.TrimSpace(query.VIN))+"'")
}
if query.DateFrom != "" {
where = append(where, "ts >= '"+quoteTDengine(normalizeTDengineTime(query.DateFrom))+"'")
}
if query.DateTo != "" {
where = append(where, "ts <= '"+quoteTDengine(normalizeTDengineTime(query.DateTo))+"'")
}
return where
}
func buildHistoryLocationSQL(database string, query map[string]string) SQLQuery {
table := qualifyTDengine(database, "vehicle_locations")
where := []string{"1 = 1"}
table := locationTable(database, query)
where := make([]string, 0, 4)
args := []any{}
if protocol := strings.TrimSpace(query["protocol"]); protocol != "" {
where = append(where, "protocol = ?")
args = append(args, protocol)
where = append(where, "protocol = '"+quoteTDengine(strings.ToUpper(protocol))+"'")
}
if vin := strings.TrimSpace(query["vin"]); vin != "" {
where = append(where, "vin = ?")
args = append(args, vin)
where = append(where, "vin = '"+quoteTDengine(vin)+"'")
}
if dateFrom := strings.TrimSpace(query["dateFrom"]); dateFrom != "" {
where = append(where, "ts >= '"+quoteTDengine(normalizeTDengineTime(dateFrom))+"'")
}
if dateTo := strings.TrimSpace(query["dateTo"]); dateTo != "" {
where = append(where, "ts <= '"+quoteTDengine(normalizeTDengineTime(dateTo))+"'")
}
limit, offset := parseLimitOffset(query["limit"], query["offset"])
args = append(args, limit, offset)
return SQLQuery{
Text: `SELECT ts, vin, protocol, longitude, latitude, speed_kmh, total_mileage_km, event_time, received_at FROM ` +
table + ` WHERE ` + strings.Join(where, " AND ") + ` ORDER BY ts DESC LIMIT ? OFFSET ?`,
Args: args,
text := `SELECT ts, vin, protocol, longitude, latitude, speed_kmh, total_mileage_km, received_at FROM ` + table
if len(where) > 0 {
text += ` WHERE ` + strings.Join(where, " AND ")
}
text += ` ORDER BY ts DESC LIMIT ` + strconv.Itoa(limit) + ` OFFSET ` + strconv.Itoa(offset)
return SQLQuery{Text: text, Args: args}
}
func qualifyTDengine(database, table string) string {
@@ -62,3 +81,50 @@ func qualifyTDengine(database, table string) string {
}
return database + "." + table
}
func rawFrameTable(database string, query RawFrameQuery) string {
protocol := strings.ToUpper(strings.TrimSpace(query.Protocol))
vin := strings.TrimSpace(query.VIN)
if protocol == "" || vin == "" {
return qualifyTDengine(database, "raw_frames")
}
if protocol == "JT808" {
return qualifyTDengine(database, "raw_frames")
}
return qualifyTDengine(database, "raw_"+strings.ToLower(protocol)+"_"+hash16(vin))
}
func locationTable(database string, query map[string]string) string {
protocol := strings.ToUpper(strings.TrimSpace(query["protocol"]))
vin := strings.TrimSpace(query["vin"])
if protocol == "" || vin == "" {
return qualifyTDengine(database, "vehicle_locations")
}
return qualifyTDengine(database, "loc_"+strings.ToLower(protocol)+"_"+hash16(vin))
}
func hash16(value string) string {
sum := sha1.Sum([]byte(value))
return hex.EncodeToString(sum[:8])
}
func quoteTDengine(value string) string {
return strings.ReplaceAll(value, "'", "''")
}
func normalizeTDengineTime(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
shanghai := time.FixedZone("Asia/Shanghai", 8*3600)
for _, layout := range []string{"2006-01-02T15:04:05", "2006-01-02 15:04:05", "2006-01-02"} {
if parsed, err := time.ParseInLocation(layout, value, shanghai); err == nil {
return parsed.UTC().Format("2006-01-02 15:04:05")
}
}
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
return parsed.UTC().Format("2006-01-02 15:04:05")
}
return value
}

View File

@@ -30,7 +30,8 @@ REDIS_ADDR=r-bp1u741kij7e51i481.redis.rds.aliyuncs.com:6379
REDIS_USERNAME=lingniu_vehicle
REDIS_PASSWORD=***
REDIS_DB=50
TDENGINE_DSN=http://root:***@172.17.111.57:6041
TDENGINE_DRIVER=taosWS
TDENGINE_DSN=root:***@ws(172.17.111.57:6041)/
TDENGINE_DATABASE=lingniu_vehicle_ts
CAPACITY_CHECK_BIN=/opt/lingniu-go-native/current/capacity-check
AUTH_TOKEN=***