Compare commits

..

110 Commits

Author SHA1 Message Date
vipg
99741d44b5 add 2025-11-29 15:04:50 +08:00
vipg
62adbd70a4 add 2025-11-28 18:22:34 +08:00
vipg
6c31b71f9f add 2025-11-28 16:47:53 +08:00
vipg
9173a7ac20 add 2025-11-28 16:46:46 +08:00
vipg
4a648d53e9 add 2025-11-28 16:40:08 +08:00
vipg
e97c8e00c5 add 2025-11-26 16:28:37 +08:00
vipg
a2271b4e0d add 2025-11-26 16:27:18 +08:00
vipg
4430d77c81 add 2025-11-26 16:23:04 +08:00
vipg
2f05b86f74 ad 2025-11-26 16:22:09 +08:00
vipg
cc63fece65 add 2025-11-26 16:19:35 +08:00
vipg
269d9e5857 add 2025-11-26 16:16:06 +08:00
vipg
37a6ec63ba add 2025-11-26 16:07:31 +08:00
vipg
7f49c0cdc0 add 2025-11-26 16:06:00 +08:00
vipg
3833ed68db add 2025-11-26 16:04:28 +08:00
vipg
276be30387 add 2025-11-26 16:02:12 +08:00
vipg
9b061b8992 add 2025-11-26 16:01:49 +08:00
vipg
f5ecc9a151 add 2025-11-26 15:55:36 +08:00
vipg
7cd2ea11da add 2025-11-25 17:09:19 +08:00
vipg
19f9c84718 add 2025-11-25 17:08:52 +08:00
vipg
fede591197 add 2025-11-25 17:08:22 +08:00
vipg
075181cc32 add 2025-11-25 16:47:31 +08:00
vipg
e41b3a8dbc add 2025-11-25 16:33:54 +08:00
vipg
b9e840a2ba add 2025-11-25 16:27:08 +08:00
vipg
a1ea55dffa add 2025-11-25 16:16:26 +08:00
vipg
1a638eab5e add 2025-11-25 16:15:31 +08:00
vipg
76153930dc add 2025-11-25 16:11:50 +08:00
vipg
9f3aa79aa5 add 2025-11-25 16:09:34 +08:00
vipg
a573993365 add 2025-11-25 16:08:30 +08:00
vipg
6817626669 add 2025-11-25 15:55:07 +08:00
vipg
94c07397a0 add 2025-11-25 15:52:58 +08:00
vipg
87a037616e add 2025-11-25 15:52:43 +08:00
vipg
01c63e1b82 add 2025-11-25 15:49:15 +08:00
vipg
4191843802 add 2025-11-25 15:47:08 +08:00
vipg
a2c758abae add 2025-11-25 15:43:26 +08:00
vipg
6ca4489ad7 add 2025-11-25 15:39:52 +08:00
vipg
e9474e672a add 2025-11-25 15:36:38 +08:00
vipg
e0dcaf4ff6 add 2025-11-25 15:29:02 +08:00
vipg
902a6a9b75 add 2025-11-25 15:24:27 +08:00
vipg
97740d0447 add 2025-11-25 15:20:41 +08:00
vipg
291cf01983 add 2025-11-25 15:11:12 +08:00
vipg
1ccbc3c6d3 add 2025-11-25 12:57:49 +08:00
vipg
29f134c3e5 add 2025-11-25 12:51:17 +08:00
vipg
2293899780 add 2025-11-25 12:47:29 +08:00
vipg
5c32d8977c add 2025-11-25 12:43:41 +08:00
vipg
175dc327c3 add 2025-11-25 12:38:02 +08:00
vipg
5b58186c96 add 2025-11-25 12:24:52 +08:00
vipg
590cace08a add 2025-11-25 12:24:41 +08:00
vipg
6f8b1d9b2b add 2025-11-25 12:01:38 +08:00
vipg
e9945d67aa add 2025-11-19 17:18:59 +08:00
vipg
e716663731 add 2025-11-19 17:13:25 +08:00
vipg
30cfd98e92 add 2025-11-19 17:10:00 +08:00
vipg
261fbd7180 add 2025-11-19 17:04:47 +08:00
vipg
e2114845b5 add 2025-11-19 17:04:19 +08:00
vipg
abb1c8500c add 2025-11-19 17:03:20 +08:00
vipg
edade96d4a add 2025-11-19 17:01:08 +08:00
vipg
c8bb3d4ebd add 2025-11-19 17:00:29 +08:00
vipg
b2e89bf5bd add 2025-11-19 16:57:18 +08:00
vipg
8a8dd48726 add 2025-11-19 16:51:52 +08:00
vipg
b2882ed70a add 2025-11-19 16:45:06 +08:00
vipg
5fd2c2d38f add 2025-11-19 16:44:28 +08:00
vipg
ebe85f99ef add 2025-11-19 16:44:10 +08:00
vipg
2abd401b41 add 2025-11-19 16:43:09 +08:00
vipg
73c498ea37 add 2025-11-19 16:43:03 +08:00
vipg
e33a5e36ad add 2025-11-19 16:42:33 +08:00
vipg
01c6d0fda2 add 2025-11-19 16:41:20 +08:00
vipg
55df63985a add 2025-11-19 16:40:12 +08:00
vipg
088586f126 add 2025-11-19 16:36:01 +08:00
vipg
c661084bb8 add 2025-11-19 16:33:48 +08:00
vipg
edfc11d198 add 2025-11-19 16:28:23 +08:00
vipg
89031f86fe add 2025-11-19 16:24:43 +08:00
vipg
d05a4cb7e2 add 2025-11-19 16:17:01 +08:00
vipg
8b38fb2bb7 add 2025-11-18 17:13:49 +08:00
vipg
a6e2dda7a7 add 2025-11-18 17:10:35 +08:00
vipg
6571076ae6 add 2025-11-18 13:00:35 +08:00
vipg
fadd3d6ff3 add 2025-11-18 12:55:43 +08:00
vipg
6f32e781b6 add 2025-11-18 12:52:17 +08:00
vipg
c734d71c36 add 2025-11-18 12:37:30 +08:00
vipg
4a86c66b1c add 2025-11-18 12:30:17 +08:00
vipg
5870edb938 add 2025-11-18 12:17:29 +08:00
vipg
d1bee92563 add 2025-11-18 12:11:27 +08:00
vipg
53e55c1123 add 2025-11-18 11:53:08 +08:00
vipg
74e87033ed add 2025-11-18 11:50:53 +08:00
vipg
d2e8cd3bcc add 2025-11-18 11:42:42 +08:00
vipg
cca0e14823 add 2025-11-17 18:17:14 +08:00
vipg
fde1929b2c add 2025-11-17 18:14:31 +08:00
vipg
bcd87c3b73 add 2025-11-17 18:12:33 +08:00
vipg
53c4450d58 add 2025-11-17 18:11:57 +08:00
vipg
af33b34237 add 2025-11-17 18:11:33 +08:00
vipg
44e2d7c1f6 add 2025-11-17 18:10:40 +08:00
vipg
a47e544657 add 2025-11-17 17:50:03 +08:00
vipg
2f461a3a95 add 2025-11-17 17:48:06 +08:00
vipg
f13a58e116 add 2025-11-17 17:43:32 +08:00
vipg
8bedb4681f add 2025-11-17 16:14:59 +08:00
vipg
144042595c add 2025-11-17 16:13:32 +08:00
vipg
5592dabc62 add 2025-11-17 16:11:16 +08:00
vipg
55f10f344a add 2025-11-17 16:10:15 +08:00
vipg
3a7d40ae7b add 2025-11-17 15:43:35 +08:00
vipg
0e8afddcb7 add 2025-11-17 15:38:40 +08:00
vipg
c14ed2bf45 add 2025-11-17 15:36:23 +08:00
vipg
fed9de8090 add 2025-11-17 15:35:52 +08:00
vipg
79c631d356 add 2025-11-17 15:33:31 +08:00
vipg
e57d48568e add 2025-11-17 15:30:27 +08:00
vipg
a57a04c448 add 2025-11-17 15:28:24 +08:00
vipg
6ff0d6561e add 2025-11-17 15:22:41 +08:00
vipg
300d4d257d add 2025-11-17 15:17:36 +08:00
vipg
a67e76bcb8 add 2025-11-17 15:12:44 +08:00
vipg
1ebc924efb add 2025-11-17 15:10:48 +08:00
vipg
fcc758dd32 add 2025-11-17 14:59:59 +08:00
vipg
c2439db17f add 2025-11-17 14:57:34 +08:00
vipg
0175b0823e add 2025-11-15 18:06:23 +08:00
91 changed files with 4636 additions and 2495 deletions

119
backend/README.md Normal file
View File

@@ -0,0 +1,119 @@
curl -X POST "http://127.0.0.1:20000/user/register" \
-H "Content-Type: application/json" \
-d '{
"account": "test",
"password": "test12341234"
}'
---
# 创建美国
curl -X POST "http://127.0.0.1:20000/country/create" \
-H "Content-Type: application/json" \
-d '{"name": "美国", "code": "US"}'
# 创建中国
curl -X POST "http://127.0.0.1:20000/country/create" \
-H "Content-Type: application/json" \
-d '{"name": "中国", "code": "CN"}'
# 创建日本
curl -X POST "http://127.0.0.1:20000/country/create" \
-H "Content-Type: application/json" \
-d '{"name": "日本", "code": "JP"}'
# 创建香港
curl -X POST "http://127.0.0.1:20000/country/create" \
-H "Content-Type: application/json" \
-d '{"name": "香港", "code": "HK"}'
---
# 创建美国
curl -X POST "http://127.0.0.1:20000/currency/create" \
-H "Content-Type: application/json" \
-d '{"name": "美元", "code": "USD"}'
# 创建中国
curl -X POST "http://127.0.0.1:20000/currency/create" \
-H "Content-Type: application/json" \
-d '{"name": "人民币", "code": "CNY"}'
# 创建日本
curl -X POST "http://127.0.0.1:20000/currency/create" \
-H "Content-Type: application/json" \
-d '{"name": "日元", "code": "JPY"}'
# 创建香港
curl -X POST "http://127.0.0.1:20000/currency/create" \
-H "Content-Type: application/json" \
-d '{"name": "港币", "code": "HKD"}'
---
# 创建郑州商品交易所
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "郑州商品交易所",
"short_name": "郑商所",
"code": "CZCE"
}'
# 创建上海证券交易所
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "上海证券交易所",
"short_name": "上交所",
"code": "SSE"
}'
# 创建深圳证券交易所
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "深圳证券交易所",
"short_name": "深交所",
"code": "SZSE"
}'
# 创建上海期货交易所
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "上海期货交易所",
"short_name": "上期所",
"code": "SHFE"
}'
# 创建中国金融期货交易所
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "中国金融期货交易所",
"short_name": "中金所",
"code": "CFFEX"
}'
# 创建大连商品期货交易所
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "大连商品交易所",
"short_name": "大商所",
"code": "DCE"
}'
# 创建广州期货交易所
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "广州期货交易所",
"short_name": "广期所",
"code": "GFEX"
}'
# 创建上海国际能源交易中心
curl -X POST http://127.0.0.1:20000/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "上海国际能源交易中心",
"short_name": "上期能源",
"code": "INE"
}'
---

12
backend/chat.md Normal file
View File

@@ -0,0 +1,12 @@
{
"biz":“新增国家“,
"data": [
{
"id": "",
"name": "",
"code": "",
"flag": "",
}
],
}
---

View File

@@ -1 +0,0 @@
docker run -itd --name go_country_dev -v $(pwd)/src:/app -p 20010:80 golang:1.25.0-alpine3.22

View File

@@ -1,44 +0,0 @@
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: {}

View File

@@ -1,44 +0,0 @@
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: {}

View File

@@ -1,30 +0,0 @@
-- 切换到目标数据库
\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 $$;

View File

@@ -1,32 +0,0 @@
-- 切换到目标数据库
\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 $$;

View File

@@ -1,32 +0,0 @@
-- 切换到目标数据库
\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 $$;

View File

@@ -1,38 +0,0 @@
\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 $$;

View File

@@ -1,54 +0,0 @@
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,57 +0,0 @@
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
)

View File

@@ -1,86 +0,0 @@
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,72 +0,0 @@
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")
}

View File

@@ -15,10 +15,10 @@ log_error() {
}
# 定义配置常量(等号两侧无空格!集中管理,便于修改)
IMAGE_NAME="country-api"
IMAGE_NAME="asset-assistant-api"
IMAGE_TAG="1.0.0"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
COMPOSE_PROJECT_NAME="country_service"
COMPOSE_PROJECT_NAME="asset_assistant_service"
DOCKER_COMPOSE_FILE="./docker-compose.yaml"
SRC_DIR="./src"
DOCKERFILE_PATH="${SRC_DIR}/Dockerfile"

3
backend/dev-test.sh Normal file
View File

@@ -0,0 +1,3 @@
sudo docker stop go_asset_assistant_dev
sudo docker rm go_asset_assistant_dev
sudo docker run -itd --name go_asset_assistant_dev -v $(pwd)/src:/app -p 20010:80 golang:1.25.0-alpine3.22

View File

@@ -1,7 +1,7 @@
services:
postgres:
image: postgres:17.4-alpine
container_name: user_db
container_name: asset_assistant_db
restart: always
ports:
- 20001:5432
@@ -13,21 +13,21 @@ services:
POSTGRES_DB: ${DB_NAME}
TZ: ${TZ}
volumes:
- ./shared_data/user_db:/var/lib/postgresql/data
- ./shared_data/asset_assistant_db:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d
- ./scripts:/scripts
networks:
- user-network
user:
- asset_assistant-network
asset_assistant:
image: golang:1.25.0-alpine3.22
container_name: user_api
container_name: asset_assistant_api
restart: always
ports:
- 20000:80
depends_on:
- postgres
networks:
- user-network
- asset_assistant-network
environment:
DB_HOST: postgres
DB_PORT: ${DB_PORT}
@@ -39,6 +39,6 @@ services:
- ./src:/app
command: sh -c "cd /app && go mod tidy && go run main.go"
networks:
user-network:
asset_assistant-network:
driver: bridge
volumes: {}

View File

@@ -1,7 +1,7 @@
services:
postgres:
image: postgres:17.4-alpine
container_name: user_db
container_name: asset_assistant_db
restart: always
ports:
- 20001:5432
@@ -13,21 +13,21 @@ services:
POSTGRES_DB: ${DB_NAME}
TZ: ${TZ}
volumes:
- ./shared_data/user_db:/var/lib/postgresql/data
- ./shared_data/asset_assistant_db:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d
- ./scripts:/scripts
networks:
- user-network
user:
image: user-api:1.0.0
container_name: user_api
- asset-assistant-network
asset_assistant:
image: asset-assistant-api:1.0.0
container_name: asset_assistant_api
restart: always
ports:
- 20000:80
depends_on:
- postgres
networks:
- user-network
- asset-assistant-network
environment:
DB_HOST: postgres
DB_PORT: ${DB_PORT}
@@ -39,6 +39,6 @@ services:
# 挂载添加日志目录挂载,将容器内日志日志目录映射到宿主机的 ./logs 目录
- ./logs:/app/logs # 假设代码中日志存储路径为 /app/logs
networks:
user-network:
asset-assistant-network:
driver: bridge
volumes: {}

View File

@@ -1,21 +0,0 @@
# 数据库配置
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

@@ -1,16 +0,0 @@
curl -X POST http://localhost:20010/variety/create \
-H "Content-Type: application/json" \
-d '{
"name": "螺纹钢",
"code": "RB",
"tick": 1.0,
"tick_price": 10.0
}'
curl -X POST http://localhost:20010/exchange/create \
-H "Content-Type: application/json" \
-d '{
"name": "1234",
"code": "ads"
}'

View File

@@ -1,86 +0,0 @@
#!/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="futures-trade-record-api"
IMAGE_TAG="1.0.0"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
COMPOSE_PROJECT_NAME="futures_trade_record_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 "===== 构建脚本执行完成 ====="

View File

@@ -1,3 +0,0 @@
docker stop go_futures_trade_record_dev
docker rm go_futures_trade_record_dev
docker run -itd --name go_futures_trade_record_dev -v $(pwd)/src:/app -p 20110:80 golang:1.25.0-alpine3.22

View File

@@ -1,35 +0,0 @@
#!/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

View File

@@ -1,44 +0,0 @@
services:
postgres:
image: postgres:17.4-alpine
container_name: futures_trade_record_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/futures_trade_record_db:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d
- ./scripts:/scripts
networks:
- futures-trade-record-network
futures_trade_record:
image: golang:1.25.0-alpine3.22
container_name: futures_trade_record_api
restart: always
ports:
- 20010:80
depends_on:
- postgres
networks:
- futures-trade-record-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:
futures-trade-record-network:
driver: bridge
volumes: {}

View File

@@ -1,44 +0,0 @@
services:
postgres:
image: postgres:17.4-alpine
container_name: futures_trade_record_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/futures_trade_record_db:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d
- ./scripts:/scripts
networks:
- futures-trade-record-network
futures_trade_record:
image: futures-trade-record-api:1.0.0
container_name: futures_trade_record_api
restart: always
ports:
- 20010:80
depends_on:
- postgres
networks:
- futures-trade-record-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:
futures-trade-record-network:
driver: bridge
volumes: {}

View File

@@ -1,59 +0,0 @@
#!/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进程已退出"

View File

@@ -1,48 +0,0 @@
-- 切换到目标数据库
\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;

View File

@@ -1,75 +0,0 @@
-- 切换到目标数据库
\c postgres;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'record') THEN
CREATE TABLE record (
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_record_updated_at
BEFORE UPDATE ON "record"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE 'created record table and trigger';
ELSE
RAISE NOTICE 'record table already exists';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'record_deal') THEN
CREATE TABLE record_deal (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
record_id UUID NOT NULL,
variety_id VARCHAR(50) NOT NULL, -- 品种id
variety_name VARCHAR(50) NOT NULL, -- 品种
contract VARCHAR(50) NOT NULL, -- 合约2401
direction VARCHAR(20) NOT NULL CHECK (direction IN ('', '')), -- 限制合法方向
open_year INT NOT NULL,
open_month INT NOT NULL,
open_day INT NOT NULL,
open_price NUMERIC(12, 6),
open_commission NUMERIC(10, 6) DEFAULT 0.00,
close_year INT NOT NULL DEFAULT 0,
close_month INT NOT NULL DEFAULT 0,
close_day INT NOT NULL DEFAULT 0,
close_price NUMERIC(12, 6) DEFAULT 0.00,
close_commission NUMERIC(10, 6) DEFAULT 0.00,
variety_tick NUMERIC(12, 6),
close_tick NUMERIC(12, 6) DEFAULT 0.00, -- 平仓跳点
variety_tick_price NUMERIC(12, 6),
close_tick_profit NUMERIC(12, 6) DEFAULT 0.00,
total_commission NUMERIC(10, 6) DEFAULT 0.00,
close_profit NUMERIC(12, 6) DEFAULT 0.00,
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_record_deal_updated_at
BEFORE UPDATE ON "record_deal"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 1. 开仓日期索引(支持「未删除+开仓日期」查询)
CREATE INDEX idx_trade_open_date ON record_deal (deleted, open_year, open_month, open_day);
-- 2. 品种+合约+方向组合索引(支持「未删除+品种+合约+方向」高频查询)
CREATE INDEX idx_trade_variety_contract_dir ON record_deal (deleted, variety_name, contract, direction);
-- 3. 未平仓记录索引(支持「未删除+未平仓」查询)
CREATE INDEX idx_trade_unclosed ON record_deal (deleted, variety_name, contract) WHERE close_year IS NULL;
-- 4. 平仓日期索引(支持「未删除+已平仓+平仓日期」查询)
CREATE INDEX idx_trade_close_date ON record_deal (deleted, close_year, close_month, close_day) WHERE close_year IS NOT NULL;
-- 5. 盈亏排序索引(支持「未删除+已平仓+盈亏排序」查询)
CREATE INDEX idx_trade_profit ON record_deal (deleted, close_profit DESC) WHERE close_year IS NOT NULL;
RAISE NOTICE 'created record_deal table and trigger';
ELSE
RAISE NOTICE 'record_deal table already exists';
END IF;
END $$;

View File

@@ -1,199 +0,0 @@
-- 切换到目标数据库
\c postgres;
DO $$
BEGIN
-- 创建主表 variety
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety') THEN
CREATE TABLE variety (
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_variety_updated_at
BEFORE UPDATE ON "variety"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 主表索引deleted+created_at 组合索引(常用于查询未删除数据并排序)
CREATE INDEX idx_variety_deleted_created_at ON variety(deleted, created_at);
RAISE NOTICE 'created variety table, trigger and indexes';
ELSE
RAISE NOTICE 'variety table already exists';
END IF;
-- 创建子表 variety_name 并添加外键和索引
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_name') THEN
CREATE TABLE variety_name (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
variety_id UUID NOT NULL,
name VARCHAR(50) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 外键约束关联variety主表
CONSTRAINT fk_variety_name_variety
FOREIGN KEY (variety_id)
REFERENCES variety(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TRIGGER update_variety_name_updated_at
BEFORE UPDATE ON "variety_name"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 子表索引:外键+deleted组合索引关联查询时高效过滤
CREATE INDEX idx_variety_name_variety_id_deleted ON variety_name(variety_id, deleted);
-- 名称查询索引(支持按名称搜索品种)
CREATE INDEX idx_variety_name_name_deleted ON variety_name(name, deleted);
RAISE NOTICE 'created variety_name table, trigger, foreign key and indexes';
ELSE
RAISE NOTICE 'variety_name table already exists';
END IF;
-- 创建子表 variety_code 并添加外键和索引
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_code') THEN
CREATE TABLE variety_code (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
variety_id UUID NOT NULL,
code VARCHAR(50) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 外键约束关联variety主表
CONSTRAINT fk_variety_code_variety
FOREIGN KEY (variety_id)
REFERENCES variety(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TRIGGER update_variety_code_updated_at
BEFORE UPDATE ON "variety_code"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 子表索引:外键+deleted组合索引
CREATE INDEX idx_variety_code_variety_id_deleted ON variety_code(variety_id, deleted);
-- 代码查询索引(支持按代码搜索品种)
CREATE INDEX idx_variety_code_code_deleted ON variety_code(code, deleted);
RAISE NOTICE 'created variety_code table, trigger, foreign key and indexes';
ELSE
RAISE NOTICE 'variety_code table already exists';
END IF;
-- 创建子表 variety_tick 并添加外键和索引
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick') THEN
CREATE TABLE variety_tick (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
variety_id UUID NOT NULL,
tick NUMERIC(12, 6),
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 外键约束关联variety主表
CONSTRAINT fk_variety_tick_variety
FOREIGN KEY (variety_id)
REFERENCES variety(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TRIGGER update_variety_tick_updated_at
BEFORE UPDATE ON "variety_tick"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 子表索引:外键+deleted组合索引
CREATE INDEX idx_variety_tick_variety_id_deleted ON variety_tick(variety_id, deleted);
-- 跳点值范围查询索引
CREATE INDEX idx_variety_tick_tick_deleted ON variety_tick(tick, deleted);
RAISE NOTICE 'created variety_tick table, trigger, foreign key and indexes';
ELSE
RAISE NOTICE 'variety_tick table already exists';
END IF;
-- 创建子表 variety_tick_price 并添加外键和索引
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick_price') THEN
CREATE TABLE variety_tick_price (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
variety_id UUID NOT NULL,
price NUMERIC(12, 6),
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 外键约束关联variety主表
CONSTRAINT fk_variety_tick_price_variety
FOREIGN KEY (variety_id)
REFERENCES variety(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TRIGGER update_variety_tick_price_updated_at
BEFORE UPDATE ON "variety_tick_price"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 子表索引:外键+deleted组合索引
CREATE INDEX idx_variety_tick_price_variety_id_deleted ON variety_tick_price(variety_id, deleted);
-- 价格范围查询索引
CREATE INDEX idx_variety_tick_price_price_deleted ON variety_tick_price(price, deleted);
RAISE NOTICE 'created variety_tick_price table, trigger, foreign key and indexes';
ELSE
RAISE NOTICE 'variety_tick_price table already exists';
END IF;
END $$;
DO $$
DECLARE
view_exists BOOLEAN;
BEGIN
-- 检查视图是否已存在
SELECT EXISTS (
SELECT 1 FROM information_schema.views
WHERE table_name = 'variety_info_view'
) INTO view_exists;
-- 创建或更新视图
CREATE OR REPLACE VIEW variety_info_view AS
SELECT
v.id AS variety_id,
vn.name AS name,
vc.code AS code,
-- 调用格式化函数自动去除尾部多余0
format_numeric_to_original(vt.tick) AS tick,
format_numeric_to_original(vtp.price) AS tick_price,
-- 可选:保留原始精度字段(供计算使用)
vt.tick AS tick_original,
vtp.price AS tick_price_original
FROM
variety v
LEFT JOIN variety_name vn
ON v.id = vn.variety_id
AND vn.deleted = FALSE
LEFT JOIN variety_code vc
ON v.id = vc.variety_id
AND vc.deleted = FALSE
LEFT JOIN variety_tick vt
ON v.id = vt.variety_id
AND vt.deleted = FALSE
LEFT JOIN variety_tick_price vtp
ON v.id = vtp.variety_id
AND vtp.deleted = FALSE
WHERE
v.deleted = FALSE;
IF view_exists THEN
RAISE NOTICE '视图 variety_info_view 已更新';
ELSE
RAISE NOTICE '视图 variety_info_view 已创建';
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '处理视图时发生错误: %', SQLERRM;
END $$;

View File

@@ -1,126 +0,0 @@
-- 切换到目标数据库
\c postgres;
DO $$
BEGIN
-- 创建主表 exchange
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange') THEN
CREATE TABLE exchange (
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_exchange_updated_at
BEFORE UPDATE ON "exchange"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 主表索引deleted+created_at 组合索引(常用于查询未删除数据并排序)
CREATE INDEX idx_exchange_deleted_created_at ON exchange(deleted, created_at);
RAISE NOTICE 'created exchange table, trigger and indexes';
ELSE
RAISE NOTICE 'exchange table already exists';
END IF;
-- 创建子表 exchange_name 并添加外键和索引
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_name') THEN
CREATE TABLE exchange_name (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
exchange_id UUID NOT NULL,
name VARCHAR(50) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 外键约束关联exchange主表
CONSTRAINT fk_exchange_name_exchange
FOREIGN KEY (exchange_id)
REFERENCES exchange(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TRIGGER update_exchange_name_updated_at
BEFORE UPDATE ON "exchange_name"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 子表索引:外键+deleted组合索引关联查询时高效过滤
CREATE INDEX idx_exchange_name_exchange_id_deleted ON exchange_name(exchange_id, deleted);
-- 名称查询索引(支持按名称搜索品种)
CREATE INDEX idx_exchange_name_name_deleted ON exchange_name(name, deleted);
RAISE NOTICE 'created exchange_name table, trigger, foreign key and indexes';
ELSE
RAISE NOTICE 'exchange_name table already exists';
END IF;
-- 创建子表 exchange_code 并添加外键和索引
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_code') THEN
CREATE TABLE exchange_code (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
exchange_id UUID NOT NULL,
code VARCHAR(50) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 外键约束关联exchange主表
CONSTRAINT fk_exchange_code_exchange
FOREIGN KEY (exchange_id)
REFERENCES exchange(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE TRIGGER update_exchange_code_updated_at
BEFORE UPDATE ON "exchange_code"
FOR EACH ROW
EXECUTE FUNCTION update_data_modified_column();
-- 子表索引:外键+deleted组合索引
CREATE INDEX idx_exchange_code_exchange_id_deleted ON exchange_code(exchange_id, deleted);
-- 代码查询索引(支持按代码搜索品种)
CREATE INDEX idx_exchange_code_code_deleted ON exchange_code(code, deleted);
RAISE NOTICE 'created exchange_code table, trigger, foreign key and indexes';
ELSE
RAISE NOTICE 'exchange_code table already exists';
END IF;
END $$;
DO $$
DECLARE
view_exists BOOLEAN;
BEGIN
-- 检查视图是否已存在
SELECT EXISTS (
SELECT 1 FROM information_schema.views
WHERE table_name = 'exchange_info_view'
) INTO view_exists;
-- 创建/更新视图整合交易所ID、名称、代码仅显示未删除数据
CREATE OR REPLACE VIEW exchange_info_view AS
SELECT
e.id AS exchange_id, -- 交易所主表唯一ID
en.name AS name, -- 交易所名称来自exchange_name子表
ec.code AS code -- 交易所代码来自exchange_code子表
FROM
exchange e
-- 左连接:主表存在时,即使子表无数据也保留记录(避免数据丢失)
LEFT JOIN exchange_name en
ON e.id = en.exchange_id
AND en.deleted = FALSE -- 子句中筛选未删除数据,关联时直接过滤更高效
LEFT JOIN exchange_code ec
ON e.id = ec.exchange_id
AND ec.deleted = FALSE -- 子表未删除数据筛选
WHERE
e.deleted = FALSE; -- 主表筛选未删除的交易所
IF view_exists THEN
RAISE NOTICE '视图 exchange_info_view 已更新';
ELSE
RAISE NOTICE '视图 exchange_info_view 已创建';
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '处理视图时发生错误: %', SQLERRM;
END $$;

View File

@@ -1,38 +0,0 @@
# ==================== 第一阶段构建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"]

View File

@@ -1,57 +0,0 @@
module futures_trade_record
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
)

View File

@@ -1,133 +0,0 @@
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=

126
backend/prompt.md Normal file
View File

@@ -0,0 +1,126 @@
---
分析这个项目,在 create.go 中完成以下需求:
1、接收 namecode 两个参数。
2、确认提交的 namecode 两个参数不能为空,如果有空,则返回提示。
3、第二步通过后在 country 表中,通过: "INSERT INTO country DEFAULT VALUES RETURNING id" 获得ID。
4、通过 3 中的 id开启事务保存到 name 和 code 的表中。
---
分析这个项目,在 delete.go 中完成以下需求:
1、接收 country_id 参数。
2、确认提交的 country_id 参数不能为空,如果有空,则返回提示。
3、开启事务处理以下逻辑
3.1、把 country 中country.id==req.country_id 的 deleted 字段更新为true。
3.2、把 name 中name.country_id==req.country_id 的 deleted 字段更新为true。
3.3、把 code code.country_id==req.country_id 的 deleted 字段更新为true。
---
分析这个项目,在 update.go 中完成以下需求:
1、接收 country_id,namecode 参数。
2、确认提交的 country_id 参数有不能为空,如果为空,则返回提示。
3、确认提交的 namecode 两个参数,必须有一个不能为空,如果都为空,则返回提示。
4、如果 name 不为空,开启事务保存到 name 中。
5、如果 code 不为空,开启事务保存到 code 中。
6、如果 namecode 都不为空,开启事务保存到 namecode 中。
---
分析这个项目,在 read.go 中完成以下需求:
1、接收 country_idnamecodepagepage_size 参数。
2、确认提交的 country_idnamecode 必须有一个不能为空,如果都为空,则返回提示。
3、确认提交的 pagepage_size, 如果为空,则 page 默认为 1page_size 默认为20。
3、根据参数去 country_info_view 中查找数据,并做分页查询。
4、将查找的数据分页返回。
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_variety子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
trade_id UUID NOT NULL,
variety_id UUID NOT NULL,
variety_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
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_direction子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
trade_id UUID NOT NULL,
direction 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
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_open_price子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
trade_id UUID NOT NULL,
open_price NUMERIC(10,2) NOT NULL CHECK (price >= 0.00) DEFAULT 0.00,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_open_fee子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
trade_id UUID NOT NULL,
open_fee NUMERIC(10,2) NOT NULL CHECK (price >= 0.00) DEFAULT 0.00,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_close_date子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL,
year INT NOT NULL DEFAULT 0,
month INT NOT NULL DEFAULT 0,
day INT NOT NULL DEFAULT 0,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_close_price子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
trade_id UUID NOT NULL,
close_price NUMERIC(10,2) NOT NULL CHECK (price >= 0.00) DEFAULT 0.00,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
---
读取./sql/08_trade.sql完善trade_info_view视图逻辑加入close_date和close_price信息。
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_close_fee子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
trade_id UUID NOT NULL,
close_fee NUMERIC(10,2) NOT NULL CHECK (close_fee >= 0.00) DEFAULT 0.00,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
---
读取./sql/08_trade.sql完善trade_info_view视图逻辑加入trade_close_fee信息。
---
读取./sql/08_trade.sql然后多加一个子表子表名叫trade_profit子表逻辑字段为:
id UUID DEFAULT gen_random_uuid() PRIMARY KEY
trade_id UUID NOT NULL,
variety_tick NUMERIC(12,6) NOT NULL DEFAULT 0.00,
variety_tick_price NUMERIC(12,6) NOT NULL CHECK (variety_tick_price >= 0.00) DEFAULT 0.00,
win_tick NUMERIC(12,6) NOT NULL DEFAULT 0.00,
win_tick_price NUMERIC(12,6) NOT NULL DEFAULT 0.00,
fee_cost NUMERIC(12,6) NOT NULL DEFAULT 0.00,
trade_win NUMERIC(12,6) NOT NULL DEFAULT 0.00,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
---
读取./sql/08_trade.sql完善trade_info_view视图逻辑加入trade_profit信息。
---
1、看一下./sql/07_variety.sql的内容并记住这些内容。
2、打开./src/logic4variety/create.go并维持风格不变的前提下将业务调整为1中的信息。
---
1、看一下./sql/07_variety.sql的内容并记住这些内容。
2、打开./src/logic4variety/delete.go并维持风格不变的前提下将业务调整为1中的信息。
---
1、看一下和了解./sql/07_variety.sql的内容 然后帮忙再视图中加入exchange_name的信息。
---
1、看一下./sql/07_variety.sql的内容并记住这些内容。
2、打开./src/logic4variety/read.go并维持风格不变的前提下将业务调整为1中的信息。
---
1、看一下./sql/07_variety.sql的内容并记住这些内容。
2、打开./src/logic4variety/update.go并维持风格不变的前提下将业务调整为1中的信息。
---

116
backend/sql/03_user.sql Normal file
View File

@@ -0,0 +1,116 @@
-- =========================================================
-- user.sql 👤
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$
BEGIN
RAISE NOTICE '🚀============ user 部署开始 ============🚀';
END $$;
-- 2⃣ 连接目标库
\c postgres;
-- 3⃣ 表结构 -----------------------------------
DO $$
BEGIN
-- user 主表(关键字,双引号包裹)
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user') THEN
CREATE TABLE "user" (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
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_user_updated_at
BEFORE UPDATE ON "user"
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '1⃣✅ user 主表已创建';
ELSE
RAISE NOTICE '1⃣⏩ user 主表已存在,跳过';
END IF;
-- user_account
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_account') THEN
CREATE TABLE user_account (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
account 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_user_account_updated_at
BEFORE UPDATE ON user_account
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '2⃣✅ user_account 子表已创建';
ELSE
RAISE NOTICE '2⃣⏩ user_account 子表已存在,跳过';
END IF;
-- user_password
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_password') THEN
CREATE TABLE user_password (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
password 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_user_password_updated_at
BEFORE UPDATE ON user_password
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '3⃣✅ user_password 子表已创建';
ELSE
RAISE NOTICE '3⃣⏩ user_password 子表已存在,跳过';
END IF;
END $$;
-- 4⃣ 视图 ------------------------------------
DO $$
DECLARE
view_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT 1 FROM information_schema.views
WHERE table_name = 'user_info_view'
) INTO view_exists;
IF view_exists THEN
DROP VIEW user_info_view;
RAISE NOTICE '4⃣♻ 已删除旧视图 user_info_view';
END IF;
CREATE OR REPLACE VIEW user_info_view AS
SELECT
u.id AS user_id,
ua.account,
up.password
FROM "user" u
JOIN user_account ua ON u.id = ua.user_id AND ua.deleted = FALSE
JOIN user_password up ON u.id = up.user_id AND up.deleted = FALSE
WHERE u.deleted = FALSE;
RAISE NOTICE '4⃣✅ user_info_view 已创建/更新';
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '4⃣❌ 处理视图时发生错误: %', SQLERRM;
END $$;
-- 5⃣ 性能索引 ------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_user_account_user_id_deleted ON user_account(user_id, deleted);
CREATE INDEX IF NOT EXISTS idx_user_password_user_id_deleted ON user_password(user_id, deleted);
DO $$
BEGIN
RAISE NOTICE '5⃣✅ 全部索引已确保存在';
END $$;
-- 6⃣ 完成 🎉
DO $$
BEGIN
RAISE NOTICE '🎉============ user 部署完成 ============🎉';
END $$;

137
backend/sql/04_country.sql Normal file
View File

@@ -0,0 +1,137 @@
-- =========================================================
-- country.sql 🌍
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$
BEGIN
RAISE NOTICE '🚀============ country 部署开始 ============🚀';
END $$;
-- 2⃣ 连接目标库
\c postgres;
-- 3⃣ 表结构 -----------------------------------
DO $$
BEGIN
-- country 主表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country') THEN
CREATE TABLE country (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
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_data_modified_column();
RAISE NOTICE '1⃣✅ country 主表已创建';
ELSE
RAISE NOTICE '1⃣⏩ country 主表已存在,跳过';
END IF;
-- country_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_name') THEN
CREATE TABLE country_name (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
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_country_name_updated_at
BEFORE UPDATE ON country_name
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '2⃣✅ country_name 子表已创建';
ELSE
RAISE NOTICE '2⃣⏩ country_name 子表已存在,跳过';
END IF;
-- country_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_code') THEN
CREATE TABLE country_code (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
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_country_code_updated_at
BEFORE UPDATE ON country_code
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '3⃣✅ country_code 子表已创建';
ELSE
RAISE NOTICE '3⃣⏩ country_code 子表已存在,跳过';
END IF;
-- country_flag
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_flag') THEN
CREATE TABLE country_flag (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
country_id UUID NOT NULL,
flag 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_country_flag_updated_at
BEFORE UPDATE ON country_flag
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '4⃣✅ country_flag 子表已创建';
ELSE
RAISE NOTICE '4⃣⏩ country_flag 子表已存在,跳过';
END IF;
END $$;
-- 4⃣ 视图 ------------------------------------
DO $$
DECLARE
view_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT 1 FROM information_schema.views
WHERE table_name = 'country_info_view'
) INTO view_exists;
IF view_exists THEN
DROP VIEW country_info_view;
RAISE NOTICE '4⃣♻ 已删除旧视图 country_info_view';
END IF;
CREATE OR REPLACE VIEW country_info_view AS
SELECT
u.id AS country_id,
n.name,
c.code,
f.flag
FROM country u
LEFT JOIN country_name n ON u.id = n.country_id AND n.deleted = FALSE
LEFT JOIN country_code c ON u.id = c.country_id AND c.deleted = FALSE
LEFT JOIN country_flag f ON u.id = f.country_id AND f.deleted = FALSE
WHERE u.deleted = FALSE;
RAISE NOTICE '4⃣✅ country_info_view 已创建/更新';
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '4⃣❌ 处理视图时发生错误: %', SQLERRM;
END $$;
-- 5⃣ 性能索引 ------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_country_name_country_id_deleted ON country_name(country_id, deleted);
CREATE INDEX IF NOT EXISTS idx_country_code_country_id_deleted ON country_code(country_id, deleted);
CREATE INDEX IF NOT EXISTS idx_country_flag_country_id_deleted ON country_flag(country_id, deleted);
DO $$
BEGIN
RAISE NOTICE '5⃣✅ 全部索引已确保存在';
END $$;
-- 6⃣ 完成 🎉
DO $$
BEGIN
RAISE NOTICE '🎉============ country 部署完成 ============🎉';
END $$;

138
backend/sql/05_exchange.sql Normal file
View File

@@ -0,0 +1,138 @@
-- =========================================================
-- exchange.sql 🏛️
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$
BEGIN
RAISE NOTICE '🚀============ exchange 部署开始 ============🚀';
END $$;
-- 2⃣ 连接目标库
\c postgres;
-- 3⃣ 表结构 -----------------------------------
DO $$
BEGIN
-- exchange 主表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange') THEN
CREATE TABLE exchange (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
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_exchange_updated_at
BEFORE UPDATE ON exchange
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '1⃣✅ exchange 主表已创建';
ELSE
RAISE NOTICE '1⃣⏩ exchange 主表已存在,跳过';
END IF;
-- exchange_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_name') THEN
CREATE TABLE exchange_name (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
exchange_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_exchange_name_updated_at
BEFORE UPDATE ON exchange_name
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '2⃣✅ exchange_name 子表已创建';
ELSE
RAISE NOTICE '2⃣⏩ exchange_name 子表已存在,跳过';
END IF;
-- exchange_short_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_short_name') THEN
CREATE TABLE exchange_short_name (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
exchange_id UUID NOT NULL,
short_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_exchange_short_name_updated_at
BEFORE UPDATE ON exchange_short_name
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '3⃣✅ exchange_short_name 子表已创建';
ELSE
RAISE NOTICE '3⃣⏩ exchange_short_name 子表已存在,跳过';
END IF;
-- exchange_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_code') THEN
CREATE TABLE exchange_code (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
exchange_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_exchange_code_updated_at
BEFORE UPDATE ON exchange_code
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '4⃣✅ exchange_code 子表已创建';
ELSE
RAISE NOTICE '4⃣⏩ exchange_code 子表已存在,跳过';
END IF;
END $$;
-- 4⃣ 视图 ------------------------------------
DO $$
DECLARE
view_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT 1 FROM information_schema.views
WHERE table_name = 'exchange_info_view'
) INTO view_exists;
IF view_exists THEN
DROP VIEW exchange_info_view;
RAISE NOTICE '4⃣♻ 已删除旧视图 exchange_info_view';
END IF;
CREATE OR REPLACE VIEW exchange_info_view AS
SELECT
u.id AS exchange_id,
n.name,
sn.short_name,
c.code,
u.deleted
FROM exchange u
JOIN exchange_name n ON u.id = n.exchange_id AND n.deleted = FALSE
JOIN exchange_short_name sn ON u.id = sn.exchange_id AND sn.deleted = FALSE
JOIN exchange_code c ON u.id = c.exchange_id AND c.deleted = FALSE
WHERE u.deleted = FALSE;
RAISE NOTICE '4⃣✅ exchange_info_view 已创建/更新';
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '4⃣❌ 处理视图时发生错误: %', SQLERRM;
END $$;
-- 5⃣ 性能索引 ------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_exchange_name_exchange_id_deleted ON exchange_name(exchange_id, deleted);
CREATE INDEX IF NOT EXISTS idx_exchange_short_name_exchange_id_deleted ON exchange_short_name(exchange_id, deleted);
CREATE INDEX IF NOT EXISTS idx_exchange_code_exchange_id_deleted ON exchange_code(exchange_id, deleted);
DO $$
BEGIN
RAISE NOTICE '5⃣✅ 全部索引已确保存在';
END $$;
-- 6⃣ 完成 🎉
DO $$
BEGIN
RAISE NOTICE '🎉============ exchange 部署完成 ============🎉';
END $$;

117
backend/sql/06_currency.sql Normal file
View File

@@ -0,0 +1,117 @@
-- =========================================================
-- currency.sql 💰
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$
BEGIN
RAISE NOTICE '🚀============ currency 部署开始 ============🚀';
END $$;
-- 2⃣ 连接目标库
\c postgres;
-- 3⃣ 表结构 -----------------------------------
DO $$
BEGIN
-- currency 主表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency') THEN
CREATE TABLE currency (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
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_currency_updated_at
BEFORE UPDATE ON currency
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '1⃣✅ currency 主表已创建';
ELSE
RAISE NOTICE '1⃣⏩ currency 主表已存在,跳过';
END IF;
-- currency_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency_name') THEN
CREATE TABLE currency_name (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
currency_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_currency_name_updated_at
BEFORE UPDATE ON currency_name
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '2⃣✅ currency_name 子表已创建';
ELSE
RAISE NOTICE '2⃣⏩ currency_name 子表已存在,跳过';
END IF;
-- currency_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency_code') THEN
CREATE TABLE currency_code (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
currency_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_currency_code_updated_at
BEFORE UPDATE ON currency_code
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '3⃣✅ currency_code 子表已创建';
ELSE
RAISE NOTICE '3⃣⏩ currency_code 子表已存在,跳过';
END IF;
END $$;
-- 4⃣ 视图 ------------------------------------
DO $$
DECLARE
view_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT 1 FROM information_schema.views
WHERE table_name = 'currency_info_view'
) INTO view_exists;
IF view_exists THEN
DROP VIEW currency_info_view;
RAISE NOTICE '4⃣♻ 已删除旧视图 currency_info_view';
END IF;
CREATE OR REPLACE VIEW currency_info_view AS
SELECT
u.id AS currency_id,
n.name,
c.code,
u.deleted
FROM currency u
JOIN currency_name n ON u.id = n.currency_id AND n.deleted = FALSE
JOIN currency_code c ON u.id = c.currency_id AND c.deleted = FALSE
WHERE u.deleted = FALSE;
RAISE NOTICE '4⃣✅ currency_info_view 已创建/更新';
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '4⃣❌ 处理视图时发生错误: %', SQLERRM;
END $$;
-- 5⃣ 性能索引 ------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_currency_name_currency_id_deleted ON currency_name(currency_id, deleted);
CREATE INDEX IF NOT EXISTS idx_currency_code_currency_id_deleted ON currency_code(currency_id, deleted);
DO $$
BEGIN
RAISE NOTICE '5⃣✅ 全部索引已确保存在';
END $$;
-- 6⃣ 完成 🎉
DO $$
BEGIN
RAISE NOTICE '🎉============ currency 部署完成 ============🎉';
END $$;

165
backend/sql/07_variety.sql Normal file
View File

@@ -0,0 +1,165 @@
-- =========================================================
-- variety.sql 🎉
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$
BEGIN
RAISE NOTICE '🚀============ variety 部署开始 ============🚀';
END $$;
-- 2⃣ 连接目标库
\c postgres;
-- 3⃣ 表结构 -----------------------------------
DO $$
BEGIN
-- variety 主表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety') THEN
CREATE TABLE variety (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
RAISE NOTICE '1⃣✅ variety 主表已创建';
ELSE
RAISE NOTICE '1⃣⏩ variety 主表已存在,跳过';
END IF;
-- variety_exchange
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_exchange') THEN
CREATE TABLE variety_exchange (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL,
exchange_id VARCHAR(50) NOT NULL,
exchange_name VARCHAR(50) 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_variety_exchange_updated_at
BEFORE UPDATE ON variety_exchange
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '2⃣✅ variety_exchange 子表已创建';
ELSE
RAISE NOTICE '2⃣⏩ variety_exchange 子表已存在,跳过';
END IF;
-- variety_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_name') THEN
CREATE TABLE variety_name (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL,
name VARCHAR(50) 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_variety_name_updated_at
BEFORE UPDATE ON variety_name
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '3⃣✅ variety_name 子表已创建';
ELSE
RAISE NOTICE '3⃣⏩ variety_name 子表已存在,跳过';
END IF;
-- variety_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_code') THEN
CREATE TABLE variety_code (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL,
code VARCHAR(50) 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_variety_code_updated_at
BEFORE UPDATE ON variety_code
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '4⃣✅ variety_code 子表已创建';
ELSE
RAISE NOTICE '4⃣⏩ variety_code 子表已存在,跳过';
END IF;
-- variety_tick
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick') THEN
CREATE TABLE variety_tick (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL,
tick NUMERIC(12,6),
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_variety_tick_updated_at
BEFORE UPDATE ON variety_tick
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '5⃣✅ variety_tick 子表已创建';
ELSE
RAISE NOTICE '5⃣⏩ variety_tick 子表已存在,跳过';
END IF;
-- variety_tick_price
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick_price') THEN
CREATE TABLE variety_tick_price (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL,
price NUMERIC(12,6),
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_variety_tick_price_updated_at
BEFORE UPDATE ON variety_tick_price
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '6⃣✅ variety_tick_price 子表已创建';
ELSE
RAISE NOTICE '6⃣⏩ variety_tick_price 子表已存在,跳过';
END IF;
END $$;
-- 4⃣ 视图 ------------------------------------
DROP VIEW IF EXISTS variety_info_view;
CREATE OR REPLACE VIEW variety_info_view AS
SELECT
v.id AS variety_id,
vn.name,
vc.code,
ve.exchange_name,
format_numeric_to_original(vt.tick) AS tick,
format_numeric_to_original(vtp.price) AS tick_price,
vt.tick AS tick_original,
vtp.price AS tick_price_original
FROM variety v
LEFT JOIN variety_name vn ON v.id = vn.variety_id AND vn.deleted = FALSE
LEFT JOIN variety_code vc ON v.id = vc.variety_id AND vc.deleted = FALSE
LEFT JOIN variety_exchange ve ON v.id = ve.variety_id AND ve.deleted = FALSE
LEFT JOIN variety_tick vt ON v.id = vt.variety_id AND vt.deleted = FALSE
LEFT JOIN variety_tick_price vtp ON v.id = vtp.variety_id AND vtp.deleted = FALSE
WHERE v.deleted = FALSE;
DO $$
BEGIN
RAISE NOTICE '7⃣✅ variety_info_view 已创建/更新';
END $$;
-- 5⃣ 性能索引 ------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_variety_exchange_variety_id_deleted ON variety_exchange(variety_id, deleted);
CREATE INDEX IF NOT EXISTS idx_variety_name_variety_id_deleted ON variety_name(variety_id, deleted);
CREATE INDEX IF NOT EXISTS idx_variety_code_variety_id_deleted ON variety_code(variety_id, deleted);
CREATE INDEX IF NOT EXISTS idx_variety_tick_variety_id_deleted ON variety_tick(variety_id, deleted);
CREATE INDEX IF NOT EXISTS idx_variety_tick_price_variety_id_deleted ON variety_tick_price(variety_id, deleted);
DO $$
BEGIN
RAISE NOTICE '8⃣✅ 全部索引已确保存在';
END $$;
-- 6⃣ 完成 🎉
DO $$
BEGIN
RAISE NOTICE '🎉============ variety 部署完成 ============🎉';
END $$;

286
backend/sql/08_trade.sql Normal file
View File

@@ -0,0 +1,286 @@
-- =========================================================
-- trade.sql 🎉
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$
BEGIN
RAISE NOTICE '🚀============ trade 部署开始 ============🚀';
END $$;
-- 2⃣ 连接目标库
\c postgres;
-- 3⃣ 表结构 -----------------------------------
DO $$
BEGIN
-- trade 主表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade') THEN
CREATE TABLE trade (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
RAISE NOTICE '1⃣✅ trade 主表已创建';
ELSE
RAISE NOTICE '1⃣⏩ trade 主表已存在,跳过';
END IF;
-- remark 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_remark') THEN
CREATE TABLE trade_remark (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
remark 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_trade_remark_updated_at
BEFORE UPDATE ON trade_remark
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '2⃣✅ trade_remark 子表已创建';
ELSE
RAISE NOTICE '2⃣⏩ trade_remark 子表已存在,跳过';
END IF;
-- open_date 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_open_date') THEN
CREATE TABLE trade_open_date (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
year INT NOT NULL DEFAULT 0,
month INT NOT NULL DEFAULT 0,
day INT NOT NULL DEFAULT 0,
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_trade_open_date_updated_at
BEFORE UPDATE ON trade_open_date
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '3⃣✅ trade_open_date 子表已创建';
ELSE
RAISE NOTICE '3⃣⏩ trade_open_date 子表已存在,跳过';
END IF;
-- variety 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_variety') THEN
CREATE TABLE trade_variety (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
variety_id UUID NOT NULL,
variety_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_trade_variety_updated_at
BEFORE UPDATE ON trade_variety
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '4⃣✅ trade_variety 子表已创建';
ELSE
RAISE NOTICE '4⃣⏩ trade_variety 子表已存在,跳过';
END IF;
-- direction 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_direction') THEN
CREATE TABLE trade_direction (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
direction 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_trade_direction_updated_at
BEFORE UPDATE ON trade_direction
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '5⃣✅ trade_direction 子表已创建';
ELSE
RAISE NOTICE '5⃣⏩ trade_direction 子表已存在,跳过';
END IF;
-- open_price 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_open_price') THEN
CREATE TABLE trade_open_price (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
open_price NUMERIC(10,6) NOT NULL CHECK (open_price >= 0.00) DEFAULT 0.00,
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_trade_open_price_updated_at
BEFORE UPDATE ON trade_open_price
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '6⃣✅ trade_open_price 子表已创建';
ELSE
RAISE NOTICE '6⃣⏩ trade_open_price 子表已存在,跳过';
END IF;
-- open_fee 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_open_fee') THEN
CREATE TABLE trade_open_fee (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
open_fee NUMERIC(10,6) NOT NULL CHECK (open_fee >= 0.00) DEFAULT 0.00,
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_trade_open_fee_updated_at
BEFORE UPDATE ON trade_open_fee
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '7⃣✅ trade_open_fee 子表已创建';
ELSE
RAISE NOTICE '7⃣⏩ trade_open_fee 子表已存在,跳过';
END IF;
-- close_date 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_close_date') THEN
CREATE TABLE trade_close_date (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
year INT NOT NULL DEFAULT 0,
month INT NOT NULL DEFAULT 0,
day INT NOT NULL DEFAULT 0,
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_trade_close_date_updated_at
BEFORE UPDATE ON trade_close_date
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '8⃣✅ trade_close_date 子表已创建';
ELSE
RAISE NOTICE '8⃣⏩ trade_close_date 子表已存在,跳过';
END IF;
-- close_price 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_close_price') THEN
CREATE TABLE trade_close_price (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
close_price NUMERIC(10,6) NOT NULL CHECK (close_price >= 0.00) DEFAULT 0.00,
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_trade_close_price_updated_at
BEFORE UPDATE ON trade_close_price
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '9⃣✅ trade_close_price 子表已创建';
ELSE
RAISE NOTICE '9⃣⏩ trade_close_price 子表已存在,跳过';
END IF;
-- close_fee 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_close_fee') THEN
CREATE TABLE trade_close_fee (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
close_fee NUMERIC(10,6) NOT NULL CHECK (close_fee >= 0.00) DEFAULT 0.00,
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_trade_close_fee_updated_at
BEFORE UPDATE ON trade_close_fee
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '🔟✅ trade_close_fee 子表已创建';
ELSE
RAISE NOTICE '🔟⏩ trade_close_fee 子表已存在,跳过';
END IF;
-- profit 子表
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'trade_profit') THEN
CREATE TABLE trade_profit (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trade(id) ON DELETE CASCADE,
variety_tick NUMERIC(12,6) NOT NULL DEFAULT 0.00,
variety_tick_price NUMERIC(12,6) NOT NULL CHECK (variety_tick_price >= 0.00) DEFAULT 0.00,
win_tick NUMERIC(12,6) NOT NULL DEFAULT 0.00,
win_tick_price NUMERIC(12,6) NOT NULL DEFAULT 0.00,
fee_cost NUMERIC(12,6) NOT NULL DEFAULT 0.00,
trade_win NUMERIC(12,6) NOT NULL DEFAULT 0.00,
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_trade_profit_updated_at
BEFORE UPDATE ON trade_profit
FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
RAISE NOTICE '1⃣1⃣✅ trade_profit 子表已创建';
ELSE
RAISE NOTICE '1⃣1⃣⏩ trade_profit 子表已存在,跳过';
END IF;
END $$;
-- 4⃣ 视图 ------------------------------------
DROP VIEW IF EXISTS trade_info_view;
CREATE OR REPLACE VIEW trade_info_view AS
SELECT
t.id AS trade_id,
r.remark AS remark,
od.year AS open_year,
od.month AS open_month,
od.day AS open_day,
v.variety_name AS variety_name,
d.direction AS direction,
format_numeric_to_original(op.open_price) AS open_price,
format_numeric_to_original(of.open_fee) AS open_fee,
cd.year AS close_year,
cd.month AS close_month,
cd.day AS close_day,
format_numeric_to_original(cp.close_price) AS close_price,
format_numeric_to_original(cf.close_fee) AS close_fee,
p.variety_tick AS variety_tick,
p.variety_tick_price AS variety_tick_price,
p.win_tick AS win_tick,
p.win_tick_price AS win_tick_price,
format_numeric_to_original(p.fee_cost) AS fee_cost,
format_numeric_to_original(p.trade_win) AS trade_win
FROM trade t
LEFT JOIN trade_remark r ON t.id = r.trade_id AND r.deleted = FALSE
LEFT JOIN trade_open_date od ON t.id = od.trade_id AND od.deleted = FALSE
LEFT JOIN trade_variety v ON t.id = v.trade_id AND v.deleted = FALSE
LEFT JOIN trade_direction d ON t.id = d.trade_id AND d.deleted = FALSE
LEFT JOIN trade_open_price op ON t.id = op.trade_id AND op.deleted = FALSE
LEFT JOIN trade_open_fee of ON t.id = of.trade_id AND of.deleted = FALSE
LEFT JOIN trade_close_date cd ON t.id = cd.trade_id AND cd.deleted = FALSE
LEFT JOIN trade_close_price cp ON t.id = cp.trade_id AND cp.deleted = FALSE
LEFT JOIN trade_close_fee cf ON t.id = cf.trade_id AND cf.deleted = FALSE
LEFT JOIN trade_profit p ON t.id = p.trade_id AND p.deleted = FALSE
WHERE t.deleted = FALSE;
DO $$
BEGIN
RAISE NOTICE '4⃣✅ trade_info_view 已创建/更新';
END $$;
-- 5⃣ 性能索引 ------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_trade_remark_trade_id_deleted ON trade_remark(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_open_date_trade_id_deleted ON trade_open_date(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_variety_trade_id_deleted ON trade_variety(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_direction_trade_id_deleted ON trade_direction(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_open_price_trade_id_deleted ON trade_open_price(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_open_fee_trade_id_deleted ON trade_open_fee(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_close_date_trade_id_deleted ON trade_close_date(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_close_price_trade_id_deleted ON trade_close_price(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_close_fee_trade_id_deleted ON trade_close_fee(trade_id, deleted);
CREATE INDEX IF NOT EXISTS idx_trade_profit_trade_id_deleted ON trade_profit(trade_id, deleted);
DO $$
BEGIN
RAISE NOTICE '5⃣✅ 全部索引已确保存在';
END $$;
-- 6⃣ 完成 🎉
DO $$
BEGIN
RAISE NOTICE '🎉============ trade 部署完成 ============🎉';
END $$;

View File

@@ -21,7 +21,7 @@ func Init() {
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),

View File

@@ -1,4 +1,4 @@
module user
module asset_assistant
go 1.25.0

View File

@@ -0,0 +1,303 @@
package logic4country
import (
"asset_assistant/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

@@ -1,11 +1,12 @@
package logic
package logic4country
import (
"asset_assistant/db"
"net/http"
"country/db"
"time"
"github.com/google/uuid"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
@@ -20,7 +21,7 @@ type DeleteResponse struct {
Message string `json:"message"` // 提示信息
}
// DeleteHandler 处理国家删除逻辑(软删除)
// DeleteHandler 处理删除逻辑(软删除)
func DeleteHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-DeleteRequest-ID")
@@ -105,10 +106,10 @@ func DeleteHandler(c *gin.Context) {
}
// 3.2 更新name表
_, err = tx.Exec("UPDATE name SET deleted = TRUE WHERE country_id = $1", req.CountryID)
_, err = tx.Exec("UPDATE country_name SET deleted = TRUE WHERE country_id = $1", req.CountryID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ name表更新失败",
zap.L().Error("❌ country_name表更新失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.Error(err),
@@ -121,10 +122,10 @@ func DeleteHandler(c *gin.Context) {
}
// 3.3 更新code表
_, err = tx.Exec("UPDATE code SET deleted = TRUE WHERE country_id = $1", req.CountryID)
_, err = tx.Exec("UPDATE country_code SET deleted = TRUE WHERE country_id = $1", req.CountryID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ code表更新失败",
zap.L().Error("❌ country_code表更新失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.Error(err),
@@ -136,6 +137,26 @@ func DeleteHandler(c *gin.Context) {
return
}
// 新增3.4 更新flag表软删除国旗信息
_, err = tx.Exec("UPDATE country_flag SET deleted = TRUE WHERE country_id = $1", req.CountryID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ country_flag表更新失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除国旗信息失败",
})
return
}
zap.L().Debug("📝 flag表软删除成功",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
)
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()

View File

@@ -1,13 +1,14 @@
package logic
package logic4country
import (
"country/db"
"asset_assistant/db"
"net/http"
"strconv"
"time"
"strings"
"time"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
@@ -18,6 +19,7 @@ type ReadRequest struct {
CountryID string `form:"country_id"` // 国家ID可选
Name string `form:"name"` // 国家名称,可选
Code string `form:"code"` // 国家代码,可选
Flag string `form:"flag"` // 国旗信息,新增可选参数
Page string `form:"page"` // 页码,可选
PageSize string `form:"page_size"` // 每页条数,可选
}
@@ -26,15 +28,16 @@ type ReadRequest struct {
type ReadData struct {
Total int64 `json:"total"` // 总条数
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"`// 每页条数
PageSize int `json:"page_size"` // 每页条数
Items []CountryInfoViewItem `json:"items"` // 数据列表
}
// CountryInfoViewItem 视图数据项结构
// CountryInfoViewItem 视图数据项结构,新增国旗字段
type CountryInfoViewItem struct {
CountryID string `json:"country_id"` // 国家ID
Name string `json:"name"` // 国家名称
Code string `json:"code"` // 国家代码
Flag string `json:"flag"` // 国旗信息,新增字段
}
// ReadResponse 读取响应结构
@@ -44,7 +47,7 @@ type ReadResponse struct {
Data ReadData `json:"data"` // 响应数据
}
// ReadHandler 处理国家信息查询逻辑
// ReadHandler 处理查询逻辑
func ReadHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
@@ -75,19 +78,6 @@ func ReadHandler(c *gin.Context) {
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 {
@@ -103,6 +93,7 @@ func ReadHandler(c *gin.Context) {
zap.String("country_id", req.CountryID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("flag", req.Flag), // 新增国旗查询参数日志
zap.Int("page", page),
zap.Int("page_size", pageSize),
)
@@ -127,9 +118,15 @@ func ReadHandler(c *gin.Context) {
args = append(args, "%"+req.Code+"%")
paramIndex++
}
// 新增国旗查询条件
if req.Flag != "" {
whereClauses = append(whereClauses, "flag LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Flag+"%")
paramIndex++
}
// 构建基础SQL
baseSQL := "SELECT country_id, name, code FROM country_info_view"
// 构建基础SQL新增flag字段查询
baseSQL := "SELECT country_id, name, code, flag FROM country_info_view"
countSQL := "SELECT COUNT(*) FROM country_info_view"
if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
@@ -140,11 +137,11 @@ func ReadHandler(c *gin.Context) {
// 计算分页偏移量
offset := (page - 1) * pageSize
// 拼接分页SQL使用fmt.Sprintf更清晰
// 拼接分页SQL
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)
@@ -175,11 +172,11 @@ func ReadHandler(c *gin.Context) {
}
defer rows.Close()
// 处理查询结果
// 处理查询结果新增flag字段扫描
var items []CountryInfoViewItem
for rows.Next() {
var item CountryInfoViewItem
if err := rows.Scan(&item.CountryID, &item.Name, &item.Code); err != nil {
if err := rows.Scan(&item.CountryID, &item.Name, &item.Code, &item.Flag); err != nil {
zap.L().Error("❌ 解析查询结果失败",
zap.String("req_id", reqID),
zap.Error(err),

View File

@@ -0,0 +1,232 @@
package logic4country
import (
"asset_assistant/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"` // 国家代码,可选
Flag string `json:"flag"` // 国旗信息,可选(新增字段)
}
// 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和flag不能同时为空
if req.Name == "" && req.Code == "" && req.Flag == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("reason", "name、code和flag不能同时为空"),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误name、code和flag不能同时为空",
})
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),
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, 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 country_name SET name = $1, updated_at = CURRENT_TIMESTAMP WHERE country_id = $2", req.Name, req.CountryID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ country_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 country_code SET code = $1, updated_at = CURRENT_TIMESTAMP WHERE country_id = $2", req.Code, req.CountryID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ country_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),
)
}
// 新增如果flag不为空更新flag表
if req.Flag != "" {
// 先检查是否存在国旗记录
var flagExists bool
err = tx.QueryRow("SELECT EXISTS(SELECT 1 FROM country_flag WHERE country_id = $1 AND deleted = FALSE)", req.CountryID).Scan(&flagExists)
if 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
}
if flagExists {
// 存在则更新
_, err = tx.Exec("UPDATE country_flag SET flag = $1, updated_at = CURRENT_TIMESTAMP WHERE country_id = $2", req.Flag, req.CountryID)
} else {
// 不存在则插入新记录
_, err = tx.Exec("INSERT INTO country_flag (country_id, flag) VALUES ($1, $2)", req.CountryID, req.Flag)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ country_flag表更新/插入失败",
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("📝 flag表更新/插入成功",
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: "更新成功",
})
}

View File

@@ -1,19 +1,20 @@
package logic
package logic4currency
import (
"asset_assistant/db" // 数据库操作相关包
"net/http"
"country/db" // 数据库操作相关包
"time" // 时间处理包
"github.com/google/uuid" // UUID生成工具
"github.com/gin-gonic/gin" // Gin框架用于处理HTTP请求
"github.com/google/uuid" // UUID生成工具
"go.uber.org/zap" // 日志库
)
// CreateRequest 注册请求参数结构
// 用于接收客户端发送的JSON数据绑定并验证必填字段
type CreateRequest struct {
Name string `json:"name" binding:"required"` // 国家名称,必填
Code string `json:"code" binding:"required"` // 国家代码,必填
Name string `json:"name" binding:"required"` // 货币名称,必填
Code string `json:"code" binding:"required"` // 货币代码,必填
}
// CreateResponse 注册响应结构
@@ -25,13 +26,12 @@ type CreateResponse struct {
}
// CreateData 响应数据结构
// 包含创建成功后的国家ID
// 包含创建成功后的货币ID
type CreateData struct {
CountryID string `json:"country_id"` // 国家唯一标识ID
CurrencyID string `json:"currency_id"` // 货币唯一标识ID
}
// CreateHandler 处理国家创建逻辑
// 接收HTTP请求完成参数验证、数据库事务处理并返回响应
// CreateHandler 处理创建逻辑
func CreateHandler(c *gin.Context) {
startTime := time.Now() // 记录请求开始时间,用于统计耗时
// 获取或生成请求ID用于追踪整个请求链路
@@ -42,7 +42,7 @@ func CreateHandler(c *gin.Context) {
}
// 记录请求接收日志,包含关键追踪信息
zap.L().Info("📥 收到国家创建请求",
zap.L().Info("📥 收到货币创建请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
@@ -106,34 +106,34 @@ func CreateHandler(c *gin.Context) {
}
}()
// 1. 在country表中创建记录并获取自动生成的ID
var countryID string
err = tx.QueryRow("INSERT INTO country DEFAULT VALUES RETURNING id").Scan(&countryID)
// 1. 在currency表中创建记录并获取自动生成的ID
var currencyID string
err = tx.QueryRow("INSERT INTO currency DEFAULT VALUES RETURNING id").Scan(&currencyID)
if err != nil {
tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ country表插入失败",
zap.L().Error("❌ currency表插入失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建国家记录失败",
Message: "创建货币记录失败",
})
return
}
zap.L().Debug("📝 country表插入成功",
zap.L().Debug("📝 currency表插入成功",
zap.String("req_id", reqID),
zap.String("country_id", countryID),
zap.String("currency_id", currencyID),
)
// 2. 插入国家名称到name表与country_id关联
_, err = tx.Exec("INSERT INTO name (country_id, name) VALUES ($1, $2)", countryID, req.Name)
// 2. 插入货币名称到name表与currency_id关联
_, err = tx.Exec("INSERT INTO currency_name (currency_id, name) VALUES ($1, $2)", currencyID, req.Name)
if err != nil {
tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ name表插入失败",
zap.L().Error("❌ currency_name表插入失败",
zap.String("req_id", reqID),
zap.String("country_id", countryID),
zap.String("currency_id", currencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
@@ -143,13 +143,13 @@ func CreateHandler(c *gin.Context) {
return
}
// 3. 插入国家代码到code表与country_id关联
_, err = tx.Exec("INSERT INTO code (country_id, code) VALUES ($1, $2)", countryID, req.Code)
// 3. 插入货币代码到code表与currency_id关联
_, err = tx.Exec("INSERT INTO currency_code (currency_id, code) VALUES ($1, $2)", currencyID, req.Code)
if err != nil {
tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ code表插入失败",
zap.L().Error("❌ currency_code表插入失败",
zap.String("req_id", reqID),
zap.String("country_id", countryID),
zap.String("currency_id", currencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
@@ -164,7 +164,7 @@ func CreateHandler(c *gin.Context) {
tx.Rollback() // 提交失败时尝试回滚
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("country_id", countryID),
zap.String("currency_id", currencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
@@ -176,18 +176,18 @@ func CreateHandler(c *gin.Context) {
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 国家创建请求处理完成",
zap.L().Info("✅ 货币创建请求处理完成",
zap.String("req_id", reqID),
zap.String("country_id", countryID),
zap.String("currency_id", currencyID),
zap.Duration("duration", duration),
)
// 返回成功响应,包含创建的国家ID
// 返回成功响应,包含创建的货币ID
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
Data: CreateData{
CountryID: countryID,
CurrencyID: currencyID,
},
})
}

View File

@@ -0,0 +1,168 @@
package logic4currency
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DeleteRequest 删除请求参数结构
type DeleteRequest struct {
CurrencyID string `json:"currency_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: "请求参数错误currency_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
)
// 开启数据库事务
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 更新currency表
_, err = tx.Exec("UPDATE currency SET deleted = TRUE WHERE id = $1", req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ currency表更新失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除货币记录失败",
})
return
}
// 3.2 更新name表
_, err = tx.Exec("UPDATE currency_name SET deleted = TRUE WHERE currency_id = $1", req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ currency_name表更新失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除名称信息失败",
})
return
}
// 3.3 更新code表
_, err = tx.Exec("UPDATE currency_code SET deleted = TRUE WHERE currency_id = $1", req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ currency_code表更新失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
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("currency_id", req.CurrencyID),
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("currency_id", req.CurrencyID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, DeleteResponse{
Success: true,
Message: "删除成功",
})
}

View File

@@ -0,0 +1,231 @@
package logic4currency
import (
"asset_assistant/db"
"net/http"
"strconv"
"strings"
"time"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// ReadRequest 读取请求参数结构
type ReadRequest struct {
CurrencyID string `form:"currency_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 []CurrencyInfoViewItem `json:"items"` // 数据列表
}
// CurrencyInfoViewItem 视图数据项结构
type CurrencyInfoViewItem struct {
CurrencyID string `json:"currency_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.CurrencyID == "" && req.Name == "" && req.Code == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("reason", "currency_id、name、code不能同时为空"),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数错误currency_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("currency_id", req.CurrencyID),
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.CurrencyID != "" {
whereClauses = append(whereClauses, "currency_id = $"+strconv.Itoa(paramIndex))
args = append(args, req.CurrencyID)
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 currency_id, name, code FROM currency_info_view"
countSQL := "SELECT COUNT(*) FROM currency_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 currency_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 []CurrencyInfoViewItem
for rows.Next() {
var item CurrencyInfoViewItem
if err := rows.Scan(&item.CurrencyID, &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,
},
})
}

View File

@@ -1,7 +1,7 @@
package logic
package logic4currency
import (
"country/db"
"asset_assistant/db"
"net/http"
"time"
@@ -12,9 +12,9 @@ import (
// UpdateRequest 更新请求参数结构
type UpdateRequest struct {
CountryID string `json:"country_id" binding:"required"` // 国家ID必填
Name string `json:"name"` // 国家名称,可选
Code string `json:"code"` // 国家代码,可选
CurrencyID string `json:"currency_id" binding:"required"` // 货币ID必填
Name string `json:"name"` // 货币名称,可选
Code string `json:"code"` // 货币代码,可选
}
// UpdateResponse 更新响应结构
@@ -23,7 +23,7 @@ type UpdateResponse struct {
Message string `json:"message"` // 提示信息
}
// UpdateHandler 处理国家信息更新逻辑
// UpdateHandler 处理信息更新逻辑
func UpdateHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
@@ -34,14 +34,14 @@ func UpdateHandler(c *gin.Context) {
}
// 记录请求接收日志
zap.L().Info("📥 收到国家更新请求",
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必填
// 绑定并验证请求参数主要验证currency_id必填
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
@@ -49,7 +49,7 @@ func UpdateHandler(c *gin.Context) {
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误country_id为必填项",
Message: "请求参数错误currency_id为必填项",
})
return
}
@@ -58,7 +58,7 @@ func UpdateHandler(c *gin.Context) {
if req.Name == "" && req.Code == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
zap.String("reason", "name和code不能同时为空"),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
@@ -70,7 +70,7 @@ func UpdateHandler(c *gin.Context) {
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
zap.String("name", req.Name),
zap.String("code", req.Code),
)
@@ -111,12 +111,12 @@ func UpdateHandler(c *gin.Context) {
// 如果name不为空更新name表
if req.Name != "" {
_, err = tx.Exec("UPDATE name SET name = $1 WHERE country_id = $2", req.Name, req.CountryID)
_, err = tx.Exec("UPDATE currency_name SET name = $1 WHERE currency_id = $2", req.Name, req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ name表更新失败",
zap.L().Error("❌ currency_name表更新失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
@@ -127,18 +127,18 @@ func UpdateHandler(c *gin.Context) {
}
zap.L().Debug("📝 name表更新成功",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
)
}
// 如果code不为空更新code表
if req.Code != "" {
_, err = tx.Exec("UPDATE code SET code = $1 WHERE country_id = $2", req.Code, req.CountryID)
_, err = tx.Exec("UPDATE currency_code SET code = $1 WHERE currency_id = $2", req.Code, req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ code表更新失败",
zap.L().Error("❌ currency_code表更新失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
@@ -149,7 +149,7 @@ func UpdateHandler(c *gin.Context) {
}
zap.L().Debug("📝 code表更新成功",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
)
}
@@ -158,7 +158,7 @@ func UpdateHandler(c *gin.Context) {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
@@ -170,9 +170,9 @@ func UpdateHandler(c *gin.Context) {
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 国家更新请求处理完成",
zap.L().Info("✅ 货币更新请求处理完成",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("currency_id", req.CurrencyID),
zap.Duration("duration", duration),
)

View File

@@ -1,7 +1,7 @@
package logic4exchange
import (
"futures_trade_record/db"
"asset_assistant/db"
"net/http"
"time"
@@ -10,26 +10,28 @@ import (
"go.uber.org/zap"
)
// CreateExchangeRequest 交易所创建请求参数仅包含SQL中定义的必要字段
type CreateExchangeRequest struct {
Name string `json:"name" binding:"required"` // 交易所名称对应exchange_name表
Code string `json:"code" binding:"required"` // 交易所代码对应exchange_code表
// CreateRequest 注册请求参数结构
// 新增short_name字段保持必填性与其他核心字段一致
type CreateRequest struct {
Name string `json:"name" binding:"required"` // 交易所名称,必填
Code string `json:"code" binding:"required"` // 交易所代码,必填
ShortName string `json:"short_name" binding:"required"` // 交易所短名称,必填
}
// CreateExchangeResponse 响应结构
type CreateExchangeResponse struct {
// CreateResponse 注册响应结构
type CreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data CreateExchangeData `json:"data"`
Data CreateData `json:"data"`
}
// CreateExchangeData 响应数据
type CreateExchangeData struct {
// CreateData 响应数据结构
type CreateData struct {
ExchangeID string `json:"exchange_id"`
}
// CreateExchangeHandler 处理交易所创建逻辑
func CreateExchangeHandler(c *gin.Context) {
// CreateHandler 处理创建逻辑
func CreateHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-RegisterRequest-ID")
if reqID == "" {
@@ -43,17 +45,17 @@ func CreateExchangeHandler(c *gin.Context) {
zap.String("method", c.Request.Method),
)
var req CreateExchangeRequest
// 绑定并验证请求参数(仅验证namecode
var req CreateRequest
// 绑定参数时会自动验证namecode、short_name三个必填字段
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("🔴 请求参数验证失败",
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
zap.Any("request_body", c.Request.Body),
)
c.JSON(http.StatusBadRequest, CreateExchangeResponse{
c.JSON(http.StatusBadRequest, CreateResponse{
Success: false,
Message: "请求参数错误namecode为必填项",
Message: "请求参数错误namecode和short_name为必填项",
})
return
}
@@ -62,21 +64,22 @@ func CreateExchangeHandler(c *gin.Context) {
zap.String("req_id", reqID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("short_name", req.ShortName), // 新增短名称日志
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateExchangeResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
@@ -89,14 +92,14 @@ func CreateExchangeHandler(c *gin.Context) {
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, CreateExchangeResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 1. exchange主表创建记录并获取ID
// 1. 创建exchange主记录
var exchangeID string
err = tx.QueryRow("INSERT INTO exchange DEFAULT VALUES RETURNING id").Scan(&exchangeID)
if err != nil {
@@ -105,14 +108,19 @@ func CreateExchangeHandler(c *gin.Context) {
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateExchangeResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建交易所记录失败",
})
return
}
// 2. 插入交易所名称到exchange_name子表
zap.L().Debug("📝 exchange表插入成功",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
)
// 2. 插入名称信息
_, err = tx.Exec("INSERT INTO exchange_name (exchange_id, name) VALUES ($1, $2)", exchangeID, req.Name)
if err != nil {
tx.Rollback()
@@ -121,14 +129,30 @@ func CreateExchangeHandler(c *gin.Context) {
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateExchangeResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存名称信息失败",
})
return
}
// 3. 插入交易所代码到exchange_code子表
// 3. 新增插入短名称信息对应exchange_short_name表
_, err = tx.Exec("INSERT INTO exchange_short_name (exchange_id, short_name) VALUES ($1, $2)", exchangeID, req.ShortName)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_short_name表插入失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存短名称信息失败",
})
return
}
// 4. 插入代码信息
_, err = tx.Exec("INSERT INTO exchange_code (exchange_id, code) VALUES ($1, $2)", exchangeID, req.Code)
if err != nil {
tx.Rollback()
@@ -137,7 +161,7 @@ func CreateExchangeHandler(c *gin.Context) {
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateExchangeResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存代码信息失败",
})
@@ -152,14 +176,13 @@ func CreateExchangeHandler(c *gin.Context) {
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateExchangeResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
// 记录处理耗时并返回成功响应
duration := time.Since(startTime)
zap.L().Info("✅ 交易所创建请求处理完成",
zap.String("req_id", reqID),
@@ -167,10 +190,10 @@ func CreateExchangeHandler(c *gin.Context) {
zap.Duration("duration", duration),
)
c.JSON(http.StatusOK, CreateExchangeResponse{
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
Data: CreateExchangeData{
Data: CreateData{
ExchangeID: exchangeID,
},
})

View File

@@ -0,0 +1,184 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DeleteRequest 删除请求参数结构
type DeleteRequest struct {
ExchangeID string `json:"exchange_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: "请求参数错误exchange_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
// 开启数据库事务
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: "系统错误,请稍后重试",
})
}
}()
// 1. 更新exchange表
_, err = tx.Exec("UPDATE exchange SET deleted = TRUE WHERE id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除交易所记录失败",
})
return
}
// 2. 更新exchange_name表
_, err = tx.Exec("UPDATE exchange_name SET deleted = TRUE WHERE exchange_id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除名称信息失败",
})
return
}
// 3. 新增更新exchange_short_name表软删除短名称记录
_, err = tx.Exec("UPDATE exchange_short_name SET deleted = TRUE WHERE exchange_id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_short_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除短名称信息失败",
})
return
}
// 4. 更新exchange_code表
_, err = tx.Exec("UPDATE exchange_code SET deleted = TRUE WHERE exchange_id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_code表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
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("exchange_id", req.ExchangeID),
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("exchange_id", req.ExchangeID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, DeleteResponse{
Success: true,
Message: "删除成功",
})
}

View File

@@ -0,0 +1,240 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"strconv"
"strings"
"time"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// ReadRequest 读取请求参数结构
type ReadRequest struct {
ExchangeID string `form:"exchange_id"` // 交易所ID可选
Name string `form:"name"` // 交易所名称,可选
Code string `form:"code"` // 交易所代码,可选
ShortName string `form:"short_name"` // 交易所短名称,新增查询条件
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 []ExchangeInfoViewItem `json:"items"` // 数据列表
}
// ExchangeInfoViewItem 视图数据项结构
type ExchangeInfoViewItem struct {
ExchangeID string `json:"exchange_id"` // 交易所ID
Name string `json:"name"` // 交易所名称
Code string `json:"code"` // 交易所代码
ShortName string `json:"short_name"` // 新增:交易所短名称
}
// 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
}
// 验证查询条件至少有一个不为空新增short_name作为可选条件
if req.ExchangeID == "" && req.Name == "" && req.Code == "" && req.ShortName == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("reason", "exchange_id、name、code、short_name不能同时为空"),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数错误exchange_id、name、code、short_name不能同时为空",
})
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("exchange_id", req.ExchangeID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("short_name", req.ShortName), // 新增短名称日志
zap.Int("page", page),
zap.Int("page_size", pageSize),
)
// 构建查询条件和参数
whereClauses := []string{}
args := []interface{}{}
paramIndex := 1
if req.ExchangeID != "" {
whereClauses = append(whereClauses, "exchange_id = $"+strconv.Itoa(paramIndex))
args = append(args, req.ExchangeID)
paramIndex++
}
if req.Name != "" {
whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Name+"%")
paramIndex++
}
// 新增:短名称查询条件
if req.ShortName != "" {
whereClauses = append(whereClauses, "short_name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.ShortName+"%")
paramIndex++
}
if req.Code != "" {
whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Code+"%")
paramIndex++
}
// 构建基础SQL新增查询short_name字段
baseSQL := "SELECT exchange_id, name, short_name, code FROM exchange_info_view"
countSQL := "SELECT COUNT(*) FROM exchange_info_view"
if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
baseSQL += whereStr
countSQL += whereStr
}
// 计算分页偏移量
offset := (page - 1) * pageSize
// 拼接分页SQL
querySQL := fmt.Sprintf("%s ORDER BY exchange_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()
// 处理查询结果新增扫描short_name字段
var items []ExchangeInfoViewItem
for rows.Next() {
var item ExchangeInfoViewItem
if err := rows.Scan(&item.ExchangeID, &item.Name, &item.ShortName, &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,
},
})
}

View File

@@ -0,0 +1,208 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// UpdateRequest 更新请求参数结构
type UpdateRequest struct {
ExchangeID string `json:"exchange_id" binding:"required"` // 交易所ID必填
Name string `json:"name"` // 交易所名称,可选
Code string `json:"code"` // 交易所代码,可选
ShortName string `json:"short_name"` // 新增:交易所短名称,可选
}
// 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
// 绑定并验证请求参数主要验证exchange_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: "请求参数错误exchange_id为必填项",
})
return
}
// 验证name、code和short_name不能同时为空
if req.Name == "" && req.Code == "" && req.ShortName == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.String("reason", "name、code和short_name不能同时为空"),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误name、code和short_name不能同时为空",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("short_name", req.ShortName), // 新增短名称日志
)
// 开启数据库事务
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 exchange_name SET name = $1 WHERE exchange_id = $2", req.Name, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新名称信息失败",
})
return
}
zap.L().Debug("📝 name表更新成功",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
}
// 如果code不为空更新code表
if req.Code != "" {
_, err = tx.Exec("UPDATE exchange_code SET code = $1 WHERE exchange_id = $2", req.Code, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_code表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新代码信息失败",
})
return
}
zap.L().Debug("📝 code表更新成功",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
}
// 新增如果short_name不为空更新short_name表
if req.ShortName != "" {
_, err = tx.Exec("UPDATE exchange_short_name SET short_name = $1 WHERE exchange_id = $2", req.ShortName, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_short_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新短名称信息失败",
})
return
}
zap.L().Debug("📝 short_name表更新成功",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
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("exchange_id", req.ExchangeID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, UpdateResponse{
Success: true,
Message: "更新成功",
})
}

View File

@@ -1,9 +1,9 @@
package logic
package logic4user
import (
"asset_assistant/db"
"database/sql"
"net/http"
"user/db"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -30,7 +30,7 @@ type LoginResponse struct {
} `json:"data"`
}
// LoginHandler 处理用户登录请求的处理器函数
// LoginHandler 处理登录请求的处理器函数
// 参数c是gin.Context用于获取请求信息和返回响应
func LoginHandler(c *gin.Context) {
// 获取请求ID用于追踪请求链路若请求头中没有则生成一个新的UUID
@@ -54,7 +54,7 @@ func LoginHandler(c *gin.Context) {
zap.Error(err),
zap.Any("请求体", c.Request.Body),
)
c.JSON(http.StatusBadRequest, LoginResponse{
c.JSON(http.StatusOK, LoginResponse{
Success: false,
Message: "账号或密码不能为空",
})
@@ -72,7 +72,7 @@ func LoginHandler(c *gin.Context) {
zap.String("reqID", reqID),
zap.String("账号", req.Account),
)
c.JSON(http.StatusBadRequest, LoginResponse{
c.JSON(http.StatusOK, LoginResponse{
Success: false,
Message: "账号或密码不能为空",
})

View File

@@ -1,9 +1,9 @@
package logic
package logic4user
import (
"asset_assistant/db"
"net/http"
"time"
"user/db"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -28,7 +28,7 @@ type RegisterResponse struct {
} `json:"data"`
}
// registerHandler 处理用户注册逻辑
// registerHandler 处理注册逻辑
func RegisterHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-RegisterRequest-ID")
@@ -171,7 +171,7 @@ func RegisterHandler(c *gin.Context) {
// 7. 插入account表
insertAccountQuery := `
INSERT INTO account (user_id, account)
INSERT INTO user_account (user_id, account)
VALUES ($1, $2)
`
zap.L().Info("💡 执行账号插入",
@@ -196,7 +196,7 @@ func RegisterHandler(c *gin.Context) {
// 8. 插入password表
insertPasswordQuery := `
INSERT INTO password (user_id, password)
INSERT INTO user_password (user_id, password)
VALUES ($1, $2)
`
zap.L().Info("💡 执行密码插入",

View File

@@ -1,39 +1,40 @@
package logic4variety
import (
"futures_trade_record/db" // 数据库操作相关包
"asset_assistant/db"
"net/http"
"time" // 时间处理包
"time"
"github.com/gin-gonic/gin" // Gin框架用于处理HTTP请求
"github.com/google/uuid" // UUID生成工具
"go.uber.org/zap" // 日志库
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// CreateVarietyRequest 注册请求参数结构
type CreateVarietyRequest struct {
// CreateRequest 创建品种请求参数结构
type CreateRequest struct {
Name string `json:"name" binding:"required"` // 品种名称,必填
Code string `json:"code" binding:"required"` // 品种代码,必填
Tick float64 `json:"tick" binding:"min=0"` // 跳点值必填需大于等于0
TickPrice float64 `json:"tick_price" binding:"min=0"` // 跳点价格必填需大于等于0
Tick float64 `json:"tick" binding:"required"` // 品种tick必填
TickPrice float64 `json:"tick_price" binding:"required"` // 品种tick价格必填
ExchangeID string `json:"exchange_id" binding:"required"` // 交易所ID必填
}
// CreateVarietyResponse 注册响应结构
type CreateVarietyResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
Data CreateVarietyData `json:"data"` // 响应数据
// CreateResponse 创建品种响应结构
type CreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data CreateData `json:"data"`
}
// CreateVarietyData 响应数据结构
type CreateVarietyData struct {
VarietyID string `json:"variety_id"` // 品种唯一标识ID
// CreateData 响应数据结构
type CreateData struct {
VarietyID string `json:"variety_id"`
}
// CreateVarietyHandler 处理品种创建逻辑
func CreateVarietyHandler(c *gin.Context) {
// CreateHandler 处理创建品种逻辑
func CreateHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-RegisterRequest-ID")
reqID := c.Request.Header.Get("X-VarietyCreateRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
@@ -45,17 +46,17 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("method", c.Request.Method),
)
var req CreateVarietyRequest
// 绑定并验证请求参数包含新增的tick和tick_price
var req CreateRequest
// 绑定参数时会自动验证所有必填字段
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("🔴 请求参数验证失败",
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
zap.Any("request_body", c.Request.Body),
)
c.JSON(http.StatusBadRequest, CreateVarietyResponse{
c.JSON(http.StatusBadRequest, CreateResponse{
Success: false,
Message: "请求参数错误name、code、ticktick_price为必填项且tick和tick_price需大于等于0",
Message: "请求参数错误name、code、ticktick_price和exchange_id为必填项",
})
return
}
@@ -66,21 +67,22 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("code", req.Code),
zap.Float64("tick", req.Tick),
zap.Float64("tick_price", req.TickPrice),
zap.String("exchange_id", req.ExchangeID),
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
@@ -93,14 +95,14 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 1. variety表中创建记录并获取自动生成的ID
// 1. 创建variety主记录
var varietyID string
err = tx.QueryRow("INSERT INTO variety DEFAULT VALUES RETURNING id").Scan(&varietyID)
if err != nil {
@@ -109,7 +111,7 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建品种记录失败",
})
@@ -121,7 +123,23 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("variety_id", varietyID),
)
// 2. 插入品种名称到variety_name表修正表名
// 2. 插入交易所关联信息
_, err = tx.Exec("INSERT INTO variety_exchange (variety_id, exchange_id) VALUES ($1, $2)", varietyID, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存交易所关联信息失败",
})
return
}
// 3. 插入名称信息
_, err = tx.Exec("INSERT INTO variety_name (variety_id, name) VALUES ($1, $2)", varietyID, req.Name)
if err != nil {
tx.Rollback()
@@ -130,14 +148,14 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存名称信息失败",
})
return
}
// 3. 插入品种代码到variety_code表修正表名
// 4. 插入代码信息
_, err = tx.Exec("INSERT INTO variety_code (variety_id, code) VALUES ($1, $2)", varietyID, req.Code)
if err != nil {
tx.Rollback()
@@ -146,14 +164,14 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存代码信息失败",
})
return
}
// 4. 插入跳点值到variety_tick表新增
// 5. 插入tick信息
_, err = tx.Exec("INSERT INTO variety_tick (variety_id, tick) VALUES ($1, $2)", varietyID, req.Tick)
if err != nil {
tx.Rollback()
@@ -162,14 +180,14 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存跳点信息失败",
Message: "保存tick信息失败",
})
return
}
// 5. 插入跳点价格到variety_tick_price表新增
// 6. 插入tick价格信息
_, err = tx.Exec("INSERT INTO variety_tick_price (variety_id, price) VALUES ($1, $2)", varietyID, req.TickPrice)
if err != nil {
tx.Rollback()
@@ -178,9 +196,9 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存跳点价格信息失败",
Message: "保存tick价格信息失败",
})
return
}
@@ -193,14 +211,13 @@ func CreateVarietyHandler(c *gin.Context) {
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateVarietyResponse{
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 品种创建请求处理完成",
zap.String("req_id", reqID),
@@ -208,11 +225,10 @@ func CreateVarietyHandler(c *gin.Context) {
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, CreateVarietyResponse{
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
Data: CreateVarietyData{
Data: CreateData{
VarietyID: varietyID,
},
})

View File

@@ -0,0 +1,216 @@
package logic4variety
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DeleteRequest 删除请求参数结构
type DeleteRequest struct {
VarietyID string `json:"variety_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: "请求参数错误variety_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
// 开启数据库事务
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: "系统错误,请稍后重试",
})
}
}()
// 1. 更新variety表
_, err = tx.Exec("UPDATE variety SET deleted = TRUE WHERE id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除品种记录失败",
})
return
}
// 2. 更新variety_exchange表
_, err = tx.Exec("UPDATE variety_exchange SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除交易所关联信息失败",
})
return
}
// 3. 更新variety_name表
_, err = tx.Exec("UPDATE variety_name SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_name表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除名称信息失败",
})
return
}
// 4. 更新variety_code表
_, err = tx.Exec("UPDATE variety_code SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除代码信息失败",
})
return
}
// 5. 更新variety_tick表
_, err = tx.Exec("UPDATE variety_tick SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除最小变动价位信息失败",
})
return
}
// 6. 更新variety_tick_price表
_, err = tx.Exec("UPDATE variety_tick_price SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
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("variety_id", req.VarietyID),
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("variety_id", req.VarietyID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, DeleteResponse{
Success: true,
Message: "删除成功",
})
}

View File

@@ -0,0 +1,243 @@
package logic4variety
import (
"asset_assistant/db"
"net/http"
"strconv"
"strings"
"time"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// ReadRequest 读取请求参数结构
type ReadRequest struct {
VarietyID string `form:"variety_id"` // 品种ID可选
Name string `form:"name"` // 品种名称,可选
Code string `form:"code"` // 品种代码,可选
ExchangeName string `form:"exchange_name"` // 交易所名称,可选
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 []VarietyInfoViewItem `json:"items"` // 数据列表
}
// VarietyInfoViewItem 视图数据项结构
type VarietyInfoViewItem struct {
VarietyID string `json:"variety_id"` // 品种ID
Name string `json:"name"` // 品种名称
Code string `json:"code"` // 品种代码
ExchangeName string `json:"exchange_name"` // 交易所名称
Tick string `json:"tick"` // 最小变动价位
TickPrice string `json:"tick_price"` // 最小变动价位对应的价值
TickOriginal float64 `json:"tick_original"` // 最小变动价位(原始值)
TickPriceOriginal float64 `json:"tick_price_original"` // 最小变动价位对应的价值(原始值)
}
// 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.VarietyID == "" && req.Name == "" && req.Code == "" && req.ExchangeName == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("reason", "variety_id、name、code、exchange_name不能同时为空"),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数错误variety_id、name、code、exchange_name不能同时为空",
})
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("variety_id", req.VarietyID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("exchange_name", req.ExchangeName),
zap.Int("page", page),
zap.Int("page_size", pageSize),
)
// 构建查询条件和参数
whereClauses := []string{}
args := []interface{}{}
paramIndex := 1
if req.VarietyID != "" {
whereClauses = append(whereClauses, "variety_id = $"+strconv.Itoa(paramIndex))
args = append(args, req.VarietyID)
paramIndex++
}
if req.Name != "" {
whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Name+"%")
paramIndex++
}
if req.ExchangeName != "" {
whereClauses = append(whereClauses, "exchange_name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.ExchangeName+"%")
paramIndex++
}
if req.Code != "" {
whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Code+"%")
paramIndex++
}
// 构建基础SQL
baseSQL := "SELECT variety_id, name, code, exchange_name, tick, tick_price, tick_original, tick_price_original FROM variety_info_view"
countSQL := "SELECT COUNT(*) FROM variety_info_view"
if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
baseSQL += whereStr
countSQL += whereStr
}
// 计算分页偏移量
offset := (page - 1) * pageSize
// 拼接分页SQL
querySQL := fmt.Sprintf("%s ORDER BY variety_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 []VarietyInfoViewItem
for rows.Next() {
var item VarietyInfoViewItem
if err := rows.Scan(&item.VarietyID, &item.Name, &item.Code, &item.ExchangeName, &item.Tick, &item.TickPrice, &item.TickOriginal, &item.TickPriceOriginal); 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,
},
})
}

View File

@@ -0,0 +1,370 @@
package logic4variety
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// UpdateRequest 更新请求参数结构
type UpdateRequest struct {
VarietyID string `json:"variety_id" binding:"required"` // 品种ID必填
ExchangeID string `json:"exchange_id"` // 交易所ID可选
ExchangeName string `json:"exchange_name"` // 交易所名称,可选
Name string `json:"name"` // 品种名称,可选
Code string `json:"code"` // 品种代码,可选
Tick float64 `json:"tick"` // 最小变动价位,可选
TickPrice float64 `json:"tick_price"` // 最小变动价值,可选
}
// 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
// 绑定并验证请求参数主要验证variety_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: "请求参数错误variety_id为必填项",
})
return
}
// 验证所有可选字段不能同时为空
if req.ExchangeID == "" && req.ExchangeName == "" && req.Name == "" && req.Code == "" && req.Tick == 0 && req.TickPrice == 0 {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.String("reason", "所有更新字段不能同时为空"),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误:至少提供一个需要更新的字段",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.String("exchange_id", req.ExchangeID),
zap.String("exchange_name", req.ExchangeName),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.Float64("tick", req.Tick),
zap.Float64("tick_price", req.TickPrice),
)
// 开启数据库事务
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: "系统错误,请稍后重试",
})
}
}()
// 如果exchange_id或exchange_name不为空更新variety_exchange表
if req.ExchangeID != "" || req.ExchangeName != "" {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_exchange WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询交易所信息失败",
})
return
}
if count > 0 {
// 更新现有记录
_, err = tx.Exec("UPDATE variety_exchange SET exchange_id = $1, exchange_name = $2 WHERE variety_id = $3", req.ExchangeID, req.ExchangeName, req.VarietyID)
} else {
// 插入新记录
_, err = tx.Exec("INSERT INTO variety_exchange (variety_id, exchange_id, exchange_name) VALUES ($1, $2, $3)", req.VarietyID, req.ExchangeID, req.ExchangeName)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新交易所信息失败",
})
return
}
zap.L().Debug("📝 variety_exchange表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果name不为空更新variety_name表
if req.Name != "" {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_name WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_name表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询品种名称失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_name SET name = $1 WHERE variety_id = $2", req.Name, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_name (variety_id, name) VALUES ($1, $2)", req.VarietyID, req.Name)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_name表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新品种名称失败",
})
return
}
zap.L().Debug("📝 variety_name表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果code不为空更新variety_code表
if req.Code != "" {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_code WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询品种代码失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_code SET code = $1 WHERE variety_id = $2", req.Code, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_code (variety_id, code) VALUES ($1, $2)", req.VarietyID, req.Code)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新品种代码失败",
})
return
}
zap.L().Debug("📝 variety_code表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果tick不为0更新variety_tick表
if req.Tick != 0 {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_tick WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询最小变动价位失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_tick SET tick = $1 WHERE variety_id = $2", req.Tick, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_tick (variety_id, tick) VALUES ($1, $2)", req.VarietyID, req.Tick)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新最小变动价位失败",
})
return
}
zap.L().Debug("📝 variety_tick表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果tick_price不为0更新variety_tick_price表
if req.TickPrice != 0 {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_tick_price WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询最小变动价值失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_tick_price SET price = $1 WHERE variety_id = $2", req.TickPrice, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_tick_price (variety_id, price) VALUES ($1, $2)", req.VarietyID, req.TickPrice)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新最小变动价值失败",
})
return
}
zap.L().Debug("📝 variety_tick_price表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
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("variety_id", req.VarietyID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, UpdateResponse{
Success: true,
Message: "更新成功",
})
}

View File

@@ -1,10 +1,12 @@
package main
import (
"futures_trade_record/db" // 数据库相关操作包
"futures_trade_record/logger" // 日志工具包
"futures_trade_record/logic4exchange"
"futures_trade_record/logic4variety"
"asset_assistant/db" // 数据库相关操作包
"asset_assistant/logger" // 日志工具包
"asset_assistant/logic4country"
"asset_assistant/logic4currency"
"asset_assistant/logic4exchange"
"asset_assistant/logic4user"
// 业务逻辑处理包
"time"
@@ -20,10 +22,10 @@ func main() {
// 初始化日志配置
logger.Init()
// 记录服务初始化日志
zap.L().Info("🟢 用户服务初始化")
zap.L().Info("🚀 用户服务初始化")
// 记录数据库初始化开始日志
zap.L().Info("🟢 数据库初始化开始")
zap.L().Info("⌛️ 数据库初始化开始")
// 初始化数据库连接
db.Init()
// 程序退出时关闭数据库连接defer确保在函数退出前执行
@@ -53,20 +55,44 @@ func main() {
}))
zap.L().Info("✅ 配置跨域中间件完成")
// 注册品种接口
variety := r.Group("/variety")
// 注册用户接口
user := r.Group("/user")
{
variety.POST("/create", logic4variety.CreateVarietyHandler)
user.POST("/register", logic4user.RegisterHandler)
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("✅ 国家接口注册完成")
// 注册交易所接口
exchangeGroup := r.Group("/exchange")
exchange := r.Group("/exchange")
{
exchangeGroup.POST("/create", logic4exchange.CreateExchangeHandler)
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端口
zap.L().Info("✅ 服务启动在80端口")
r.Run(":80")

View File

@@ -1,21 +0,0 @@
# 数据库配置
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

@@ -1,86 +0,0 @@
#!/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="user-api"
IMAGE_TAG="1.0.0"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
COMPOSE_PROJECT_NAME="user_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 "===== 构建脚本执行完成 ====="

View File

@@ -1 +0,0 @@
docker run -itd --name go_user_dev -v $(pwd)/src:/app -p 20000:80 golang:1.25.0-alpine3.22

View File

@@ -1,35 +0,0 @@
#!/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

View File

@@ -1,59 +0,0 @@
#!/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进程已退出"

View File

@@ -1,48 +0,0 @@
-- 切换到目标数据库
\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;

View File

@@ -1,30 +0,0 @@
-- 切换到目标数据库
\c postgres;
CREATE OR REPLACE FUNCTION update_user_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 = 'user') THEN
CREATE TABLE "user" ( -- user是关键字用双引号包裹
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_user_updated_at
BEFORE UPDATE ON "user"
FOR EACH ROW
EXECUTE FUNCTION update_user_modified_column();
RAISE NOTICE 'Created user table and trigger';
ELSE
RAISE NOTICE 'user table already exists';
END IF;
END $$;

View File

@@ -1,32 +0,0 @@
-- 切换到目标数据库
\c postgres;
CREATE OR REPLACE FUNCTION update_account_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 = 'account') THEN
CREATE TABLE account (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
user_id UUID NOT NULL,
account 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_account_updated_at
BEFORE UPDATE ON "account"
FOR EACH ROW
EXECUTE FUNCTION update_account_modified_column();
RAISE NOTICE 'Created account table and trigger';
ELSE
RAISE NOTICE 'account table already exists';
END IF;
END $$;

View File

@@ -1,32 +0,0 @@
-- 切换到目标数据库
\c postgres;
CREATE OR REPLACE FUNCTION update_password_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 = 'password') THEN
CREATE TABLE password (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
user_id UUID NOT NULL,
password 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_password_updated_at
BEFORE UPDATE ON "password"
FOR EACH ROW
EXECUTE FUNCTION update_password_modified_column();
RAISE NOTICE 'Created password table and trigger';
ELSE
RAISE NOTICE 'password table already exists';
END IF;
END $$;

View File

@@ -1,38 +0,0 @@
\c postgres;
DO $$
DECLARE
view_exists BOOLEAN;
BEGIN
-- 检查视图是否已存在
SELECT EXISTS (
SELECT 1 FROM information_schema.views
WHERE table_name = 'user_info_view'
) INTO view_exists;
-- 创建或更新视图
CREATE OR REPLACE VIEW user_info_view AS
SELECT
u.id AS user_id,
ua.account AS account,
up.password AS password,
u.deleted AS deleted
FROM
"user" u
JOIN
account ua ON u.id = ua.user_id
JOIN
password up ON u.id = up.user_id
WHERE
u.deleted = FALSE;
-- 根据视图是否已存在输出不同提示
IF view_exists THEN
RAISE NOTICE '视图 user_info_view 已更新';
ELSE
RAISE NOTICE '视图 user_info_view 已创建';
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '处理视图时发生错误: %', SQLERRM;
END $$;

View File

@@ -1,54 +0,0 @@
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,38 +0,0 @@
# ==================== 第一阶段构建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"]

View File

@@ -1,133 +0,0 @@
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=

View File

@@ -1,86 +0,0 @@
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,53 +0,0 @@
package main
import (
"user/db"
"user/logger"
"user/logic"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
"go.uber.org/zap"
"time"
)
func main() {
logger.Init()
zap.L().Info("🚀 用户服务初始化")
zap.L().Info("⌛️ 数据库初始化开始")
db.Init()
defer db.DB.Close() // 应用退出时关闭连接
zap.L().Info("✅ 数据库初始化成功")
gin.SetMode(gin.ReleaseMode)
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("✅ 配置跨域中间件完成")
// 登录接口
r.POST("/user/login", logic.LoginHandler)
zap.L().Info("✅ 登录接口注册完成: POST /user/login")
// 注册接口
r.POST("/user/register", logic.RegisterHandler)
zap.L().Info("✅ 注册接口注册完成: POST /user/register")
// 启动服务监听80端口
zap.L().Info("✅ 服务启动在80端口")
r.Run(":80")
}

View File

@@ -1,34 +0,0 @@
---
分析这个项目,在 create.go 中完成以下需求:
1、接收 namecode 两个参数。
2、确认提交的 namecode 两个参数不能为空,如果有空,则返回提示。
3、第二步通过后在 country 表中,通过: "INSERT INTO country DEFAULT VALUES RETURNING id" 获得ID。
4、通过 3 中的 id开启事务保存到 name 和 code 的表中。
---
分析这个项目,在 delete.go 中完成以下需求:
1、接收 country_id 参数。
2、确认提交的 country_id 参数不能为空,如果有空,则返回提示。
3、开启事务处理以下逻辑
3.1、把 country 中country.id==req.country_id 的 deleted 字段更新为true。
3.2、把 name 中name.country_id==req.country_id 的 deleted 字段更新为true。
3.3、把 code code.country_id==req.country_id 的 deleted 字段更新为true。
---
分析这个项目,在 update.go 中完成以下需求:
1、接收 country_id,namecode 参数。
2、确认提交的 country_id 参数有不能为空,如果为空,则返回提示。
3、确认提交的 namecode 两个参数,必须有一个不能为空,如果都为空,则返回提示。
4、如果 name 不为空,开启事务保存到 name 中。
5、如果 code 不为空,开启事务保存到 code 中。
6、如果 namecode 都不为空,开启事务保存到 namecode 中。
---
分析这个项目,在 read.go 中完成以下需求:
1、接收 country_idnamecodepagepage_size 参数。
2、确认提交的 country_idnamecode 必须有一个不能为空,如果都为空,则返回提示。
3、确认提交的 pagepage_size, 如果为空,则 page 默认为 1page_size 默认为20。
3、根据参数去 country_info_view 中查找数据,并做分页查询。
4、将查找的数据分页返回。
---

View File

@@ -1,3 +1,4 @@
import 'package:asset_assistant/pages/country_page.dart';
import 'package:asset_assistant/pages/exchange_add_page.dart';
import 'package:asset_assistant/pages/exchange_page.dart';
import 'package:asset_assistant/pages/home_page.dart';
@@ -27,8 +28,9 @@ class MyApp extends StatelessWidget {
routes: {
'/login': (context) => const LoginPage(),
'/home': (context) => HomePage(),
'/country': (context) => CountryPage(),
'/exchange': (context) => ExchangePage(),
'/exchange/add': (context) => AddExchangePage(), // 推荐首选
'/exchange/add': (context) => AddExchangePage(),
},
theme: ThemeData(
// 金融暗夜风格主题配置

View File

@@ -0,0 +1,238 @@
import 'package:asset_assistant/utils/host_utils.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AddCountryPage extends StatefulWidget {
const AddCountryPage({super.key});
@override
State<AddCountryPage> createState() => _AddCountryPageState();
}
class _AddCountryPageState extends State<AddCountryPage> {
// 输入控制器 - 新增国旗控制器
final TextEditingController _nameController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
final TextEditingController _flagController = TextEditingController();
// 加载状态
bool _isLoading = false;
// 表单验证键
final _formKey = GlobalKey<FormState>();
// 创建国家 - 调整请求数据
Future<void> _createCountry() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// 获取用户ID保持不变
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('user_id');
if (userId == null) {
if (mounted) {
_showDialog('错误', '请先登录');
Navigator.pushReplacementNamed(context, '/login');
}
return;
}
// 准备请求数据 - 新增flag字段
final baseUrl = HostUtils().currentHost;
const path = '/country/create';
final url = '$baseUrl$path';
final requestData = {
'name': _nameController.text.trim(),
'code': _codeController.text.trim(),
'flag': _flagController.text.trim(), // 新增国旗参数
};
// 发送请求(保持不变)
final dio = Dio();
final response = await dio.post(
url,
data: requestData,
options: Options(headers: {'Content-Type': 'application/json'}),
);
// 处理响应(保持不变)
if (response.statusCode == 200) {
final result = response.data;
if (result['success'] == true) {
if (mounted) {
_showDialog('成功', '国家创建成功', () {
Navigator.pop(context, true); // 返回并通知上一页刷新
});
}
} else {
if (mounted) {
_showDialog('失败', result['message'] ?? '创建失败,请重试');
}
}
} else {
if (mounted) {
// 处理400错误时获取服务器返回的具体消息
String errorMessage = '服务器响应异常: ${response.statusCode}';
if (response.statusCode == 400 && response.data != null) {
errorMessage = response.data['message'] ?? errorMessage;
}
_showDialog('错误', errorMessage);
}
}
} on DioException catch (e) {
// 异常处理(优化错误信息提取)
String errorMessage = '网络请求失败';
if (e.response != null) {
// 从响应数据中提取错误信息
if (e.response?.data != null && e.response?.data['message'] != null) {
errorMessage = e.response?.data['message'];
} else {
errorMessage = '请求失败: ${e.response?.statusCode}';
}
} else if (e.type == DioExceptionType.connectionTimeout) {
errorMessage = '连接超时,请检查网络';
} else if (e.type == DioExceptionType.connectionError) {
errorMessage = '网络连接错误';
}
if (mounted) {
_showDialog('错误', errorMessage);
}
} catch (e) {
if (mounted) {
_showDialog('错误', '发生未知错误: $e');
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
// 显示对话框(保持不变)
void _showDialog(String title, String content, [VoidCallback? onConfirm]) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
onConfirm?.call();
},
child: const Text('确定'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('新增国家'),
centerTitle: true,
elevation: 4,
shadowColor: Colors.black12,
backgroundColor: theme.colorScheme.surfaceContainerHighest,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _isLoading ? null : _createCountry,
),
],
),
body: SafeArea(
child: Container(
color: theme.colorScheme.surface,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Form(
key: _formKey,
child: Column(
children: [
// 国家名称输入框(保持不变)
TextFormField(
controller: _nameController,
style: TextStyle(color: theme.colorScheme.onSurface),
decoration: InputDecoration(
labelText: '国家名称',
hintText: '请输入国家名称',
prefixIcon: Icon(
Icons.account_balance,
color: theme.colorScheme.secondary,
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入国家名称';
}
return null;
},
),
const SizedBox(height: 24),
// 国家代码输入框(保持不变)
TextFormField(
controller: _codeController,
style: TextStyle(color: theme.colorScheme.onSurface),
decoration: InputDecoration(
labelText: '国家代码',
hintText: '请输入国家代码',
prefixIcon: Icon(
Icons.code,
color: theme.colorScheme.secondary,
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入国家代码';
}
return null;
},
),
const SizedBox(height: 24),
// 新增国旗输入框
TextFormField(
controller: _flagController,
style: TextStyle(color: theme.colorScheme.onSurface),
decoration: InputDecoration(
labelText: '国旗emoji',
hintText: '请输入国旗图片emoji',
prefixIcon: Icon(
Icons.flag,
color: theme.colorScheme.secondary,
),
),
// 国旗为可选字段,不添加验证器
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,439 @@
import 'package:asset_assistant/pages/country_add_page.dart';
import 'package:asset_assistant/utils/host_utils.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
// 国家数据模型flag字段存储emoji
class Country {
final String countryId;
final String name;
final String code;
final String? flag; // 国旗字段存储emoji
Country({
required this.countryId,
required this.name,
required this.code,
this.flag,
});
// 从JSON构建对象
factory Country.fromJson(Map<String, dynamic> json) {
return Country(
countryId: json['country_id'],
name: json['name'],
code: json['code'],
flag: json['flag'], // 解析emoji字段
);
}
}
// 接口响应模型
class CountryResponse {
final bool success;
final String message;
final CountryData data;
CountryResponse({
required this.success,
required this.message,
required this.data,
});
factory CountryResponse.fromJson(Map<String, dynamic> json) {
return CountryResponse(
success: json['success'],
message: json['message'],
data: CountryData.fromJson(json['data']),
);
}
}
// 响应数据模型
class CountryData {
final int total;
final int page;
final int pageSize;
final List<Country> items;
CountryData({
required this.total,
required this.page,
required this.pageSize,
required this.items,
});
factory CountryData.fromJson(Map<String, dynamic> json) {
var itemsList = json['items'] as List? ?? [];
List<Country> items = itemsList.map((i) => Country.fromJson(i)).toList();
return CountryData(
total: json['total'],
page: json['page'],
pageSize: json['page_size'],
items: items,
);
}
}
class CountryPage extends StatefulWidget {
const CountryPage({super.key});
@override
State<CountryPage> createState() => _CountryPageState();
}
class _CountryPageState extends State<CountryPage> {
List<Country> _countries = [];
bool _isLoading = true;
String? _errorMessage;
int _currentPage = 1;
final int _pageSize = 20;
bool _hasMoreData = true;
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
_fetchCountries();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_isLoading) return;
if (_hasMoreData &&
_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
Future<void> _fetchCountries({bool isRefresh = false}) async {
if (isRefresh) {
setState(() {
_currentPage = 1;
_hasMoreData = true;
});
}
if (!_hasMoreData && !isRefresh) return;
setState(() {
_isLoading = true;
});
try {
final baseUrl = HostUtils().currentHost;
final path = '/country/read';
final url = '$baseUrl$path';
debugPrint('====== 开始请求国家列表 ======');
debugPrint('请求URL: $url');
debugPrint('请求参数: page=$_currentPage, page_size=$_pageSize');
final dio = Dio();
final response = await dio.post(
url,
data: {
'page': _currentPage,
'page_size': _pageSize,
'name': '',
'code': '',
'country_id': '',
'flag': '',
},
options: Options(
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
),
);
debugPrint('请求成功,状态码: ${response.statusCode}');
debugPrint('响应数据: ${response.data}');
if (response.statusCode == 200) {
final CountryResponse countryResponse = CountryResponse.fromJson(
response.data,
);
if (countryResponse.success) {
setState(() {
if (isRefresh) {
_countries = countryResponse.data.items;
} else {
_countries.addAll(countryResponse.data.items);
}
_hasMoreData = _countries.length < countryResponse.data.total;
_currentPage++;
_errorMessage = null;
});
debugPrint('数据解析成功,当前列表总数: ${_countries.length}');
debugPrint('是否还有更多数据: $_hasMoreData,下一页: $_currentPage');
} else {
setState(() {
_errorMessage = countryResponse.message;
});
debugPrint('接口返回失败: ${countryResponse.message}');
}
} else {
setState(() {
_errorMessage = '服务器响应异常: ${response.statusCode}';
});
debugPrint('服务器响应异常: ${response.statusCode}');
}
} on DioException catch (e) {
String errorMsg = '网络请求失败';
if (e.response != null) {
errorMsg = '请求失败: ${e.response?.statusCode}';
debugPrint(
'请求错误,状态码: ${e.response?.statusCode},响应数据: ${e.response?.data}',
);
} else if (e.type == DioExceptionType.connectionTimeout) {
errorMsg = '连接超时,请检查网络';
debugPrint('连接超时: ${e.message}');
} else if (e.type == DioExceptionType.connectionError) {
errorMsg = '网络连接错误';
debugPrint('网络连接错误: ${e.message}');
} else {
debugPrint('Dio异常: ${e.type},消息: ${e.message}');
}
setState(() {
_errorMessage = errorMsg;
});
} catch (e) {
setState(() {
_errorMessage = '发生未知错误: $e';
});
debugPrint('未知错误: $e');
} finally {
setState(() {
_isLoading = false;
});
debugPrint('====== 请求结束 ======\n');
}
}
Future<void> _refresh() async {
debugPrint('触发下拉刷新');
await _fetchCountries(isRefresh: true);
}
void _loadMore() {
if (!_isLoading && _hasMoreData) {
debugPrint('触发加载更多,当前页: $_currentPage');
_fetchCountries();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('国家列表'),
centerTitle: true,
elevation: 4,
shadowColor: Colors.black12,
backgroundColor: theme.colorScheme.surfaceContainerHighest,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
debugPrint('点击新增按钮,跳转到新增页面');
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AddCountryPage()),
);
if (result == true) {
debugPrint('从新增页面返回,刷新列表数据');
_fetchCountries(isRefresh: true);
}
},
),
],
),
body: SafeArea(child: _buildBody(theme)),
);
}
Widget _buildBody(ThemeData theme) {
// 加载中且列表为空
if (_isLoading && _countries.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
// 有错误信息且列表为空
if (_errorMessage != null && _countries.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_errorMessage!,
style: TextStyle(color: theme.colorScheme.error),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _fetchCountries(isRefresh: true),
child: const Text('重试'),
),
],
),
);
}
// 无数据状态(列表为空且无错误)
if (_countries.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 无数据背景图标
Icon(
Icons.flag_outlined,
size: 120,
color: theme.colorScheme.onSurface.withOpacity(0.1),
),
const SizedBox(height: 24),
// 无数据提示文字
Text(
'暂无国家数据',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
const SizedBox(height: 16),
// 刷新按钮
IconButton(
icon: Icon(
Icons.refresh,
size: 28,
color: theme.colorScheme.primary,
),
onPressed: () => _fetchCountries(isRefresh: true),
tooltip: '刷新数据',
),
],
),
);
}
// 正常列表展示
return RefreshIndicator(
onRefresh: _refresh,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _countries.length + (_hasMoreData ? 1 : 0),
itemBuilder: (context, index) {
if (index < _countries.length) {
final country = _countries[index];
return _buildCountryItem(theme, country);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: _isLoading
? const CircularProgressIndicator()
: const Text('没有更多数据了'),
),
);
}
},
controller: _scrollController,
),
);
}
Widget _buildCountryItem(ThemeData theme, Country country) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// 可以添加点击事件
},
borderRadius: BorderRadius.circular(8),
splashColor: theme.colorScheme.primary.withAlpha(26),
highlightColor: theme.colorScheme.primary.withAlpha(13),
child: Container(
height: 64,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// 国旗Emoji展示区域
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
// 显示国旗emoji如果没有则显示默认图标
country.flag != null && country.flag!.isNotEmpty
? country.flag!
: '',
style: const TextStyle(fontSize: 24), // 适当调整emoji大小
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
country.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'代码: ${country.code}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 18,
color: theme.hintColor,
),
],
),
),
),
),
Divider(
height: 1,
thickness: 1,
indent: 72,
endIndent: 16,
color: theme.dividerColor,
),
],
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:asset_assistant/utils/host_utils.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -43,8 +44,7 @@ class _AddExchangePageState extends State<AddExchangePage> {
}
// 准备请求数据
// const baseUrl = 'https://api.fishestlife.com';
const baseUrl = 'http://localhost:20010';
final baseUrl = HostUtils().currentHost;
const path = '/exchange/create';
final url = '$baseUrl$path';
@@ -58,9 +58,7 @@ class _AddExchangePageState extends State<AddExchangePage> {
final response = await dio.post(
url,
data: requestData,
options: Options(
headers: {'Content-Type': 'application/json', 'X-User-ID': userId},
),
options: Options(headers: {'Content-Type': 'application/json'}),
);
// 处理响应

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:asset_assistant/pages/exchange_page.dart'; // 导入ExchangePage
class HomePage extends StatelessWidget {
// 功能列表数据 - 为交易所项添加路由信息
final List<Map<String, dynamic>> features = [
{'icon': Icons.bar_chart, 'title': '数据分析', 'route': null},
{'icon': Icons.balance, 'title': '交易', 'route': null},
{'icon': Icons.flag_circle, 'title': '国家', 'route': '/country'},
{'icon': Icons.account_balance, 'title': '交易所', 'route': '/exchange'},
{'icon': Icons.branding_watermark, 'title': '品种', 'route': null},
];
@@ -115,14 +115,12 @@ class HomePage extends StatelessWidget {
onTap: () {
// 点击事件处理 - 如果有路由信息则导航
if (route != null && context.mounted) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ExchangePage()),
);
// 使用路由路径进行导航
Navigator.pushNamed(context, route);
}
},
borderRadius: BorderRadius.circular(8),
splashColor: theme.colorScheme.primary.withAlpha(26), // 修改为withAlpha更兼容
splashColor: theme.colorScheme.primary.withAlpha(26),
highlightColor: theme.colorScheme.primary.withAlpha(13),
child: Container(
height: 64,

View File

@@ -1,4 +1,5 @@
import 'package:asset_assistant/pages/home_page.dart';
import 'package:asset_assistant/utils/host_utils.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -40,7 +41,7 @@ class _LoginPageState extends State<LoginPage> {
try {
// 构建请求URL
const baseUrl = 'https://api.fishestlife.com';
final baseUrl = HostUtils().currentHost;
const path = '/user/login';
final url = '$baseUrl$path';
debugPrint('请求URL: $url');

View File

@@ -0,0 +1,22 @@
// Host
class HostUtils {
// 单例实例
static final HostUtils _instance = HostUtils._internal();
// 工厂构造函数,确保全局唯一实例
factory HostUtils() => _instance;
// 私有构造函数
HostUtils._internal();
// 根据运行环境获取当前Host
String get currentHost {
// 区分debug和release环境
bool isDebug = false;
assert(() {
isDebug = true;
return true;
}());
return isDebug ? 'http://127.0.0.1:20000' : 'https://api.fishestlife.com';
}
}