This commit is contained in:
vipg
2025-12-22 18:28:26 +08:00
parent 6a74bfdcd4
commit bccc20e307
8 changed files with 106 additions and 81 deletions

View File

@@ -2,50 +2,29 @@ package crud
import (
"bytes"
"cn_futures_trading_records/db"
"encoding/csv"
"encoding/json"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"trading_records/infra"
"trading_records/model"
"github.com/gin-gonic/gin"
"github.com/gocarina/gocsv"
"github.com/google/uuid"
"go.uber.org/zap"
)
/* ---------- 公共 Payload ---------- */
type Payload struct {
Status int `csv:"status" json:"status" binding:"required,min=-1,max=1"`
OpenYear int `csv:"open_year" json:"open_year" binding:"required,min=1900,max=2200"`
OpenMonth int `csv:"open_month" json:"open_month" binding:"required,min=1,max=12"`
OpenDay int `csv:"open_day" json:"open_day" binding:"required,min=1,max=31"`
Symbol string `csv:"symbol" json:"symbol" binding:"required"`
Contract string `csv:"contract" json:"contract" binding:"required"`
Direction int `csv:"direction" json:"direction" binding:"required,min=-1,max=1"`
OpenPrice float64 `csv:"open_price" json:"open_price" binding:"required"`
OpenFee float64 `csv:"open_fee" json:"open_fee" binding:"required,min=0"`
CloseYear int `csv:"close_year" json:"close_year" binding:"required,min=1900,max=2200"`
CloseMonth int `csv:"close_month" json:"close_month" binding:"required,min=1,max=12"`
CloseDay int `csv:"close_day" json:"close_day" binding:"required,min=1,max=31"`
ClosePrice float64 `csv:"close_price" json:"close_price" binding:"required"`
CloseFee float64 `csv:"close_fee" json:"close_fee" binding:"required,min=0"`
PriceDiff float64 `csv:"price_diff" json:"price_diff" binding:"required"`
MinTick float64 `csv:"min_tick" json:"min_tick" binding:"required"`
TickPrice float64 `csv:"tick_price" json:"tick_price" binding:"required"`
DiffPnL float64 `csv:"diff_pnl" json:"diff_pnl" binding:"required"`
TotalFee float64 `csv:"total_fee" json:"total_fee" binding:"required,min=0"`
ClosePnL float64 `csv:"close_pnl" json:"close_pnl" binding:"required"`
}
/* ---------- 包级常量:带 csv 标签的字段个数 ---------- */
var payloadCSVCols int
func init() {
typ := reflect.TypeOf(Payload{})
typ := reflect.TypeFor[model.Payload]()
for i := 0; i < typ.NumField(); i++ {
if _, ok := typ.Field(i).Tag.Lookup("csv"); ok {
payloadCSVCols++
@@ -55,7 +34,7 @@ func init() {
/* ---------- 单条创建 ---------- */
type TradingRecordsCreateRequest struct {
Payload Payload `json:"payload" binding:"required"`
Payload model.Payload `json:"payload" binding:"required"`
}
type TradingRecordsCreateResponse struct {
@@ -91,7 +70,7 @@ func CreateHandler(c *gin.Context) {
zap.L().Debug("✅ 请求参数验证通过", zap.String("req_id", reqID), zap.Any("payload", req.Payload))
tx, err := db.DB.Begin()
tx, err := infra.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败", zap.String("req_id", reqID), zap.Error(err))
c.JSON(http.StatusOK, TradingRecordsCreateResponse{Success: false, Message: "系统错误,请稍后重试"})
@@ -114,7 +93,7 @@ func CreateHandler(c *gin.Context) {
}
var recordID string
err = tx.QueryRow(`INSERT INTO cn_futures_trading_records (payload) VALUES ($1) RETURNING id`, payloadBytes).Scan(&recordID)
err = tx.QueryRow(`INSERT INTO trading_records (payload) VALUES ($1) RETURNING id`, payloadBytes).Scan(&recordID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ 插入失败", zap.String("req_id", reqID), zap.Error(err))
@@ -167,7 +146,7 @@ func CreateBatchHandler(c *gin.Context) {
zap.L().Debug("✅ 请求参数验证通过", zap.String("req_id", reqID), zap.Int("row_count", len(req.Rows)))
tx, err := db.DB.Begin()
tx, err := infra.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败", zap.String("req_id", reqID), zap.Error(err))
c.JSON(http.StatusOK, TradingRecordsBatchCreateResponse{Success: false, Message: "系统错误,请稍后重试"})
@@ -181,7 +160,7 @@ func CreateBatchHandler(c *gin.Context) {
}
}()
stmt, err := tx.Prepare(`INSERT INTO cn_futures_trading_records (payload) VALUES ($1) RETURNING id`)
stmt, err := tx.Prepare(`INSERT INTO trading_records (payload) VALUES ($1) RETURNING id`)
if err != nil {
tx.Rollback()
zap.L().Error("❌ 预编译语句失败", zap.String("req_id", reqID), zap.Error(err))
@@ -200,7 +179,7 @@ func CreateBatchHandler(c *gin.Context) {
}
// 一次性映射
var p Payload
var p model.Payload
if err := gocsv.UnmarshalCSV(csv.NewReader(bytes.NewReader([]byte(strings.Join(row, ",")+"\n"))), &p); err != nil {
tx.Rollback()
zap.L().Error("❌ CSV 映射失败", zap.String("req_id", reqID), zap.Int("row_index", idx), zap.Error(err))
@@ -228,4 +207,4 @@ func CreateBatchHandler(c *gin.Context) {
zap.L().Info("✅ 交易记录批量创建完成", zap.String("req_id", reqID), zap.Int("inserted_count", len(recordIDs)), zap.Duration("duration", time.Since(start)))
c.JSON(http.StatusOK, TradingRecordsBatchCreateResponse{Success: true, Message: "批量创建成功", RecordIDs: recordIDs})
}
}

View File

@@ -1,10 +1,11 @@
module cn_futures_trading_records
module trading_records
go 1.25.0
require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9
github.com/spf13/viper v1.21.0

View File

@@ -29,6 +29,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
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-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=

View File

@@ -0,0 +1,50 @@
package infra
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// Init 初始化基础设施
func Init() {
launchLogger()
launchDB()
cnfigGin()
}
func launchLogger() {
InitLogger()
}
func launchDB() {
InitDB()
// 程序退出时关闭数据库连接defer确保在函数退出前执行
defer DB.Close()
}
func cnfigGin() {
// 设置Gin框架为发布模式关闭调试信息
gin.SetMode(gin.ReleaseMode)
// 创建Gin默认路由器
r := gin.Default()
// 配置跨域中间件
r.Use(cors.New(cors.Config{
// 允许所有来源(生产环境建议指定具体域名)
AllowOrigins: []string{"*"},
// 允许的请求方法
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
// 允许的请求头
AllowHeaders: []string{"Origin", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "X-LoginRequest-ID"},
// 允许前端读取的响应头
ExposeHeaders: []string{"Content-Length"},
// 是否允许携带cookie
AllowCredentials: true,
// 预检请求的缓存时间
MaxAge: 12 * time.Hour,
}))
zap.L().Info("✅ 配置跨域中间件完成")
}

View File

@@ -1,4 +1,4 @@
package logger
package infra
import (
"log"
@@ -28,7 +28,7 @@ func init() {
}
// Init 初始化日志(依赖配置文件已加载)
func Init() {
func InitLogger() {
// 日志级别转换
level := zap.InfoLevel
switch viper.GetString("logger.level") {
@@ -73,7 +73,6 @@ func Init() {
// 创建logger实例开启调用者信息和堆栈跟踪
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
zap.ReplaceGlobals(logger)
zap.L().Info("✅ 日志初始化成功", zap.String("level", level.String()))
}

View File

@@ -1,4 +1,4 @@
package db
package infra
import (
"database/sql"
@@ -13,7 +13,7 @@ import (
var DB *sql.DB
// 初始化数据库连接
func Init() {
func InitDB() {
// 从环境变量获取数据库配置
dbHost := os.Getenv("DB_HOST")
dbPort := os.Getenv("DB_PORT")
@@ -49,6 +49,5 @@ func Init() {
if err := DB.Ping(); err != nil {
zap.L().Panic("❌ 数据库连接失败", zap.Error(err))
}
zap.L().Info("✅ 数据库连接验证成功")
}

View File

@@ -1,64 +1,34 @@
package main
import (
"cn_futures_trading_records/crud"
"cn_futures_trading_records/db" // 数据库相关操作包
"cn_futures_trading_records/logger" // 日志工具包
"trading_records/crud"
"trading_records/infra"
// 业务逻辑处理包
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" // Gin框架用于构建HTTP服务
_ "github.com/lib/pq" // PostgreSQL数据库驱动下划线表示仅初始化不直接使用
"go.uber.org/zap" // Zap日志库用于结构化日志输出
)
// main函数是程序的入口点
func main() {
// 初始化日志配置
logger.Init()
// 记录服务初始化日志
zap.L().Info("🚀 用户服务初始化")
infra.Init()
configRouter()
startServe()
}
// 记录数据库初始化开始日志
zap.L().Info("⌛️ 数据库初始化开始")
// 初始化数据库连接
db.Init()
// 程序退出时关闭数据库连接defer确保在函数退出前执行
defer db.DB.Close()
// 记录数据库初始化成功日志
zap.L().Info("✅ 数据库初始化成功")
// 设置Gin框架为发布模式关闭调试信息
gin.SetMode(gin.ReleaseMode)
func configRouter() {
// 创建Gin默认路由器
r := gin.Default()
// 配置跨域中间件
r.Use(cors.New(cors.Config{
// 允许所有来源(生产环境建议指定具体域名)
AllowOrigins: []string{"*"},
// 允许的请求方法
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
// 允许的请求头
AllowHeaders: []string{"Origin", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "X-LoginRequest-ID"},
// 允许前端读取的响应头
ExposeHeaders: []string{"Content-Length"},
// 是否允许携带cookie
AllowCredentials: true,
// 预检请求的缓存时间
MaxAge: 12 * time.Hour,
}))
zap.L().Info("✅ 配置跨域中间件完成")
// 注册用户接口
trading := r.Group("/cn_futures_trading_record")
{
trading.POST("/open", crud.CreateHandler)
}
zap.L().Info("✅ 中国期货交易记录接口注册完成")
}
func startServe() {
// 创建Gin默认路由器
r := gin.Default()
// 记录服务启动日志监听80端口
zap.L().Info("✅ 服务启动在80端口")
r.Run(":80")

View File

@@ -0,0 +1,25 @@
package model
type Payload struct {
Status int `csv:"status" json:"status" binding:"required,min=-1,max=1"`
Market int `csv:"market" json:"market" binding:"required,min=0,max=500"`
OpenYear int `csv:"open_year" json:"open_year" binding:"required,min=1900,max=2200"`
OpenMonth int `csv:"open_month" json:"open_month" binding:"required,min=1,max=12"`
OpenDay int `csv:"open_day" json:"open_day" binding:"required,min=1,max=31"`
Symbol string `csv:"symbol" json:"symbol" binding:"required"`
Contract string `csv:"contract" json:"contract" binding:"required"`
Direction int `csv:"direction" json:"direction" binding:"required,min=-1,max=1"`
OpenPrice float64 `csv:"open_price" json:"open_price" binding:"required"`
OpenFee float64 `csv:"open_fee" json:"open_fee" binding:"required,min=0"`
CloseYear int `csv:"close_year" json:"close_year" binding:"required,min=1900,max=2200"`
CloseMonth int `csv:"close_month" json:"close_month" binding:"required,min=1,max=12"`
CloseDay int `csv:"close_day" json:"close_day" binding:"required,min=1,max=31"`
ClosePrice float64 `csv:"close_price" json:"close_price" binding:"required"`
CloseFee float64 `csv:"close_fee" json:"close_fee" binding:"required,min=0"`
PriceDiff float64 `csv:"price_diff" json:"price_diff" binding:"required"`
MinTick float64 `csv:"min_tick" json:"min_tick" binding:"required"`
TickPrice float64 `csv:"tick_price" json:"tick_price" binding:"required"`
DiffPnL float64 `csv:"diff_pnl" json:"diff_pnl" binding:"required"`
TotalFee float64 `csv:"total_fee" json:"total_fee" binding:"required,min=0"`
ClosePnL float64 `csv:"close_pnl" json:"close_pnl" binding:"required"`
}