diff --git a/README.md b/README.md index 07a340c..916a860 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,71 @@ # laic-backend -低空智控平台 后端go \ No newline at end of file +低空智控平台 后端 Go 服务。 + +## 生产部署 + +部署脚本参考 `D:/GoProject/roller-service/roller-manage/publish-prod.sh`,使用 Supervisor 管理服务。 + +### 服务器准备 + +在生产服务器安装 Supervisor。生产配置保存在仓库根目录的 `config-prod.yaml`,部署脚本会将它上传为服务器部署目录中的 `config.yaml`: + +```bash +sudo apt update +sudo apt install supervisor -y +sudo systemctl enable --now supervisor +``` + +生产配置至少包含: + +```yaml +oss: + endpoint: https://obs.cn-east-2.myhuaweicloud.com + access-key: 实际的华为云 OBS AK + secret-key: 实际的华为云 OBS SK + bucket: gcs-edu + region: cn-east-2 + use-ssl: true +``` + +后端启动时直接读取部署目录下的 `config.yaml`。本地开发不设置 `LAIC_CONFIG_FILE` 时读取本地 `config.yaml`;Supervisor 显式设置 `LAIC_CONFIG_FILE=config.yaml`。 + +### 执行部署 + +修改 `publish-prod.sh` 顶部的 `REMOTE`、`REMOTE_DIR` 和服务参数,然后执行: + +```bash +chmod +x publish-prod.sh +./publish-prod.sh +``` + +脚本会完成: + +1. 交叉编译 Linux amd64 二进制。 +2. 创建远程部署目录、日志目录和备份目录。 +3. 备份旧二进制和 `config.yaml`(首次部署没有旧文件时跳过)。 +4. 将本地 `config-prod.yaml` 上传为远程 `config.yaml`。 +5. 更新 Supervisor 配置、重启服务并检查状态。 +6. 启动失败时自动恢复最近的旧二进制。 + +首次部署前确认远程服务器已安装并运行 Supervisor: + +```bash +sudo apt update +sudo apt install supervisor -y +sudo systemctl enable --now supervisor +``` + +查看状态和日志: + +```bash +sudo supervisorctl status laic-backend-prod +sudo tail -f /usr/local/laic-backend-prod/log/info.log +sudo tail -f /usr/local/laic-backend-prod/log/err.log +``` + +部署目录和服务名可以在 `publish-prod.sh` 顶部调整。回滚时,脚本会使用远程 `backup/` 目录中最近的二进制备份;也可以手工恢复后执行: + +```bash +sudo supervisorctl restart laic-backend-prod +``` diff --git a/common/config.go b/common/config.go index 6808f9e..5632e98 100644 --- a/common/config.go +++ b/common/config.go @@ -8,6 +8,7 @@ import ( type AppConfig struct { ApiPort int `mapstructure:"api-port"` + SmsURL string `mapstructure:"sms-url"` Mysql Mysql `mapstructure:"mysql"` TDengine TDengine `mapstructure:"tdengine"` Redis string `mapstructure:"redis"` diff --git a/config-prod.yaml b/config-prod.yaml new file mode 100644 index 0000000..9712f2e --- /dev/null +++ b/config-prod.yaml @@ -0,0 +1,48 @@ +api-port: 9913 +sms-url: 192.168.1.195:21001 +mysql: + url: "root:jiagu^2058@tcp(192.168.0.111:3306)/laic?charset=utf8&parseTime=true&loc=Asia%2FShanghai" + idle: 10 + max-conn: 100 + max-wait: 30 +tdengine: + dsn: "root:taosdata@ws(172.16.0.216:6041)/laic_track" +redis: "redis://:Test123@192.168.1.13:6379/7" +mqtt: + broker: "tcp://192.168.0.6:1883" + client-id: laic-backend-prod + username: laic + password: "" +jwt: + secret: laic-backend-production-secret-change-me + access-expire-hours: 168 + refresh-expire-hours: 720 +oss: + endpoint: https://obs.cn-east-2.myhuaweicloud.com + access-key: 7TGDNNPBKVOPQ66JQNRU + secret-key: MMW9OSFChH7haJmTXoN4m8roCfIc22ffG5zrt2zT + bucket: gcs-edu + region: cn-east-2 + use-ssl: true +live: + mode: fake + provider: fake + push-domain: local-push + play-domain: local-play + app-name: dock-live + auth-expire-seconds: 7200 + play-url-ttl-seconds: 300 + protocol: srt + srt-port: 1105 + max-session-seconds: 7200 + viewer-lease-seconds: 30 + stop-grace-seconds: 15 + reconcile-interval-seconds: 10 + record-policy: disabled + allow-real-cloud: false +heartbeat: + timeout-seconds: 30 + scan-seconds: 5 +log: + level: info + path: ./logs/ diff --git a/config.yaml b/config.yaml index b20c8ae..dc23898 100644 --- a/config.yaml +++ b/config.yaml @@ -1,21 +1,22 @@ api-port: 8080 +sms-url: 192.168.1.195:21001 mysql: - url: root:123456@tcp(127.0.0.1:3306)/laic?charset=utf8mb4&parseTime=True&loc=Local + url: "root:123456@tcp(127.0.0.1:3306)/laic?charset=utf8mb4&parseTime=True&loc=Local" idle: 10 max-conn: 50 max-wait: 3600 tdengine: - dsn: root:taosdata@ws(127.0.0.1:6041)/laic -redis: redis://127.0.0.1:6379/0 + dsn: "root:taosdata@ws(127.0.0.1:6041)/laic" +redis: "redis://127.0.0.1:6379/0" mqtt: - broker: tcp://127.0.0.1:1883 + broker: "tcp://127.0.0.1:1883" client-id: laic-backend username: laic password: "" jwt: secret: laic-jwt-secret-change-me - access-expire-hours: 168 # 7 天 - refresh-expire-hours: 720 # 30 天 + access-expire-hours: 168 + refresh-expire-hours: 720 oss: endpoint: https://obs.cn-east-2.myhuaweicloud.com access-key: "" diff --git a/config/laic-backend-prod.conf b/config/laic-backend-prod.conf new file mode 100644 index 0000000..8990d00 --- /dev/null +++ b/config/laic-backend-prod.conf @@ -0,0 +1,12 @@ +[program:laic-backend-prod] +command=/usr/local/laic-backend-prod/laic-backend-prod.srv +directory=/usr/local/laic-backend-prod/ +autostart=true +autorestart=true +startsecs=3 +environment=LAIC_CONFIG_FILE="config.yaml" +stopasgroup=true +killasgroup=true +stderr_logfile=/usr/local/laic-backend-prod/log/err.log +stdout_logfile=/usr/local/laic-backend-prod/log/info.log +user=root diff --git a/go.mod b/go.mod index ffde7d1..7b7f4a3 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,8 @@ require ( github.com/spf13/viper v1.21.0 github.com/taosdata/driver-go/v3 v3.3.1 golang.org/x/crypto v0.52.0 + google.golang.org/grpc v1.79.2 + google.golang.org/protobuf v1.36.10 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.2 @@ -61,5 +63,5 @@ require ( golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect ) diff --git a/go.sum b/go.sum index 67625be..fe719ae 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uS github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -23,6 +25,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -41,6 +47,8 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -121,6 +129,18 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -138,6 +158,12 @@ golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 4e6e6be..7840cdb 100644 --- a/main.go +++ b/main.go @@ -19,8 +19,12 @@ func main() { logger.InitCustomLog(logger.Level_D, nil, "laic-backend") // 2. 加载 config.yaml + configPath := os.Getenv("LAIC_CONFIG_FILE") + if configPath == "" { + configPath = "config.yaml" + } conf := &common.AppConfig{} - common.LoadConfig("config.yaml", conf) + common.LoadConfig(configPath, conf) if err := service.InitLiveProvider(conf.Live); err != nil { logger.ERROR("直播提供商初始化失败", err) panic(err) @@ -35,7 +39,6 @@ func main() { // 4. 预热缓存 + 启动遥测批量写入 cache.WarmUp() - service.BootstrapOperationLog() service.DefaultTelemetryStore.Start() // 5. 启动 MQTT(订阅设备上行消息)与 HTTP diff --git a/mock/dock.go b/mock/dock.go index de35a65..5bffccf 100644 --- a/mock/dock.go +++ b/mock/dock.go @@ -15,7 +15,7 @@ import ( type downlinkEnvelope struct { RequestID string `json:"requestId"` DockID string `json:"dockId"` - DroneSN string `json:"droneSn"` + DroneSN *string `json:"droneSn"` Payload json.RawMessage `json:"payload"` } @@ -211,7 +211,9 @@ func (d *MockDock) onCommand(_ paho.Client, msg paho.Message) { return } cmd.RequestID = env.RequestID - cmd.DroneSN = env.DroneSN + if env.DroneSN != nil { + cmd.DroneSN = *env.DroneSN + } log.Printf("[%s] 收到指令 %s (commandId=%s)", d.spec.DockID, cmd.Type, cmd.CommandID) switch { diff --git a/mqtt/message.go b/mqtt/message.go index b63aac1..338495f 100644 --- a/mqtt/message.go +++ b/mqtt/message.go @@ -19,7 +19,7 @@ type Envelope struct { EventID string `json:"eventId"` Version int64 `json:"version"` DockID string `json:"dockId"` - DroneSN string `json:"droneSn"` + DroneSN *string `json:"droneSn"` Timestamp int64 `json:"timestamp"` Payload json.RawMessage `json:"payload"` } @@ -44,10 +44,14 @@ func NewEnvelope(requestID, dockID, droneSN string, payload any) *Envelope { raw = b } } + var droneSNValue *string + if droneSN != "" { + droneSNValue = &droneSN + } return &Envelope{ RequestID: requestID, DockID: dockID, - DroneSN: droneSN, + DroneSN: droneSNValue, Timestamp: time.Now().UnixMilli(), Payload: raw, } diff --git a/publish-prod.sh b/publish-prod.sh new file mode 100644 index 0000000..e1f8559 --- /dev/null +++ b/publish-prod.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +TARGET="laic-backend-prod.srv" +REMOTE="root@jg-serv1" +REMOTE_DIR="/usr/local/laic-backend-prod" +SERVICE_NAME="laic-backend-prod" +SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d" +WAIT_TIME=3 +BACKUP_DIR="$REMOTE_DIR/backup" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +LOCAL_CONF="$SCRIPT_DIR/config/$SERVICE_NAME.conf" +LOCAL_CONFIG="$SCRIPT_DIR/config-prod.yaml" +LOCAL_ARTIFACT="$SCRIPT_DIR/$TARGET" + +cleanup() { + rm -f -- "$LOCAL_ARTIFACT" +} +trap cleanup EXIT + +remote_cmd() { + ssh "$REMOTE" "$@" /dev/null 2>&1 || { + printf 'Missing required command: %s\n' "$1" >&2 + exit 1 + } +} + +check_inputs() { + require_command go + require_command ssh + require_command scp + require_command diff + [[ -f "$LOCAL_CONF" ]] || { printf 'Missing Supervisor config: %s\n' "$LOCAL_CONF" >&2; exit 1; } + [[ -f "$LOCAL_CONFIG" ]] || { printf 'Missing production config: %s\n' "$LOCAL_CONFIG" >&2; exit 1; } + if [[ "$REMOTE" == *your-production-host* ]]; then + printf 'Set REMOTE in publish-prod.sh before deploying.\n' >&2 + exit 1 + fi +} + +build_local() { + printf 'Building Linux amd64 binary...\n' + (cd "$SCRIPT_DIR" && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o "$LOCAL_ARTIFACT" .) +} + +ensure_remote_dirs() { + remote_cmd "mkdir -p '$REMOTE_DIR' '$REMOTE_DIR/config' '$REMOTE_DIR/log' '$BACKUP_DIR'" +} + +check_remote_config() { + if ! remote_cmd "test -s '$REMOTE_DIR/config.yaml'"; then + printf 'Remote config.yaml is missing or empty: %s/config.yaml\n' "$REMOTE_DIR" >&2 + printf 'Upload config.yaml with OBS credentials before deploying.\n' >&2 + exit 1 + fi +} + +backup_old() { + remote_cmd " + if [ -f '$REMOTE_DIR/$TARGET' ]; then + timestamp=\$(date +'%Y%m%d%H%M%S') + cp '$REMOTE_DIR/$TARGET' '$BACKUP_DIR/$TARGET.'\$timestamp + echo 'Previous binary backed up.' + fi + if [ -f '$REMOTE_DIR/config.yaml' ]; then + timestamp=\$(date +'%Y%m%d%H%M%S') + cp '$REMOTE_DIR/config.yaml' '$BACKUP_DIR/config.yaml.'\$timestamp + fi + " +} + +upload_files() { + scp "$LOCAL_ARTIFACT" "$REMOTE:$REMOTE_DIR/$TARGET.new" + remote_cmd "mv -f '$REMOTE_DIR/$TARGET.new' '$REMOTE_DIR/$TARGET' && chmod 755 '$REMOTE_DIR/$TARGET'" + + scp "$LOCAL_CONFIG" "$REMOTE:$REMOTE_DIR/config.yaml.new" + remote_cmd "mv -f '$REMOTE_DIR/config.yaml.new' '$REMOTE_DIR/config.yaml' && chmod 640 '$REMOTE_DIR/config.yaml'" +} + +upload_supervisor_conf() { + local remote_conf="$SUPERVISOR_CONF_DIR/$SERVICE_NAME.conf" + if remote_cmd "test -f '$remote_conf'"; then + if diff -q "$LOCAL_CONF" <(ssh "$REMOTE" "cat '$remote_conf'") >/dev/null; then + printf 'Supervisor config unchanged.\n' + return + fi + fi + scp "$LOCAL_CONF" "$REMOTE:$remote_conf" + remote_cmd "supervisorctl reread && supervisorctl update" +} + +restart_service() { + local status + status="$(remote_cmd "supervisorctl status '$SERVICE_NAME' 2>/dev/null | awk '{print \$2}'" || true)" + if [[ "$status" == RUNNING ]]; then + remote_cmd "supervisorctl restart '$SERVICE_NAME'" + else + remote_cmd "supervisorctl start '$SERVICE_NAME'" + fi + sleep "$WAIT_TIME" + if remote_cmd "supervisorctl status '$SERVICE_NAME' | grep -q RUNNING"; then + printf 'Service is running.\n' + remote_cmd "tail -n 20 '$REMOTE_DIR/log/info.log'" || true + return + fi + printf 'Service failed to start; rolling back binary.\n' >&2 + rollback + exit 1 +} + +rollback() { + local latest_backup + latest_backup="$(remote_cmd "ls -t '$BACKUP_DIR/$TARGET.'* 2>/dev/null | head -n 1" || true)" + [[ -n "$latest_backup" ]] || { printf 'No binary backup available.\n' >&2; return 1; } + remote_cmd "cp '$latest_backup' '$REMOTE_DIR/$TARGET' && chmod 755 '$REMOTE_DIR/$TARGET' && supervisorctl restart '$SERVICE_NAME'" +} + +main() { + check_inputs + build_local + ensure_remote_dirs + backup_old + upload_files + upload_supervisor_conf + restart_service + printf 'Deployment complete.\n' +} + +main "$@" diff --git a/service/dock_service.go b/service/dock_service.go index 6420a0a..bfb5b8c 100644 --- a/service/dock_service.go +++ b/service/dock_service.go @@ -93,6 +93,50 @@ func (s *DockService) Create(userID int64, isAdmin bool, req *vo.DockCreateReq) owner = req.UserID } + var existing model.Dock + if err := common.DB.Where("dock_id = ?", req.DockID).First(&existing).Error; err == nil { + if existing.UserID != 0 { + return nil, common.ErrDockExists + } + updates := map[string]any{ + "user_id": owner, + "register_status": "registered", + "updated_at": time.Now(), + } + if req.Name != "" { + updates["name"] = req.Name + } + if req.Code != "" { + updates["code"] = req.Code + } + if req.SN != "" { + updates["sn"] = req.SN + } + if req.Iccid != "" { + updates["iccid"] = req.Iccid + } + if req.Location != "" { + updates["location"] = req.Location + } + updates["longitude"] = req.Longitude + updates["latitude"] = req.Latitude + updates["altitude"] = req.Altitude + if err := common.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&existing).Updates(updates).Error; err != nil { + return err + } + return tx.Model(&model.Drone{}).Where("dock_id = ?", existing.DockID).Update("user_id", owner).Error + }); err != nil { + logger.ERROR("认领机巢失败", err) + return nil, common.ErrInternal + } + common.DB.First(&existing, existing.ID) + return &existing, nil + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + logger.ERROR("查询机巢失败", err) + return nil, common.ErrInternal + } + id, err := tool.NextID() if err != nil { return nil, common.ErrInternal diff --git a/service/mqtt_subscriber.go b/service/mqtt_subscriber.go index 1fde256..f94be32 100644 --- a/service/mqtt_subscriber.go +++ b/service/mqtt_subscriber.go @@ -230,8 +230,8 @@ func handleStateDrone(dockID string, env *mqtt.Envelope) { return } droneSN := st.DroneSN - if droneSN == "" { - droneSN = env.DroneSN + if droneSN == "" && env.DroneSN != nil { + droneSN = *env.DroneSN } battery := int(st.BatteryPercent) DefaultDroneService.EnsureFromState(dockID, droneSN, st.Name, st.Online, battery, "") @@ -256,9 +256,9 @@ func handleTelemetry(dockID string, env *mqtt.Envelope) { DefaultTelemetryStore.Append(dockID, p) DefaultTrajectoryStore.Append(dockID, p) } - if env.DroneSN != "" { - DefaultTelemetryStore.SetDroneSN(dockID, env.DroneSN) - refreshDeviceHeartbeat(cache.DroneHeartbeatKeyOf(env.DroneSN)) + if env.DroneSN != nil && *env.DroneSN != "" { + DefaultTelemetryStore.SetDroneSN(dockID, *env.DroneSN) + refreshDeviceHeartbeat(cache.DroneHeartbeatKeyOf(*env.DroneSN)) } } diff --git a/service/operation_log_service.go b/service/operation_log_service.go index aab5f45..64ec96a 100644 --- a/service/operation_log_service.go +++ b/service/operation_log_service.go @@ -14,13 +14,6 @@ type OperationLogService struct{} var DefaultOperationLogService = &OperationLogService{} -// BootstrapOperationLog 确保基础运行时表存在(历史库未跑 DDL 时兜底)。 -func BootstrapOperationLog() { - if err := common.DB.AutoMigrate(&model.User{}, &model.OperationLog{}, &model.LiveViewerLease{}, &model.UserLoginSession{}, &model.InvoiceProfile{}, &model.InvoiceRequest{}); err != nil { - logger.ERROR("迁移基础运行时表失败", err) - } -} - // Record 写入一条操作日志 func (s *OperationLogService) Record(userID int64, userName, module, action, detail, result, ip string) { if userID == 0 { diff --git a/service/sms_service.go b/service/sms_service.go new file mode 100644 index 0000000..d8e6fd3 --- /dev/null +++ b/service/sms_service.go @@ -0,0 +1,86 @@ +package service + +import ( + "context" + "errors" + "sync" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "laic-backend/service/smsrpc" +) + +const ( + smsRequestTimeout = time.Second + smsLoginTemplateID = "SMS_73670083" + smsRegisterTemplate = "SMS_73670081" +) + +type smsTemplate struct { + verifyType int32 + id string +} + +var smsTemplates = map[string]smsTemplate{ + "login": {verifyType: 1, id: smsLoginTemplateID}, + "register": {verifyType: 2, id: smsRegisterTemplate}, + "reset": {verifyType: 1, id: smsLoginTemplateID}, +} + +type smsRPCClient struct { + mu sync.Mutex + url string + conn *grpc.ClientConn + close func() error +} + +var defaultSMSRPCClient smsRPCClient + +func sendSMS(url, scene, phone, code string) error { + template, ok := smsTemplates[scene] + if !ok || url == "" { + return errors.New("短信服务配置错误") + } + + client, err := defaultSMSRPCClient.client(url) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), smsRequestTimeout) + defer cancel() + + result, err := client.SendSms(ctx, &message.SmsRequest{ + VerifyCode: code, + VerifyType: template.verifyType, + SmsId: template.id, + Phone: phone, + }) + if err != nil || result == nil || !result.Value { + return errors.New("短信发送失败") + } + return nil +} + +func (c *smsRPCClient) client(url string) (message.SmsClient, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.conn != nil && c.url == url { + return message.NewSmsClient(c.conn), nil + } + if c.close != nil { + _ = c.close() + } + + conn, err := grpc.NewClient(url, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + c.url = url + c.conn = conn + c.close = conn.Close + return message.NewSmsClient(conn), nil +} diff --git a/service/smsrpc/sms.pb.go b/service/smsrpc/sms.pb.go new file mode 100644 index 0000000..76f5f59 --- /dev/null +++ b/service/smsrpc/sms.pb.go @@ -0,0 +1,205 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v4.25.3 +// source: sms.proto + +package message + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SmsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + VerifyCode string `protobuf:"bytes,1,opt,name=verify_code,json=verifyCode,proto3" json:"verify_code,omitempty"` + VerifyType int32 `protobuf:"varint,2,opt,name=verify_type,json=verifyType,proto3" json:"verify_type,omitempty"` + SmsId string `protobuf:"bytes,3,opt,name=sms_id,json=smsId,proto3" json:"sms_id,omitempty"` + Phone string `protobuf:"bytes,4,opt,name=phone,proto3" json:"phone,omitempty"` + Sender string `protobuf:"bytes,5,opt,name=sender,proto3" json:"sender,omitempty"` + Email string `protobuf:"bytes,6,opt,name=email,proto3" json:"email,omitempty"` +} + +func (x *SmsRequest) Reset() { + *x = SmsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_sms_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SmsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SmsRequest) ProtoMessage() {} + +func (x *SmsRequest) ProtoReflect() protoreflect.Message { + mi := &file_sms_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SmsRequest.ProtoReflect.Descriptor instead. +func (*SmsRequest) Descriptor() ([]byte, []int) { + return file_sms_proto_rawDescGZIP(), []int{0} +} + +func (x *SmsRequest) GetVerifyCode() string { + if x != nil { + return x.VerifyCode + } + return "" +} + +func (x *SmsRequest) GetVerifyType() int32 { + if x != nil { + return x.VerifyType + } + return 0 +} + +func (x *SmsRequest) GetSmsId() string { + if x != nil { + return x.SmsId + } + return "" +} + +func (x *SmsRequest) GetPhone() string { + if x != nil { + return x.Phone + } + return "" +} + +func (x *SmsRequest) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *SmsRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +var File_sms_proto protoreflect.FileDescriptor + +var file_sms_proto_rawDesc = []byte{ + 0x0a, 0x09, 0x73, 0x6d, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, + 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa9, 0x01, 0x0a, 0x0a, + 0x53, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x65, + 0x72, 0x69, 0x66, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, + 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0a, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x06, + 0x73, 0x6d, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x6d, + 0x73, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, + 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, + 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x32, 0x73, 0x0a, 0x03, 0x73, 0x6d, 0x73, 0x12, 0x34, + 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x6d, 0x73, 0x12, 0x0b, 0x2e, 0x53, 0x6d, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x09, 0x53, 0x65, 0x6e, 0x64, 0x45, 0x6d, 0x61, 0x69, + 0x6c, 0x12, 0x0b, 0x2e, 0x53, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x00, 0x42, 0x0b, 0x5a, 0x09, + 0x2e, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_sms_proto_rawDescOnce sync.Once + file_sms_proto_rawDescData = file_sms_proto_rawDesc +) + +func file_sms_proto_rawDescGZIP() []byte { + file_sms_proto_rawDescOnce.Do(func() { + file_sms_proto_rawDescData = protoimpl.X.CompressGZIP(file_sms_proto_rawDescData) + }) + return file_sms_proto_rawDescData +} + +var file_sms_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_sms_proto_goTypes = []interface{}{ + (*SmsRequest)(nil), // 0: SmsRequest + (*wrapperspb.BoolValue)(nil), // 1: google.protobuf.BoolValue +} +var file_sms_proto_depIdxs = []int32{ + 0, // 0: sms.SendSms:input_type -> SmsRequest + 0, // 1: sms.SendEmail:input_type -> SmsRequest + 1, // 2: sms.SendSms:output_type -> google.protobuf.BoolValue + 1, // 3: sms.SendEmail:output_type -> google.protobuf.BoolValue + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_sms_proto_init() } +func file_sms_proto_init() { + if File_sms_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_sms_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SmsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sms_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sms_proto_goTypes, + DependencyIndexes: file_sms_proto_depIdxs, + MessageInfos: file_sms_proto_msgTypes, + }.Build() + File_sms_proto = out.File + file_sms_proto_rawDesc = nil + file_sms_proto_goTypes = nil + file_sms_proto_depIdxs = nil +} diff --git a/service/smsrpc/sms_grpc.pb.go b/service/smsrpc/sms_grpc.pb.go new file mode 100644 index 0000000..5c14d8b --- /dev/null +++ b/service/smsrpc/sms_grpc.pb.go @@ -0,0 +1,147 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.25.3 +// source: sms.proto + +package message + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Sms_SendSms_FullMethodName = "/sms/SendSms" + Sms_SendEmail_FullMethodName = "/sms/SendEmail" +) + +// SmsClient is the client API for Sms service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SmsClient interface { + SendSms(ctx context.Context, in *SmsRequest, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) + SendEmail(ctx context.Context, in *SmsRequest, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) +} + +type smsClient struct { + cc grpc.ClientConnInterface +} + +func NewSmsClient(cc grpc.ClientConnInterface) SmsClient { + return &smsClient{cc} +} + +func (c *smsClient) SendSms(ctx context.Context, in *SmsRequest, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) { + out := new(wrapperspb.BoolValue) + err := c.cc.Invoke(ctx, Sms_SendSms_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *smsClient) SendEmail(ctx context.Context, in *SmsRequest, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) { + out := new(wrapperspb.BoolValue) + err := c.cc.Invoke(ctx, Sms_SendEmail_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SmsServer is the server API for Sms service. +// All implementations must embed UnimplementedSmsServer +// for forward compatibility +type SmsServer interface { + SendSms(context.Context, *SmsRequest) (*wrapperspb.BoolValue, error) + SendEmail(context.Context, *SmsRequest) (*wrapperspb.BoolValue, error) + mustEmbedUnimplementedSmsServer() +} + +// UnimplementedSmsServer must be embedded to have forward compatible implementations. +type UnimplementedSmsServer struct { +} + +func (UnimplementedSmsServer) SendSms(context.Context, *SmsRequest) (*wrapperspb.BoolValue, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendSms not implemented") +} +func (UnimplementedSmsServer) SendEmail(context.Context, *SmsRequest) (*wrapperspb.BoolValue, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendEmail not implemented") +} +func (UnimplementedSmsServer) mustEmbedUnimplementedSmsServer() {} + +// UnsafeSmsServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SmsServer will +// result in compilation errors. +type UnsafeSmsServer interface { + mustEmbedUnimplementedSmsServer() +} + +func RegisterSmsServer(s grpc.ServiceRegistrar, srv SmsServer) { + s.RegisterService(&Sms_ServiceDesc, srv) +} + +func _Sms_SendSms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SmsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SmsServer).SendSms(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Sms_SendSms_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SmsServer).SendSms(ctx, req.(*SmsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Sms_SendEmail_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SmsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SmsServer).SendEmail(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Sms_SendEmail_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SmsServer).SendEmail(ctx, req.(*SmsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Sms_ServiceDesc is the grpc.ServiceDesc for Sms service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Sms_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sms", + HandlerType: (*SmsServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendSms", + Handler: _Sms_SendSms_Handler, + }, + { + MethodName: "SendEmail", + Handler: _Sms_SendEmail_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sms.proto", +} diff --git a/service/user_service.go b/service/user_service.go index 8e76c41..8ee7c35 100644 --- a/service/user_service.go +++ b/service/user_service.go @@ -204,13 +204,15 @@ func (s *UserService) SendSmsCode(phone, scene string) (string, *common.BusiErro return "", common.NewBusiError(common.ParamError, "验证码发送过于频繁,请稍后再试") } code := tool.RandDigit(6) + if err := sendSMS(common.AppConf.SmsURL, scene, phone, code); err != nil { + return "", common.ErrInternal + } if err := common.SetValueWithExpired(cache.SmsCodeKeyOf(phone), code, 5*60); err != nil { return "", common.ErrInternal } if err := common.SetValueWithExpired(cache.SmsRateKeyOf(phone), "1", 60); err != nil { return "", common.ErrInternal } - logger.INFO("send sms code to", phone, "code:", code) return code, nil } diff --git a/sql/tdengine_schema.sql b/sql/tdengine_schema.sql index 34147f6..209a52e 100644 --- a/sql/tdengine_schema.sql +++ b/sql/tdengine_schema.sql @@ -1,10 +1,12 @@ -- TDengine 时序库 schema(与 MySQL DDL 分离,需用 taos / REST 手动执行) -- 对应 DESIGN.md §5.9 -CREATE DATABASE IF NOT EXISTS laic VGROUPS 2 DURATION 10d KEEP 3650d; +CREATE DATABASE IF NOT EXISTS laic_track VGROUPS 2 DURATION 10d KEEP 3650d; + +USE laic_track; -- 遥测超级表(device_telemetry):列顺序必须与 service/telemetry_store.go insert() 的 VALUES 元组一致 -CREATE STABLE IF NOT EXISTS laic.device_telemetry ( +CREATE STABLE IF NOT EXISTS device_telemetry ( ts TIMESTAMP, longitude DOUBLE, latitude DOUBLE, @@ -26,7 +28,7 @@ CREATE STABLE IF NOT EXISTS laic.device_telemetry ( ); -- 轨迹超级表(任务回放用) -CREATE STABLE IF NOT EXISTS laic.task_trajectory ( +CREATE STABLE IF NOT EXISTS task_trajectory ( ts TIMESTAMP, longitude DOUBLE, latitude DOUBLE,