This commit is contained in:
vipg
2025-12-19 15:35:28 +08:00
parent 560f5dc54b
commit 99fcb5fdd7
9 changed files with 493 additions and 53 deletions

21
.env Normal file
View File

@@ -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

View File

@@ -9,8 +9,10 @@ services:
entrypoint: entrypoint:
- infra/postgres/scripts/db-lanuch-entrypoint.sh - infra/postgres/scripts/db-lanuch-entrypoint.sh
environment: environment:
POSTGRES_PASSWORD: postgres12341234 POSTGRES_USER: ${DB_USER}
TZ: Asia/Shanghai POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
TZ: ${TZ}
volumes: volumes:
- ./shared_data/ai_trading_db:/var/lib/postgresql/data - ./shared_data/ai_trading_db:/var/lib/postgresql/data
- ./infra/postgres/sql:/docker-entrypoint-initdb.d - ./infra/postgres/sql:/docker-entrypoint-initdb.d
@@ -29,9 +31,15 @@ services:
networks: networks:
- ai-trading-network - ai-trading-network
environment: environment:
DATABASE_URL: postgres://postgres:postgres12341234@postgres:5432/postgres DB_HOST: postgres
RUST_LOG: info DB_PORT: ${DB_PORT}
TZ: Asia/Shanghai 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: networks:
ai-trading-network: ai-trading-network:

View File

@@ -44,11 +44,6 @@ BEGIN
END IF; END IF;
END $$; END $$;
DO $$
BEGIN
RAISE NOTICE '全部索引已确保存在';
END $$;
DO $$ DO $$
BEGIN BEGIN
RAISE NOTICE '============ trading_records 部署完成 ============'; RAISE NOTICE '============ trading_records 部署完成 ============';

View File

@@ -1,3 +1,10 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
sudo docker stop go_cn_futures_trading_records_dev sudo docker stop go_cn_futures_trading_records_dev
sudo docker rm 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 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

View File

@@ -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,
},
})
}

View File

@@ -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("✅ 数据库连接验证成功")
}

View File

@@ -1,4 +1,4 @@
module asset_assistant module cn_futures_trading_records
go 1.25.0 go 1.25.0
@@ -9,7 +9,6 @@ require (
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.43.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
@@ -47,6 +46,7 @@ require (
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.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.20.0 // 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/mod v0.28.0 // indirect
golang.org/x/net v0.45.0 // indirect golang.org/x/net v0.45.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.17.0 // indirect

View File

@@ -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"))
}

View File

@@ -1,12 +1,9 @@
package main package main
import ( import (
"asset_assistant/db" // 数据库相关操作包 "cn_futures_trading_records/crud"
"asset_assistant/logger" // 日志工具 "cn_futures_trading_records/db" // 数据库相关操作
"asset_assistant/logic4country" "cn_futures_trading_records/logger" // 日志工具包
"asset_assistant/logic4currency"
"asset_assistant/logic4exchange"
"asset_assistant/logic4user"
// 业务逻辑处理包 // 业务逻辑处理包
"time" "time"
@@ -56,42 +53,11 @@ func main() {
zap.L().Info("✅ 配置跨域中间件完成") zap.L().Info("✅ 配置跨域中间件完成")
// 注册用户接口 // 注册用户接口
user := r.Group("/user") trading := r.Group("/cn_futures_trading_record")
{ {
user.POST("/register", logic4user.RegisterHandler) trading.POST("/register", crud.CreateHandler)
user.POST("/login", logic4user.LoginHandler)
} }
zap.L().Info("✅ 用户接口注册完成") 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("✅ 货币接口注册完成")
// 记录服务启动日志监听80端口 // 记录服务启动日志监听80端口
zap.L().Info("✅ 服务启动在80端口") zap.L().Info("✅ 服务启动在80端口")