Compare commits

...

2 Commits

Author SHA1 Message Date
刘浩东 602c1a4015 feat:init 4 weeks ago
刘浩东 ee70de986e feat: complete auth, account, media, and OBS workflows 4 weeks ago
  1. 15
      DESIGN.md
  2. 70
      README.md
  3. 10
      cache/cache_key.go
  4. 14
      casbin.model
  5. 2
      common/busi_error.go
  6. 54
      common/casbin.go
  7. 10
      common/config.go
  8. 48
      config-prod.yaml
  9. 25
      config.yaml
  10. 12
      config/laic-backend-prod.conf
  11. 35
      go.mod
  12. 279
      go.sum
  13. 163
      handler/account_handler.go
  14. 139
      handler/account_security_invoice_handler.go
  15. 48
      handler/auth_handler.go
  16. 5
      handler/video_handler.go
  17. 11
      main.go
  18. 34
      middleware/auth.go
  19. 6
      mock/dock.go
  20. 2
      model/billing.go
  21. 2
      model/dock.go
  22. 35
      model/invoice.go
  23. 1
      model/user.go
  24. 19
      model/user_session.go
  25. 8
      mqtt/message.go
  26. 133
      publish-prod.sh
  27. 16
      route/route.go
  28. 68
      service/account_resource_service.go
  29. 25
      service/account_service.go
  30. 92
      service/billing_service.go
  31. 120
      service/captcha_service.go
  32. 90
      service/casbin_bootstrap.go
  33. 55
      service/dock_service.go
  34. 9
      service/drone_service.go
  35. 19
      service/heartbeat.go
  36. 130
      service/invoice_service.go
  37. 10
      service/mqtt_subscriber.go
  38. 7
      service/operation_log_service.go
  39. 86
      service/sms_service.go
  40. 205
      service/smsrpc/sms.pb.go
  41. 147
      service/smsrpc/sms_grpc.pb.go
  42. 6
      service/system_service.go
  43. 286
      service/user_service.go
  44. 18
      service/video_service.go
  45. 5
      sql/001_schema.sql
  46. 5
      sql/003_seed.sql
  47. 26
      sql/006_remove_device_status.sql
  48. 2
      sql/007_remove_casbin_rule.sql
  49. 9
      sql/008_billing_indexes.sql
  50. 49
      sql/009_account_security_invoice.sql
  51. 8
      sql/tdengine_schema.sql
  52. 16
      token/token.go
  53. 52
      vo/account_extra_vo.go
  54. 18
      vo/account_vo.go
  55. 12
      vo/auth_vo.go
  56. 1
      vo/dock_vo.go
  57. 1
      vo/drone_vo.go

15
DESIGN.md

@ -14,7 +14,7 @@
- **Controller → Service → Model/VO** 分层,Service 直操 `common.DB`,无独立 Repository 层 - **Controller → Service → Model/VO** 分层,Service 直操 `common.DB`,无独立 Repository 层
- **包级单例**:`common.DB`、`common.RedisPool`、`service.DefaultXxxService` - **包级单例**:`common.DB`、`common.RedisPool`、`service.DefaultXxxService`
- **本地 YAML 配置**:一个 `config.yaml`,Viper 加载 - **本地 YAML 配置**:一个 `config.yaml`,Viper 加载
- **JWT + Casbin RBAC**:`gorm-adapter` 持久化,Redis 缓存权限。admin 全量数据,user`user_id` 过滤
- **JWT + 路由管理员鉴权**:JWT 负责身份认证,`user.role` 和 `AdminMiddleware` 限制平台管理接口;业务数据由 service`user_id` 过滤
- **统一响应** `{code, msg, data}` + 中文验证错误翻译 - **统一响应** `{code, msg, data}` + 中文验证错误翻译
- **Redigo Redis Pool**(非 go-redis) - **Redigo Redis Pool**(非 go-redis)
@ -30,10 +30,10 @@
| 时序数据库 | **TDengine 3.x** | 遥测/轨迹等时序数据,对齐 pilot-data 的用法 | | 时序数据库 | **TDengine 3.x** | 遥测/轨迹等时序数据,对齐 pilot-data 的用法 |
| 缓存 | **Redis 7+(Redigo Pool)** | 设备实时状态、Session、权限、流量余额 | | 缓存 | **Redis 7+(Redigo Pool)** | 设备实时状态、Session、权限、流量余额 |
| MQTT Broker | **EMQX** | 设备通信,由甲方/边缘侧部署,后台只作为客户端连接 | | MQTT Broker | **EMQX** | 设备通信,由甲方/边缘侧部署,后台只作为客户端连接 |
| 对象存储 | **MinIO / 阿里云 OSS** | S3 兼容 API,视频和升级包存储 |
| 对象存储 | **华为云 OBS** | S3 兼容 API,视频和升级包存储;通过 path-style SigV4 预签名直传 |
| 直播 | **阿里云直播 + SRT 推流** | 后台生成签名推流地址,通过 MQTT 下发给工控机 | | 直播 | **阿里云直播 + SRT 推流** | 后台生成签名推流地址,通过 MQTT 下发给工控机 |
| 配置 | **Viper + config.yaml** | 本地配置文件 | | 配置 | **Viper + config.yaml** | 本地配置文件 |
| 认证 | **JWT HS256 + Casbin** | 无状态认证 + RBAC,7 天 access / 30 天 refresh |
| 认证 | **JWT HS256 + 路由管理员鉴权** | 身份认证 + `user.role` 管理员接口限制,7 天 access / 30 天 refresh |
| 日志 | **Zap + Lumberjack** | 结构化 + 日志轮转,对齐 pilot-train-server | | 日志 | **Zap + Lumberjack** | 结构化 + 日志轮转,对齐 pilot-train-server |
| 验证 | **go-playground/validator** | 中文翻译(`zh`) | | 验证 | **go-playground/validator** | 中文翻译(`zh`) |
| ID 生成 | **Snowflake** | `tool/snowflake.go` | | ID 生成 | **Snowflake** | `tool/snowflake.go` |
@ -48,7 +48,6 @@
laic-backend/ laic-backend/
├── main.go # 入口,初始化 → MQTT + WS + HTTP ├── main.go # 入口,初始化 → MQTT + WS + HTTP
├── go.mod / go.sum ├── go.mod / go.sum
├── casbin.model
├── config.yaml # 本地配置文件 ├── config.yaml # 本地配置文件
├── Makefile ├── Makefile
@ -58,14 +57,13 @@ laic-backend/
│ ├── tdengine.go # TDengine 连接池 │ ├── tdengine.go # TDengine 连接池
│ ├── redis.go # Redigo Pool、Lua 原子操作 │ ├── redis.go # Redigo Pool、Lua 原子操作
│ ├── config.go # Viper 加载 config.yaml │ ├── config.go # Viper 加载 config.yaml
│ ├── casbin.go # Casbin 初始化 + Redis 缓存适配器
│ ├── busi_error.go # BusiError + 错误码常量 │ ├── busi_error.go # BusiError + 错误码常量
│ ├── auth_util.go # admin 全量 / user 按 user_id 过滤 │ ├── auth_util.go # admin 全量 / user 按 user_id 过滤
│ ├── pageutil.go # 分页 │ ├── pageutil.go # 分页
│ └── validator.go # 自定义验证器(phone, password 等) │ └── validator.go # 自定义验证器(phone, password 等)
├── middleware/ # HTTP 中间件 ├── middleware/ # HTTP 中间件
│ ├── auth.go # JWT 校验 + Casbin 鉴权
│ ├── auth.go # JWT 校验 + 管理员角色鉴权
│ ├── cros.go # CORS │ ├── cros.go # CORS
│ ├── trace.go # X-Trace-Id │ ├── trace.go # X-Trace-Id
│ ├── default_log.go # 访问日志 │ ├── default_log.go # 访问日志
@ -259,7 +257,7 @@ CREATE TABLE user (
- 用户自行注册,默认角色 `user` - 用户自行注册,默认角色 `user`
- 管理员在系统初始化时手动指定,不需要组织/租户概念 - 管理员在系统初始化时手动指定,不需要组织/租户概念
- Casbin 策略:`admin` 全量数据,`user` 按 `user_id` 过滤自己的机巢
- 业务数据按 `user_id` 过滤,管理员通过路由 `AdminMiddleware` 访问平台管理接口
### 5.2 机巢 & 无人机 ### 5.2 机巢 & 无人机
@ -959,7 +957,6 @@ func main() {
common.InitConnection(&conf.Mysql) // GORM + MySQL common.InitConnection(&conf.Mysql) // GORM + MySQL
common.InitTDengine(&conf.TDengine) // TDengine 连接池 common.InitTDengine(&conf.TDengine) // TDengine 连接池
common.InitRedis(conf.Redis) // Redis Pool common.InitRedis(conf.Redis) // Redis Pool
common.InitCasbinEnforcer() // Casbin RBAC
// 4. 预热缓存 // 4. 预热缓存
go cache.WarmUp() go cache.WarmUp()
@ -1013,7 +1010,7 @@ log:
| 数据库 | MySQL | MySQL(相同) | | 数据库 | MySQL | MySQL(相同) |
| 时序数据 | TDengine | TDengine(相同) | | 时序数据 | TDengine | TDengine(相同) |
| 设备协议 | MAVLink over TCP | **MQTT**(EMQX broker) | | 设备协议 | MAVLink over TCP | **MQTT**(EMQX broker) |
| 对象存储 | 华为云 OBS | **MinIO / 阿里云 OSS**(S3 兼容) |
| 对象存储 | 华为云 OBS | **华为云 OBS**(S3 兼容,path-style SigV4 预签名) |
| 直播 | 无 | **阿里云直播 SRT 推流** | | 直播 | 无 | **阿里云直播 SRT 推流** |
| 计费 | 无 | **流量余额 + 订单 + 消费明细** | | 计费 | 无 | **流量余额 + 订单 + 消费明细** |
| 告警 | 无 | **alarmCodes diff 引擎 + 告警生命周期** | | 告警 | 无 | **alarmCodes diff 引擎 + 告警生命周期** |

70
README.md

@ -1,3 +1,71 @@
# laic-backend # laic-backend
低空智控平台 后端go
低空智控平台 后端 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
```

10
cache/cache_key.go

@ -6,6 +6,8 @@ const (
UserTokenKey = "laic:token:%d:%s" // 用户 access token 存证(userId, shortId) UserTokenKey = "laic:token:%d:%s" // 用户 access token 存证(userId, shortId)
RefreshTokenKey = "laic:refresh:%d:%s" // 用户 refresh token 存证(userId, shortId) RefreshTokenKey = "laic:refresh:%d:%s" // 用户 refresh token 存证(userId, shortId)
SmsCodeKey = "laic:sms:%s" // 短信验证码(phone) SmsCodeKey = "laic:sms:%s" // 短信验证码(phone)
SmsRateKey = "laic:sms-rate:%s" // 短信发送频率限制
CaptchaKey = "laic:captcha:%s" // 图片验证码(captcha id)
DockStatusKey = "device:dock:%s:status" // 机巢实时状态 Hash(dockId) DockStatusKey = "device:dock:%s:status" // 机巢实时状态 Hash(dockId)
DockAlarmKey = "device:dock:%s:alarm_codes" // 机巢告警码 Set(dockId) DockAlarmKey = "device:dock:%s:alarm_codes" // 机巢告警码 Set(dockId)
@ -37,6 +39,14 @@ func SmsCodeKeyOf(phone string) string {
return fmt.Sprintf(SmsCodeKey, phone) return fmt.Sprintf(SmsCodeKey, phone)
} }
func SmsRateKeyOf(phone string) string {
return fmt.Sprintf(SmsRateKey, phone)
}
func CaptchaKeyOf(id string) string {
return fmt.Sprintf(CaptchaKey, id)
}
func DockStatusKeyOf(dockID string) string { func DockStatusKeyOf(dockID string) string {
return fmt.Sprintf(DockStatusKey, dockID) return fmt.Sprintf(DockStatusKey, dockID)
} }

14
casbin.model

@ -1,14 +0,0 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == "*")

2
common/busi_error.go

@ -67,6 +67,7 @@ const (
OrderPaid = 58002 OrderPaid = 58002
SimCardNotFound = 58003 SimCardNotFound = 58003
OrderNotFound = 58004 OrderNotFound = 58004
CarrierUnavailable = 58005
) )
// 预定义的业务错误 // 预定义的业务错误
@ -117,6 +118,7 @@ var (
ErrOrderPaid = &BusiError{Code: OrderPaid, Msg: "该订单已支付"} ErrOrderPaid = &BusiError{Code: OrderPaid, Msg: "该订单已支付"}
ErrSimCardNotFound = &BusiError{Code: SimCardNotFound, Msg: "SIM卡不存在"} ErrSimCardNotFound = &BusiError{Code: SimCardNotFound, Msg: "SIM卡不存在"}
ErrOrderNotFound = &BusiError{Code: OrderNotFound, Msg: "订单不存在"} ErrOrderNotFound = &BusiError{Code: OrderNotFound, Msg: "订单不存在"}
ErrCarrierUnavailable = &BusiError{Code: CarrierUnavailable, Msg: "运营商通道未接入"}
) )
// NewBusiError 创建自定义业务错误 // NewBusiError 创建自定义业务错误

54
common/casbin.go

@ -1,54 +0,0 @@
package common
import (
"fmt"
"strconv"
"laic-backend/logger"
"github.com/casbin/casbin/v3"
gormadapter "github.com/casbin/gorm-adapter/v3"
)
var CasbinEnforcer *casbin.Enforcer
// SetUserRole 同步用户在 Casbin 中的角色映射(g 策略),role 为空则清空
func SetUserRole(userID int64, role string) error {
if CasbinEnforcer == nil {
return nil
}
uid := strconv.FormatInt(userID, 10)
if _, err := CasbinEnforcer.DeleteRolesForUser(uid); err != nil {
return err
}
if role == "" {
return nil
}
_, err := CasbinEnforcer.AddRoleForUser(uid, role)
return err
}
func InitCasbinEnforcer() {
e, err := mysqlCasbin()
if err != nil {
logger.ERROR("初始化Casbin失败", err)
panic(fmt.Sprintf("初始化Casbin失败: %v", err))
}
CasbinEnforcer = e
logger.INFO("初始化Casbin完成")
}
func mysqlCasbin() (*casbin.Enforcer, error) {
a, err := gormadapter.NewAdapterByDB(DB)
if err != nil {
return nil, err
}
e, err := casbin.NewEnforcer("casbin.model", a)
if err != nil {
return nil, err
}
if err = e.LoadPolicy(); err != nil {
return nil, err
}
return e, nil
}

10
common/config.go

@ -1,11 +1,14 @@
package common package common
import ( import (
"strings"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
type AppConfig struct { type AppConfig struct {
ApiPort int `mapstructure:"api-port"` ApiPort int `mapstructure:"api-port"`
SmsURL string `mapstructure:"sms-url"`
Mysql Mysql `mapstructure:"mysql"` Mysql Mysql `mapstructure:"mysql"`
TDengine TDengine `mapstructure:"tdengine"` TDengine TDengine `mapstructure:"tdengine"`
Redis string `mapstructure:"redis"` Redis string `mapstructure:"redis"`
@ -49,9 +52,9 @@ type JWT struct {
RefreshExpireH int `mapstructure:"refresh-expire-hours"` RefreshExpireH int `mapstructure:"refresh-expire-hours"`
} }
// OSS 对象存储(S3 兼容,MinIO / 阿里云 OSS
// OSS 对象存储(S3 兼容,华为云 OBS / MinIO
type OSS struct { type OSS struct {
Endpoint string `mapstructure:"endpoint"` // http://127.0.0.1:9000 或 https://oss-cn-hangzhou.aliyuncs.com
Endpoint string `mapstructure:"endpoint"`
AccessKey string `mapstructure:"access-key"` AccessKey string `mapstructure:"access-key"`
SecretKey string `mapstructure:"secret-key"` SecretKey string `mapstructure:"secret-key"`
Bucket string `mapstructure:"bucket"` Bucket string `mapstructure:"bucket"`
@ -89,6 +92,9 @@ type Heartbeat struct {
func LoadConfig(path string, conf *AppConfig) { func LoadConfig(path string, conf *AppConfig) {
viper.SetConfigFile(path) viper.SetConfigFile(path)
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
viper.SetEnvPrefix("LAIC")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
panic("加载配置文件失败: " + err.Error()) panic("加载配置文件失败: " + err.Error())
} }

48
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/

25
config.yaml

@ -1,28 +1,29 @@
api-port: 8080 api-port: 8080
sms-url: 192.168.1.195:21001
mysql: 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 idle: 10
max-conn: 50 max-conn: 50
max-wait: 3600 max-wait: 3600
tdengine: 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: mqtt:
broker: tcp://127.0.0.1:1883
broker: "tcp://127.0.0.1:1883"
client-id: laic-backend client-id: laic-backend
username: laic username: laic
password: "" password: ""
jwt: jwt:
secret: laic-jwt-secret-change-me 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: oss:
endpoint: http://127.0.0.1:9000
access-key: minioadmin
secret-key: minioadmin
bucket: laic-media
region: us-east-1
use-ssl: false
endpoint: https://obs.cn-east-2.myhuaweicloud.com
access-key: ""
secret-key: ""
bucket: gcs-edu
region: cn-east-2
use-ssl: true
live: live:
mode: fake mode: fake
provider: fake provider: fake

12
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

35
go.mod

@ -3,9 +3,6 @@ module laic-backend
go 1.26.4 go 1.26.4
require ( require (
github.com/bluenviron/gomavlib/v3 v3.3.5
github.com/casbin/casbin/v3 v3.8.1
github.com/casbin/gorm-adapter/v3 v3.41.0
github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/go-playground/locales v0.14.1 github.com/go-playground/locales v0.14.1
@ -20,6 +17,8 @@ require (
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/taosdata/driver-go/v3 v3.3.1 github.com/taosdata/driver-go/v3 v3.3.1
golang.org/x/crypto v0.52.0 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 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.2 gorm.io/gorm v1.31.2
@ -27,46 +26,29 @@ require (
require ( require (
filippo.io/edwards25519 v1.2.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microsoft/go-mssqldb v1.9.5 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
@ -74,21 +56,12 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
go.bug.st/serial v1.7.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/net v0.54.0 // indirect golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect golang.org/x/text v0.37.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/driver/sqlserver v1.6.3 // indirect
gorm.io/plugin/dbresolver v1.6.2 // indirect
modernc.org/libc v1.67.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.42.2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
) )

279
go.sum

@ -1,57 +1,18 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/bluenviron/gomavlib/v3 v3.3.5 h1:4e0rxKqPH1ar1cJFkft0mjsdBDnVJKWUhDQQuuqhvyk=
github.com/bluenviron/gomavlib/v3 v3.3.5/go.mod h1:tm3CnJMWdWqDGtHwUpmM2B8VY7TF67hCrCNBrwjYJpc=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= 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 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/casbin/casbin/v3 v3.8.1 h1:D4dEY4knePPR4YgNP5WZtWNaOxD0UK0LpPy9+zxtBwo=
github.com/casbin/casbin/v3 v3.8.1/go.mod h1:5rJbQr2e6AuuDDNxnPc5lQlC9nIgg6nS1zYwKXhpHC8=
github.com/casbin/gorm-adapter/v3 v3.41.0 h1:Xhpi0tfRP9aKPDWDf6dgBxHZ9UM6IophxxPIEGWqCNM=
github.com/casbin/gorm-adapter/v3 v3.41.0/go.mod h1:BQZRJhwUnwMpI+pT2m7/cUJwXxrHfzpBpPcNTyMGeGA=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
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 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -64,10 +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-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 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -84,49 +45,21 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= 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-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
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 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -135,66 +68,35 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.6 h1:k1mQU06bmmX143qSWgXFqSH1KUJceQvIUuVH/K5ELWw=
github.com/pion/transport/v3 v3.0.6/go.mod h1:HvJr2N/JwNJAfipsRleqwFoR3t/pWyHeZUs89v3+t5s=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@ -210,13 +112,10 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
@ -228,130 +127,43 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.bug.st/serial v1.7.1 h1:5aP8wYL0UjEYOVs3oPAGscjaSfRQLHtCvBFXNN/rwtc=
go.bug.st/serial v1.7.1/go.mod h1:d0MmS16Qt9b1m06yoYRNUXhRRTJV5Qg2S5EKqQtnayQ=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= 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.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 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= 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/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -359,51 +171,12 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

163
handler/account_handler.go

@ -2,154 +2,132 @@ package handler
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"laic-backend/common" "laic-backend/common"
"laic-backend/service" "laic-backend/service"
"laic-backend/vo" "laic-backend/vo"
) )
// GetProfile 查询个人资料
func GetProfile(c *gin.Context) { func GetProfile(c *gin.Context) {
user, busiErr := service.DefaultAccountService.GetProfile(common.GetUserId(c))
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.GetProfile(common.GetUserId(c))
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, user)
common.OKWithData(c, data)
} }
// UpdateProfile 更新个人资料
func UpdateProfile(c *gin.Context) { func UpdateProfile(c *gin.Context) {
var req vo.ProfileUpdateReq var req vo.ProfileUpdateReq
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err) common.FailWithBindError(c, common.ErrParam, err)
return return
} }
user, busiErr := service.DefaultAccountService.UpdateProfile(common.GetUserId(c), &req)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.UpdateProfile(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, user)
common.OKWithData(c, data)
} }
// GetTrafficBalance 查询流量余额
func GetTrafficBalance(c *gin.Context) { func GetTrafficBalance(c *gin.Context) {
bal, busiErr := service.DefaultAccountService.GetTrafficBalance(common.GetUserId(c))
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.GetTrafficBalance(common.GetUserId(c))
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, bal)
common.OKWithData(c, data)
}
func GetResourceSummary(c *gin.Context) {
data, e := service.DefaultAccountService.GetResourceSummary(common.GetUserId(c))
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, data)
} }
// GetUsagePage 流量消费流水分页
func GetUsagePage(c *gin.Context) { func GetUsagePage(c *gin.Context) {
var req vo.UsagePageReq var req vo.UsagePageReq
bindPage(c, &req.Pagination)
_ = c.ShouldBindQuery(&req) _ = c.ShouldBindQuery(&req)
if req.PageNum <= 0 {
req.PageNum = 1
data, e := service.DefaultAccountService.GetUsagePage(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return
} }
if req.PageSize <= 0 {
req.PageSize = 10
common.OKWithData(c, data)
} }
page, busiErr := service.DefaultAccountService.GetUsagePage(common.GetUserId(c), &req)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
func GetDownloadPage(c *gin.Context) {
var req vo.UsagePageReq
bindPage(c, &req.Pagination)
data, e := service.DefaultAccountService.GetDownloadPage(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, page)
common.OKWithData(c, data)
} }
// GetOrderPage 流量订单分页
func GetOrderPage(c *gin.Context) { func GetOrderPage(c *gin.Context) {
var req vo.OrderPageReq var req vo.OrderPageReq
bindPage(c, &req.Pagination)
_ = c.ShouldBindQuery(&req) _ = c.ShouldBindQuery(&req)
if req.PageNum <= 0 {
req.PageNum = 1
}
if req.PageSize <= 0 {
req.PageSize = 10
}
page, busiErr := service.DefaultAccountService.GetOrderPage(common.GetUserId(c), &req)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.GetOrderPage(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, page)
common.OKWithData(c, data)
} }
// CreateTrafficOrder 创建流量充值订单
func CreateTrafficOrder(c *gin.Context) { func CreateTrafficOrder(c *gin.Context) {
var req vo.TrafficOrderCreateReq var req vo.TrafficOrderCreateReq
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err) common.FailWithBindError(c, common.ErrParam, err)
return return
} }
order, busiErr := service.DefaultAccountService.CreateTrafficOrder(common.GetUserId(c), &req)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.CreateTrafficOrder(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, order)
common.OKWithData(c, data)
} }
// PayTrafficOrder 订单支付入账(admin)
func PayTrafficOrder(c *gin.Context) { func PayTrafficOrder(c *gin.Context) {
orderID, busiErr := parseID(c)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
id, e := parseID(c)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
order, busiErr := service.DefaultAccountService.PayTrafficOrder(orderID)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.PayTrafficOrder(id)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, order)
common.OKWithData(c, data)
} }
// ListSimCards SIM 卡列表
func ListSimCards(c *gin.Context) { func ListSimCards(c *gin.Context) {
var req vo.SimCardPageReq var req vo.SimCardPageReq
bindPage(c, &req.Pagination)
_ = c.ShouldBindQuery(&req) _ = c.ShouldBindQuery(&req)
if req.PageNum <= 0 {
req.PageNum = 1
}
if req.PageSize <= 0 {
req.PageSize = 10
}
page, busiErr := service.DefaultAccountService.ListSimCards(common.GetUserId(c), &req)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.ListSimCards(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, page)
common.OKWithData(c, data)
} }
// GetSimRechargeLogPage SIM 卡充值记录分页
func GetSimRechargeLogPage(c *gin.Context) { func GetSimRechargeLogPage(c *gin.Context) {
var req vo.SimRechargeLogPageReq var req vo.SimRechargeLogPageReq
bindPage(c, &req.Pagination)
_ = c.ShouldBindQuery(&req) _ = c.ShouldBindQuery(&req)
if req.PageNum <= 0 {
req.PageNum = 1
}
if req.PageSize <= 0 {
req.PageSize = 10
}
page, busiErr := service.DefaultAccountService.GetSimRechargeLogPage(common.GetUserId(c), &req)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.GetSimRechargeLogPage(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, page)
common.OKWithData(c, data)
} }
// RechargeSimCard SIM 卡充值
func RechargeSimCard(c *gin.Context) { func RechargeSimCard(c *gin.Context) {
simCardID, busiErr := parseID(c)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
id, e := parseID(c)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
var req vo.SimRechargeReq var req vo.SimRechargeReq
@ -157,10 +135,19 @@ func RechargeSimCard(c *gin.Context) {
common.FailWithBindError(c, common.ErrParam, err) common.FailWithBindError(c, common.ErrParam, err)
return return
} }
log, busiErr := service.DefaultAccountService.RechargeSimCard(common.GetUserId(c), simCardID, &req)
if busiErr != nil {
common.FailWithBusiError(c, busiErr)
data, e := service.DefaultAccountService.RechargeSimCard(common.GetUserId(c), id, &req)
if e != nil {
common.FailWithBusiError(c, e)
return return
} }
common.OKWithData(c, log)
common.OKWithData(c, data)
}
func bindPage(c *gin.Context, p *common.Pagination) {
_ = c.ShouldBindQuery(p)
if p.PageNum <= 0 {
p.PageNum = 1
}
if p.PageSize <= 0 {
p.PageSize = 10
}
} }

139
handler/account_security_invoice_handler.go

@ -0,0 +1,139 @@
package handler
import (
"github.com/gin-gonic/gin"
"laic-backend/common"
"laic-backend/service"
"laic-backend/vo"
"strconv"
)
func ChangePassword(c *gin.Context) {
var req vo.ChangePasswordReq
if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err)
return
}
if e := service.DefaultUserService.ChangePassword(common.GetUserId(c), &req); e != nil {
common.FailWithBusiError(c, e)
return
}
common.OK(c)
}
func ListSessions(c *gin.Context) {
data, e := service.DefaultUserService.ListSessions(common.GetUserId(c), c.GetInt64("session_id"))
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, data)
}
func RevokeSession(c *gin.Context) {
id, e := parseID(c)
if e != nil {
common.FailWithBusiError(c, e)
return
}
if be := service.DefaultUserService.RevokeSession(common.GetUserId(c), id); be != nil {
common.FailWithBusiError(c, be)
return
}
common.OK(c)
}
func RevokeOtherSessions(c *gin.Context) {
if e := service.DefaultUserService.RevokeOtherSessions(common.GetUserId(c), c.GetInt64("session_id")); e != nil {
common.FailWithBusiError(c, e)
return
}
common.OK(c)
}
func ListInvoiceProfiles(c *gin.Context) {
d, e := service.DefaultInvoiceService.ListProfiles(common.GetUserId(c))
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, d)
}
func SaveInvoiceProfile(c *gin.Context) {
var req vo.InvoiceProfileReq
if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err)
return
}
id := int64(0)
if raw := c.Param("id"); raw != "" {
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil || parsed <= 0 {
common.FailWithBusiError(c, common.ErrParam)
return
}
id = parsed
}
d, e := service.DefaultInvoiceService.SaveProfile(common.GetUserId(c), id, &req)
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, d)
}
func DeleteInvoiceProfile(c *gin.Context) {
id, e := parseID(c)
if e != nil {
common.FailWithBusiError(c, e)
return
}
if e = service.DefaultInvoiceService.DeleteProfile(common.GetUserId(c), id); e != nil {
common.FailWithBusiError(c, e)
return
}
common.OK(c)
}
func ListInvoiceOrders(c *gin.Context) {
d, e := service.DefaultInvoiceService.ListEligibleOrders(common.GetUserId(c))
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, d)
}
func CreateInvoiceRequest(c *gin.Context) {
var req vo.InvoiceRequestCreateReq
if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err)
return
}
d, e := service.DefaultInvoiceService.CreateRequest(common.GetUserId(c), &req)
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, d)
}
func ListInvoiceRequests(c *gin.Context) {
d, e := service.DefaultInvoiceService.ListRequests(common.GetUserId(c))
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, d)
}
func ReviewInvoiceRequest(c *gin.Context) {
id, e := parseID(c)
if e != nil {
common.FailWithBusiError(c, e)
return
}
var req vo.InvoiceReviewReq
if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err)
return
}
d, e := service.DefaultInvoiceService.ReviewRequest(id, &req)
if e != nil {
common.FailWithBusiError(c, e)
return
}
common.OKWithData(c, d)
}

48
handler/auth_handler.go

@ -10,6 +10,31 @@ import (
"laic-backend/vo" "laic-backend/vo"
) )
func Captcha(c *gin.Context) {
id, image, err := service.NewCaptcha()
if err != nil {
common.FailWithBusiErrorByServer(c, common.ErrInternal)
return
}
c.SetCookie(service.CaptchaCookieName(), id, int(service.CaptchaTTL().Seconds()), "/", "", false, true)
c.Header("Cache-Control", "no-store")
c.Data(200, "image/png", image)
}
// SmsCode 发送短信验证码
func SmsCode(c *gin.Context) {
var req vo.SmsCodeReq
if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err)
return
}
if _, busiErr := service.DefaultUserService.SendSmsCode(req.Phone, req.Scene); busiErr != nil {
common.FailWithBusiError(c, busiErr)
return
}
common.OK(c)
}
// Register 用户注册 // Register 用户注册
func Register(c *gin.Context) { func Register(c *gin.Context) {
var req vo.RegisterReq var req vo.RegisterReq
@ -17,7 +42,7 @@ func Register(c *gin.Context) {
common.FailWithBindError(c, common.ErrParam, err) common.FailWithBindError(c, common.ErrParam, err)
return return
} }
resp, busiErr := service.DefaultUserService.Register(req.Name, req.Phone, req.Email, req.Password)
resp, busiErr := service.DefaultUserService.Register(req.Name, req.Phone, req.Email, req.Password, req.SmsCode, c.GetHeader("User-Agent"), c.ClientIP())
if busiErr != nil { if busiErr != nil {
common.FailWithBusiError(c, busiErr) common.FailWithBusiError(c, busiErr)
return return
@ -32,7 +57,8 @@ func Login(c *gin.Context) {
common.FailWithBindError(c, common.ErrParam, err) common.FailWithBindError(c, common.ErrParam, err)
return return
} }
resp, busiErr := service.DefaultUserService.Login(req.Phone, req.Password)
captchaID, _ := c.Cookie(service.CaptchaCookieName())
resp, busiErr := service.DefaultUserService.Login(req.Phone, req.Password, req.Captcha, captchaID, req.SmsCode, c.GetHeader("User-Agent"), c.ClientIP())
if busiErr != nil { if busiErr != nil {
common.FailWithBusiError(c, busiErr) common.FailWithBusiError(c, busiErr)
return return
@ -47,7 +73,7 @@ func Refresh(c *gin.Context) {
common.FailWithBindError(c, common.ErrParam, err) common.FailWithBindError(c, common.ErrParam, err)
return return
} }
resp, busiErr := service.DefaultUserService.Refresh(req.RefreshToken)
resp, busiErr := service.DefaultUserService.Refresh(req.RefreshToken, c.GetHeader("User-Agent"), c.ClientIP())
if busiErr != nil { if busiErr != nil {
common.FailWithBusiError(c, busiErr) common.FailWithBusiError(c, busiErr)
return return
@ -62,21 +88,7 @@ func Logout(c *gin.Context) {
accessToken := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") accessToken := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
userID := common.GetUserId(c) userID := common.GetUserId(c)
if busiErr := service.DefaultUserService.Logout(userID, accessToken, req.RefreshToken); busiErr != nil {
common.FailWithBusiError(c, busiErr)
return
}
common.OK(c)
}
// SmsCode 发送短信验证码
func SmsCode(c *gin.Context) {
var req vo.SmsCodeReq
if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err)
return
}
if _, busiErr := service.DefaultUserService.SendSmsCode(req.Phone); busiErr != nil {
if busiErr := service.DefaultUserService.Logout(userID, c.GetInt64("session_id"), accessToken, req.RefreshToken); busiErr != nil {
common.FailWithBusiError(c, busiErr) common.FailWithBusiError(c, busiErr)
return return
} }

5
handler/video_handler.go

@ -31,7 +31,10 @@ func CompleteVideoUpload(c *gin.Context) {
return return
} }
var req vo.VideoCompleteReq var req vo.VideoCompleteReq
_ = c.ShouldBindJSON(&req)
if err := c.ShouldBindJSON(&req); err != nil {
common.FailWithBindError(c, common.ErrParam, err)
return
}
video, busiErr := service.DefaultVideoService.Complete(common.GetUserId(c), videoID, &req) video, busiErr := service.DefaultVideoService.Complete(common.GetUserId(c), videoID, &req)
if busiErr != nil { if busiErr != nil {
common.FailWithBusiError(c, busiErr) common.FailWithBusiError(c, busiErr)

11
main.go

@ -19,8 +19,12 @@ func main() {
logger.InitCustomLog(logger.Level_D, nil, "laic-backend") logger.InitCustomLog(logger.Level_D, nil, "laic-backend")
// 2. 加载 config.yaml // 2. 加载 config.yaml
configPath := os.Getenv("LAIC_CONFIG_FILE")
if configPath == "" {
configPath = "config.yaml"
}
conf := &common.AppConfig{} conf := &common.AppConfig{}
common.LoadConfig("config.yaml", conf)
common.LoadConfig(configPath, conf)
if err := service.InitLiveProvider(conf.Live); err != nil { if err := service.InitLiveProvider(conf.Live); err != nil {
logger.ERROR("直播提供商初始化失败", err) logger.ERROR("直播提供商初始化失败", err)
panic(err) panic(err)
@ -32,12 +36,9 @@ func main() {
common.InitConnection(&conf.Mysql) common.InitConnection(&conf.Mysql)
common.InitTDengine(&conf.TDengine) common.InitTDengine(&conf.TDengine)
common.InitRedis(conf.Redis) common.InitRedis(conf.Redis)
common.InitCasbinEnforcer()
// 4. 预热缓存 + 引导 Casbin 策略 + 启动遥测批量写入
// 4. 预热缓存 + 启动遥测批量写入
cache.WarmUp() cache.WarmUp()
service.BootstrapCasbin()
service.BootstrapOperationLog()
service.DefaultTelemetryStore.Start() service.DefaultTelemetryStore.Start()
// 5. 启动 MQTT(订阅设备上行消息)与 HTTP // 5. 启动 MQTT(订阅设备上行消息)与 HTTP

34
middleware/auth.go

@ -3,71 +3,53 @@ package middleware
import ( import (
"net/http" "net/http"
"slices" "slices"
"strconv"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"laic-backend/cache" "laic-backend/cache"
"laic-backend/common" "laic-backend/common"
"laic-backend/service"
"laic-backend/token" "laic-backend/token"
) )
// AuthMiddleware 鉴权中间件:解析 JWT + Redis 存证校验 + 注入上下文
// AuthMiddleware 鉴权中间件:解析 JWT + Redis 存证校验 + 会话校验。
func AuthMiddleware() gin.HandlerFunc { func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
tokenString := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
if tokenString == "" { if tokenString == "" {
common.FailWithBusiErrorWithHttpStatus(c, http.StatusUnauthorized, common.ErrUnauthorized) common.FailWithBusiErrorWithHttpStatus(c, http.StatusUnauthorized, common.ErrUnauthorized)
return return
} }
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
if tokenString == "" {
common.FailWithBusiErrorWithHttpStatus(c, http.StatusUnauthorized, common.ErrUnauthorized)
return
}
claims, err := token.ParseToken(tokenString) claims, err := token.ParseToken(tokenString)
if err != nil { if err != nil {
common.FailWithBusiErrorWithHttpStatus(c, http.StatusUnauthorized, common.ErrToken) common.FailWithBusiErrorWithHttpStatus(c, http.StatusUnauthorized, common.ErrToken)
return return
} }
// Redis 存证校验:登出会删除该 key,从而让已签发 token 失效
storeKey := cache.UserTokenKeyOf(claims.UserID, token.GenerateShortID(tokenString)) storeKey := cache.UserTokenKeyOf(claims.UserID, token.GenerateShortID(tokenString))
if !common.HasKey(storeKey) {
if !common.HasKey(storeKey) || !service.DefaultUserService.SessionValid(claims.UserID, claims.SessionID, tokenString) {
common.FailWithBusiErrorWithHttpStatus(c, http.StatusUnauthorized, common.SignOutToken) common.FailWithBusiErrorWithHttpStatus(c, http.StatusUnauthorized, common.SignOutToken)
return return
} }
roles := claims.Roles roles := claims.Roles
if roles == nil { if roles == nil {
roles = []string{} roles = []string{}
} }
isAdmin := slices.Contains(roles, common.RoleAdmin)
c.Set("user_id", claims.UserID) c.Set("user_id", claims.UserID)
c.Set("session_id", claims.SessionID)
c.Set("username", claims.Username) c.Set("username", claims.Username)
c.Set("roles", roles) c.Set("roles", roles)
c.Set("is_admin", isAdmin)
// Casbin 权限校验(admin 与普通用户均按 p/g 策略校验,admin 仅限平台管理职能)
if common.CasbinEnforcer != nil {
ok, err := common.CasbinEnforcer.Enforce(strconv.FormatInt(claims.UserID, 10), c.Request.URL.Path, c.Request.Method)
if err != nil || !ok {
common.FailWithBusiErrorWithHttpStatus(c, http.StatusForbidden, common.ErrForbidden)
c.Abort()
return
}
}
c.Set("is_admin", slices.Contains(roles, common.RoleAdmin))
service.DefaultUserService.TouchSession(claims.UserID, claims.SessionID)
c.Next() c.Next()
} }
} }
// AdminMiddleware 仅管理员可访问
func AdminMiddleware() gin.HandlerFunc { func AdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if !c.GetBool("is_admin") { if !c.GetBool("is_admin") {
common.FailWithBusiErrorWithHttpStatus(c, http.StatusForbidden, common.ErrForbidden) common.FailWithBusiErrorWithHttpStatus(c, http.StatusForbidden, common.ErrForbidden)
c.Abort()
return return
} }
c.Next() c.Next()

6
mock/dock.go

@ -15,7 +15,7 @@ import (
type downlinkEnvelope struct { type downlinkEnvelope struct {
RequestID string `json:"requestId"` RequestID string `json:"requestId"`
DockID string `json:"dockId"` DockID string `json:"dockId"`
DroneSN string `json:"droneSn"`
DroneSN *string `json:"droneSn"`
Payload json.RawMessage `json:"payload"` Payload json.RawMessage `json:"payload"`
} }
@ -211,7 +211,9 @@ func (d *MockDock) onCommand(_ paho.Client, msg paho.Message) {
return return
} }
cmd.RequestID = env.RequestID 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) log.Printf("[%s] 收到指令 %s (commandId=%s)", d.spec.DockID, cmd.Type, cmd.CommandID)
switch { switch {

2
model/billing.go

@ -64,7 +64,7 @@ type SimRechargeLog struct {
AmountGb int `gorm:"column:amount_gb;type:INT;not null" json:"amountGb"` AmountGb int `gorm:"column:amount_gb;type:INT;not null" json:"amountGb"`
UnitPrice float64 `gorm:"column:unit_price;type:DECIMAL(6,2)" json:"unitPrice"` UnitPrice float64 `gorm:"column:unit_price;type:DECIMAL(6,2)" json:"unitPrice"`
TotalPrice float64 `gorm:"column:total_price;type:DECIMAL(10,2)" json:"totalPrice"` TotalPrice float64 `gorm:"column:total_price;type:DECIMAL(10,2)" json:"totalPrice"`
PayStatus string `gorm:"column:pay_status;type:VARCHAR(16);default:paid" json:"payStatus"`
PayStatus string `gorm:"column:pay_status;type:VARCHAR(16);default:unpaid" json:"payStatus"`
RechargeStatus string `gorm:"column:recharge_status;type:VARCHAR(16);default:pending" json:"rechargeStatus"` // pending / success / failed RechargeStatus string `gorm:"column:recharge_status;type:VARCHAR(16);default:pending" json:"rechargeStatus"` // pending / success / failed
RechargeMsg string `gorm:"column:recharge_msg;type:VARCHAR(256);default:''" json:"rechargeMsg"` RechargeMsg string `gorm:"column:recharge_msg;type:VARCHAR(256);default:''" json:"rechargeMsg"`
IdempotentKey string `gorm:"column:idempotent_key;type:VARCHAR(64);not null;uniqueIndex:uk_recharge_idem" json:"idempotentKey"` IdempotentKey string `gorm:"column:idempotent_key;type:VARCHAR(64);not null;uniqueIndex:uk_recharge_idem" json:"idempotentKey"`

2
model/dock.go

@ -15,7 +15,6 @@ type Dock struct {
Latitude float64 `gorm:"column:latitude;type:DOUBLE;default:0" json:"latitude"` Latitude float64 `gorm:"column:latitude;type:DOUBLE;default:0" json:"latitude"`
Altitude float64 `gorm:"column:altitude;type:DOUBLE;default:0" json:"altitude"` Altitude float64 `gorm:"column:altitude;type:DOUBLE;default:0" json:"altitude"`
Location string `gorm:"column:location;type:VARCHAR(128);default:''" json:"location"` Location string `gorm:"column:location;type:VARCHAR(128);default:''" json:"location"`
Status string `gorm:"column:status;type:VARCHAR(32);default:offline" json:"status"`
RegisterStatus string `gorm:"column:register_status;type:VARCHAR(16);default:pending" json:"registerStatus"` RegisterStatus string `gorm:"column:register_status;type:VARCHAR(16);default:pending" json:"registerStatus"`
DockIDSource string `gorm:"column:dock_id_source;type:VARCHAR(32);default:''" json:"dockIdSource"` DockIDSource string `gorm:"column:dock_id_source;type:VARCHAR(32);default:''" json:"dockIdSource"`
SoftwareVer string `gorm:"column:software_ver;type:VARCHAR(16);default:''" json:"softwareVer"` SoftwareVer string `gorm:"column:software_ver;type:VARCHAR(16);default:''" json:"softwareVer"`
@ -37,7 +36,6 @@ type Drone struct {
Name string `gorm:"column:name;type:VARCHAR(64);default:''" json:"name"` Name string `gorm:"column:name;type:VARCHAR(64);default:''" json:"name"`
Code string `gorm:"column:code;type:VARCHAR(32);default:''" json:"code"` Code string `gorm:"column:code;type:VARCHAR(32);default:''" json:"code"`
Model string `gorm:"column:model;type:VARCHAR(32);default:''" json:"model"` Model string `gorm:"column:model;type:VARCHAR(32);default:''" json:"model"`
Status string `gorm:"column:status;type:VARCHAR(32);default:offline" json:"status"`
Battery int `gorm:"column:battery;type:INT;default:0" json:"battery"` Battery int `gorm:"column:battery;type:INT;default:0" json:"battery"`
FirmwareVer string `gorm:"column:firmware_ver;type:VARCHAR(16);default:''" json:"firmwareVer"` FirmwareVer string `gorm:"column:firmware_ver;type:VARCHAR(16);default:''" json:"firmwareVer"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"` CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`

35
model/invoice.go

@ -0,0 +1,35 @@
package model
import "time"
// InvoiceProfile 用户发票资料
type InvoiceProfile struct {
ID int64 `gorm:"primaryKey;column:id;type:BIGINT;not null" json:"id"`
UserID int64 `gorm:"column:user_id;type:BIGINT;not null;index" json:"userId"`
Title string `gorm:"column:title;type:VARCHAR(128);not null" json:"title"`
TaxpayerNumber string `gorm:"column:taxpayer_number;type:VARCHAR(64);not null" json:"taxpayerNumber"`
Email string `gorm:"column:email;type:VARCHAR(128);not null" json:"email"`
IsDefault bool `gorm:"column:is_default;type:TINYINT;default:0" json:"isDefault"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
func (InvoiceProfile) TableName() string { return "invoice_profile" }
// InvoiceRequest 人工发票申请
type InvoiceRequest struct {
ID int64 `gorm:"primaryKey;column:id;type:BIGINT;not null" json:"id"`
UserID int64 `gorm:"column:user_id;type:BIGINT;not null;index" json:"userId"`
OrderID int64 `gorm:"column:order_id;type:BIGINT;not null;uniqueIndex" json:"orderId"`
Amount float64 `gorm:"column:amount;type:DECIMAL(10,2);not null" json:"amount"`
Title string `gorm:"column:title;type:VARCHAR(128);not null" json:"title"`
TaxpayerNumber string `gorm:"column:taxpayer_number;type:VARCHAR(64);not null" json:"taxpayerNumber"`
Email string `gorm:"column:email;type:VARCHAR(128);not null" json:"email"`
Status string `gorm:"column:status;type:VARCHAR(16);default:pending" json:"status"`
InvoiceNumber string `gorm:"column:invoice_number;type:VARCHAR(64)" json:"invoiceNumber"`
AdminRemark string `gorm:"column:admin_remark;type:VARCHAR(256)" json:"adminRemark"`
RequestedAt time.Time `gorm:"column:requested_at" json:"requestedAt"`
ProcessedAt *time.Time `gorm:"column:processed_at" json:"processedAt"`
}
func (InvoiceRequest) TableName() string { return "invoice_request" }

1
model/user.go

@ -13,6 +13,7 @@ type User struct {
TrafficBalance int64 `gorm:"column:traffic_balance;type:BIGINT;default:0" json:"trafficBalance"` TrafficBalance int64 `gorm:"column:traffic_balance;type:BIGINT;default:0" json:"trafficBalance"`
Status int8 `gorm:"column:status;type:TINYINT;default:1" json:"status"` Status int8 `gorm:"column:status;type:TINYINT;default:1" json:"status"`
LastLogin *time.Time `gorm:"column:last_login" json:"lastLogin"` LastLogin *time.Time `gorm:"column:last_login" json:"lastLogin"`
PasswordChangedAt *time.Time `gorm:"column:password_changed_at" json:"passwordChangedAt"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"` CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
} }

19
model/user_session.go

@ -0,0 +1,19 @@
package model
import "time"
// UserLoginSession stores token fingerprints and the device metadata for one login.
type UserLoginSession struct {
ID int64 `gorm:"primaryKey;column:id;type:BIGINT;not null" json:"id"`
UserID int64 `gorm:"column:user_id;type:BIGINT;not null;index" json:"userId"`
AccessTokenID string `gorm:"column:access_token_id;type:VARCHAR(64);not null;uniqueIndex" json:"-"`
RefreshTokenID string `gorm:"column:refresh_token_id;type:VARCHAR(64);not null;uniqueIndex" json:"-"`
UserAgent string `gorm:"column:user_agent;type:VARCHAR(512)" json:"userAgent"`
IP string `gorm:"column:ip;type:VARCHAR(64)" json:"ip"`
LastActiveAt time.Time `gorm:"column:last_active_at;not null" json:"lastActiveAt"`
ExpiresAt time.Time `gorm:"column:expires_at;not null" json:"expiresAt"`
RevokedAt *time.Time `gorm:"column:revoked_at" json:"revokedAt"`
CreatedAt time.Time `gorm:"column:created_at;not null" json:"createdAt"`
}
func (UserLoginSession) TableName() string { return "user_login_session" }

8
mqtt/message.go

@ -19,7 +19,7 @@ type Envelope struct {
EventID string `json:"eventId"` EventID string `json:"eventId"`
Version int64 `json:"version"` Version int64 `json:"version"`
DockID string `json:"dockId"` DockID string `json:"dockId"`
DroneSN string `json:"droneSn"`
DroneSN *string `json:"droneSn"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
Payload json.RawMessage `json:"payload"` Payload json.RawMessage `json:"payload"`
} }
@ -44,10 +44,14 @@ func NewEnvelope(requestID, dockID, droneSN string, payload any) *Envelope {
raw = b raw = b
} }
} }
var droneSNValue *string
if droneSN != "" {
droneSNValue = &droneSN
}
return &Envelope{ return &Envelope{
RequestID: requestID, RequestID: requestID,
DockID: dockID, DockID: dockID,
DroneSN: droneSN,
DroneSN: droneSNValue,
Timestamp: time.Now().UnixMilli(), Timestamp: time.Now().UnixMilli(),
Payload: raw, Payload: raw,
} }

133
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
}
require_command() {
command -v "$1" >/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 "$@"

16
route/route.go

@ -33,6 +33,7 @@ func InitRouter(port int32) {
// 认证(公开) // 认证(公开)
auth := v1.Group("/auth") auth := v1.Group("/auth")
{ {
auth.GET("/captcha", handler.Captcha)
auth.POST("/register", handler.Register) auth.POST("/register", handler.Register)
auth.POST("/login", handler.Login) auth.POST("/login", handler.Login)
auth.POST("/refresh", handler.Refresh) auth.POST("/refresh", handler.Refresh)
@ -47,13 +48,26 @@ func InitRouter(port int32) {
account.GET("/profile", handler.GetProfile) account.GET("/profile", handler.GetProfile)
account.PUT("/profile", handler.UpdateProfile) account.PUT("/profile", handler.UpdateProfile)
account.GET("/traffic/balance", handler.GetTrafficBalance) account.GET("/traffic/balance", handler.GetTrafficBalance)
account.GET("/resource-summary", handler.GetResourceSummary)
account.GET("/downloads", handler.GetDownloadPage)
account.GET("/traffic/usage", handler.GetUsagePage) account.GET("/traffic/usage", handler.GetUsagePage)
account.GET("/traffic/orders", handler.GetOrderPage) account.GET("/traffic/orders", handler.GetOrderPage)
account.POST("/traffic/orders", handler.CreateTrafficOrder) account.POST("/traffic/orders", handler.CreateTrafficOrder)
account.POST("/traffic/orders/:id/pay", middleware.AdminMiddleware(), handler.PayTrafficOrder) account.POST("/traffic/orders/:id/pay", middleware.AdminMiddleware(), handler.PayTrafficOrder)
account.GET("/sim-cards", handler.ListSimCards) account.GET("/sim-cards", handler.ListSimCards)
account.GET("/sim-recharge-logs", handler.GetSimRechargeLogPage) account.GET("/sim-recharge-logs", handler.GetSimRechargeLogPage)
account.POST("/sim-cards/:id/recharge", handler.RechargeSimCard)
account.POST("/security/change-password", handler.ChangePassword)
account.GET("/security/sessions", handler.ListSessions)
account.DELETE("/security/sessions", handler.RevokeOtherSessions)
account.DELETE("/security/sessions/:id", handler.RevokeSession)
account.GET("/invoice-profiles", handler.ListInvoiceProfiles)
account.POST("/invoice-profiles", handler.SaveInvoiceProfile)
account.PUT("/invoice-profiles/:id", handler.SaveInvoiceProfile)
account.DELETE("/invoice-profiles/:id", handler.DeleteInvoiceProfile)
account.GET("/invoice-orders", handler.ListInvoiceOrders)
account.GET("/invoice-requests", handler.ListInvoiceRequests)
account.POST("/invoice-requests", handler.CreateInvoiceRequest)
account.PUT("/invoice-requests/:id/review", middleware.AdminMiddleware(), handler.ReviewInvoiceRequest)
} }
// 机巢 & 无人机 // 机巢 & 无人机

68
service/account_resource_service.go

@ -0,0 +1,68 @@
package service
import (
"time"
"laic-backend/common"
"laic-backend/model"
"laic-backend/vo"
)
const bytesPerGB = 1024 * 1024 * 1024
func (s *AccountService) GetDownloadPage(userID int64, req *vo.UsagePageReq) (*common.PageResponse[vo.DownloadRecordVO], *common.BusiError) {
type row struct {
model.DownloadLog
FileName string
FileSize int64
DroneSN string
ExecutionID int64
}
db := common.DB.Table("download_log").Select("download_log.*, video.file_name, video.file_size, video.drone_sn, video.execution_id").Joins("JOIN video ON video.id = download_log.video_id").Where("download_log.user_id = ?", userID)
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, common.ErrInternal
}
var rows []row
if err := db.Scopes(req.Paginate).Order("download_log.id DESC").Find(&rows).Error; err != nil {
return nil, common.ErrInternal
}
list := make([]vo.DownloadRecordVO, 0, len(rows))
for _, row := range rows {
list = append(list, vo.DownloadRecordVO{ID: row.ID, VideoID: row.VideoID, FileName: row.FileName, FileSize: row.FileSize, DroneSN: row.DroneSN, ExecutionID: row.ExecutionID, Bytes: row.Bytes, CreatedAt: row.CreatedAt})
}
return common.Page(req.Pagination, total, list), nil
}
func (s *AccountService) GetResourceSummary(userID int64) (*vo.ResourceSummaryVO, *common.BusiError) {
balance, busiErr := s.GetTrafficBalance(userID)
if busiErr != nil {
return nil, busiErr
}
now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
var usage struct{ Total, Live, Replay, Download int64 }
if err := common.DB.Model(&model.TrafficUsageLog{}).Where("user_id = ? AND created_at >= ?", userID, monthStart).Select("COALESCE(SUM(bytes_used), 0) AS total, COALESCE(SUM(CASE WHEN source_type = 'live' THEN bytes_used ELSE 0 END), 0) AS live, COALESCE(SUM(CASE WHEN source_type = 'replay' THEN bytes_used ELSE 0 END), 0) AS replay, COALESCE(SUM(CASE WHEN source_type = 'download' THEN bytes_used ELSE 0 END), 0) AS download").Scan(&usage).Error; err != nil {
return nil, common.ErrInternal
}
var simCount, dockCount, droneCount, videoCount, downloadCount int64
queries := []struct {
model any
target *int64
where string
args []any
}{
{&model.SimCard{}, &simCount, "user_id = ?", []any{userID}},
{&model.Dock{}, &dockCount, "user_id = ?", []any{userID}},
{&model.Drone{}, &droneCount, "user_id = ?", []any{userID}},
{&model.Video{}, &videoCount, "user_id = ?", []any{userID}},
{&model.DownloadLog{}, &downloadCount, "user_id = ? AND created_at >= ?", []any{userID, monthStart}},
}
for _, query := range queries {
if err := common.DB.Model(query.model).Where(query.where, query.args...).Count(query.target).Error; err != nil {
return nil, common.ErrInternal
}
}
toGB := func(value int64) float64 { return float64(value) / bytesPerGB }
return &vo.ResourceSummaryVO{BalanceGb: balance.BalanceGb, MonthUsageGb: toGB(usage.Total), MonthLiveGb: toGB(usage.Live), MonthReplayGb: toGB(usage.Replay), MonthDownloadGb: toGB(usage.Download), SimCount: simCount, DockCount: dockCount, DroneCount: droneCount, VideoCount: videoCount, MonthDownloadCnt: downloadCount}, nil
}

25
service/account_service.go

@ -16,20 +16,26 @@ type AccountService struct{}
var DefaultAccountService = &AccountService{} var DefaultAccountService = &AccountService{}
// GetProfile 查询个人资料
func (s *AccountService) GetProfile(userID int64) (*model.User, *common.BusiError) {
func (s *AccountService) GetProfile(userID int64) (*vo.ProfileVO, *common.BusiError) {
var user model.User var user model.User
if err := common.DB.First(&user, userID).Error; err != nil {
if err := common.DB.Select("id", "name", "phone", "email", "role", "status", "created_at", "updated_at").First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrUserNotFound return nil, common.ErrUserNotFound
} }
return nil, common.ErrInternal return nil, common.ErrInternal
} }
return &user, nil
return profileVO(&user), nil
}
func profileVO(user *model.User) *vo.ProfileVO {
return &vo.ProfileVO{
ID: user.ID, Name: user.Name, Phone: user.Phone, Email: user.Email,
Role: user.Role, Status: user.Status, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt,
}
} }
// UpdateProfile 更新个人资料(姓名/邮箱) // UpdateProfile 更新个人资料(姓名/邮箱)
func (s *AccountService) UpdateProfile(userID int64, req *vo.ProfileUpdateReq) (*model.User, *common.BusiError) {
func (s *AccountService) UpdateProfile(userID int64, req *vo.ProfileUpdateReq) (*vo.ProfileVO, *common.BusiError) {
var user model.User var user model.User
if err := common.DB.First(&user, userID).Error; err != nil { if err := common.DB.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@ -49,8 +55,13 @@ func (s *AccountService) UpdateProfile(userID int64, req *vo.ProfileUpdateReq) (
logger.ERROR("更新个人资料失败", err) logger.ERROR("更新个人资料失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
} }
common.DB.First(&user, userID)
return &user, nil
if err := common.DB.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrUserNotFound
}
return nil, common.ErrInternal
}
return profileVO(&user), nil
} }
// GetTrafficBalance 查询云媒体流量余额 // GetTrafficBalance 查询云媒体流量余额

92
service/billing_service.go

@ -8,6 +8,7 @@ import (
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
"laic-backend/cache" "laic-backend/cache"
"laic-backend/common" "laic-backend/common"
@ -52,7 +53,7 @@ type CarrierAPI interface {
Recharge(iccid string, amountGb int) (orderNo string, err error) Recharge(iccid string, amountGb int) (orderNo string, err error)
} }
// MockCarrierAPI 运营商 Mock(未接入真实通道前使用
// MockCarrierAPI 运营商 Mock(仅供开发注入,不作为默认生产通道
type MockCarrierAPI struct{} type MockCarrierAPI struct{}
func (m *MockCarrierAPI) QueryUsage(iccid string) (float64, string, error) { func (m *MockCarrierAPI) QueryUsage(iccid string) (float64, string, error) {
@ -63,8 +64,8 @@ func (m *MockCarrierAPI) Recharge(iccid string, amountGb int) (string, error) {
return fmt.Sprintf("MOCK-%d", time.Now().UnixNano()), nil return fmt.Sprintf("MOCK-%d", time.Now().UnixNano()), nil
} }
// carrier 当前使用的运营商通道(后续可替换为真实实现)
var carrier CarrierAPI = &MockCarrierAPI{}
// carrier 默认禁用,未接入真实运营商前不产生虚假用量或充值成功。
var carrier CarrierAPI
// mysqlBalance 读取用户 MySQL 流量快照 // mysqlBalance 读取用户 MySQL 流量快照
func (b *BillingService) mysqlBalance(userID int64) (int64, error) { func (b *BillingService) mysqlBalance(userID int64) (int64, error) {
@ -146,6 +147,7 @@ func (b *BillingService) Deduct(userID int64, bytes int64, sourceType string, so
} }
if err := common.DB.Create(log).Error; err != nil { if err := common.DB.Create(log).Error; err != nil {
logger.ERROR("流量账本落库失败", err) logger.ERROR("流量账本落库失败", err)
return false, common.ErrInternal
} }
return true, nil return true, nil
} }
@ -156,12 +158,9 @@ func (b *BillingService) credit(userID int64, bytes int64) error {
if _, err := b.currentBalance(userID); err != nil { if _, err := b.currentBalance(userID); err != nil {
return err return err
} }
if _, err := common.IncrBy(key, bytes); err != nil {
_, err := common.IncrBy(key, bytes)
return err return err
} }
return common.DB.Model(&model.User{}).Where("id = ?", userID).
UpdateColumn("traffic_balance", gorm.Expr("traffic_balance + ?", bytes)).Error
}
// GetUsagePage 流量消费流水分页 // GetUsagePage 流量消费流水分页
func (b *BillingService) GetUsagePage(userID int64, req *vo.UsagePageReq) (*common.PageResponse[model.TrafficUsageLog], *common.BusiError) { func (b *BillingService) GetUsagePage(userID int64, req *vo.UsagePageReq) (*common.PageResponse[model.TrafficUsageLog], *common.BusiError) {
@ -218,33 +217,32 @@ func (b *BillingService) CreateOrder(userID int64, amountGb int) (*model.Traffic
// PayOrder 订单支付入账(admin 确认支付) // PayOrder 订单支付入账(admin 确认支付)
func (b *BillingService) PayOrder(orderID int64) (*model.TrafficOrder, *common.BusiError) { func (b *BillingService) PayOrder(orderID int64) (*model.TrafficOrder, *common.BusiError) {
var order model.TrafficOrder var order model.TrafficOrder
if err := common.DB.First(&order, orderID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrOrderNotFound
}
return nil, common.ErrInternal
now := time.Now()
if err := common.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
return err
} }
if order.PayStatus == "paid" { if order.PayStatus == "paid" {
return nil, common.ErrOrderPaid
return common.ErrOrderPaid
} }
bytes := int64(order.AmountGb) * gbBytes bytes := int64(order.AmountGb) * gbBytes
now := time.Now()
if err := common.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&order).Updates(map[string]any{
"pay_status": "paid",
"paid_at": now,
}).Error; err != nil {
if err := tx.Model(&order).Updates(map[string]any{"pay_status": "paid", "paid_at": now}).Error; err != nil {
return err return err
} }
return tx.Model(&model.User{}).Where("id = ?", order.UserID). return tx.Model(&model.User{}).Where("id = ?", order.UserID).
UpdateColumn("traffic_balance", gorm.Expr("traffic_balance + ?", bytes)).Error UpdateColumn("traffic_balance", gorm.Expr("traffic_balance + ?", bytes)).Error
}); err != nil { }); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrOrderNotFound
}
if busiErr, ok := err.(*common.BusiError); ok {
return nil, busiErr
}
logger.ERROR("订单支付失败", err) logger.ERROR("订单支付失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
} }
// Redis 权威余额入账(幂等:多次支付由 pay_status 校验拦截)
bytes := int64(order.AmountGb) * gbBytes
if err := b.credit(order.UserID, bytes); err != nil { if err := b.credit(order.UserID, bytes); err != nil {
logger.ERROR("订单支付 Redis 入账失败", err) logger.ERROR("订单支付 Redis 入账失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
@ -291,53 +289,9 @@ func (b *BillingService) GetSimRechargeLogPage(userID int64, req *vo.SimRecharge
return common.Page(req.Pagination, total, list), nil return common.Page(req.Pagination, total, list), nil
} }
// RechargeSimCard SIM 卡充值(运营商下单 + 回写状态)
// RechargeSimCard 当前未接入支付确认流程,不向运营商提交充值订单。
func (b *BillingService) RechargeSimCard(userID, simCardID int64, amountGb int) (*model.SimRechargeLog, *common.BusiError) { func (b *BillingService) RechargeSimCard(userID, simCardID int64, amountGb int) (*model.SimRechargeLog, *common.BusiError) {
var card model.SimCard
if err := common.DB.Where("id = ? AND user_id = ?", simCardID, userID).First(&card).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrSimCardNotFound
}
return nil, common.ErrInternal
}
log := &model.SimRechargeLog{
ID: mustID(),
SimCardID: simCardID,
UserID: userID,
AmountGb: amountGb,
UnitPrice: defaultTrafficUnitPrice,
TotalPrice: float64(amountGb) * defaultTrafficUnitPrice,
PayStatus: "paid",
RechargeStatus: "pending",
IdempotentKey: fmt.Sprintf("%d-%d", simCardID, time.Now().UnixNano()),
CreatedAt: time.Now(),
}
if err := common.DB.Create(log).Error; err != nil {
logger.ERROR("创建 SIM 充值记录失败", err)
return nil, common.ErrInternal
}
orderNo, err := carrier.Recharge(card.Iccid, amountGb)
if err != nil {
log.RechargeStatus = "failed"
log.RechargeMsg = err.Error()
common.DB.Model(log).Updates(map[string]any{"recharge_status": "failed", "recharge_msg": err.Error()})
return log, nil
}
updates := map[string]any{
"recharge_status": "success",
"recharge_msg": orderNo,
}
common.DB.Model(log).Updates(updates)
common.DB.Model(&card).Updates(map[string]any{
"plan_gb": gorm.Expr("plan_gb + ?", amountGb),
"status": "active",
})
log.RechargeStatus = "success"
log.RechargeMsg = orderNo
return log, nil
return nil, common.ErrCarrierUnavailable
} }
// mustID 生成雪花 ID(失败返回 0,由 DB 约束兜底) // mustID 生成雪花 ID(失败返回 0,由 DB 约束兜底)
@ -437,6 +391,10 @@ func (b *BillingService) syncSimUsage() {
} }
func (b *BillingService) syncSimCard(card *model.SimCard, now time.Time) { func (b *BillingService) syncSimCard(card *model.SimCard, now time.Time) {
if carrier == nil {
logger.INFO("SIM 运营商通道未接入:", card.Iccid)
return
}
usedGb, carrierStatus, err := carrier.QueryUsage(card.Iccid) usedGb, carrierStatus, err := carrier.QueryUsage(card.Iccid)
if err != nil { if err != nil {
logger.WARN("查询 SIM 用量失败:", card.Iccid, err) logger.WARN("查询 SIM 用量失败:", card.Iccid, err)

120
service/captcha_service.go

@ -0,0 +1,120 @@
package service
import (
"bytes"
"crypto/rand"
"image"
"image/color"
"image/png"
"math/big"
"strings"
"time"
"github.com/google/uuid"
"laic-backend/cache"
"laic-backend/common"
)
const captchaCookie = "laic_captcha"
var captchaChars = []byte("23456789ABCDEFGHJKLMNPQRSTUVWXYZ")
var captchaGlyphs = map[byte][7]string{
'2': {"11110", "00001", "00010", "00100", "01000", "10000", "11111"},
'3': {"11110", "00001", "00110", "00001", "00001", "00001", "11110"},
'4': {"00010", "00110", "01010", "10010", "11111", "00010", "00010"},
'5': {"11111", "10000", "11110", "00001", "00001", "10001", "01110"},
'6': {"00110", "01000", "10000", "11110", "10001", "10001", "01110"},
'7': {"11111", "00001", "00010", "00100", "01000", "01000", "01000"},
'8': {"01110", "10001", "10001", "01110", "10001", "10001", "01110"},
'9': {"01110", "10001", "10001", "01111", "00001", "00010", "11100"},
'A': {"01110", "10001", "10001", "11111", "10001", "10001", "10001"},
'B': {"11110", "10001", "10001", "11110", "10001", "10001", "11110"},
'C': {"01111", "10000", "10000", "10000", "10000", "10000", "01111"},
'D': {"11110", "10001", "10001", "10001", "10001", "10001", "11110"},
'E': {"11111", "10000", "10000", "11110", "10000", "10000", "11111"},
'F': {"11111", "10000", "10000", "11110", "10000", "10000", "10000"},
'G': {"01111", "10000", "10000", "10111", "10001", "10001", "01111"},
'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
'J': {"00001", "00001", "00001", "00001", "10001", "10001", "01110"},
'K': {"10001", "10010", "10100", "11000", "10100", "10010", "10001"},
'M': {"10001", "11011", "10101", "10101", "10001", "10001", "10001"},
'N': {"10001", "11001", "10101", "10011", "10001", "10001", "10001"},
'P': {"11110", "10001", "10001", "11110", "10000", "10000", "10000"},
'Q': {"01110", "10001", "10001", "10001", "10101", "10010", "01101"},
'R': {"11110", "10001", "10001", "11110", "10100", "10010", "10001"},
'S': {"01111", "10000", "10000", "01110", "00001", "00001", "11110"},
'T': {"11111", "00100", "00100", "00100", "00100", "00100", "00100"},
'U': {"10001", "10001", "10001", "10001", "10001", "10001", "01110"},
'V': {"10001", "10001", "10001", "10001", "10001", "01010", "00100"},
'W': {"10001", "10001", "10001", "10101", "10101", "11011", "10001"},
'X': {"10001", "10001", "01010", "00100", "01010", "10001", "10001"},
'Y': {"10001", "10001", "01010", "00100", "00100", "00100", "00100"},
'Z': {"11111", "00001", "00010", "00100", "01000", "10000", "11111"},
}
func NewCaptcha() (string, []byte, error) {
id := uuid.NewString()
answer := make([]byte, 4)
for i := range answer {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(captchaChars))))
if err != nil {
return "", nil, err
}
answer[i] = captchaChars[n.Int64()]
}
if err := common.SetValueWithExpired(cache.CaptchaKeyOf(id), string(answer), 5*60); err != nil {
return "", nil, err
}
img := image.NewRGBA(image.Rect(0, 0, 160, 48))
for y := 0; y < 48; y++ {
for x := 0; x < 160; x++ {
img.Set(x, y, color.RGBA{R: 242, G: 247, B: 250, A: 255})
}
}
for i := 0; i < 7; i++ {
x := 9 + i*23
for y := 5; y < 43; y++ {
img.Set(x, y, color.RGBA{R: 130, G: 180, B: 205, A: 180})
}
}
for i, ch := range answer {
drawGlyph(img, ch, 15+i*35, 12, color.RGBA{R: 31, G: 91, B: 130, A: 255})
}
var out bytes.Buffer
if err := png.Encode(&out, img); err != nil {
return "", nil, err
}
return id, out.Bytes(), nil
}
func drawGlyph(img *image.RGBA, ch byte, left, top int, fill color.Color) {
glyph, ok := captchaGlyphs[ch]
if !ok {
return
}
for y, row := range glyph {
for x, bit := range row {
if bit == '1' {
for dy := 0; dy < 3; dy++ {
for dx := 0; dx < 3; dx++ {
img.Set(left+x*3+dx, top+y*3+dy, fill)
}
}
}
}
}
}
func CheckCaptcha(id, answer string) bool {
stored, err := common.GetString(cache.CaptchaKeyOf(id))
if err != nil || stored == "" || !strings.EqualFold(strings.TrimSpace(stored), strings.TrimSpace(answer)) {
return false
}
_ = common.Delete(cache.CaptchaKeyOf(id))
return true
}
func CaptchaCookieName() string { return captchaCookie }
func CaptchaTTL() time.Duration { return 5 * time.Minute }

90
service/casbin_bootstrap.go

@ -1,90 +0,0 @@
package service
import (
"laic-backend/common"
"laic-backend/logger"
"laic-backend/model"
)
// BootstrapCasbin 引导默认 RBAC 策略(幂等):p 策略(角色→路径→方法)+ 用户角色映射 g 策略
func BootstrapCasbin() {
e := common.CasbinEnforcer
if e == nil {
return
}
// 清理旧的 admin 全量策略,防止历史 "admin /* *" 残留导致越权(幂等)
if _, err := e.RemoveFilteredPolicy(0, "admin"); err != nil {
logger.ERROR("清理 admin 旧策略失败", err)
}
// p 策略:
// admin —— 平台管理职能:查看用户/机巢/无人机,管理固件,管理主账号云媒体流量
// user —— 业务操作:机巢/无人机/任务/航线等(自注册普通用户)
// keyMatch2 下 "/prefix/*" 匹配所有子路径(含多级),再补 "/prefix" 精确匹配列表/新增基路径。
adminPolicies := [][]string{
// 查看所有用户信息(暂只读)
{"admin", "/v1/users", "GET"},
{"admin", "/v1/users/*", "GET"},
// 查看机巢 / 无人机(含状态与遥测)
{"admin", "/v1/docks", "GET"},
{"admin", "/v1/docks/*", "GET"},
{"admin", "/v1/drones", "GET"},
{"admin", "/v1/drones/*", "GET"},
// 固件包管理
{"admin", "/v1/firmwares", "*"},
{"admin", "/v1/firmwares/*", "*"},
// 主账号云媒体流量管理(流量余额/订单/SIM 卡)
{"admin", "/v1/account/traffic", "*"},
{"admin", "/v1/account/traffic/*", "*"},
{"admin", "/v1/account/sim-cards", "*"},
{"admin", "/v1/account/sim-cards/*", "*"},
// 平台监控:角色字典
{"admin", "/v1/roles", "GET"},
}
userPrefixes := []string{
"/v1/docks",
"/v1/drones",
"/v1/alarms",
"/v1/commands",
"/v1/tasks",
"/v1/routes",
"/v1/executions",
"/v1/account",
"/v1/live",
"/v1/videos",
"/v1/ws",
}
policies := append([][]string{}, adminPolicies...)
for _, prefix := range userPrefixes {
policies = append(policies, []string{"user", prefix, "*"}, []string{"user", prefix + "/*", "*"})
}
// 普通用户可读取已发布固件列表(选择升级)
policies = append(policies, []string{"user", "/v1/firmwares/released", "GET"})
// 普通用户查看自己的操作记录
policies = append(policies, []string{"user", "/v1/logs/operation", "GET"})
for _, p := range policies {
args := make([]any, len(p))
for i, v := range p {
args[i] = v
}
if _, err := e.AddPolicy(args...); err != nil {
logger.ERROR("添加 Casbin 策略失败", err)
}
}
// g 策略:同步现有用户角色映射
var users []model.User
if err := common.DB.Find(&users).Error; err != nil {
logger.ERROR("查询用户同步 Casbin 角色失败", err)
return
}
for _, u := range users {
if err := common.SetUserRole(u.ID, u.Role); err != nil {
logger.ERROR("同步用户角色失败", err)
}
}
}

55
service/dock_service.go

@ -39,9 +39,6 @@ func withDockFilter(userID int64, isAdmin bool) func(*gorm.DB) *gorm.DB {
// GetPage 机巢分页列表 // GetPage 机巢分页列表
func (s *DockService) GetPage(userID int64, isAdmin bool, req *vo.DockPageReq) (*common.PageResponse[model.Dock], *common.BusiError) { func (s *DockService) GetPage(userID int64, isAdmin bool, req *vo.DockPageReq) (*common.PageResponse[model.Dock], *common.BusiError) {
db := common.DB.Model(&model.Dock{}).Scopes(withUserFilter(userID, isAdmin)) db := common.DB.Model(&model.Dock{}).Scopes(withUserFilter(userID, isAdmin))
if req.Status != "" {
db = db.Where("status = ?", req.Status)
}
if req.RegisterStatus != "" { if req.RegisterStatus != "" {
db = db.Where("register_status = ?", req.RegisterStatus) db = db.Where("register_status = ?", req.RegisterStatus)
} }
@ -96,6 +93,50 @@ func (s *DockService) Create(userID int64, isAdmin bool, req *vo.DockCreateReq)
owner = req.UserID 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() id, err := tool.NextID()
if err != nil { if err != nil {
return nil, common.ErrInternal return nil, common.ErrInternal
@ -113,7 +154,6 @@ func (s *DockService) Create(userID int64, isAdmin bool, req *vo.DockCreateReq)
Longitude: req.Longitude, Longitude: req.Longitude,
Latitude: req.Latitude, Latitude: req.Latitude,
Altitude: req.Altitude, Altitude: req.Altitude,
Status: "offline",
RegisterStatus: "registered", RegisterStatus: "registered",
DockIDSource: "manual", DockIDSource: "manual",
ProtocolVer: "1.0", ProtocolVer: "1.0",
@ -276,10 +316,6 @@ func (s *DockService) realtime(dockID string) (bool, map[string]string) {
// EnsureFromOnline 设备自发现:不存在则登记为 pending,存在则更新在线状态与软件版本 // EnsureFromOnline 设备自发现:不存在则登记为 pending,存在则更新在线状态与软件版本
func (s *DockService) EnsureFromOnline(dockID string, st statusOnline) { func (s *DockService) EnsureFromOnline(dockID string, st statusOnline) {
online := st.Status == "online" online := st.Status == "online"
status := "offline"
if online {
status = "online"
}
var dock model.Dock var dock model.Dock
err := common.DB.Where("dock_id = ?", dockID).First(&dock).Error err := common.DB.Where("dock_id = ?", dockID).First(&dock).Error
@ -304,7 +340,6 @@ func (s *DockService) EnsureFromOnline(dockID string, st statusOnline) {
DockIDSource: "auto", DockIDSource: "auto",
SoftwareVer: st.SoftwareVersion, SoftwareVer: st.SoftwareVersion,
ProtocolVer: st.ProtocolVersion, ProtocolVer: st.ProtocolVersion,
Status: status,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@ -313,7 +348,7 @@ func (s *DockService) EnsureFromOnline(dockID string, st statusOnline) {
return return
} }
} else if err == nil { } else if err == nil {
updates := map[string]any{"status": status, "updated_at": time.Now()}
updates := map[string]any{"updated_at": time.Now()}
if st.SoftwareVersion != "" { if st.SoftwareVersion != "" {
updates["software_ver"] = st.SoftwareVersion updates["software_ver"] = st.SoftwareVersion
} }

9
service/drone_service.go

@ -21,9 +21,6 @@ var DefaultDroneService = &DroneService{}
// GetPage 无人机分页列表 // GetPage 无人机分页列表
func (s *DroneService) GetPage(userID int64, isAdmin bool, req *vo.DronePageReq) (*common.PageResponse[model.Drone], *common.BusiError) { func (s *DroneService) GetPage(userID int64, isAdmin bool, req *vo.DronePageReq) (*common.PageResponse[model.Drone], *common.BusiError) {
db := common.DB.Model(&model.Drone{}).Scopes(withUserFilter(userID, isAdmin)) db := common.DB.Model(&model.Drone{}).Scopes(withUserFilter(userID, isAdmin))
if req.Status != "" {
db = db.Where("status = ?", req.Status)
}
if req.Keyword != "" { if req.Keyword != "" {
kw := "%" + req.Keyword + "%" kw := "%" + req.Keyword + "%"
db = db.Where("drone_sn LIKE ? OR name LIKE ? OR code LIKE ? OR model LIKE ?", kw, kw, kw, kw) db = db.Where("drone_sn LIKE ? OR name LIKE ? OR code LIKE ? OR model LIKE ?", kw, kw, kw, kw)
@ -126,10 +123,6 @@ func (s *DroneService) realtime(dockID, droneSN string) (bool, map[string]string
// EnsureFromState 无人机自动关联:按 droneSn 或 dockId 创建/更新(1:1 绑定) // EnsureFromState 无人机自动关联:按 droneSn 或 dockId 创建/更新(1:1 绑定)
func (s *DroneService) EnsureFromState(dockID, droneSN, name string, online bool, battery int, firmwareVer string) { func (s *DroneService) EnsureFromState(dockID, droneSN, name string, online bool, battery int, firmwareVer string) {
status := "offline"
if online {
status = "online"
}
if droneSN == "" { if droneSN == "" {
return return
} }
@ -157,7 +150,6 @@ func (s *DroneService) EnsureFromState(dockID, droneSN, name string, online bool
DroneSN: droneSN, DroneSN: droneSN,
DockID: dockID, DockID: dockID,
Name: name, Name: name,
Status: status,
Battery: battery, Battery: battery,
FirmwareVer: firmwareVer, FirmwareVer: firmwareVer,
CreatedAt: now, CreatedAt: now,
@ -172,7 +164,6 @@ func (s *DroneService) EnsureFromState(dockID, droneSN, name string, online bool
"user_id": dock.UserID, "user_id": dock.UserID,
"drone_sn": droneSN, "drone_sn": droneSN,
"dock_id": dockID, "dock_id": dockID,
"status": status,
"battery": battery, "battery": battery,
"updated_at": time.Now(), "updated_at": time.Now(),
} }

19
service/heartbeat.go

@ -6,7 +6,6 @@ import (
"laic-backend/cache" "laic-backend/cache"
"laic-backend/common" "laic-backend/common"
"laic-backend/logger" "laic-backend/logger"
"laic-backend/model"
) )
func heartbeatTimeout() uint { func heartbeatTimeout() uint {
@ -57,30 +56,12 @@ func expireDeviceHeartbeats() {
} }
func markDockOffline(dockID string) { func markDockOffline(dockID string) {
result := common.DB.Model(&model.Dock{}).
Where("dock_id = ? AND status = ?", dockID, "online").
Update("status", "offline")
if result.Error != nil {
logger.WARN("更新机巢离线状态失败:", result.Error)
return
}
if result.RowsAffected > 0 {
broadcast("dock.status", map[string]any{"dockId": dockID, "online": false}) broadcast("dock.status", map[string]any{"dockId": dockID, "online": false})
} }
}
func markDroneOffline(droneSN string) { func markDroneOffline(droneSN string) {
result := common.DB.Model(&model.Drone{}).
Where("drone_sn = ? AND status = ?", droneSN, "online").
Update("status", "offline")
if result.Error != nil {
logger.WARN("更新无人机离线状态失败:", result.Error)
return
}
if result.RowsAffected > 0 {
broadcast("drone.status", map[string]any{"droneSn": droneSN, "online": false}) broadcast("drone.status", map[string]any{"droneSn": droneSN, "online": false})
} }
}
func heartbeatScanInterval() time.Duration { func heartbeatScanInterval() time.Duration {
if common.AppConf != nil && common.AppConf.Heartbeat.ScanSeconds > 0 { if common.AppConf != nil && common.AppConf.Heartbeat.ScanSeconds > 0 {

130
service/invoice_service.go

@ -0,0 +1,130 @@
package service
import (
"errors"
"time"
"gorm.io/gorm"
"laic-backend/common"
"laic-backend/model"
"laic-backend/tool"
"laic-backend/vo"
)
type InvoiceService struct{}
var DefaultInvoiceService = &InvoiceService{}
func (s *InvoiceService) ListProfiles(userID int64) ([]model.InvoiceProfile, *common.BusiError) {
var profiles []model.InvoiceProfile
if err := common.DB.Where("user_id = ?", userID).Order("is_default DESC, id DESC").Find(&profiles).Error; err != nil {
return nil, common.ErrInternal
}
return profiles, nil
}
func (s *InvoiceService) SaveProfile(userID, profileID int64, req *vo.InvoiceProfileReq) (*model.InvoiceProfile, *common.BusiError) {
var profile model.InvoiceProfile
if profileID > 0 {
if err := common.DB.Where("id = ? AND user_id = ?", profileID, userID).First(&profile).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrNotFound
}
return nil, common.ErrInternal
}
} else {
id, err := tool.NextID()
if err != nil {
return nil, common.ErrInternal
}
profile = model.InvoiceProfile{ID: id, UserID: userID, CreatedAt: time.Now()}
}
profile.Title, profile.TaxpayerNumber, profile.Email, profile.IsDefault, profile.UpdatedAt = req.Title, req.TaxpayerNumber, req.Email, req.IsDefault, time.Now()
if err := common.DB.Transaction(func(tx *gorm.DB) error {
if req.IsDefault {
if err := tx.Model(&model.InvoiceProfile{}).Where("user_id = ?", userID).Update("is_default", false).Error; err != nil {
return err
}
}
return tx.Save(&profile).Error
}).Error; err != nil {
return nil, common.ErrInternal
}
return &profile, nil
}
func (s *InvoiceService) DeleteProfile(userID, profileID int64) *common.BusiError {
result := common.DB.Where("id = ? AND user_id = ?", profileID, userID).Delete(&model.InvoiceProfile{})
if result.Error != nil {
return common.ErrInternal
}
if result.RowsAffected == 0 {
return common.ErrNotFound
}
return nil
}
func (s *InvoiceService) ListEligibleOrders(userID int64) ([]model.TrafficOrder, *common.BusiError) {
var orders []model.TrafficOrder
if err := common.DB.Where("user_id = ? AND pay_status = ? AND NOT EXISTS (SELECT 1 FROM invoice_request WHERE invoice_request.order_id = traffic_order.id)", userID, "paid").Order("paid_at DESC").Find(&orders).Error; err != nil {
return nil, common.ErrInternal
}
return orders, nil
}
func (s *InvoiceService) CreateRequest(userID int64, req *vo.InvoiceRequestCreateReq) (*model.InvoiceRequest, *common.BusiError) {
var profile model.InvoiceProfile
if err := common.DB.Where("user_id = ? AND is_default = ?", userID, true).First(&profile).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.NewBusiError(common.ParamError, "请先设置默认发票资料")
}
return nil, common.ErrInternal
}
var order model.TrafficOrder
if err := common.DB.Where("id = ? AND user_id = ? AND pay_status = ?", req.OrderID, userID, "paid").First(&order).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrOrderNotFound
}
return nil, common.ErrInternal
}
id, err := tool.NextID()
if err != nil {
return nil, common.ErrInternal
}
request := &model.InvoiceRequest{ID: id, UserID: userID, OrderID: order.ID, Amount: order.TotalPrice, Title: profile.Title, TaxpayerNumber: profile.TaxpayerNumber, Email: profile.Email, Status: "pending", RequestedAt: time.Now()}
if err := common.DB.Create(request).Error; err != nil {
if code, _ := common.ParseError(err); code == 1062 {
return nil, common.NewBusiError(common.ParamError, "该订单已申请发票")
}
return nil, common.ErrInternal
}
return request, nil
}
func (s *InvoiceService) ListRequests(userID int64) ([]model.InvoiceRequest, *common.BusiError) {
var requests []model.InvoiceRequest
if err := common.DB.Where("user_id = ?", userID).Order("requested_at DESC").Find(&requests).Error; err != nil {
return nil, common.ErrInternal
}
return requests, nil
}
func (s *InvoiceService) ReviewRequest(requestID int64, req *vo.InvoiceReviewReq) (*model.InvoiceRequest, *common.BusiError) {
if req.Status != "issued" && req.Status != "rejected" {
return nil, common.ErrParam
}
var request model.InvoiceRequest
if err := common.DB.Where("id = ? AND status = ?", requestID, "pending").First(&request).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrNotFound
}
return nil, common.ErrInternal
}
now := time.Now()
if err := common.DB.Model(&request).Updates(map[string]any{"status": req.Status, "invoice_number": req.InvoiceNumber, "admin_remark": req.AdminRemark, "processed_at": now}).Error; err != nil {
return nil, common.ErrInternal
}
request.Status, request.InvoiceNumber, request.AdminRemark, request.ProcessedAt = req.Status, req.InvoiceNumber, req.AdminRemark, &now
return &request, nil
}

10
service/mqtt_subscriber.go

@ -230,8 +230,8 @@ func handleStateDrone(dockID string, env *mqtt.Envelope) {
return return
} }
droneSN := st.DroneSN droneSN := st.DroneSN
if droneSN == "" {
droneSN = env.DroneSN
if droneSN == "" && env.DroneSN != nil {
droneSN = *env.DroneSN
} }
battery := int(st.BatteryPercent) battery := int(st.BatteryPercent)
DefaultDroneService.EnsureFromState(dockID, droneSN, st.Name, st.Online, battery, "") DefaultDroneService.EnsureFromState(dockID, droneSN, st.Name, st.Online, battery, "")
@ -256,9 +256,9 @@ func handleTelemetry(dockID string, env *mqtt.Envelope) {
DefaultTelemetryStore.Append(dockID, p) DefaultTelemetryStore.Append(dockID, p)
DefaultTrajectoryStore.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))
} }
} }

7
service/operation_log_service.go

@ -14,13 +14,6 @@ type OperationLogService struct{}
var DefaultOperationLogService = &OperationLogService{} var DefaultOperationLogService = &OperationLogService{}
// BootstrapOperationLog 确保基础运行时表存在(历史库未跑 DDL 时兜底)。
func BootstrapOperationLog() {
if err := common.DB.AutoMigrate(&model.OperationLog{}, &model.LiveViewerLease{}); err != nil {
logger.ERROR("迁移基础运行时表失败", err)
}
}
// Record 写入一条操作日志 // Record 写入一条操作日志
func (s *OperationLogService) Record(userID int64, userName, module, action, detail, result, ip string) { func (s *OperationLogService) Record(userID int64, userName, module, action, detail, result, ip string) {
if userID == 0 { if userID == 0 {

86
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
}

205
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
}

147
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",
}

6
service/system_service.go

@ -86,7 +86,6 @@ func (s *SystemService) CreateUser(req *vo.UserCreateReq) (*model.User, *common.
logger.ERROR("创建用户失败", err) logger.ERROR("创建用户失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
} }
_ = common.SetUserRole(id, role)
return user, nil return user, nil
} }
@ -125,10 +124,6 @@ func (s *SystemService) UpdateUser(id int64, req *vo.UserUpdateReq) (*model.User
logger.ERROR("更新用户失败", err) logger.ERROR("更新用户失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
} }
common.DB.First(&user, id)
if req.Role != "" {
_ = common.SetUserRole(id, req.Role)
}
return &user, nil return &user, nil
} }
@ -142,7 +137,6 @@ func (s *SystemService) DeleteUser(id int64) *common.BusiError {
if res.RowsAffected == 0 { if res.RowsAffected == 0 {
return common.ErrUserNotFound return common.ErrUserNotFound
} }
_ = common.SetUserRole(id, "")
return nil return nil
} }

286
service/user_service.go

@ -2,6 +2,7 @@ package service
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -20,9 +21,10 @@ type UserService struct{}
var DefaultUserService = &UserService{} var DefaultUserService = &UserService{}
// Register 注册用户(默认角色 user),成功后直接返回登录态
func (s *UserService) Register(name, phone, email, password string) (*vo.LoginResp, *common.BusiError) {
// 手机号唯一性预检(兼顾并发,唯一键兜底)
func (s *UserService) Register(name, phone, email, password, smsCode, userAgent, ip string) (*vo.LoginResp, *common.BusiError) {
if !s.consumeSmsCode(phone, smsCode) {
return nil, common.ErrSmsCodeError
}
var count int64 var count int64
if err := common.DB.Model(&model.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil { if err := common.DB.Model(&model.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil {
return nil, common.ErrInternal return nil, common.ErrInternal
@ -30,7 +32,6 @@ func (s *UserService) Register(name, phone, email, password string) (*vo.LoginRe
if count > 0 { if count > 0 {
return nil, common.ErrUserPhoneExists return nil, common.ErrUserPhoneExists
} }
id, err := tool.NextID() id, err := tool.NextID()
if err != nil { if err != nil {
return nil, common.ErrInternal return nil, common.ErrInternal
@ -39,18 +40,8 @@ func (s *UserService) Register(name, phone, email, password string) (*vo.LoginRe
if err != nil { if err != nil {
return nil, common.ErrInternal return nil, common.ErrInternal
} }
user := &model.User{
ID: id,
Name: name,
Phone: phone,
Email: email,
Password: string(hash),
Role: common.RoleUser,
Status: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
now := time.Now()
user := &model.User{ID: id, Name: name, Phone: phone, Email: email, Password: string(hash), Role: common.RoleUser, Status: 1, CreatedAt: now, UpdatedAt: now}
if err := common.DB.Create(user).Error; err != nil { if err := common.DB.Create(user).Error; err != nil {
if code, _ := common.ParseError(err); code == 1062 { if code, _ := common.ParseError(err); code == 1062 {
return nil, common.ErrUserPhoneExists return nil, common.ErrUserPhoneExists
@ -58,45 +49,48 @@ func (s *UserService) Register(name, phone, email, password string) (*vo.LoginRe
logger.ERROR("注册用户失败", err) logger.ERROR("注册用户失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
} }
_ = common.SetUserRole(id, common.RoleUser)
return s.buildLoginResp(user)
return s.buildLoginResp(user, userAgent, ip)
} }
// Login 手机号 + 密码登录
func (s *UserService) Login(phone, password string) (*vo.LoginResp, *common.BusiError) {
func (s *UserService) Login(phone, password, captchaValue, captchaID, smsCode, userAgent, ip string) (*vo.LoginResp, *common.BusiError) {
passwordLogin := password != ""
if passwordLogin {
if !CheckCaptcha(captchaID, captchaValue) {
return nil, common.NewBusiError(common.ParamError, "图形验证码错误或已过期")
}
} else if !s.consumeSmsCode(phone, smsCode) {
return nil, common.ErrSmsCodeError
}
var user model.User var user model.User
if err := common.DB.Where("phone = ? AND status = 1", phone).First(&user).Error; err != nil { if err := common.DB.Where("phone = ? AND status = 1", phone).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, common.ErrPasswordError // 统一返回"用户名或密码错误",避免暴露账号是否存在
return nil, common.ErrPasswordError
} }
logger.ERROR("登录查询用户失败", err) logger.ERROR("登录查询用户失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
} }
if !checkPassword(password, user.Password) {
if passwordLogin && !checkPassword(password, user.Password) {
return nil, common.ErrPasswordError return nil, common.ErrPasswordError
} }
// 更新最后登录时间(失败不阻断登录)
common.DB.Model(&user).Update("last_login", time.Now())
return s.buildLoginResp(&user)
_ = common.DB.Model(&user).Update("last_login", time.Now()).Error
return s.buildLoginResp(&user, userAgent, ip)
} }
// Refresh 用 refresh token 换取新的 access + refresh token(刷新令牌轮换)
func (s *UserService) Refresh(refreshToken string) (*vo.LoginResp, *common.BusiError) {
userID, err := token.ParseRefreshToken(refreshToken)
func (s *UserService) Refresh(refreshToken, userAgent, ip string) (*vo.LoginResp, *common.BusiError) {
userID, sessionID, err := token.ParseRefreshToken(refreshToken)
if err != nil { if err != nil {
return nil, common.ErrTokenInvalid return nil, common.ErrTokenInvalid
} }
// 校验 refresh token 是否仍存证(登出会删除)
refreshKey := cache.RefreshTokenKeyOf(userID, token.GenerateShortID(refreshToken))
refreshID := token.GenerateShortID(refreshToken)
refreshKey := cache.RefreshTokenKeyOf(userID, refreshID)
stored, err := common.GetString(refreshKey) stored, err := common.GetString(refreshKey)
if err != nil || stored != refreshToken {
if err != nil || stored != sessionValue(sessionID) {
return nil, common.ErrTokenInvalid
}
var session model.UserLoginSession
if err := common.DB.Where("id = ? AND user_id = ? AND refresh_token_id = ? AND revoked_at IS NULL AND expires_at > ?", sessionID, userID, refreshID, time.Now()).First(&session).Error; err != nil {
return nil, common.ErrTokenInvalid return nil, common.ErrTokenInvalid
} }
var user model.User var user model.User
if err := common.DB.Where("id = ? AND status = 1", userID).First(&user).Error; err != nil { if err := common.DB.Where("id = ? AND status = 1", userID).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@ -104,45 +98,143 @@ func (s *UserService) Refresh(refreshToken string) (*vo.LoginResp, *common.BusiE
} }
return nil, common.ErrInternal return nil, common.ErrInternal
} }
// 轮换:删除旧 refresh 存证
_ = common.Delete(refreshKey) _ = common.Delete(refreshKey)
return s.buildLoginResp(&user)
_ = common.Delete(cache.UserTokenKeyOf(userID, session.AccessTokenID))
return s.issueForSession(&user, &session, userAgent, ip)
} }
// Logout 登出:删除 access 存证(及可选 refresh 存证),使 token 立即失效
func (s *UserService) Logout(userID int64, accessToken, refreshToken string) *common.BusiError {
accessKey := cache.UserTokenKeyOf(userID, token.GenerateShortID(accessToken))
_ = common.Delete(accessKey)
func (s *UserService) Logout(userID, sessionID int64, accessToken, refreshToken string) *common.BusiError {
if sessionID == 0 {
_ = common.Delete(cache.UserTokenKeyOf(userID, token.GenerateShortID(accessToken)))
if refreshToken != "" { if refreshToken != "" {
refreshKey := cache.RefreshTokenKeyOf(userID, token.GenerateShortID(refreshToken))
_ = common.Delete(refreshKey)
_ = common.Delete(cache.RefreshTokenKeyOf(userID, token.GenerateShortID(refreshToken)))
}
return nil
}
return s.revokeSession(userID, sessionID)
}
func (s *UserService) ChangePassword(userID int64, req *vo.ChangePasswordReq) *common.BusiError {
var user model.User
if err := common.DB.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return common.ErrUserNotFound
}
return common.ErrInternal
}
if !checkPassword(req.OldPassword, user.Password) {
return common.ErrPasswordError
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return common.ErrInternal
}
now := time.Now()
if err := common.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&user).Updates(map[string]any{"password": string(hash), "password_changed_at": now, "updated_at": now}).Error; err != nil {
return err
}
return tx.Model(&model.UserLoginSession{}).Where("user_id = ? AND revoked_at IS NULL", userID).Update("revoked_at", now).Error
}).Error; err != nil {
return common.ErrInternal
}
s.deleteUserSessions(userID)
return nil
}
func (s *UserService) ListSessions(userID, currentSessionID int64) ([]vo.SessionVO, *common.BusiError) {
var sessions []model.UserLoginSession
if err := common.DB.Where("user_id = ? AND revoked_at IS NULL", userID).Order("last_active_at DESC").Find(&sessions).Error; err != nil {
return nil, common.ErrInternal
}
result := make([]vo.SessionVO, 0, len(sessions))
for _, item := range sessions {
result = append(result, vo.SessionVO{ID: item.ID, UserAgent: item.UserAgent, IP: item.IP, LastActiveAt: item.LastActiveAt, ExpiresAt: item.ExpiresAt, Current: item.ID == currentSessionID})
}
return result, nil
}
func (s *UserService) RevokeSession(userID, sessionID int64) *common.BusiError {
return s.revokeSession(userID, sessionID)
}
func (s *UserService) RevokeOtherSessions(userID, currentSessionID int64) *common.BusiError {
var sessions []model.UserLoginSession
if err := common.DB.Where("user_id = ? AND id <> ? AND revoked_at IS NULL", userID, currentSessionID).Find(&sessions).Error; err != nil {
return common.ErrInternal
}
now := time.Now()
if err := common.DB.Model(&model.UserLoginSession{}).Where("user_id = ? AND id <> ? AND revoked_at IS NULL", userID, currentSessionID).Update("revoked_at", now).Error; err != nil {
return common.ErrInternal
}
for _, session := range sessions {
s.deleteSessionTokens(userID, &session)
} }
return nil return nil
} }
// SendSmsCode 生成并"发送"短信验证码(暂存 Redis,短信通道未接入前记日志)
func (s *UserService) SendSmsCode(phone string) (string, *common.BusiError) {
func (s *UserService) TouchSession(userID, sessionID int64) {
if sessionID == 0 {
return
}
_ = common.DB.Model(&model.UserLoginSession{}).Where("id = ? AND user_id = ? AND revoked_at IS NULL AND last_active_at < ?", sessionID, userID, time.Now().Add(-5*time.Minute)).Update("last_active_at", time.Now()).Error
}
func (s *UserService) SessionValid(userID, sessionID int64, accessToken string) bool {
if sessionID == 0 {
return false
}
var count int64
err := common.DB.Model(&model.UserLoginSession{}).Where("id = ? AND user_id = ? AND access_token_id = ? AND revoked_at IS NULL AND expires_at > ?", sessionID, userID, token.GenerateShortID(accessToken), time.Now()).Count(&count).Error
return err == nil && count == 1
}
func (s *UserService) SendSmsCode(phone, scene string) (string, *common.BusiError) {
var count int64
if err := common.DB.Model(&model.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil {
return "", common.ErrInternal
}
if (scene == "login" || scene == "reset") && count == 0 {
return "", common.ErrUserNotFound
}
if scene == "register" && count > 0 {
return "", common.ErrUserPhoneExists
}
if common.HasKey(cache.SmsRateKeyOf(phone)) {
return "", common.NewBusiError(common.ParamError, "验证码发送过于频繁,请稍后再试")
}
code := tool.RandDigit(6) code := tool.RandDigit(6)
key := cache.SmsCodeKeyOf(phone)
if err := common.SetValueWithExpired(key, code, 5*60); err != nil {
logger.ERROR("短信验证码写 Redis 失败", err)
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 return "", common.ErrInternal
} }
// TODO: 接入短信服务商后,此处调用真实发送接口;当前仅在日志输出验证码便于联调
logger.INFO("send sms code to", phone, "code:", code)
return code, nil return code, nil
} }
// ResetPassword 通过短信验证码重置密码
func (s *UserService) consumeSmsCode(phone, code string) bool {
if code == "" {
return false
}
key := cache.SmsCodeKeyOf(phone)
stored, err := common.GetString(key)
if err != nil || stored != code {
return false
}
_ = common.Delete(key)
return true
}
func (s *UserService) ResetPassword(phone, smsCode, newPassword string) *common.BusiError { func (s *UserService) ResetPassword(phone, smsCode, newPassword string) *common.BusiError {
key := cache.SmsCodeKeyOf(phone) key := cache.SmsCodeKeyOf(phone)
stored, err := common.GetString(key) stored, err := common.GetString(key)
if err != nil || stored == "" || stored != smsCode { if err != nil || stored == "" || stored != smsCode {
return common.ErrSmsCodeError return common.ErrSmsCodeError
} }
var user model.User var user model.User
if err := common.DB.Where("phone = ?", phone).First(&user).Error; err != nil { if err := common.DB.Where("phone = ?", phone).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@ -150,22 +242,24 @@ func (s *UserService) ResetPassword(phone, smsCode, newPassword string) *common.
} }
return common.ErrInternal return common.ErrInternal
} }
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil { if err != nil {
return common.ErrInternal return common.ErrInternal
} }
if err := common.DB.Model(&user).Updates(map[string]any{
"password": string(hash),
"updated_at": time.Now(),
now := time.Now()
if err := common.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&user).Updates(map[string]any{"password": string(hash), "password_changed_at": now, "updated_at": now}).Error; err != nil {
return err
}
return tx.Model(&model.UserLoginSession{}).Where("user_id = ? AND revoked_at IS NULL", user.ID).Update("revoked_at", now).Error
}).Error; err != nil { }).Error; err != nil {
return common.ErrInternal return common.ErrInternal
} }
s.deleteUserSessions(user.ID)
_ = common.Delete(key) _ = common.Delete(key)
return nil return nil
} }
// GetByID 按 ID 查询用户
func (s *UserService) GetByID(id int64) (*model.User, *common.BusiError) { func (s *UserService) GetByID(id int64) (*model.User, *common.BusiError) {
var user model.User var user model.User
if err := common.DB.First(&user, id).Error; err != nil { if err := common.DB.First(&user, id).Error; err != nil {
@ -177,31 +271,75 @@ func (s *UserService) GetByID(id int64) (*model.User, *common.BusiError) {
return &user, nil return &user, nil
} }
// buildLoginResp 生成 token 并存证 Redis,返回登录响应
func (s *UserService) buildLoginResp(user *model.User) (*vo.LoginResp, *common.BusiError) {
roles := []string{user.Role}
accessToken, refreshToken, err := token.GenerateToken(user.ID, user.Phone, roles)
func (s *UserService) buildLoginResp(user *model.User, userAgent, ip string) (*vo.LoginResp, *common.BusiError) {
id, err := tool.NextID()
if err != nil { if err != nil {
logger.ERROR("生成 token 失败", err)
return nil, common.ErrInternal return nil, common.ErrInternal
} }
now := time.Now()
session := &model.UserLoginSession{ID: id, UserID: user.ID, UserAgent: userAgent, IP: ip, LastActiveAt: now, ExpiresAt: now.Add(time.Duration(token.RefreshExpireSeconds()) * time.Second), CreatedAt: now}
return s.issueForSession(user, session, userAgent, ip)
}
accessKey := cache.UserTokenKeyOf(user.ID, token.GenerateShortID(accessToken))
refreshKey := cache.RefreshTokenKeyOf(user.ID, token.GenerateShortID(refreshToken))
if err := common.SetValueWithExpired(accessKey, "1", token.AccessExpireSeconds()); err != nil {
func (s *UserService) issueForSession(user *model.User, session *model.UserLoginSession, userAgent, ip string) (*vo.LoginResp, *common.BusiError) {
accessToken, refreshToken, err := token.GenerateToken(user.ID, session.ID, user.Phone, []string{user.Role})
if err != nil {
return nil, common.ErrInternal return nil, common.ErrInternal
} }
if err := common.SetValueWithExpired(refreshKey, refreshToken, token.RefreshExpireSeconds()); err != nil {
session.AccessTokenID = token.GenerateShortID(accessToken)
session.RefreshTokenID = token.GenerateShortID(refreshToken)
session.UserAgent, session.IP = userAgent, ip
session.LastActiveAt = time.Now()
session.ExpiresAt = time.Now().Add(time.Duration(token.RefreshExpireSeconds()) * time.Second)
if session.CreatedAt.IsZero() {
session.CreatedAt = time.Now()
}
if err := common.DB.Save(session).Error; err != nil {
logger.ERROR("保存登录会话失败", err)
return nil, common.ErrInternal
}
if err := common.SetValueWithExpired(cache.UserTokenKeyOf(user.ID, session.AccessTokenID), sessionValue(session.ID), token.AccessExpireSeconds()); err != nil {
return nil, common.ErrInternal return nil, common.ErrInternal
} }
if err := common.SetValueWithExpired(cache.RefreshTokenKeyOf(user.ID, session.RefreshTokenID), sessionValue(session.ID), token.RefreshExpireSeconds()); err != nil {
return nil, common.ErrInternal
}
return &vo.LoginResp{AccessToken: accessToken, RefreshToken: refreshToken, User: vo.NewUserVO(user)}, nil
}
func (s *UserService) revokeSession(userID, sessionID int64) *common.BusiError {
var session model.UserLoginSession
if err := common.DB.Where("id = ? AND user_id = ?", sessionID, userID).First(&session).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return common.ErrNotFound
}
return common.ErrInternal
}
now := time.Now()
if session.RevokedAt == nil {
_ = common.DB.Model(&session).Update("revoked_at", now).Error
}
s.deleteSessionTokens(userID, &session)
return nil
}
func (s *UserService) deleteUserSessions(userID int64) {
var sessions []model.UserLoginSession
if common.DB.Where("user_id = ?", userID).Find(&sessions).Error != nil {
return
}
for _, session := range sessions {
s.deleteSessionTokens(userID, &session)
}
}
return &vo.LoginResp{
AccessToken: accessToken,
RefreshToken: refreshToken,
User: vo.NewUserVO(user),
}, nil
func (s *UserService) deleteSessionTokens(userID int64, session *model.UserLoginSession) {
_ = common.Delete(cache.UserTokenKeyOf(userID, session.AccessTokenID))
_ = common.Delete(cache.RefreshTokenKeyOf(userID, session.RefreshTokenID))
} }
func sessionValue(sessionID int64) string { return stringID(sessionID) }
func stringID(id int64) string { return fmt.Sprintf("%d", id) }
func checkPassword(password, hash string) bool { func checkPassword(password, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
} }

18
service/video_service.go

@ -142,6 +142,19 @@ func (s *VideoService) Complete(userID int64, videoID int64, req *vo.VideoComple
} }
return nil, common.ErrInternal return nil, common.ErrInternal
} }
if video.Status == "ready" {
return &video, nil
}
if video.Status != "uploading" && video.Status != "pending" {
return nil, common.ErrVideoUploading
}
if video.UploadExpireAt != nil && time.Now().After(*video.UploadExpireAt) {
return nil, common.NewBusiError(common.ParamError, "视频上传已过期,请重新申请上传")
}
if err := headOSS(video.OssKey); err != nil {
logger.ERROR("确认视频对象失败", err)
return nil, common.NewBusiError(common.ParamError, "视频文件尚未上传完成")
}
updates := map[string]any{"status": "ready"} updates := map[string]any{"status": "ready"}
if req.FileSize > 0 { if req.FileSize > 0 {
updates["file_size"] = req.FileSize updates["file_size"] = req.FileSize
@ -149,8 +162,11 @@ func (s *VideoService) Complete(userID int64, videoID int64, req *vo.VideoComple
if req.Duration > 0 { if req.Duration > 0 {
updates["duration"] = req.Duration updates["duration"] = req.Duration
} }
if err := common.DB.Model(&video).Updates(updates).Error; err != nil {
if result := common.DB.Model(&video).Where("id = ? AND user_id = ? AND status IN ?", videoID, userID, []string{"pending", "uploading"}).Updates(updates); result.Error != nil {
return nil, common.ErrInternal return nil, common.ErrInternal
} else if result.RowsAffected == 0 {
common.DB.First(&video, videoID)
return &video, nil
} }
common.DB.First(&video, videoID) common.DB.First(&video, videoID)
return &video, nil return &video, nil

5
sql/001_schema.sql

@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS `user` (
traffic_balance BIGINT DEFAULT 0, -- 云媒体剩余流量(字节),慢快照,权威在 Redis traffic_balance BIGINT DEFAULT 0, -- 云媒体剩余流量(字节),慢快照,权威在 Redis
status TINYINT DEFAULT 1, -- 1 正常 / 0 禁用 status TINYINT DEFAULT 1, -- 1 正常 / 0 禁用
last_login DATETIME, last_login DATETIME,
password_changed_at DATETIME,
created_at DATETIME, created_at DATETIME,
updated_at DATETIME, updated_at DATETIME,
UNIQUE KEY idx_phone (phone) UNIQUE KEY idx_phone (phone)
@ -40,7 +41,6 @@ CREATE TABLE IF NOT EXISTS dock (
latitude DOUBLE DEFAULT 0, latitude DOUBLE DEFAULT 0,
altitude DOUBLE DEFAULT 0, altitude DOUBLE DEFAULT 0,
location VARCHAR(128) DEFAULT '', location VARCHAR(128) DEFAULT '',
status VARCHAR(32) DEFAULT 'offline',
register_status VARCHAR(16) DEFAULT 'pending', -- pending/registered register_status VARCHAR(16) DEFAULT 'pending', -- pending/registered
dock_id_source VARCHAR(32) DEFAULT '', dock_id_source VARCHAR(32) DEFAULT '',
software_ver VARCHAR(16) DEFAULT '', software_ver VARCHAR(16) DEFAULT '',
@ -58,7 +58,6 @@ CREATE TABLE IF NOT EXISTS drone (
name VARCHAR(64) DEFAULT '', name VARCHAR(64) DEFAULT '',
code VARCHAR(32) DEFAULT '', code VARCHAR(32) DEFAULT '',
model VARCHAR(32) DEFAULT '', model VARCHAR(32) DEFAULT '',
status VARCHAR(32) DEFAULT 'offline',
battery INT DEFAULT 0, battery INT DEFAULT 0,
firmware_ver VARCHAR(16) DEFAULT '', firmware_ver VARCHAR(16) DEFAULT '',
created_at DATETIME, created_at DATETIME,
@ -324,7 +323,7 @@ CREATE TABLE IF NOT EXISTS sim_recharge_log (
amount_gb INT NOT NULL, amount_gb INT NOT NULL,
unit_price DECIMAL(6,2), unit_price DECIMAL(6,2),
total_price DECIMAL(10,2), total_price DECIMAL(10,2),
pay_status VARCHAR(16) DEFAULT 'paid',
pay_status VARCHAR(16) DEFAULT 'unpaid',
recharge_status VARCHAR(16) DEFAULT 'pending', -- pending/success/failed recharge_status VARCHAR(16) DEFAULT 'pending', -- pending/success/failed
recharge_msg VARCHAR(256) DEFAULT '', recharge_msg VARCHAR(256) DEFAULT '',
idempotent_key VARCHAR(64) NOT NULL, -- 平台充值单号,超时重试去重 idempotent_key VARCHAR(64) NOT NULL, -- 平台充值单号,超时重试去重

5
sql/003_seed.sql

@ -67,7 +67,4 @@ ON DUPLICATE KEY UPDATE
DELETE FROM drone WHERE drone_sn = 'JG-UAV-MOCK01'; DELETE FROM drone WHERE drone_sn = 'JG-UAV-MOCK01';
DELETE FROM dock WHERE dock_id IN ('JG-DK-MOCK01', '333'); DELETE FROM dock WHERE dock_id IN ('JG-DK-MOCK01', '333');
-- ---------------------------------------------------------------
-- 说明: Casbin RBAC 策略(g: user_id -> role / p: role -> path -> method)
-- 由应用首次启动时通过 gorm-adapter 自动建表并引导,不在本脚本中静态写入。
-- ---------------------------------------------------------------
-- 角色由 user.role 维护,管理员接口由路由 AdminMiddleware 保护。

26
sql/006_remove_device_status.sql

@ -0,0 +1,26 @@
-- 设备在线状态仅由 Redis 在线集合和心跳维护;执行前确认所有应用实例已升级。
DELIMITER $$
DROP PROCEDURE IF EXISTS remove_device_status_columns $$
CREATE PROCEDURE remove_device_status_columns()
BEGIN
DECLARE item_count INT DEFAULT 0;
SELECT COUNT(*) INTO item_count
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'dock' AND column_name = 'status';
IF item_count > 0 THEN
ALTER TABLE dock DROP COLUMN status;
END IF;
SELECT COUNT(*) INTO item_count
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'drone' AND column_name = 'status';
IF item_count > 0 THEN
ALTER TABLE drone DROP COLUMN status;
END IF;
END $$
CALL remove_device_status_columns() $$
DROP PROCEDURE remove_device_status_columns $$
DELIMITER ;

2
sql/007_remove_casbin_rule.sql

@ -0,0 +1,2 @@
-- Casbin 已移除,策略表不再使用。
DROP TABLE IF EXISTS `casbin_rule`;

9
sql/008_billing_indexes.sql

@ -0,0 +1,9 @@
-- 账户中心分页与用户隔离查询索引
CREATE INDEX idx_traffic_order_user_created ON traffic_order (user_id, created_at DESC);
CREATE INDEX idx_traffic_order_user_status_created ON traffic_order (user_id, pay_status, created_at DESC);
CREATE INDEX idx_traffic_usage_user_created ON traffic_usage_log (user_id, created_at DESC);
CREATE INDEX idx_traffic_usage_user_source_created ON traffic_usage_log (user_id, source_type, created_at DESC);
CREATE INDEX idx_sim_card_user_created ON sim_card (user_id, created_at DESC);
CREATE INDEX idx_sim_card_user_status_created ON sim_card (user_id, status, created_at DESC);
CREATE INDEX idx_sim_recharge_user_created ON sim_recharge_log (user_id, created_at DESC);
CREATE INDEX idx_sim_recharge_user_status_created ON sim_recharge_log (user_id, recharge_status, created_at DESC);

49
sql/009_account_security_invoice.sql

@ -0,0 +1,49 @@
ALTER TABLE `user` ADD COLUMN `password_changed_at` DATETIME NULL AFTER `last_login`;
CREATE TABLE IF NOT EXISTS `user_login_session` (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
access_token_id VARCHAR(64) NOT NULL,
refresh_token_id VARCHAR(64) NOT NULL,
user_agent VARCHAR(512),
ip VARCHAR(64),
last_active_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME,
created_at DATETIME,
UNIQUE KEY uk_session_access (access_token_id),
UNIQUE KEY uk_session_refresh (refresh_token_id),
KEY idx_session_user_active (user_id, revoked_at, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `invoice_profile` (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
title VARCHAR(128) NOT NULL,
taxpayer_number VARCHAR(64) NOT NULL,
email VARCHAR(128) NOT NULL,
is_default TINYINT DEFAULT 0,
created_at DATETIME,
updated_at DATETIME,
KEY idx_invoice_profile_user (user_id, is_default)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `invoice_request` (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
title VARCHAR(128) NOT NULL,
taxpayer_number VARCHAR(64) NOT NULL,
email VARCHAR(128) NOT NULL,
status VARCHAR(16) DEFAULT 'pending',
invoice_number VARCHAR(64),
admin_remark VARCHAR(256),
requested_at DATETIME,
processed_at DATETIME,
UNIQUE KEY uk_invoice_order (order_id),
KEY idx_invoice_request_user (user_id, requested_at),
KEY idx_invoice_request_status (status, requested_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE INDEX idx_download_log_user_created ON download_log (user_id, created_at DESC);

8
sql/tdengine_schema.sql

@ -1,10 +1,12 @@
-- TDengine 时序库 schema(与 MySQL DDL 分离,需用 taos / REST 手动执行) -- TDengine 时序库 schema(与 MySQL DDL 分离,需用 taos / REST 手动执行)
-- 对应 DESIGN.md §5.9 -- 对应 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 元组一致 -- 遥测超级表(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, ts TIMESTAMP,
longitude DOUBLE, longitude DOUBLE,
latitude 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, ts TIMESTAMP,
longitude DOUBLE, longitude DOUBLE,
latitude DOUBLE, latitude DOUBLE,

16
token/token.go

@ -31,22 +31,25 @@ type Claims struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
SessionID int64 `json:"session_id"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
// RefreshClaims refresh token 载荷 // RefreshClaims refresh token 载荷
type RefreshClaims struct { type RefreshClaims struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
SessionID int64 `json:"session_id"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
// GenerateToken 生成 access + refresh token // GenerateToken 生成 access + refresh token
func GenerateToken(userID int64, username string, roles []string) (string, string, error) {
func GenerateToken(userID, sessionID int64, username string, roles []string) (string, string, error) {
now := time.Now() now := time.Now()
claims := Claims{ claims := Claims{
UserID: userID, UserID: userID,
Username: username, Username: username,
Roles: roles, Roles: roles,
SessionID: sessionID,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(accessExpireH) * time.Hour)), ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(accessExpireH) * time.Hour)),
IssuedAt: jwt.NewNumericDate(now), IssuedAt: jwt.NewNumericDate(now),
@ -60,6 +63,7 @@ func GenerateToken(userID int64, username string, roles []string) (string, strin
refreshClaims := RefreshClaims{ refreshClaims := RefreshClaims{
UserID: userID, UserID: userID,
SessionID: sessionID,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(refreshExpireH) * time.Hour)), ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(refreshExpireH) * time.Hour)),
IssuedAt: jwt.NewNumericDate(now), IssuedAt: jwt.NewNumericDate(now),
@ -85,16 +89,16 @@ func ParseToken(tokenString string) (*Claims, error) {
return nil, errors.New("invalid token") return nil, errors.New("invalid token")
} }
// ParseRefreshToken 解析并校验 refresh token,返回用户 ID
func ParseRefreshToken(tokenString string) (int64, error) {
// ParseRefreshToken 解析并校验 refresh token,返回用户 ID 和会话 ID
func ParseRefreshToken(tokenString string) (int64, int64, error) {
token, err := jwt.ParseWithClaims(tokenString, &RefreshClaims{}, keyFunc) token, err := jwt.ParseWithClaims(tokenString, &RefreshClaims{}, keyFunc)
if err != nil { if err != nil {
return 0, err
return 0, 0, err
} }
if claims, ok := token.Claims.(*RefreshClaims); ok && token.Valid { if claims, ok := token.Claims.(*RefreshClaims); ok && token.Valid {
return claims.UserID, nil
return claims.UserID, claims.SessionID, nil
} }
return 0, errors.New("invalid refresh token")
return 0, 0, errors.New("invalid refresh token")
} }
func keyFunc(t *jwt.Token) (any, error) { func keyFunc(t *jwt.Token) (any, error) {

52
vo/account_extra_vo.go

@ -0,0 +1,52 @@
package vo
import "time"
type SessionVO struct {
ID int64 `json:"id"`
UserAgent string `json:"userAgent"`
IP string `json:"ip"`
LastActiveAt time.Time `json:"lastActiveAt"`
ExpiresAt time.Time `json:"expiresAt"`
Current bool `json:"current"`
}
type DownloadRecordVO struct {
ID int64 `json:"id"`
VideoID int64 `json:"videoId"`
FileName string `json:"fileName"`
FileSize int64 `json:"fileSize"`
DroneSN string `json:"droneSn"`
ExecutionID int64 `json:"executionId"`
Bytes int64 `json:"bytes"`
CreatedAt time.Time `json:"createdAt"`
}
type ResourceSummaryVO struct {
BalanceGb float64 `json:"balanceGb"`
MonthUsageGb float64 `json:"monthUsageGb"`
MonthLiveGb float64 `json:"monthLiveGb"`
MonthReplayGb float64 `json:"monthReplayGb"`
MonthDownloadGb float64 `json:"monthDownloadGb"`
SimCount int64 `json:"simCount"`
DockCount int64 `json:"dockCount"`
DroneCount int64 `json:"droneCount"`
VideoCount int64 `json:"videoCount"`
MonthDownloadCnt int64 `json:"monthDownloadCount"`
}
type InvoiceProfileReq struct {
Title string `json:"title" validate:"required,max=128"`
TaxpayerNumber string `json:"taxpayerNumber" validate:"required,max=64"`
Email string `json:"email" validate:"required,email"`
IsDefault bool `json:"isDefault"`
}
type InvoiceRequestCreateReq struct {
OrderID int64 `json:"orderId" validate:"required"`
}
type InvoiceReviewReq struct {
Status string `json:"status" validate:"required"`
InvoiceNumber string `json:"invoiceNumber"`
AdminRemark string `json:"adminRemark"`
}

18
vo/account_vo.go

@ -1,6 +1,22 @@
package vo package vo
import "laic-backend/common"
import (
"time"
"laic-backend/common"
)
// ProfileVO 账户资料
type ProfileVO struct {
ID int64 `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email"`
Role string `json:"role"`
Status int8 `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// ProfileUpdateReq 更新个人资料 // ProfileUpdateReq 更新个人资料
type ProfileUpdateReq struct { type ProfileUpdateReq struct {

12
vo/auth_vo.go

@ -9,7 +9,9 @@ import (
// UserLoginReq 登录请求 // UserLoginReq 登录请求
type UserLoginReq struct { type UserLoginReq struct {
Phone string `json:"phone" validate:"required,phone"` Phone string `json:"phone" validate:"required,phone"`
Password string `json:"password" validate:"required"`
Password string `json:"password" validate:"omitempty"`
Captcha string `json:"captcha" validate:"omitempty,len=4"`
SmsCode string `json:"smsCode" validate:"omitempty,len=6"`
} }
// RegisterReq 注册请求 // RegisterReq 注册请求
@ -18,11 +20,13 @@ type RegisterReq struct {
Phone string `json:"phone" validate:"required,phone"` Phone string `json:"phone" validate:"required,phone"`
Email string `json:"email" validate:"omitempty,email"` Email string `json:"email" validate:"omitempty,email"`
Password string `json:"password" validate:"required,password"` Password string `json:"password" validate:"required,password"`
SmsCode string `json:"smsCode" validate:"required,len=6"`
} }
// SmsCodeReq 发送短信验证码请求 // SmsCodeReq 发送短信验证码请求
type SmsCodeReq struct { type SmsCodeReq struct {
Phone string `json:"phone" validate:"required,phone"` Phone string `json:"phone" validate:"required,phone"`
Scene string `json:"scene" validate:"required,oneof=login register reset"`
} }
// ResetPasswordReq 重置密码请求 // ResetPasswordReq 重置密码请求
@ -42,6 +46,12 @@ type LogoutReq struct {
RefreshToken string `json:"refreshToken"` RefreshToken string `json:"refreshToken"`
} }
// ChangePasswordReq 登录后修改密码
type ChangePasswordReq struct {
OldPassword string `json:"oldPassword" validate:"required"`
NewPassword string `json:"newPassword" validate:"required,password"`
}
// UserVO 用户信息(脱敏,不含密码) // UserVO 用户信息(脱敏,不含密码)
type UserVO struct { type UserVO struct {
ID int64 `json:"id"` ID int64 `json:"id"`

1
vo/dock_vo.go

@ -8,7 +8,6 @@ import (
// DockPageReq 机巢分页查询 // DockPageReq 机巢分页查询
type DockPageReq struct { type DockPageReq struct {
common.Pagination common.Pagination
Status string `json:"status" form:"status"`
RegisterStatus string `json:"registerStatus" form:"registerStatus"` RegisterStatus string `json:"registerStatus" form:"registerStatus"`
Keyword string `json:"keyword" form:"keyword"` // 匹配 name / dockId / code Keyword string `json:"keyword" form:"keyword"` // 匹配 name / dockId / code
} }

1
vo/drone_vo.go

@ -8,7 +8,6 @@ import (
// DronePageReq 无人机分页查询 // DronePageReq 无人机分页查询
type DronePageReq struct { type DronePageReq struct {
common.Pagination common.Pagination
Status string `json:"status" form:"status"`
Keyword string `json:"keyword" form:"keyword"` // 匹配 droneSn / name / code / model Keyword string `json:"keyword" form:"keyword"` // 匹配 droneSn / name / code / model
} }

Loading…
Cancel
Save