From 3c2da12cd87298d13d05669ade7ed35b054f6116 Mon Sep 17 00:00:00 2001 From: vipg Date: Fri, 14 Nov 2025 15:11:04 +0800 Subject: [PATCH] add --- backend/futures_trading_record/.env | 21 ++ backend/futures_trading_record/deploy.sh | 86 +++++++ backend/futures_trading_record/dev-test.sh | 1 + backend/futures_trading_record/dev.sh | 35 +++ .../docker-compose-dev.yaml | 44 ++++ .../docker-compose.yaml | 44 ++++ .../scripts/db-lanuch-entrypoint.sh | 59 +++++ .../sql/01_uuid_v7_setup.sql | 48 ++++ .../sql/02_create_country_table.sql | 30 +++ .../sql/03_create_name_table.sql | 32 +++ .../sql/04_create_code_table.sql | 32 +++ .../sql/05_create_info_view.sql | 38 +++ backend/futures_trading_record/src/Dockerfile | 38 +++ .../futures_trading_record/src/db/postgres.go | 54 ++++ backend/futures_trading_record/src/go.mod | 57 +++++ backend/futures_trading_record/src/go.sum | 133 ++++++++++ .../src/logger/logger.go | 86 +++++++ .../src/logic/create.go | 193 +++++++++++++++ .../src/logic/delete.go | 167 +++++++++++++ .../futures_trading_record/src/logic/read.go | 230 ++++++++++++++++++ .../src/logic/update.go | 184 ++++++++++++++ backend/futures_trading_record/src/main.go | 72 ++++++ 22 files changed, 1684 insertions(+) create mode 100644 backend/futures_trading_record/.env create mode 100644 backend/futures_trading_record/deploy.sh create mode 100644 backend/futures_trading_record/dev-test.sh create mode 100644 backend/futures_trading_record/dev.sh create mode 100644 backend/futures_trading_record/docker-compose-dev.yaml create mode 100644 backend/futures_trading_record/docker-compose.yaml create mode 100755 backend/futures_trading_record/scripts/db-lanuch-entrypoint.sh create mode 100644 backend/futures_trading_record/sql/01_uuid_v7_setup.sql create mode 100644 backend/futures_trading_record/sql/02_create_country_table.sql create mode 100644 backend/futures_trading_record/sql/03_create_name_table.sql create mode 100644 backend/futures_trading_record/sql/04_create_code_table.sql create mode 100644 backend/futures_trading_record/sql/05_create_info_view.sql create mode 100644 backend/futures_trading_record/src/Dockerfile create mode 100644 backend/futures_trading_record/src/db/postgres.go create mode 100644 backend/futures_trading_record/src/go.mod create mode 100644 backend/futures_trading_record/src/go.sum create mode 100644 backend/futures_trading_record/src/logger/logger.go create mode 100644 backend/futures_trading_record/src/logic/create.go create mode 100644 backend/futures_trading_record/src/logic/delete.go create mode 100644 backend/futures_trading_record/src/logic/read.go create mode 100644 backend/futures_trading_record/src/logic/update.go create mode 100644 backend/futures_trading_record/src/main.go diff --git a/backend/futures_trading_record/.env b/backend/futures_trading_record/.env new file mode 100644 index 0000000..a8b6a3f --- /dev/null +++ b/backend/futures_trading_record/.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/backend/futures_trading_record/deploy.sh b/backend/futures_trading_record/deploy.sh new file mode 100644 index 0000000..6dbdbba --- /dev/null +++ b/backend/futures_trading_record/deploy.sh @@ -0,0 +1,86 @@ +#!/bin/bash +set -euo pipefail # 更严格的错误检查:未定义变量报错、管道错误传递 + +# 定义日志函数(带时间戳和级别) +log_info() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [INFO] $1" +} + +log_warn() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [WARN] $1" >&2 +} + +log_error() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [ERROR] $1" >&2 +} + +# 定义配置常量(等号两侧无空格!集中管理,便于修改) +IMAGE_NAME="country-api" +IMAGE_TAG="1.0.0" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +COMPOSE_PROJECT_NAME="country_service" +DOCKER_COMPOSE_FILE="./docker-compose.yaml" +SRC_DIR="./src" +DOCKERFILE_PATH="${SRC_DIR}/Dockerfile" + +# 检查目录和文件存在性的通用函数 +check_exists() { + local path="$1" # 变量引用加引号,避免路径含空格报错 + local type="$2" # "file" 或 "dir" + local desc="$3" + + if [ "$type" = "file" ] && [ ! -f "$path" ]; then + log_error "缺失必要文件: $desc ($path)" + exit 1 + elif [ "$type" = "dir" ] && [ ! -d "$path" ]; then + log_error "缺失必要目录: $desc ($path)" + exit 1 + fi +} + +log_info "===== 开始执行构建脚本 =====" + +# 前置检查:确保必要文件和目录存在 +check_exists "$DOCKER_COMPOSE_FILE" "file" "docker-compose配置文件" +check_exists "$SRC_DIR" "dir" "源代码目录" +check_exists "$DOCKERFILE_PATH" "file" "Dockerfile" + +# 步骤1:停止docker-compose服务(变量引用加引号,兼容路径含空格) +log_info "开始停止编排服务: ${COMPOSE_PROJECT_NAME}" +if docker-compose -f "$DOCKER_COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" down; then + log_info "编排服务 ${COMPOSE_PROJECT_NAME} 已成功停止" +else + log_warn "编排服务 ${COMPOSE_PROJECT_NAME} 停止失败或未运行,继续执行后续步骤" +fi + +# 步骤2:删除现有镜像(忽略不存在的情况) +log_info "尝试删除现有镜像: ${FULL_IMAGE}" +if sudo docker rmi -f "${FULL_IMAGE}" >/dev/null 2>&1; then + log_info "镜像 ${FULL_IMAGE} 删除成功" +else + log_warn "镜像 ${FULL_IMAGE} 不存在或无法删除,跳过删除步骤" +fi + +# 步骤3:构建新镜像(切换到src目录,避免路径问题) +log_info "开始构建新镜像: ${FULL_IMAGE}(Dockerfile位于${DOCKERFILE_PATH})" +if cd "$SRC_DIR" && sudo docker build -t "${FULL_IMAGE}" -f Dockerfile .; then + log_info "镜像 ${FULL_IMAGE} 构建成功" +else + log_error "镜像 ${FULL_IMAGE} 构建失败" + exit 1 +fi + +# 步骤4:启动docker-compose服务(变量引用加引号) +log_info "开始启动编排服务: ${COMPOSE_PROJECT_NAME}" +cd .. +if docker-compose -f "$DOCKER_COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" up -d; then + log_info "编排服务 ${COMPOSE_PROJECT_NAME} 已成功启动" + # 额外输出运行状态,提升用户体验 + log_info "当前运行的容器:" + docker-compose -f "$DOCKER_COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" ps +else + log_error "编排服务 ${COMPOSE_PROJECT_NAME} 启动失败" + exit 1 +fi + +log_info "===== 构建脚本执行完成 =====" diff --git a/backend/futures_trading_record/dev-test.sh b/backend/futures_trading_record/dev-test.sh new file mode 100644 index 0000000..4bee787 --- /dev/null +++ b/backend/futures_trading_record/dev-test.sh @@ -0,0 +1 @@ +docker run -itd --name go_country_dev -v $(pwd)/src:/app -p 20010:80 golang:1.25.0-alpine3.22 \ No newline at end of file diff --git a/backend/futures_trading_record/dev.sh b/backend/futures_trading_record/dev.sh new file mode 100644 index 0000000..dde6ad4 --- /dev/null +++ b/backend/futures_trading_record/dev.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# 日志函数 +log_info() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [DEV_COMPOSE] $1" +} + +log_error() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [DEV_ERROR] $1" >&2 +} + +# 获取脚本所在目录的绝对路径 +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# 拼接得到 docker-compose 文件的绝对路径 +COMPOSE_FILE="$SCRIPT_DIR/docker-compose-dev.yaml" + +log_info "开始启动开发环境docker-compose服务" + +# 检查文件是否存在 +if [ ! -f "$COMPOSE_FILE" ]; then + log_error "未找到docker-compose文件: $COMPOSE_FILE" + exit 1 +fi + +# 启动服务 +log_info "执行命令: sudo docker-compose -f $COMPOSE_FILE up -d" +if sudo docker-compose -f "$COMPOSE_FILE" up -d; then + log_info "开发环境服务启动成功" + # 额外输出运行中的容器信息 + log_info "当前运行的容器:" + sudo docker-compose -f "$COMPOSE_FILE" ps +else + log_error "开发环境服务启动失败" + exit 1 +fi \ No newline at end of file diff --git a/backend/futures_trading_record/docker-compose-dev.yaml b/backend/futures_trading_record/docker-compose-dev.yaml new file mode 100644 index 0000000..b50734a --- /dev/null +++ b/backend/futures_trading_record/docker-compose-dev.yaml @@ -0,0 +1,44 @@ +services: + postgres: + image: postgres:17.4-alpine + container_name: country_db + restart: always + ports: + - 20011:5432 + entrypoint: + - /scripts/db-lanuch-entrypoint.sh + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + TZ: ${TZ} + volumes: + - ./shared_data/country_db:/var/lib/postgresql/data + - ./sql:/docker-entrypoint-initdb.d + - ./scripts:/scripts + networks: + - country-network + country: + image: golang:1.25.0-alpine3.22 + container_name: country_api + restart: always + ports: + - 20010:80 + depends_on: + - postgres + networks: + - country-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} + volumes: + - ./src:/app + command: sh -c "cd /app && go mod tidy && go run main.go" +networks: + country-network: + driver: bridge +volumes: {} diff --git a/backend/futures_trading_record/docker-compose.yaml b/backend/futures_trading_record/docker-compose.yaml new file mode 100644 index 0000000..5e5f9e9 --- /dev/null +++ b/backend/futures_trading_record/docker-compose.yaml @@ -0,0 +1,44 @@ +services: + postgres: + image: postgres:17.4-alpine + container_name: country_db + restart: always + ports: + - 20011:5432 + entrypoint: + - /scripts/db-lanuch-entrypoint.sh + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + TZ: ${TZ} + volumes: + - ./shared_data/country_db:/var/lib/postgresql/data + - ./sql:/docker-entrypoint-initdb.d + - ./scripts:/scripts + networks: + - country-network + country: + image: country-api:1.0.0 + container_name: country_api + restart: always + ports: + - 20010:80 + depends_on: + - postgres + networks: + - country-network + environment: + DB_HOST: postgres + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME} + TZ: ${TZ} + volumes: + # 挂载添加日志目录挂载,将容器内日志日志目录映射到宿主机的 ./logs 目录 + - ./logs:/app/logs # 假设代码中日志存储路径为 /app/logs +networks: + country-network: + driver: bridge +volumes: {} diff --git a/backend/futures_trading_record/scripts/db-lanuch-entrypoint.sh b/backend/futures_trading_record/scripts/db-lanuch-entrypoint.sh new file mode 100755 index 0000000..a4a8ddb --- /dev/null +++ b/backend/futures_trading_record/scripts/db-lanuch-entrypoint.sh @@ -0,0 +1,59 @@ +#!/bin/sh +set -e + +# 日志函数(带时间戳) +log_info() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [DB_INIT] $1" +} + +log_error() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] [DB_ERROR] $1" >&2 +} + +# 1. 启动PostgreSQL服务 +log_info "启动PostgreSQL服务(后台运行)" +docker-entrypoint.sh postgres & +PG_PID=$! +log_info "PostgreSQL主进程ID: $PG_PID" + +# 2. 等待数据库就绪 +log_info "等待PostgreSQL服务就绪(主机: localhost, 端口: 5432)" +retry_count=0 +max_retries=30 # 最多等待30秒 +until pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h "localhost" -p "5432"; do + retry_count=$((retry_count + 1)) + if [ $retry_count -ge $max_retries ]; then + log_error "等待PostgreSQL超时(超过30秒)" + exit 1 + fi + log_info "数据库未就绪,等待1秒(重试次数: $retry_count)" + sleep 1 +done +log_info "PostgreSQL服务已就绪" + +# 3. 执行SQL脚本 +log_info "开始执行/docker-entrypoint-initdb.d目录下的SQL脚本" +script_count=0 +for script in /docker-entrypoint-initdb.d/*.sql; do + if [ -f "$script" ]; then + script_count=$((script_count + 1)) + log_info "执行脚本 ($script_count): $script" + if psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -h "localhost" -p "5432" -f "$script" --set=ON_ERROR_STOP=1; then + log_info "脚本执行成功: $script" + else + log_error "脚本执行失败: $script" + exit 1 + fi + fi +done + +if [ $script_count -eq 0 ]; then + log_info "未发现需要执行的SQL脚本" +else + log_info "所有SQL脚本执行完成(共$script_count个)" +fi + +# 4. 等待主进程 +log_info "等待PostgreSQL主进程结束(PID: $PG_PID)" +wait $PG_PID +log_info "PostgreSQL进程已退出" \ No newline at end of file diff --git a/backend/futures_trading_record/sql/01_uuid_v7_setup.sql b/backend/futures_trading_record/sql/01_uuid_v7_setup.sql new file mode 100644 index 0000000..b317d1c --- /dev/null +++ b/backend/futures_trading_record/sql/01_uuid_v7_setup.sql @@ -0,0 +1,48 @@ +-- 切换到目标数据库 +\c postgres; + +-- 检查并创建UUID扩展(如果不存在) +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 定义检测UUID v7支持的函数 +CREATE OR REPLACE FUNCTION check_uuid_v7_support() RETURNS BOOLEAN AS $$ +DECLARE + test_uuid UUID; +BEGIN + BEGIN + SELECT gen_random_uuid_v7() INTO test_uuid; + RETURN TRUE; + EXCEPTION + WHEN undefined_function THEN + RETURN FALSE; + END; +END; +$$ LANGUAGE plpgsql VOLATILE; + +-- 创建UUID v7兼容函数(修复UUID格式长度问题) +CREATE OR REPLACE FUNCTION gen_random_uuid_v7() RETURNS uuid AS $$ +DECLARE + unix_ts_ms BIGINT; + rand_a BIGINT; + rand_b BIGINT; + hex_str TEXT; +BEGIN + -- 获取当前毫秒级Unix时间戳 + unix_ts_ms := (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT; + + -- 生成随机数(调整随机数范围以确保总长度正确) + rand_a := (random() * (2^20 - 1))::BIGINT; + rand_b := (random() * (2^44 - 1))::BIGINT; -- 从48位调整为44位,减少2个字节 + + -- 组合UUID v7格式(确保总长度为32个十六进制字符) + hex_str := + lpad(to_hex(unix_ts_ms >> 12), 8, '0') || + lpad(to_hex((unix_ts_ms & 4095) << 4), 4, '0') || + '7' || lpad(to_hex(rand_a >> 18), 3, '0') || + lpad(to_hex(8 + (rand_a & 16383) >> 12), 2, '0') || + lpad(to_hex(rand_a & 4095), 3, '0') || + lpad(to_hex(rand_b), 11, '0'); -- 从12位调整为11位 + + RETURN hex_str::uuid; +END; +$$ LANGUAGE plpgsql VOLATILE; \ No newline at end of file diff --git a/backend/futures_trading_record/sql/02_create_country_table.sql b/backend/futures_trading_record/sql/02_create_country_table.sql new file mode 100644 index 0000000..048e823 --- /dev/null +++ b/backend/futures_trading_record/sql/02_create_country_table.sql @@ -0,0 +1,30 @@ +-- 切换到目标数据库 +\c postgres; + +CREATE OR REPLACE FUNCTION update_country_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql VOLATILE; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country') THEN + CREATE TABLE "country" ( -- country是关键字,用双引号包裹 + id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE TRIGGER update_country_updated_at + BEFORE UPDATE ON "country" + FOR EACH ROW + EXECUTE FUNCTION update_country_modified_column(); + + RAISE NOTICE 'Created country table and trigger'; + ELSE + RAISE NOTICE 'country table already exists'; + END IF; +END $$; \ No newline at end of file diff --git a/backend/futures_trading_record/sql/03_create_name_table.sql b/backend/futures_trading_record/sql/03_create_name_table.sql new file mode 100644 index 0000000..938798b --- /dev/null +++ b/backend/futures_trading_record/sql/03_create_name_table.sql @@ -0,0 +1,32 @@ +-- 切换到目标数据库 +\c postgres; + +CREATE OR REPLACE FUNCTION update_name_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql VOLATILE; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'name') THEN + CREATE TABLE name ( + id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, + country_id UUID NOT NULL, + name VARCHAR NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE TRIGGER update_name_updated_at + BEFORE UPDATE ON "name" + FOR EACH ROW + EXECUTE FUNCTION update_name_modified_column(); + + RAISE NOTICE 'Created name table and trigger'; + ELSE + RAISE NOTICE 'name table already exists'; + END IF; +END $$; \ No newline at end of file diff --git a/backend/futures_trading_record/sql/04_create_code_table.sql b/backend/futures_trading_record/sql/04_create_code_table.sql new file mode 100644 index 0000000..08d09a8 --- /dev/null +++ b/backend/futures_trading_record/sql/04_create_code_table.sql @@ -0,0 +1,32 @@ +-- 切换到目标数据库 +\c postgres; + +CREATE OR REPLACE FUNCTION update_code_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql VOLATILE; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'code') THEN + CREATE TABLE code ( + id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, + country_id UUID NOT NULL, + code VARCHAR NOT NULL, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE TRIGGER update_code_updated_at + BEFORE UPDATE ON "code" + FOR EACH ROW + EXECUTE FUNCTION update_code_modified_column(); + + RAISE NOTICE 'Created code table and trigger'; + ELSE + RAISE NOTICE 'code table already exists'; + END IF; +END $$; \ No newline at end of file diff --git a/backend/futures_trading_record/sql/05_create_info_view.sql b/backend/futures_trading_record/sql/05_create_info_view.sql new file mode 100644 index 0000000..f26ed74 --- /dev/null +++ b/backend/futures_trading_record/sql/05_create_info_view.sql @@ -0,0 +1,38 @@ +\c postgres; + +DO $$ +DECLARE + view_exists BOOLEAN; +BEGIN + -- 检查视图是否已存在 + SELECT EXISTS ( + SELECT 1 FROM information_schema.views + WHERE table_name = 'country_info_view' + ) INTO view_exists; + + -- 创建或更新视图 + CREATE OR REPLACE VIEW country_info_view AS + SELECT + u.id AS country_id, + n.name AS name, + c.code AS code, + u.deleted AS deleted + FROM + "country" u + JOIN + name n ON u.id = n.country_id + JOIN + code c ON u.id = c.country_id + WHERE + u.deleted = FALSE; + + -- 根据视图是否已存在输出不同提示 + IF view_exists THEN + RAISE NOTICE '视图 country_info_view 已更新'; + ELSE + RAISE NOTICE '视图 country_info_view 已创建'; + END IF; +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE '处理视图时发生错误: %', SQLERRM; +END $$; diff --git a/backend/futures_trading_record/src/Dockerfile b/backend/futures_trading_record/src/Dockerfile new file mode 100644 index 0000000..a54892c --- /dev/null +++ b/backend/futures_trading_record/src/Dockerfile @@ -0,0 +1,38 @@ +# ==================== 第一阶段:构建Go程序(构建阶段)==================== +# 使用官方Go镜像作为构建基础,选择与项目匹配的Go版本(示例用1.25.0,可根据实际调整) +FROM golang:1.25.0-alpine3.22 AS builder + +# 设置工作目录(容器内的目录,规范文件位置) +WORKDIR /app + +# 复制go.mod和go.sum(先复制依赖文件,利用Docker缓存机制,避免每次代码变动都重新下载依赖) +COPY go.mod go.sum ./ + +# 下载项目依赖(仅当go.mod/go.sum变动时才会重新执行) +RUN go mod download + +# 复制整个项目代码到工作目录 +COPY . . + +# 构建Go程序: +# - CGO_ENABLED=0:禁用CGO,生成静态链接的二进制文件(避免依赖系统库,保证镜像兼容性) +# - -o app:指定输出二进制文件名为app +# - ./main.go:指定入口文件 +RUN CGO_ENABLED=0 GOOS=linux go build -o app ./main.go + + +# ==================== 第二阶段:运行程序(运行阶段)==================== +# 使用轻量级的alpine镜像(仅5MB左右,大幅减小最终镜像体积) +FROM alpine:3.19 + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制编译好的二进制文件到当前镜像(仅复制最终产物,减小体积) +COPY --from=builder /app/app ./ + +# 暴露程序运行端口(与代码中一致) +EXPOSE 80 + +# 容器启动时执行的命令:运行二进制文件 +CMD ["./app"] \ No newline at end of file diff --git a/backend/futures_trading_record/src/db/postgres.go b/backend/futures_trading_record/src/db/postgres.go new file mode 100644 index 0000000..40681a3 --- /dev/null +++ b/backend/futures_trading_record/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("✅ 数据库连接验证成功") +} \ No newline at end of file diff --git a/backend/futures_trading_record/src/go.mod b/backend/futures_trading_record/src/go.mod new file mode 100644 index 0000000..13d4c8f --- /dev/null +++ b/backend/futures_trading_record/src/go.mod @@ -0,0 +1,57 @@ +module country + +go 1.25.0 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/google/uuid v1.6.0 + github.com/lib/pq v1.10.9 + github.com/spf13/viper v1.21.0 + go.uber.org/zap v1.27.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.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-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + 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 + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/backend/futures_trading_record/src/go.sum b/backend/futures_trading_record/src/go.sum new file mode 100644 index 0000000..0117005 --- /dev/null +++ b/backend/futures_trading_record/src/go.sum @@ -0,0 +1,133 @@ +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +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/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= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/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/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +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/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/futures_trading_record/src/logger/logger.go b/backend/futures_trading_record/src/logger/logger.go new file mode 100644 index 0000000..8021baa --- /dev/null +++ b/backend/futures_trading_record/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")) +} \ No newline at end of file diff --git a/backend/futures_trading_record/src/logic/create.go b/backend/futures_trading_record/src/logic/create.go new file mode 100644 index 0000000..a156519 --- /dev/null +++ b/backend/futures_trading_record/src/logic/create.go @@ -0,0 +1,193 @@ +package logic + +import ( + "net/http" + "country/db" // 数据库操作相关包 + "time" // 时间处理包 + "github.com/google/uuid" // UUID生成工具 + "github.com/gin-gonic/gin" // Gin框架,用于处理HTTP请求 + "go.uber.org/zap" // 日志库 +) + +// CreateRequest 注册请求参数结构 +// 用于接收客户端发送的JSON数据,绑定并验证必填字段 +type CreateRequest struct { + Name string `json:"name" binding:"required"` // 国家名称,必填 + Code string `json:"code" binding:"required"` // 国家代码,必填 +} + +// CreateResponse 注册响应结构 +// 统一的API响应格式,包含成功状态、提示信息和数据 +type CreateResponse struct { + Success bool `json:"success"` // 操作是否成功 + Message string `json:"message"` // 提示信息 + Data CreateData `json:"data"` // 响应数据 +} + +// CreateData 响应数据结构 +// 包含创建成功后的国家ID +type CreateData struct { + CountryID string `json:"country_id"` // 国家唯一标识ID +} + +// CreateHandler 处理国家创建逻辑 +// 接收HTTP请求,完成参数验证、数据库事务处理并返回响应 +func CreateHandler(c *gin.Context) { + startTime := time.Now() // 记录请求开始时间,用于统计耗时 + // 获取或生成请求ID,用于追踪整个请求链路 + 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 + // 绑定并验证请求参数(检查name和code是否存在) + 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), + ) + + // 开启数据库事务,确保多表操作原子性(要么全成功,要么全失败) + 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 + } + // 延迟执行的恢复函数,处理panic情况 + defer func() { + if r := recover(); r != nil { // 捕获panic + // 回滚事务 + 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: "系统错误,请稍后重试", + }) + } + }() + + // 1. 在country表中创建记录并获取自动生成的ID + 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. 插入国家名称到name表(与country_id关联) + _, err = tx.Exec("INSERT INTO name (country_id, name) VALUES ($1, $2)", countryID, req.Name) + if err != nil { + tx.Rollback() // 操作失败,回滚事务 + zap.L().Error("❌ name表插入失败", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "保存名称信息失败", + }) + return + } + + // 3. 插入国家代码到code表(与country_id关联) + _, err = tx.Exec("INSERT INTO code (country_id, code) VALUES ($1, $2)", countryID, req.Code) + if err != nil { + tx.Rollback() // 操作失败,回滚事务 + zap.L().Error("❌ code表插入失败", + zap.String("req_id", reqID), + zap.String("country_id", countryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, CreateResponse{ + Success: false, + Message: "保存代码信息失败", + }) + return + } + + // 提交事务(所有操作成功后确认提交) + 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), + ) + + // 返回成功响应,包含创建的国家ID + c.JSON(http.StatusOK, CreateResponse{ + Success: true, + Message: "创建成功", + Data: CreateData{ + CountryID: countryID, + }, + }) +} \ No newline at end of file diff --git a/backend/futures_trading_record/src/logic/delete.go b/backend/futures_trading_record/src/logic/delete.go new file mode 100644 index 0000000..1ee98c8 --- /dev/null +++ b/backend/futures_trading_record/src/logic/delete.go @@ -0,0 +1,167 @@ +package logic + +import ( + "net/http" + "country/db" + "time" + "github.com/google/uuid" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// DeleteRequest 删除请求参数结构 +type DeleteRequest struct { + CountryID string `json:"country_id" binding:"required"` // 国家ID,必填 +} + +// DeleteResponse 删除响应结构 +type DeleteResponse struct { + Success bool `json:"success"` // 操作是否成功 + Message string `json:"message"` // 提示信息 +} + +// DeleteHandler 处理国家删除逻辑(软删除) +func DeleteHandler(c *gin.Context) { + startTime := time.Now() + reqID := c.Request.Header.Get("X-DeleteRequest-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 DeleteRequest + // 绑定并验证请求参数 + if err := c.ShouldBindJSON(&req); err != nil { + zap.L().Warn("⚠️ 请求参数验证失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusBadRequest, DeleteResponse{ + Success: false, + Message: "请求参数错误:country_id为必填项", + }) + return + } + + zap.L().Debug("✅ 请求参数验证通过", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + ) + + // 开启数据库事务 + tx, err := db.DB.Begin() + if err != nil { + zap.L().Error("❌ 事务开启失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, DeleteResponse{ + Success: false, + Message: "系统错误,请稍后重试", + }) + return + } + + // 延迟处理panic情况 + 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, DeleteResponse{ + Success: false, + Message: "系统错误,请稍后重试", + }) + } + }() + + // 3.1 更新country表 + _, err = tx.Exec("UPDATE country SET deleted = TRUE WHERE id = $1", req.CountryID) + if err != nil { + tx.Rollback() + zap.L().Error("❌ country表更新失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, DeleteResponse{ + Success: false, + Message: "删除国家记录失败", + }) + return + } + + // 3.2 更新name表 + _, err = tx.Exec("UPDATE name SET deleted = TRUE WHERE country_id = $1", req.CountryID) + if err != nil { + tx.Rollback() + zap.L().Error("❌ name表更新失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, DeleteResponse{ + Success: false, + Message: "删除名称信息失败", + }) + return + } + + // 3.3 更新code表 + _, err = tx.Exec("UPDATE code SET deleted = TRUE WHERE country_id = $1", req.CountryID) + if err != nil { + tx.Rollback() + zap.L().Error("❌ code表更新失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, DeleteResponse{ + Success: false, + Message: "删除代码信息失败", + }) + return + } + + // 提交事务 + if err := tx.Commit(); err != nil { + tx.Rollback() + zap.L().Error("❌ 事务提交失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, DeleteResponse{ + Success: false, + Message: "数据提交失败,请稍后重试", + }) + return + } + + // 记录请求处理耗时 + duration := time.Since(startTime) + zap.L().Info("✅ 国家删除请求处理完成", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Duration("duration", duration), + ) + + // 返回成功响应 + c.JSON(http.StatusOK, DeleteResponse{ + Success: true, + Message: "删除成功", + }) +} \ No newline at end of file diff --git a/backend/futures_trading_record/src/logic/read.go b/backend/futures_trading_record/src/logic/read.go new file mode 100644 index 0000000..84d3b88 --- /dev/null +++ b/backend/futures_trading_record/src/logic/read.go @@ -0,0 +1,230 @@ +package logic + +import ( + "country/db" + "net/http" + "strconv" + "time" + "strings" + + "fmt" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// ReadRequest 读取请求参数结构 +type ReadRequest struct { + CountryID string `form:"country_id"` // 国家ID,可选 + Name string `form:"name"` // 国家名称,可选 + Code string `form:"code"` // 国家代码,可选 + Page string `form:"page"` // 页码,可选 + PageSize string `form:"page_size"` // 每页条数,可选 +} + +// ReadData 读取响应数据结构 +type ReadData struct { + Total int64 `json:"total"` // 总条数 + Page int `json:"page"` // 当前页码 + PageSize int `json:"page_size"`// 每页条数 + Items []CountryInfoViewItem `json:"items"` // 数据列表 +} + +// CountryInfoViewItem 视图数据项结构 +type CountryInfoViewItem struct { + CountryID string `json:"country_id"` // 国家ID + Name string `json:"name"` // 国家名称 + Code string `json:"code"` // 国家代码 +} + +// ReadResponse 读取响应结构 +type ReadResponse struct { + Success bool `json:"success"` // 操作是否成功 + Message string `json:"message"` // 提示信息 + Data ReadData `json:"data"` // 响应数据 +} + +// ReadHandler 处理国家信息查询逻辑 +func ReadHandler(c *gin.Context) { + startTime := time.Now() + // 获取或生成请求ID + reqID := c.Request.Header.Get("X-ReadRequest-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 ReadRequest + if err := c.ShouldBindQuery(&req); err != nil { + zap.L().Warn("⚠️ 请求参数解析失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusBadRequest, ReadResponse{ + Success: false, + Message: "请求参数格式错误", + }) + return + } + + // 验证查询条件至少有一个不为空 + if req.CountryID == "" && req.Name == "" && req.Code == "" { + zap.L().Warn("⚠️ 请求参数验证失败", + zap.String("req_id", reqID), + zap.String("reason", "country_id、name、code不能同时为空"), + ) + c.JSON(http.StatusBadRequest, ReadResponse{ + Success: false, + Message: "请求参数错误:country_id、name、code不能同时为空", + }) + return + } + + // 处理分页参数默认值 + page, err := strconv.Atoi(req.Page) + if err != nil || page < 1 { + page = 1 + } + pageSize, err := strconv.Atoi(req.PageSize) + if err != nil || pageSize < 1 { + pageSize = 20 + } + + zap.L().Debug("✅ 请求参数验证通过", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.String("name", req.Name), + zap.String("code", req.Code), + zap.Int("page", page), + zap.Int("page_size", pageSize), + ) + + // 构建查询条件和参数 + whereClauses := []string{} + args := []interface{}{} + paramIndex := 1 + + if req.CountryID != "" { + whereClauses = append(whereClauses, "country_id = $"+strconv.Itoa(paramIndex)) + args = append(args, req.CountryID) + paramIndex++ + } + if req.Name != "" { + whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex)) + args = append(args, "%"+req.Name+"%") + paramIndex++ + } + if req.Code != "" { + whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex)) + args = append(args, "%"+req.Code+"%") + paramIndex++ + } + + // 构建基础SQL + baseSQL := "SELECT country_id, name, code FROM country_info_view" + countSQL := "SELECT COUNT(*) FROM country_info_view" + if len(whereClauses) > 0 { + whereStr := " WHERE " + strings.Join(whereClauses, " AND ") + baseSQL += whereStr + countSQL += whereStr + } + + // 计算分页偏移量 + offset := (page - 1) * pageSize + + // 拼接分页SQL(使用fmt.Sprintf更清晰) + querySQL := fmt.Sprintf("%s ORDER BY country_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1) + args = append(args, pageSize, offset) + + // 查询总条数(修正参数传递方式) + var total int64 + countArgs := args[:len(args)-2] // 排除分页参数 + err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total) + if err != nil { + zap.L().Error("❌ 查询总条数失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, ReadResponse{ + Success: false, + Message: "查询数据失败,请稍后重试", + }) + return + } + + // 执行分页查询 + rows, err := db.DB.Query(querySQL, args...) + if err != nil { + zap.L().Error("❌ 分页查询失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, ReadResponse{ + Success: false, + Message: "查询数据失败,请稍后重试", + }) + return + } + defer rows.Close() + + // 处理查询结果 + var items []CountryInfoViewItem + for rows.Next() { + var item CountryInfoViewItem + if err := rows.Scan(&item.CountryID, &item.Name, &item.Code); err != nil { + zap.L().Error("❌ 解析查询结果失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, ReadResponse{ + Success: false, + Message: "数据处理失败,请稍后重试", + }) + return + } + items = append(items, item) + } + + // 检查行迭代过程中是否发生错误 + if err := rows.Err(); err != nil { + zap.L().Error("❌ 行迭代错误", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, ReadResponse{ + Success: false, + Message: "查询数据失败,请稍后重试", + }) + return + } + + // 记录请求处理耗时 + duration := time.Since(startTime) + zap.L().Info("✅ 国家查询请求处理完成", + zap.String("req_id", reqID), + zap.Int64("total", total), + zap.Int("page", page), + zap.Int("page_size", pageSize), + zap.Duration("duration", duration), + ) + + // 返回成功响应 + c.JSON(http.StatusOK, ReadResponse{ + Success: true, + Message: "查询成功", + Data: ReadData{ + Total: total, + Page: page, + PageSize: pageSize, + Items: items, + }, + }) +} \ No newline at end of file diff --git a/backend/futures_trading_record/src/logic/update.go b/backend/futures_trading_record/src/logic/update.go new file mode 100644 index 0000000..51dc9d7 --- /dev/null +++ b/backend/futures_trading_record/src/logic/update.go @@ -0,0 +1,184 @@ +package logic + +import ( + "country/db" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// UpdateRequest 更新请求参数结构 +type UpdateRequest struct { + CountryID string `json:"country_id" binding:"required"` // 国家ID,必填 + Name string `json:"name"` // 国家名称,可选 + Code string `json:"code"` // 国家代码,可选 +} + +// UpdateResponse 更新响应结构 +type UpdateResponse struct { + Success bool `json:"success"` // 操作是否成功 + Message string `json:"message"` // 提示信息 +} + +// UpdateHandler 处理国家信息更新逻辑 +func UpdateHandler(c *gin.Context) { + startTime := time.Now() + // 获取或生成请求ID + reqID := c.Request.Header.Get("X-UpdateRequest-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 UpdateRequest + // 绑定并验证请求参数(主要验证country_id必填) + if err := c.ShouldBindJSON(&req); err != nil { + zap.L().Warn("⚠️ 请求参数验证失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusBadRequest, UpdateResponse{ + Success: false, + Message: "请求参数错误:country_id为必填项", + }) + return + } + + // 验证name和code不能同时为空 + if req.Name == "" && req.Code == "" { + zap.L().Warn("⚠️ 请求参数验证失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.String("reason", "name和code不能同时为空"), + ) + c.JSON(http.StatusBadRequest, UpdateResponse{ + Success: false, + Message: "请求参数错误:name和code不能同时为空", + }) + return + } + + zap.L().Debug("✅ 请求参数验证通过", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.String("name", req.Name), + zap.String("code", req.Code), + ) + + // 开启数据库事务 + tx, err := db.DB.Begin() + if err != nil { + zap.L().Error("❌ 事务开启失败", + zap.String("req_id", reqID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, UpdateResponse{ + Success: false, + Message: "系统错误,请稍后重试", + }) + return + } + + // 延迟处理panic情况 + 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, UpdateResponse{ + Success: false, + Message: "系统错误,请稍后重试", + }) + } + }() + + // 如果name不为空,更新name表 + if req.Name != "" { + _, err = tx.Exec("UPDATE name SET name = $1 WHERE country_id = $2", req.Name, req.CountryID) + if err != nil { + tx.Rollback() + zap.L().Error("❌ name表更新失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, UpdateResponse{ + Success: false, + Message: "更新名称信息失败", + }) + return + } + zap.L().Debug("📝 name表更新成功", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + ) + } + + // 如果code不为空,更新code表 + if req.Code != "" { + _, err = tx.Exec("UPDATE code SET code = $1 WHERE country_id = $2", req.Code, req.CountryID) + if err != nil { + tx.Rollback() + zap.L().Error("❌ code表更新失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, UpdateResponse{ + Success: false, + Message: "更新代码信息失败", + }) + return + } + zap.L().Debug("📝 code表更新成功", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + ) + } + + // 提交事务 + if err := tx.Commit(); err != nil { + tx.Rollback() + zap.L().Error("❌ 事务提交失败", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Error(err), + ) + c.JSON(http.StatusInternalServerError, UpdateResponse{ + Success: false, + Message: "数据提交失败,请稍后重试", + }) + return + } + + // 记录请求处理耗时 + duration := time.Since(startTime) + zap.L().Info("✅ 国家更新请求处理完成", + zap.String("req_id", reqID), + zap.String("country_id", req.CountryID), + zap.Duration("duration", duration), + ) + + // 返回成功响应 + c.JSON(http.StatusOK, UpdateResponse{ + Success: true, + Message: "更新成功", + }) +} \ No newline at end of file diff --git a/backend/futures_trading_record/src/main.go b/backend/futures_trading_record/src/main.go new file mode 100644 index 0000000..1dc053e --- /dev/null +++ b/backend/futures_trading_record/src/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "country/db" // 数据库相关操作包 + "country/logger" // 日志工具包 + "country/logic" // 业务逻辑处理包 + + "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("🚀 用户服务初始化") + + // 记录数据库初始化开始日志 + zap.L().Info("⌛️ 数据库初始化开始") + // 初始化数据库连接 + db.Init() + // 程序退出时关闭数据库连接(defer确保在函数退出前执行) + defer db.DB.Close() + // 记录数据库初始化成功日志 + zap.L().Info("✅ 数据库初始化成功") + + // 设置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("✅ 配置跨域中间件完成") + + // 注册创建国家的接口,POST请求,由logic.CreateHandler处理 + r.POST("/country/create", logic.CreateHandler) + zap.L().Info("✅ 创建接口注册完成: POST /country/create") + + // 注册读取国家的接口,POST请求,由logic.ReadHandler + r.POST("/country/read", logic.ReadHandler) + zap.L().Info("✅ 读取接口注册完成: POST /country/read") + + // 注册更新国家的接口,POST请求,由logic.UpdateHandler + r.POST("/country/update", logic.UpdateHandler) + zap.L().Info("✅ 更新接口注册完成: POST /country/update") + + // 注册删除国家的接口,POST请求,由logic.DeleteHandler处理 + r.POST("/country/delete", logic.DeleteHandler) + zap.L().Info("✅ 删除接口注册完成: POST /country/delete") + + // 记录服务启动日志,监听80端口 + zap.L().Info("✅ 服务启动在80端口") + r.Run(":80") +} \ No newline at end of file