diff --git a/.env b/.env new file mode 100644 index 0000000..a8b6a3f --- /dev/null +++ b/.env @@ -0,0 +1,21 @@ +# 数据库配置 +DB_USER=postgres +DB_PASSWORD=postgres12341234 +DB_NAME=postgres +DB_PORT=5432 +DB_SSL_MODE=disable +DB_MAX_OPEN_CONNS=25 +DB_MAX_IDLE_CONNS=25 +DB_TIMEOUT=30s + +# 时区配置 +TZ=Asia/Shanghai + +# 网关端口 +PORT=80 + +# 日志配置 +LOG_LEVEL=info + +# Gin模式 (debug/release/test) +GIN_MODE=debug \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 6e9a61e..0aea50d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,8 +9,10 @@ services: entrypoint: - infra/postgres/scripts/db-lanuch-entrypoint.sh environment: - POSTGRES_PASSWORD: postgres12341234 - TZ: Asia/Shanghai + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + TZ: ${TZ} volumes: - ./shared_data/ai_trading_db:/var/lib/postgresql/data - ./infra/postgres/sql:/docker-entrypoint-initdb.d @@ -29,9 +31,15 @@ services: networks: - ai-trading-network environment: - DATABASE_URL: postgres://postgres:postgres12341234@postgres:5432/postgres - RUST_LOG: info - TZ: Asia/Shanghai + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} + volumes: + # 挂载添加日志目录挂载,将容器内日志日志目录映射到宿主机的 ./logs 目录 + - ./cn_futures_trading_records_logs:/app/logs # 假设代码中日志存储路径为 /app/logs networks: ai-trading-network: diff --git a/infra/postgres/sql/03_trading_records.sql b/infra/postgres/sql/03_trading_records.sql index 5bc82e5..906938a 100644 --- a/infra/postgres/sql/03_trading_records.sql +++ b/infra/postgres/sql/03_trading_records.sql @@ -44,11 +44,6 @@ BEGIN END IF; END $$; -DO $$ -BEGIN - RAISE NOTICE '全部索引已确保存在'; -END $$; - DO $$ BEGIN RAISE NOTICE '============ trading_records 部署完成 ============'; diff --git a/services/cn_futures_trading_records/dev.sh b/services/cn_futures_trading_records/dev.sh index 2149423..18f7ac9 100644 --- a/services/cn_futures_trading_records/dev.sh +++ b/services/cn_futures_trading_records/dev.sh @@ -1,3 +1,10 @@ +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + sudo docker stop go_cn_futures_trading_records_dev sudo docker rm go_cn_futures_trading_records_dev -sudo docker run -itd --name go_cn_futures_trading_records_dev -v $(pwd)/services/cn_futures_trading_records/src:/app -p 20010:80 golang:1.25.0-alpine3.22 \ No newline at end of file +sudo docker run -itd \ + --name go_cn_futures_trading_records_dev \ + -v "$SCRIPT_DIR/src:/app" \ + -p 20010:80 \ + golang:1.25.0-alpine3.22 \ No newline at end of file diff --git a/services/cn_futures_trading_records/src/crud/create.go b/services/cn_futures_trading_records/src/crud/create.go new file mode 100644 index 0000000..6192168 --- /dev/null +++ b/services/cn_futures_trading_records/src/crud/create.go @@ -0,0 +1,303 @@ +package crud + +import ( + "cn_futures_trading_records/db" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// CreateRequest 注册请求参数结构 +type CreateRequest struct { + Name string `json:"name" binding:"required"` // 国家名称,必填 + Code string `json:"code" binding:"required"` // 国家代码,必填 + Flag string `json:"flag"` // 国旗信息,可选 +} + +// CreateResponse 注册响应结构 +type CreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data CreateData `json:"data"` +} + +// CreateData 响应数据结构 +type CreateData struct { + CountryID string `json:"country_id"` +} + +// CreateHandler 处理创建逻辑 +func CreateHandler(c *gin.Context) { + startTime := time.Now() + reqID := c.Request.Header.Get("X-RegisterRequest-ID") + if reqID == "" { + reqID = uuid.New().String() + zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID)) + } + + zap.L().Info("📥 收到国家创建请求", + zap.String("req_id", reqID), + zap.String("path", c.Request.URL.Path), + zap.String("method", c.Request.Method), + ) + + var req CreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + zap.L().Warn("⚠️ 请求参数验证失败", + zap.String("req_id", reqID), + zap.Error(err), + zap.Any("request_body", c.Request.Body), + ) + c.JSON(http.StatusBadRequest, CreateResponse{ + Success: false, + Message: "请求参数错误:name和code为必填项", + }) + return + } + + zap.L().Debug("✅ 请求参数验证通过", + zap.String("req_id", reqID), + zap.String("name", req.Name), + zap.String("code", req.Code), + zap.String("flag", req.Flag), + ) + + // 开启数据库事务 + tx, err := db.DB.Begin() + if err != nil { + zap.L().Error("❌ 事务开启失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "系统错误,请稍后重试", + }) + return + } + + defer func() { + if r := recover(); r != nil { + if err := tx.Rollback(); err != nil { + zap.L().Error("💥 panic后事务回滚失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + } + zap.L().Error("💥 事务处理发生panic", + zap.String("req_id", reqID), + zap.Any("recover", r), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "系统错误,请稍后重试", + }) + } + }() + + // 唯一性校验 - 国家名称(排除已删除数据) + var nameCount int + err = tx.QueryRow( + "SELECT COUNT(*) FROM country_name WHERE name = $1 AND deleted = false", + req.Name, + ).Scan(&nameCount) + if err != nil { + tx.Rollback() + zap.L().Error("❌ 国家名称唯一性校验失败", + zap.String("req_id", reqID), + zap.String("name", req.Name), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "系统错误,校验名称失败", + }) + return + } + if nameCount > 0 { + tx.Rollback() + zap.L().Warn("⚠️ 国家名称已存在(未删除数据)", + zap.String("req_id", reqID), + zap.String("name", req.Name), + ) + c.JSON(http.StatusBadRequest, CreateResponse{ + Success: false, + Message: "国家名称已存在,请更换名称", + }) + return + } + + // 唯一性校验 - 国家编码(排除已删除数据) + var codeCount int + err = tx.QueryRow( + "SELECT COUNT(*) FROM country_code WHERE code = $1 AND deleted = false", + req.Code, + ).Scan(&codeCount) + if err != nil { + tx.Rollback() + zap.L().Error("❌ 国家编码唯一性校验失败", + zap.String("req_id", reqID), + zap.String("code", req.Code), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "系统错误,校验编码失败", + }) + return + } + if codeCount > 0 { + tx.Rollback() + zap.L().Warn("⚠️ 国家编码已存在(未删除数据)", + zap.String("req_id", reqID), + zap.String("code", req.Code), + ) + c.JSON(http.StatusBadRequest, CreateResponse{ + Success: false, + Message: "国家编码已存在,请更换编码", + }) + return + } + + // 唯一性校验 - 国旗(排除已删除数据,仅当提供了国旗参数时) + if req.Flag != "" { + var flagCount int + err = tx.QueryRow( + "SELECT COUNT(*) FROM country_flag WHERE flag = $1 AND deleted = false", + req.Flag, + ).Scan(&flagCount) + if err != nil { + tx.Rollback() + zap.L().Error("❌ 国旗唯一性校验失败", + zap.String("req_id", reqID), + zap.String("flag", req.Flag), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "系统错误,校验国旗失败", + }) + return + } + if flagCount > 0 { + tx.Rollback() + zap.L().Warn("⚠️ 国旗信息已存在(未删除数据)", + zap.String("req_id", reqID), + zap.String("flag", req.Flag), + ) + c.JSON(http.StatusBadRequest, CreateResponse{ + Success: false, + Message: "国旗信息已存在,请更换国旗", + }) + return + } + } + + // 1. 创建country主表记录 + var countryID string + err = tx.QueryRow("INSERT INTO country DEFAULT VALUES RETURNING id").Scan(&countryID) + if err != nil { + tx.Rollback() + zap.L().Error("❌ country表插入失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "创建国家记录失败", + }) + return + } + + zap.L().Debug("📝 country表插入成功", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + ) + + // 2. 插入国家名称 + _, err = tx.Exec("INSERT INTO country_name (country_id, name) VALUES ($1, $2)", countryID, req.Name) + if err != nil { + tx.Rollback() + zap.L().Error("❌ country_name表插入失败", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "保存名称信息失败", + }) + return + } + + // 3. 插入国家代码 + _, err = tx.Exec("INSERT INTO country_code (country_id, code) VALUES ($1, $2)", countryID, req.Code) + if err != nil { + tx.Rollback() + zap.L().Error("❌ country_code表插入失败", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "保存代码信息失败", + }) + return + } + + // 4. 插入国旗信息(如果提供) + if req.Flag != "" { + _, err = tx.Exec("INSERT INTO country_flag (country_id, flag) VALUES ($1, $2)", countryID, req.Flag) + if err != nil { + tx.Rollback() + zap.L().Error("❌ country_flag表插入失败", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "保存国旗信息失败", + }) + return + } + zap.L().Debug("📝 country_flag表插入成功", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + ) + } + + // 提交事务 + if err := tx.Commit(); err != nil { + tx.Rollback() + zap.L().Error("❌ 事务提交失败", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "数据提交失败,请稍后重试", + }) + return + } + + duration := time.Since(startTime) + zap.L().Info("✅ 国家创建请求处理完成", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + zap.Duration("duration", duration), + ) + + c.JSON(http.StatusOK, CreateResponse{ + Success: true, + Message: "创建成功", + Data: CreateData{ + CountryID: countryID, + }, + }) +} diff --git a/services/cn_futures_trading_records/src/db/postgres.go b/services/cn_futures_trading_records/src/db/postgres.go new file mode 100644 index 0000000..aba5033 --- /dev/null +++ b/services/cn_futures_trading_records/src/db/postgres.go @@ -0,0 +1,54 @@ +package db + +import ( + "database/sql" + "fmt" + "os" + "time" + + _ "github.com/lib/pq" + "go.uber.org/zap" +) + +var DB *sql.DB + +// 初始化数据库连接 +func Init() { + // 从环境变量获取数据库配置 + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_NAME") + zap.L().Info( + "💡 读取数据库配置", + zap.String("host", dbHost), + zap.String("port", dbPort), + zap.String("user", dbUser), + zap.String("dbname", dbName), + ) + + // 构建数据库连接字符串 + connStr := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName, + ) + + var err error + DB, err = sql.Open("postgres", connStr) + if err != nil { + zap.L().Panic("❌ 无法连接数据库", zap.Error(err)) + } + + // 设置连接池参数 + DB.SetMaxOpenConns(100) // 最大打开连接数 + DB.SetMaxIdleConns(20) // 最大空闲连接数 + DB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间 + + // 验证数据库连接 + if err := DB.Ping(); err != nil { + zap.L().Panic("❌ 数据库连接失败", zap.Error(err)) + } + + zap.L().Info("✅ 数据库连接验证成功") +} diff --git a/services/cn_futures_trading_records/src/go.mod b/services/cn_futures_trading_records/src/go.mod index 793ab34..4ad544c 100644 --- a/services/cn_futures_trading_records/src/go.mod +++ b/services/cn_futures_trading_records/src/go.mod @@ -1,4 +1,4 @@ -module asset_assistant +module cn_futures_trading_records go 1.25.0 @@ -9,7 +9,6 @@ require ( github.com/lib/pq v1.10.9 github.com/spf13/viper v1.21.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.43.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -47,6 +46,7 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.45.0 // indirect golang.org/x/sync v0.17.0 // indirect diff --git a/services/cn_futures_trading_records/src/logger/logger.go b/services/cn_futures_trading_records/src/logger/logger.go new file mode 100644 index 0000000..d7eee0b --- /dev/null +++ b/services/cn_futures_trading_records/src/logger/logger.go @@ -0,0 +1,86 @@ +package logger + +import ( + "log" + "os" + "time" + + "github.com/spf13/viper" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +var shanghaiLoc *time.Location + +func init() { + var err error + shanghaiLoc, err = time.LoadLocation("Asia/Shanghai") + if err != nil { + // 尝试备选时区名称 + shanghaiLoc, err = time.LoadLocation("PRC") + if err != nil { + // 若仍失败,手动设置东八区偏移 + shanghaiLoc = time.FixedZone("CST", 8*3600) + log.Printf("警告:加载时区失败,使用手动东八区偏移: %v", err) + } + } +} + +// Init 初始化日志(依赖配置文件已加载) +func Init() { + // 日志级别转换 + level := zap.InfoLevel + switch viper.GetString("logger.level") { + case "debug": + level = zap.DebugLevel + case "warn": + level = zap.WarnLevel + case "error": + level = zap.ErrorLevel + } + + // 日志轮转配置(lumberjack) + hook := lumberjack.Logger{ + Filename: viper.GetString("logger.path") + "logs/app.log", // 日志文件路径 + MaxSize: viper.GetInt("logger.max_size"), // 单个文件最大大小(MB) + MaxBackups: viper.GetInt("logger.max_backup"), // 最大备份数 + MaxAge: viper.GetInt("logger.max_age"), // 最大保留天数 + Compress: true, // 是否压缩 + } + + // 编码器配置 + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, // 日志级别大写(DEBUG/INFO) + EncodeTime: customTimeEncoder, // 自定义时间格式 + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, // 精简调用者路径 + } + + // 输出配置(控制台+文件) + core := zapcore.NewTee( + zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), zapcore.AddSync(os.Stdout), level), + zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), zapcore.AddSync(&hook), level), + ) + + // 创建logger实例(开启调用者信息和堆栈跟踪) + logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)) + zap.ReplaceGlobals(logger) + + zap.L().Info("✅ 日志初始化成功", zap.String("level", level.String())) +} + +// customTimeEncoder 自定义时间格式(强制东八区,若加载失败则使用UTC) +func customTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + // 使用提前初始化好的时区,避免每次调用都加载 + beijingTime := t.In(shanghaiLoc) + // 格式化输出 + enc.AppendString(beijingTime.Format("2006-01-02 15:04:05.000")) +} diff --git a/services/cn_futures_trading_records/src/main.go b/services/cn_futures_trading_records/src/main.go index e73053e..95f12bb 100644 --- a/services/cn_futures_trading_records/src/main.go +++ b/services/cn_futures_trading_records/src/main.go @@ -1,12 +1,9 @@ package main import ( - "asset_assistant/db" // 数据库相关操作包 - "asset_assistant/logger" // 日志工具包 - "asset_assistant/logic4country" - "asset_assistant/logic4currency" - "asset_assistant/logic4exchange" - "asset_assistant/logic4user" + "cn_futures_trading_records/crud" + "cn_futures_trading_records/db" // 数据库相关操作包 + "cn_futures_trading_records/logger" // 日志工具包 // 业务逻辑处理包 "time" @@ -56,42 +53,11 @@ func main() { zap.L().Info("✅ 配置跨域中间件完成") // 注册用户接口 - user := r.Group("/user") + trading := r.Group("/cn_futures_trading_record") { - user.POST("/register", logic4user.RegisterHandler) - user.POST("/login", logic4user.LoginHandler) + trading.POST("/register", crud.CreateHandler) } - zap.L().Info("✅ 用户接口注册完成") - - // 注册国家接口 - country := r.Group("/country") - { - country.POST("/create", logic4country.CreateHandler) - country.POST("/read", logic4country.ReadHandler) - country.POST("/update", logic4country.UpdateHandler) - country.POST("/delete", logic4country.DeleteHandler) - } - zap.L().Info("✅ 国家接口注册完成") - - // 注册交易所接口 - exchange := r.Group("/exchange") - { - exchange.POST("/create", logic4exchange.CreateHandler) - exchange.POST("/read", logic4exchange.ReadHandler) - exchange.POST("/update", logic4exchange.UpdateHandler) - exchange.POST("/delete", logic4exchange.DeleteHandler) - } - zap.L().Info("✅ 交易所接口注册完成") - - // 注册货币接口 - currency := r.Group("/currency") - { - currency.POST("/create", logic4currency.CreateHandler) - currency.POST("/read", logic4currency.ReadHandler) - currency.POST("/update", logic4currency.UpdateHandler) - currency.POST("/delete", logic4currency.DeleteHandler) - } - zap.L().Info("✅ 货币接口注册完成") + zap.L().Info("✅ 中国期货交易记录接口注册完成") // 记录服务启动日志,监听80端口 zap.L().Info("✅ 服务启动在80端口")