Compare commits

...

152 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
vipg
5c71c93eee add 2025-11-15 18:01:31 +08:00
vipg
03f14394e5 add 2025-11-15 17:55:43 +08:00
vipg
396f03964a add 2025-11-15 17:50:35 +08:00
vipg
a5753f0c47 add 2025-11-15 17:35:13 +08:00
vipg
578e1596ef add 2025-11-15 17:18:00 +08:00
vipg
c1870aa135 add 2025-11-15 17:13:15 +08:00
vipg
9104b740b2 add 2025-11-15 17:07:04 +08:00
vipg
59e45b0733 add 2025-11-15 17:04:20 +08:00
vipg
8c84f791da add 2025-11-15 17:01:50 +08:00
vipg
4b85656b21 add 2025-11-15 16:56:37 +08:00
vipg
b501038836 add 2025-11-15 16:55:39 +08:00
vipg
713b3b446a add 2025-11-15 16:53:37 +08:00
vipg
8c73afd3aa add 2025-11-15 16:43:46 +08:00
vipg
d6bb904e64 add 2025-11-15 16:42:49 +08:00
vipg
b7cb6b247b add 2025-11-15 16:38:13 +08:00
vipg
caa3ca2a81 add 2025-11-15 16:34:01 +08:00
vipg
3b3b1a8807 add 2025-11-15 16:31:50 +08:00
vipg
8f61c56e9c add 2025-11-15 16:23:01 +08:00
vipg
57798cd12c add 2025-11-15 16:22:05 +08:00
vipg
11a93e8d65 add 2025-11-15 16:09:00 +08:00
vipg
f4473e47d9 add 2025-11-15 15:35:22 +08:00
vipg
13450aa87d add 2025-11-15 15:01:20 +08:00
vipg
86553ed4ff add 2025-11-15 15:00:51 +08:00
vipg
a485a65395 add 2025-11-14 18:25:35 +08:00
vipg
9c217e5622 add 2025-11-14 18:16:04 +08:00
vipg
a6c8a1c122 add 2025-11-14 18:13:40 +08:00
vipg
396afbdb37 add 2025-11-14 18:12:11 +08:00
vipg
647e72829b add 2025-11-14 18:07:50 +08:00
vipg
99bbd34c43 add 2025-11-14 18:04:50 +08:00
vipg
81170f637e add 2025-11-14 18:02:56 +08:00
vipg
54e0a5a0c4 add 2025-11-14 17:59:50 +08:00
vipg
7322fee989 add 2025-11-14 17:48:50 +08:00
vipg
43fd114804 add 2025-11-14 17:45:21 +08:00
vipg
ba18a383aa add 2025-11-14 17:36:26 +08:00
vipg
7469b4766b add 2025-11-14 17:32:23 +08:00
vipg
d2673a2760 add 2025-11-14 17:27:56 +08:00
vipg
66db58898c add 2025-11-14 17:23:29 +08:00
vipg
e2a6cfc21c add 2025-11-14 17:08:15 +08:00
vipg
c5ce461c2d add 2025-11-14 17:04:13 +08:00
vipg
6cf728b5b6 add 2025-11-14 17:01:19 +08:00
vipg
c0b55c82df add 2025-11-14 17:00:30 +08:00
vipg
0f7f692c93 add 2025-11-14 16:37:56 +08:00
98 changed files with 5545 additions and 2801 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,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" IMAGE_TAG="1.0.0"
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
COMPOSE_PROJECT_NAME="country_service" COMPOSE_PROJECT_NAME="asset_assistant_service"
DOCKER_COMPOSE_FILE="./docker-compose.yaml" DOCKER_COMPOSE_FILE="./docker-compose.yaml"
SRC_DIR="./src" SRC_DIR="./src"
DOCKERFILE_PATH="${SRC_DIR}/Dockerfile" 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: services:
postgres: postgres:
image: postgres:17.4-alpine image: postgres:17.4-alpine
container_name: user_db container_name: asset_assistant_db
restart: always restart: always
ports: ports:
- 20001:5432 - 20001:5432
@@ -13,21 +13,21 @@ services:
POSTGRES_DB: ${DB_NAME} POSTGRES_DB: ${DB_NAME}
TZ: ${TZ} TZ: ${TZ}
volumes: volumes:
- ./shared_data/user_db:/var/lib/postgresql/data - ./shared_data/asset_assistant_db:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d - ./sql:/docker-entrypoint-initdb.d
- ./scripts:/scripts - ./scripts:/scripts
networks: networks:
- user-network - asset_assistant-network
user: asset_assistant:
image: golang:1.25.0-alpine3.22 image: golang:1.25.0-alpine3.22
container_name: user_api container_name: asset_assistant_api
restart: always restart: always
ports: ports:
- 20000:80 - 20000:80
depends_on: depends_on:
- postgres - postgres
networks: networks:
- user-network - asset_assistant-network
environment: environment:
DB_HOST: postgres DB_HOST: postgres
DB_PORT: ${DB_PORT} DB_PORT: ${DB_PORT}
@@ -39,6 +39,6 @@ services:
- ./src:/app - ./src:/app
command: sh -c "cd /app && go mod tidy && go run main.go" command: sh -c "cd /app && go mod tidy && go run main.go"
networks: networks:
user-network: asset_assistant-network:
driver: bridge driver: bridge
volumes: {} volumes: {}

View File

@@ -1,7 +1,7 @@
services: services:
postgres: postgres:
image: postgres:17.4-alpine image: postgres:17.4-alpine
container_name: user_db container_name: asset_assistant_db
restart: always restart: always
ports: ports:
- 20001:5432 - 20001:5432
@@ -13,21 +13,21 @@ services:
POSTGRES_DB: ${DB_NAME} POSTGRES_DB: ${DB_NAME}
TZ: ${TZ} TZ: ${TZ}
volumes: volumes:
- ./shared_data/user_db:/var/lib/postgresql/data - ./shared_data/asset_assistant_db:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d - ./sql:/docker-entrypoint-initdb.d
- ./scripts:/scripts - ./scripts:/scripts
networks: networks:
- user-network - asset-assistant-network
user: asset_assistant:
image: user-api:1.0.0 image: asset-assistant-api:1.0.0
container_name: user_api container_name: asset_assistant_api
restart: always restart: always
ports: ports:
- 20000:80 - 20000:80
depends_on: depends_on:
- postgres - postgres
networks: networks:
- user-network - asset-assistant-network
environment: environment:
DB_HOST: postgres DB_HOST: postgres
DB_PORT: ${DB_PORT} DB_PORT: ${DB_PORT}
@@ -39,6 +39,6 @@ services:
# 挂载添加日志目录挂载,将容器内日志日志目录映射到宿主机的 ./logs 目录 # 挂载添加日志目录挂载,将容器内日志日志目录映射到宿主机的 ./logs 目录
- ./logs:/app/logs # 假设代码中日志存储路径为 /app/logs - ./logs:/app/logs # 假设代码中日志存储路径为 /app/logs
networks: networks:
user-network: asset-assistant-network:
driver: bridge driver: bridge
volumes: {} 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,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 +0,0 @@
docker run -itd --name go_futures_trade_record_dev -v $(pwd)/src:/app -p 20010: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,30 +0,0 @@
-- 切换到目标数据库
\c postgres;
CREATE OR REPLACE FUNCTION update_record_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 = '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_record_modified_column();
RAISE NOTICE 'created record table and trigger';
ELSE
RAISE NOTICE 'record table already exists';
END IF;
END $$;

View File

@@ -1,36 +0,0 @@
-- 切换到目标数据库
\c postgres;
CREATE OR REPLACE FUNCTION update_deal_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_deal = 'deal') THEN
CREATE TABLE deal (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
record_id UUID NOT NULL,
variety_id VARCHAR(50) NOT NULL, -- 品种id
contract VARCHAR(50) NOT NULL, -- 合约2401
direction VARCHAR(20) NOT NULL CHECK (direction IN ('', '')), -- 限制合法方向
variety_tick NUMERIC(12, 6),
variety_tick_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_deal_updated_at
BEFORE UPDATE ON "deal"
FOR EACH ROW
EXECUTE FUNCTION update_deal_modified_column();
RAISE NOTICE 'created deal table and trigger';
ELSE
RAISE NOTICE 'deal table already exists';
END IF;
END $$;

View File

@@ -1,36 +0,0 @@
-- 切换到目标数据库
\c postgres;
CREATE OR REPLACE FUNCTION update_open_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_open = 'open') THEN
CREATE TABLE open (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
record_id UUID NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
day INT NOT NULL,
open_price NUMERIC(12, 6),
open_commission NUMERIC(10, 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_open_updated_at
BEFORE UPDATE ON "open"
FOR EACH ROW
EXECUTE FUNCTION update_open_modified_column();
RAISE NOTICE 'created open table and trigger';
ELSE
RAISE NOTICE 'open table already exists';
END IF;
END $$;

View File

@@ -1,36 +0,0 @@
-- 切换到目标数据库
\c postgres;
CREATE OR REPLACE FUNCTION update_close_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_close = 'close') THEN
CREATE TABLE close (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL,
record_id UUID NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
day INT NOT NULL,
close_price NUMERIC(12, 6),
close_commission NUMERIC(10, 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_close_updated_at
BEFORE UPDATE ON "close"
FOR EACH ROW
EXECUTE FUNCTION update_close_modified_column();
RAISE NOTICE 'created close table and trigger';
ELSE
RAISE NOTICE 'close table already exists';
END IF;
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,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 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=

View File

@@ -1 +0,0 @@
package logic

View File

@@ -1 +0,0 @@
package logic

View File

@@ -1 +0,0 @@
package logic

View File

@@ -1 +0,0 @@
package logic

View File

@@ -1,60 +0,0 @@
package main
import (
"futures_trade_record/db" // 数据库相关操作包
"futures_trade_record/logger" // 日志工具包
// "futures_trade_record/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("/futurestraderecord/create", logic.CreateHandler)
// zap.L().Info("✅ 创建接口注册完成: POST /futurestraderecord/create")
// 记录服务启动日志监听80端口
zap.L().Info("✅ 服务启动在80端口")
r.Run(":80")
}

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中的信息。
---

View File

@@ -0,0 +1,20 @@
CREATE OR REPLACE FUNCTION update_data_modified_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql VOLATILE;
-- 创建自动格式化小数的函数按需去除尾部多余0
CREATE OR REPLACE FUNCTION format_numeric_to_original(n NUMERIC)
RETURNS TEXT AS $$
BEGIN
-- 逻辑如果是整数小数部分全0返回整数文本否则返回去除尾部0的文本
IF n = TRUNC(n) THEN
RETURN TRUNC(n)::TEXT; -- 整数场景1.000000 → '1'
ELSE
RETURN TRIM(TRAILING '0' FROM TRIM(TRAILING '.' FROM n::TEXT)); -- 小数场景1.230000 → '1.23'1.002000 → '1.002'
END IF;
END;
$$ LANGUAGE plpgsql IMMUTABLE; -- IMMUTABLE相同输入返回相同输出支持索引

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

@@ -1,4 +1,4 @@
module user module asset_assistant
go 1.25.0 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 ( import (
"asset_assistant/db"
"net/http" "net/http"
"country/db"
"time" "time"
"github.com/google/uuid"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -20,7 +21,7 @@ type DeleteResponse struct {
Message string `json:"message"` // 提示信息 Message string `json:"message"` // 提示信息
} }
// DeleteHandler 处理国家删除逻辑(软删除) // DeleteHandler 处理删除逻辑(软删除)
func DeleteHandler(c *gin.Context) { func DeleteHandler(c *gin.Context) {
startTime := time.Now() startTime := time.Now()
reqID := c.Request.Header.Get("X-DeleteRequest-ID") reqID := c.Request.Header.Get("X-DeleteRequest-ID")
@@ -105,10 +106,10 @@ func DeleteHandler(c *gin.Context) {
} }
// 3.2 更新name表 // 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 { if err != nil {
tx.Rollback() tx.Rollback()
zap.L().Error("❌ name表更新失败", zap.L().Error("❌ country_name表更新失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("country_id", req.CountryID),
zap.Error(err), zap.Error(err),
@@ -121,10 +122,10 @@ func DeleteHandler(c *gin.Context) {
} }
// 3.3 更新code表 // 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 { if err != nil {
tx.Rollback() tx.Rollback()
zap.L().Error("❌ code表更新失败", zap.L().Error("❌ country_code表更新失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("country_id", req.CountryID),
zap.Error(err), zap.Error(err),
@@ -136,6 +137,26 @@ func DeleteHandler(c *gin.Context) {
return 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 { if err := tx.Commit(); err != nil {
tx.Rollback() tx.Rollback()

View File

@@ -1,13 +1,14 @@
package logic package logic4country
import ( import (
"country/db" "asset_assistant/db"
"net/http" "net/http"
"strconv" "strconv"
"time"
"strings" "strings"
"time"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
@@ -18,6 +19,7 @@ type ReadRequest struct {
CountryID string `form:"country_id"` // 国家ID可选 CountryID string `form:"country_id"` // 国家ID可选
Name string `form:"name"` // 国家名称,可选 Name string `form:"name"` // 国家名称,可选
Code string `form:"code"` // 国家代码,可选 Code string `form:"code"` // 国家代码,可选
Flag string `form:"flag"` // 国旗信息,新增可选参数
Page string `form:"page"` // 页码,可选 Page string `form:"page"` // 页码,可选
PageSize string `form:"page_size"` // 每页条数,可选 PageSize string `form:"page_size"` // 每页条数,可选
} }
@@ -30,11 +32,12 @@ type ReadData struct {
Items []CountryInfoViewItem `json:"items"` // 数据列表 Items []CountryInfoViewItem `json:"items"` // 数据列表
} }
// CountryInfoViewItem 视图数据项结构 // CountryInfoViewItem 视图数据项结构,新增国旗字段
type CountryInfoViewItem struct { type CountryInfoViewItem struct {
CountryID string `json:"country_id"` // 国家ID CountryID string `json:"country_id"` // 国家ID
Name string `json:"name"` // 国家名称 Name string `json:"name"` // 国家名称
Code string `json:"code"` // 国家代码 Code string `json:"code"` // 国家代码
Flag string `json:"flag"` // 国旗信息,新增字段
} }
// ReadResponse 读取响应结构 // ReadResponse 读取响应结构
@@ -44,7 +47,7 @@ type ReadResponse struct {
Data ReadData `json:"data"` // 响应数据 Data ReadData `json:"data"` // 响应数据
} }
// ReadHandler 处理国家信息查询逻辑 // ReadHandler 处理查询逻辑
func ReadHandler(c *gin.Context) { func ReadHandler(c *gin.Context) {
startTime := time.Now() startTime := time.Now()
// 获取或生成请求ID // 获取或生成请求ID
@@ -75,19 +78,6 @@ func ReadHandler(c *gin.Context) {
return 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) page, err := strconv.Atoi(req.Page)
if err != nil || page < 1 { if err != nil || page < 1 {
@@ -103,6 +93,7 @@ func ReadHandler(c *gin.Context) {
zap.String("country_id", req.CountryID), zap.String("country_id", req.CountryID),
zap.String("name", req.Name), zap.String("name", req.Name),
zap.String("code", req.Code), zap.String("code", req.Code),
zap.String("flag", req.Flag), // 新增国旗查询参数日志
zap.Int("page", page), zap.Int("page", page),
zap.Int("page_size", pageSize), zap.Int("page_size", pageSize),
) )
@@ -127,9 +118,15 @@ func ReadHandler(c *gin.Context) {
args = append(args, "%"+req.Code+"%") args = append(args, "%"+req.Code+"%")
paramIndex++ paramIndex++
} }
// 新增国旗查询条件
if req.Flag != "" {
whereClauses = append(whereClauses, "flag LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Flag+"%")
paramIndex++
}
// 构建基础SQL // 构建基础SQL新增flag字段查询
baseSQL := "SELECT country_id, name, code FROM country_info_view" baseSQL := "SELECT country_id, name, code, flag FROM country_info_view"
countSQL := "SELECT COUNT(*) FROM country_info_view" countSQL := "SELECT COUNT(*) FROM country_info_view"
if len(whereClauses) > 0 { if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ") whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
@@ -140,11 +137,11 @@ func ReadHandler(c *gin.Context) {
// 计算分页偏移量 // 计算分页偏移量
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
// 拼接分页SQL使用fmt.Sprintf更清晰 // 拼接分页SQL
querySQL := fmt.Sprintf("%s ORDER BY country_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1) querySQL := fmt.Sprintf("%s ORDER BY country_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1)
args = append(args, pageSize, offset) args = append(args, pageSize, offset)
// 查询总条数(修正参数传递方式) // 查询总条数
var total int64 var total int64
countArgs := args[:len(args)-2] // 排除分页参数 countArgs := args[:len(args)-2] // 排除分页参数
err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total) err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total)
@@ -175,11 +172,11 @@ func ReadHandler(c *gin.Context) {
} }
defer rows.Close() defer rows.Close()
// 处理查询结果 // 处理查询结果新增flag字段扫描
var items []CountryInfoViewItem var items []CountryInfoViewItem
for rows.Next() { for rows.Next() {
var item CountryInfoViewItem 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.L().Error("❌ 解析查询结果失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.Error(err), 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 ( import (
"asset_assistant/db" // 数据库操作相关包
"net/http" "net/http"
"country/db" // 数据库操作相关包
"time" // 时间处理包 "time" // 时间处理包
"github.com/google/uuid" // UUID生成工具
"github.com/gin-gonic/gin" // Gin框架用于处理HTTP请求 "github.com/gin-gonic/gin" // Gin框架用于处理HTTP请求
"github.com/google/uuid" // UUID生成工具
"go.uber.org/zap" // 日志库 "go.uber.org/zap" // 日志库
) )
// CreateRequest 注册请求参数结构 // CreateRequest 注册请求参数结构
// 用于接收客户端发送的JSON数据绑定并验证必填字段 // 用于接收客户端发送的JSON数据绑定并验证必填字段
type CreateRequest struct { type CreateRequest struct {
Name string `json:"name" binding:"required"` // 国家名称,必填 Name string `json:"name" binding:"required"` // 货币名称,必填
Code string `json:"code" binding:"required"` // 国家代码,必填 Code string `json:"code" binding:"required"` // 货币代码,必填
} }
// CreateResponse 注册响应结构 // CreateResponse 注册响应结构
@@ -25,13 +26,12 @@ type CreateResponse struct {
} }
// CreateData 响应数据结构 // CreateData 响应数据结构
// 包含创建成功后的国家ID // 包含创建成功后的货币ID
type CreateData struct { type CreateData struct {
CountryID string `json:"country_id"` // 国家唯一标识ID CurrencyID string `json:"currency_id"` // 货币唯一标识ID
} }
// CreateHandler 处理国家创建逻辑 // CreateHandler 处理创建逻辑
// 接收HTTP请求完成参数验证、数据库事务处理并返回响应
func CreateHandler(c *gin.Context) { func CreateHandler(c *gin.Context) {
startTime := time.Now() // 记录请求开始时间,用于统计耗时 startTime := time.Now() // 记录请求开始时间,用于统计耗时
// 获取或生成请求ID用于追踪整个请求链路 // 获取或生成请求ID用于追踪整个请求链路
@@ -42,7 +42,7 @@ func CreateHandler(c *gin.Context) {
} }
// 记录请求接收日志,包含关键追踪信息 // 记录请求接收日志,包含关键追踪信息
zap.L().Info("📥 收到国家创建请求", zap.L().Info("📥 收到货币创建请求",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path), zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method), zap.String("method", c.Request.Method),
@@ -106,34 +106,34 @@ func CreateHandler(c *gin.Context) {
} }
}() }()
// 1. 在country表中创建记录并获取自动生成的ID // 1. 在currency表中创建记录并获取自动生成的ID
var countryID string var currencyID string
err = tx.QueryRow("INSERT INTO country DEFAULT VALUES RETURNING id").Scan(&countryID) err = tx.QueryRow("INSERT INTO currency DEFAULT VALUES RETURNING id").Scan(&currencyID)
if err != nil { if err != nil {
tx.Rollback() // 操作失败,回滚事务 tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ country表插入失败", zap.L().Error("❌ currency表插入失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, CreateResponse{ c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false, Success: false,
Message: "创建国家记录失败", Message: "创建货币记录失败",
}) })
return return
} }
zap.L().Debug("📝 country表插入成功", zap.L().Debug("📝 currency表插入成功",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", countryID), zap.String("currency_id", currencyID),
) )
// 2. 插入国家名称到name表与country_id关联 // 2. 插入货币名称到name表与currency_id关联
_, err = tx.Exec("INSERT INTO name (country_id, name) VALUES ($1, $2)", countryID, req.Name) _, err = tx.Exec("INSERT INTO currency_name (currency_id, name) VALUES ($1, $2)", currencyID, req.Name)
if err != nil { if err != nil {
tx.Rollback() // 操作失败,回滚事务 tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ name表插入失败", zap.L().Error("❌ currency_name表插入失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", countryID), zap.String("currency_id", currencyID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, CreateResponse{ c.JSON(http.StatusInternalServerError, CreateResponse{
@@ -143,13 +143,13 @@ func CreateHandler(c *gin.Context) {
return return
} }
// 3. 插入国家代码到code表与country_id关联 // 3. 插入货币代码到code表与currency_id关联
_, err = tx.Exec("INSERT INTO code (country_id, code) VALUES ($1, $2)", countryID, req.Code) _, err = tx.Exec("INSERT INTO currency_code (currency_id, code) VALUES ($1, $2)", currencyID, req.Code)
if err != nil { if err != nil {
tx.Rollback() // 操作失败,回滚事务 tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ code表插入失败", zap.L().Error("❌ currency_code表插入失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", countryID), zap.String("currency_id", currencyID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, CreateResponse{ c.JSON(http.StatusInternalServerError, CreateResponse{
@@ -164,7 +164,7 @@ func CreateHandler(c *gin.Context) {
tx.Rollback() // 提交失败时尝试回滚 tx.Rollback() // 提交失败时尝试回滚
zap.L().Error("❌ 事务提交失败", zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", countryID), zap.String("currency_id", currencyID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, CreateResponse{ c.JSON(http.StatusInternalServerError, CreateResponse{
@@ -176,18 +176,18 @@ func CreateHandler(c *gin.Context) {
// 记录请求处理耗时 // 记录请求处理耗时
duration := time.Since(startTime) duration := time.Since(startTime)
zap.L().Info("✅ 国家创建请求处理完成", zap.L().Info("✅ 货币创建请求处理完成",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", countryID), zap.String("currency_id", currencyID),
zap.Duration("duration", duration), zap.Duration("duration", duration),
) )
// 返回成功响应,包含创建的国家ID // 返回成功响应,包含创建的货币ID
c.JSON(http.StatusOK, CreateResponse{ c.JSON(http.StatusOK, CreateResponse{
Success: true, Success: true,
Message: "创建成功", Message: "创建成功",
Data: CreateData{ 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 ( import (
"country/db" "asset_assistant/db"
"net/http" "net/http"
"time" "time"
@@ -12,9 +12,9 @@ import (
// UpdateRequest 更新请求参数结构 // UpdateRequest 更新请求参数结构
type UpdateRequest struct { type UpdateRequest struct {
CountryID string `json:"country_id" binding:"required"` // 国家ID必填 CurrencyID string `json:"currency_id" binding:"required"` // 货币ID必填
Name string `json:"name"` // 国家名称,可选 Name string `json:"name"` // 货币名称,可选
Code string `json:"code"` // 国家代码,可选 Code string `json:"code"` // 货币代码,可选
} }
// UpdateResponse 更新响应结构 // UpdateResponse 更新响应结构
@@ -23,7 +23,7 @@ type UpdateResponse struct {
Message string `json:"message"` // 提示信息 Message string `json:"message"` // 提示信息
} }
// UpdateHandler 处理国家信息更新逻辑 // UpdateHandler 处理信息更新逻辑
func UpdateHandler(c *gin.Context) { func UpdateHandler(c *gin.Context) {
startTime := time.Now() startTime := time.Now()
// 获取或生成请求ID // 获取或生成请求ID
@@ -34,14 +34,14 @@ func UpdateHandler(c *gin.Context) {
} }
// 记录请求接收日志 // 记录请求接收日志
zap.L().Info("📥 收到国家更新请求", zap.L().Info("📥 收到货币更新请求",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path), zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method), zap.String("method", c.Request.Method),
) )
var req UpdateRequest var req UpdateRequest
// 绑定并验证请求参数主要验证country_id必填 // 绑定并验证请求参数主要验证currency_id必填
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败", zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
@@ -49,7 +49,7 @@ func UpdateHandler(c *gin.Context) {
) )
c.JSON(http.StatusBadRequest, UpdateResponse{ c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false, Success: false,
Message: "请求参数错误country_id为必填项", Message: "请求参数错误currency_id为必填项",
}) })
return return
} }
@@ -58,7 +58,7 @@ func UpdateHandler(c *gin.Context) {
if req.Name == "" && req.Code == "" { if req.Name == "" && req.Code == "" {
zap.L().Warn("⚠️ 请求参数验证失败", zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("currency_id", req.CurrencyID),
zap.String("reason", "name和code不能同时为空"), zap.String("reason", "name和code不能同时为空"),
) )
c.JSON(http.StatusBadRequest, UpdateResponse{ c.JSON(http.StatusBadRequest, UpdateResponse{
@@ -70,7 +70,7 @@ func UpdateHandler(c *gin.Context) {
zap.L().Debug("✅ 请求参数验证通过", zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("currency_id", req.CurrencyID),
zap.String("name", req.Name), zap.String("name", req.Name),
zap.String("code", req.Code), zap.String("code", req.Code),
) )
@@ -111,12 +111,12 @@ func UpdateHandler(c *gin.Context) {
// 如果name不为空更新name表 // 如果name不为空更新name表
if req.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 { if err != nil {
tx.Rollback() tx.Rollback()
zap.L().Error("❌ name表更新失败", zap.L().Error("❌ currency_name表更新失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("currency_id", req.CurrencyID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, UpdateResponse{ c.JSON(http.StatusInternalServerError, UpdateResponse{
@@ -127,18 +127,18 @@ func UpdateHandler(c *gin.Context) {
} }
zap.L().Debug("📝 name表更新成功", zap.L().Debug("📝 name表更新成功",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("currency_id", req.CurrencyID),
) )
} }
// 如果code不为空更新code表 // 如果code不为空更新code表
if req.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 { if err != nil {
tx.Rollback() tx.Rollback()
zap.L().Error("❌ code表更新失败", zap.L().Error("❌ currency_code表更新失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("currency_id", req.CurrencyID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, UpdateResponse{ c.JSON(http.StatusInternalServerError, UpdateResponse{
@@ -149,7 +149,7 @@ func UpdateHandler(c *gin.Context) {
} }
zap.L().Debug("📝 code表更新成功", zap.L().Debug("📝 code表更新成功",
zap.String("req_id", reqID), 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() tx.Rollback()
zap.L().Error("❌ 事务提交失败", zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("currency_id", req.CurrencyID),
zap.Error(err), zap.Error(err),
) )
c.JSON(http.StatusInternalServerError, UpdateResponse{ c.JSON(http.StatusInternalServerError, UpdateResponse{
@@ -170,9 +170,9 @@ func UpdateHandler(c *gin.Context) {
// 记录请求处理耗时 // 记录请求处理耗时
duration := time.Since(startTime) duration := time.Since(startTime)
zap.L().Info("✅ 国家更新请求处理完成", zap.L().Info("✅ 货币更新请求处理完成",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("currency_id", req.CurrencyID),
zap.Duration("duration", duration), zap.Duration("duration", duration),
) )

View File

@@ -0,0 +1,200 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// 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"` // 交易所短名称,必填
}
// CreateResponse 注册响应结构
type CreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data CreateData `json:"data"`
}
// CreateData 响应数据结构
type CreateData struct {
ExchangeID string `json:"exchange_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
// 绑定参数时会自动验证name、code、short_name三个必填字段
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和short_name为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
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, 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: "系统错误,请稍后重试",
})
}
}()
// 1. 创建exchange主记录
var exchangeID string
err = tx.QueryRow("INSERT INTO exchange DEFAULT VALUES RETURNING id").Scan(&exchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange表插入失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建交易所记录失败",
})
return
}
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()
zap.L().Error("❌ exchange_name表插入失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存名称信息失败",
})
return
}
// 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()
zap.L().Error("❌ exchange_code表插入失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存代码信息失败",
})
return
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
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("exchange_id", exchangeID),
zap.Duration("duration", duration),
)
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
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 ( import (
"asset_assistant/db"
"database/sql" "database/sql"
"net/http" "net/http"
"user/db"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@@ -30,7 +30,7 @@ type LoginResponse struct {
} `json:"data"` } `json:"data"`
} }
// LoginHandler 处理用户登录请求的处理器函数 // LoginHandler 处理登录请求的处理器函数
// 参数c是gin.Context用于获取请求信息和返回响应 // 参数c是gin.Context用于获取请求信息和返回响应
func LoginHandler(c *gin.Context) { func LoginHandler(c *gin.Context) {
// 获取请求ID用于追踪请求链路若请求头中没有则生成一个新的UUID // 获取请求ID用于追踪请求链路若请求头中没有则生成一个新的UUID
@@ -54,7 +54,7 @@ func LoginHandler(c *gin.Context) {
zap.Error(err), zap.Error(err),
zap.Any("请求体", c.Request.Body), zap.Any("请求体", c.Request.Body),
) )
c.JSON(http.StatusBadRequest, LoginResponse{ c.JSON(http.StatusOK, LoginResponse{
Success: false, Success: false,
Message: "账号或密码不能为空", Message: "账号或密码不能为空",
}) })
@@ -72,7 +72,7 @@ func LoginHandler(c *gin.Context) {
zap.String("reqID", reqID), zap.String("reqID", reqID),
zap.String("账号", req.Account), zap.String("账号", req.Account),
) )
c.JSON(http.StatusBadRequest, LoginResponse{ c.JSON(http.StatusOK, LoginResponse{
Success: false, Success: false,
Message: "账号或密码不能为空", Message: "账号或密码不能为空",
}) })

View File

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

View File

@@ -0,0 +1,235 @@
package logic4variety
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"` // 品种代码,必填
Tick float64 `json:"tick" binding:"required"` // 品种tick必填
TickPrice float64 `json:"tick_price" binding:"required"` // 品种tick价格必填
ExchangeID string `json:"exchange_id" binding:"required"` // 交易所ID必填
}
// CreateResponse 创建品种响应结构
type CreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data CreateData `json:"data"`
}
// CreateData 响应数据结构
type CreateData struct {
VarietyID string `json:"variety_id"`
}
// CreateHandler 处理创建品种逻辑
func CreateHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-VarietyCreateRequest-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、tick、tick_price和exchange_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("name", req.Name),
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, 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: "系统错误,请稍后重试",
})
}
}()
// 1. 创建variety主记录
var varietyID string
err = tx.QueryRow("INSERT INTO variety DEFAULT VALUES RETURNING id").Scan(&varietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety表插入失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建品种记录失败",
})
return
}
zap.L().Debug("📝 variety表插入成功",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
)
// 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()
zap.L().Error("❌ variety_name表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存名称信息失败",
})
return
}
// 4. 插入代码信息
_, err = tx.Exec("INSERT INTO variety_code (variety_id, code) VALUES ($1, $2)", varietyID, req.Code)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存代码信息失败",
})
return
}
// 5. 插入tick信息
_, err = tx.Exec("INSERT INTO variety_tick (variety_id, tick) VALUES ($1, $2)", varietyID, req.Tick)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存tick信息失败",
})
return
}
// 6. 插入tick价格信息
_, err = tx.Exec("INSERT INTO variety_tick_price (variety_id, price) VALUES ($1, $2)", varietyID, req.TickPrice)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存tick价格信息失败",
})
return
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
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("variety_id", varietyID),
zap.Duration("duration", duration),
)
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
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: "更新成功",
})
}

99
backend/src/main.go Normal file
View File

@@ -0,0 +1,99 @@
package main
import (
"asset_assistant/db" // 数据库相关操作包
"asset_assistant/logger" // 日志工具包
"asset_assistant/logic4country"
"asset_assistant/logic4currency"
"asset_assistant/logic4exchange"
"asset_assistant/logic4user"
// 业务逻辑处理包
"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("✅ 配置跨域中间件完成")
// 注册用户接口
user := r.Group("/user")
{
user.POST("/register", logic4user.RegisterHandler)
user.POST("/login", logic4user.LoginHandler)
}
zap.L().Info("✅ 用户接口注册完成")
// 注册国家接口
country := r.Group("/country")
{
country.POST("/create", logic4country.CreateHandler)
country.POST("/read", logic4country.ReadHandler)
country.POST("/update", logic4country.UpdateHandler)
country.POST("/delete", logic4country.DeleteHandler)
}
zap.L().Info("✅ 国家接口注册完成")
// 注册交易所接口
exchange := r.Group("/exchange")
{
exchange.POST("/create", logic4exchange.CreateHandler)
exchange.POST("/read", logic4exchange.ReadHandler)
exchange.POST("/update", logic4exchange.UpdateHandler)
exchange.POST("/delete", logic4exchange.DeleteHandler)
}
zap.L().Info("✅ 交易所接口注册完成")
// 注册货币接口
currency := r.Group("/currency")
{
currency.POST("/create", logic4currency.CreateHandler)
currency.POST("/read", logic4currency.ReadHandler)
currency.POST("/update", logic4currency.UpdateHandler)
currency.POST("/delete", logic4currency.DeleteHandler)
}
zap.L().Info("✅ 货币接口注册完成")
// 记录服务启动日志监听80端口
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,6 @@
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'; import 'package:asset_assistant/pages/home_page.dart';
import 'package:asset_assistant/pages/login_page.dart'; import 'package:asset_assistant/pages/login_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -21,16 +24,20 @@ class MyApp extends StatelessWidget {
title: '资产助手', title: '资产助手',
debugShowCheckedModeBanner: false, // 移除 debug 标签 debugShowCheckedModeBanner: false, // 移除 debug 标签
initialRoute: initialRoute, initialRoute: initialRoute,
// 在MaterialApp的routes中添加
routes: { routes: {
'/login': (context) => const LoginPage(), '/login': (context) => const LoginPage(),
'/home': (context) => const HomePage(), '/home': (context) => HomePage(),
'/country': (context) => CountryPage(),
'/exchange': (context) => ExchangePage(),
'/exchange/add': (context) => AddExchangePage(),
}, },
theme: ThemeData( theme: ThemeData(
// 金融暗夜风格主题配置 // 金融暗夜风格主题配置
brightness: Brightness.dark, brightness: Brightness.dark,
colorScheme: ColorScheme.dark( colorScheme: ColorScheme.dark(
primary: const Color(0xFF0070F3), // 主色调:深蓝色 primary: const Color(0xFF0070F3), // 主色调:深蓝色
secondary: const Color(0xFF64FFDA), // 辅助色:亮青 secondary: const Color(0xFF38B2AC), // 沉稳的绿松石
// 将 background 替换为 surface // 将 background 替换为 surface
surface: const Color(0xFF0F172A), // 背景色:深灰蓝 surface: const Color(0xFF0F172A), // 背景色:深灰蓝
// 使用 surfaceContainerHighest 替代 deprecated 的 surfaceVariant // 使用 surfaceContainerHighest 替代 deprecated 的 surfaceVariant

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

@@ -1,396 +0,0 @@
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'dart:developer'; // 引入日志工具
// 国家数据模型
class Country {
final String countryId;
final String name;
final String code;
Country({required this.countryId, required this.name, required this.code});
// 从JSON构建对象
factory Country.fromJson(Map<String, dynamic> json) {
return Country(
countryId: json['country_id'],
name: json['name'],
code: json['code'],
);
}
// 转换为JSON
Map<String, dynamic> toJson() {
return {'country_id': countryId, 'name': name, 'code': code};
}
}
class CountryHomePage extends StatefulWidget {
const CountryHomePage({super.key});
@override
State<CountryHomePage> createState() => _CountryHomePageState();
}
class _CountryHomePageState extends State<CountryHomePage> {
// 文本编辑控制器
final TextEditingController _nameController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
// 国家列表
List<Country> _countries = [];
bool _isLoading = false;
// Dio实例
final Dio _dio = Dio();
final String _baseUrl = 'https://api.fishestlife.com';
@override
void initState() {
super.initState();
_fetchCountries();
}
// 获取国家列表
Future<void> _fetchCountries() async {
setState(() {
_isLoading = true;
});
try {
final response = await _dio.post(
'$_baseUrl/country/read',
data: {}, // 空参数表示获取所有国家
);
if (response.data['success'] == true) {
final List<dynamic> items = response.data['data']['items'];
setState(() {
_countries = items.map((item) => Country.fromJson(item)).toList();
});
} else {
_showErrorDialog('获取失败', response.data['message'] ?? '未知错误');
}
} catch (e) {
// 添加网络异常日志
log('获取国家列表网络异常', error: e, stackTrace: StackTrace.current);
_showErrorDialog('网络错误', e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
// 创建新国家
Future<void> _createCountry() async {
final name = _nameController.text.trim();
final code = _codeController.text.trim();
if (name.isEmpty || code.isEmpty) {
_showErrorDialog('输入错误', '国家名称和代码不能为空');
return;
}
setState(() {
_isLoading = true;
});
try {
final response = await _dio.post(
'$_baseUrl/country/create',
data: {'name': name, 'code': code},
);
if (response.data['success'] == true) {
// 清空输入框
_nameController.clear();
_codeController.clear();
// 重新获取列表
_fetchCountries();
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('创建成功')));
} else {
_showErrorDialog('创建失败', response.data['message'] ?? '未知错误');
}
} catch (e) {
// 添加网络异常日志
log('创建国家网络异常', error: e, stackTrace: StackTrace.current);
_showErrorDialog('网络错误', e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
// 更新国家信息
Future<void> _updateCountry(Country country) async {
setState(() {
_isLoading = true;
});
try {
final response = await _dio.post(
'$_baseUrl/country/update',
data: country.toJson(),
);
if (response.data['success'] == true) {
_fetchCountries();
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('更新成功')));
} else {
_showErrorDialog('更新失败', response.data['message'] ?? '未知错误');
}
} catch (e) {
// 添加网络异常日志
log('更新国家网络异常', error: e, stackTrace: StackTrace.current);
_showErrorDialog('网络错误', e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
// 删除国家
Future<void> _deleteCountry(String countryId) async {
if (!await _showConfirmationDialog('确认删除', '确定要删除这个国家吗?')) {
return;
}
setState(() {
_isLoading = true;
});
try {
final response = await _dio.post(
'$_baseUrl/country/delete',
data: {'country_id': countryId},
);
if (response.data['success'] == true) {
_fetchCountries();
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('删除成功')));
} else {
_showErrorDialog('删除失败', response.data['message'] ?? '未知错误');
}
} catch (e) {
// 添加网络异常日志
log('删除国家网络异常', error: e, stackTrace: StackTrace.current);
_showErrorDialog('网络错误', e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
// 显示编辑对话框
void _showEditDialog(Country country) {
final TextEditingController nameController = TextEditingController(
text: country.name,
);
final TextEditingController codeController = TextEditingController(
text: country.code,
);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('编辑国家'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: '国家名称'),
),
TextField(
controller: codeController,
decoration: const InputDecoration(labelText: '国家代码'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
final updatedCountry = Country(
countryId: country.countryId,
name: nameController.text.trim(),
code: codeController.text.trim(),
);
_updateCountry(updatedCountry);
Navigator.pop(context);
},
child: const Text('保存'),
),
],
),
);
}
// 显示错误对话框
void _showErrorDialog(String title, String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
// 显示确认对话框
Future<bool> _showConfirmationDialog(String title, String message) async {
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确认'),
),
],
),
) ??
false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('国家管理')),
body: Column(
children: [
// 顶部输入区域
Padding(
padding: const EdgeInsets.all(16.0),
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'添加新国家',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '国家名称',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _codeController,
decoration: const InputDecoration(
labelText: '国家代码',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _createCountry,
child: _isLoading
? const CircularProgressIndicator()
: const Text('保存'),
),
],
),
),
),
),
// 底部列表区域
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _countries.isEmpty
? const Center(child: Text('没有国家数据'))
: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
childAspectRatio: 4,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: _countries.length,
itemBuilder: (context, index) {
final country = _countries[index];
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 国家名称(可点击编辑)
InkWell(
onTap: () => _showEditDialog(country),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
country.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text('代码: ${country.code}'),
],
),
),
// 删除按钮
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () =>
_deleteCountry(country.countryId),
),
],
),
),
);
},
),
),
],
),
);
}
}

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

@@ -0,0 +1,209 @@
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 AddExchangePage extends StatefulWidget {
const AddExchangePage({super.key});
@override
State<AddExchangePage> createState() => _AddExchangePageState();
}
class _AddExchangePageState extends State<AddExchangePage> {
// 输入控制器
final TextEditingController _nameController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
// 加载状态
bool _isLoading = false;
// 表单验证键
final _formKey = GlobalKey<FormState>();
// 创建交易所
Future<void> _createExchange() 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;
}
// 准备请求数据
final baseUrl = HostUtils().currentHost;
const path = '/exchange/create';
final url = '$baseUrl$path';
final requestData = {
'name': _nameController.text.trim(),
'code': _codeController.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) {
_showDialog('错误', '服务器响应异常: ${response.statusCode}');
}
}
} on DioException catch (e) {
String errorMessage = '网络请求失败';
if (e.response != null) {
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 : _createExchange,
),
],
),
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;
},
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,138 @@
import 'package:asset_assistant/pages/exchange_add_page.dart';
import 'package:flutter/material.dart';
class ExchangePage extends StatelessWidget {
// 功能列表数据
final List<Map<String, dynamic>> features = [
{'icon': Icons.bar_chart, 'title': '数据分析'},
{'icon': Icons.balance, 'title': '交易'},
{'icon': Icons.account_balance, 'title': '交易所'},
{'icon': Icons.branding_watermark, 'title': '品种'},
];
ExchangePage({super.key});
@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,
// 1. 左上角返回按钮及功能实现
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop(); // 返回上一页
},
),
// 2. 右上角添加按钮(展示功能)
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
// 跳转到AddExchangePage
Navigator.push(
context,
MaterialPageRoute(builder: (context) => AddExchangePage()),
);
},
),
],
),
// 使用SafeArea确保内容在安全区域内
body: SafeArea(
// 移除不必要的Expanded避免约束冲突
child: Container(
color: theme.colorScheme.surface,
// 让容器占满整个可用空间
width: double.infinity,
height: double.infinity,
child: ListView.builder(
// 修改为ListView.builder
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: features.length,
// 优化Web端滚动物理效果同时支持触摸和鼠标滚动
physics: const ScrollPhysics(parent: BouncingScrollPhysics()),
itemBuilder: (context, index) {
// 构建列表项
Widget item = _buildFeatureItem(
context: context,
icon: features[index]['icon'],
title: features[index]['title'],
);
// 在每个列表项下方添加分割线,包括最后一个
return Column(
mainAxisSize: MainAxisSize.min,
children: [
item,
Divider(
height: 1,
thickness: 1,
indent: 72,
endIndent: 16,
color: theme.dividerColor,
),
],
);
},
),
),
),
);
}
Widget _buildFeatureItem({
required BuildContext context,
required IconData icon,
required String title,
}) {
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// 点击事件可以在这里添加
},
borderRadius: BorderRadius.circular(8),
splashColor: theme.colorScheme.primary.withAlpha(26), // 0.1透明度的alpha值
highlightColor: theme.colorScheme.primary.withAlpha(
13,
), // 0.05透明度的alpha值
child: Container(
height: 64,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 24, color: theme.colorScheme.secondary),
),
const SizedBox(width: 16),
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
const Spacer(),
Icon(Icons.arrow_forward_ios, size: 18, color: theme.hintColor),
],
),
),
),
);
}
}

View File

@@ -1,216 +1,154 @@
import 'package:asset_assistant/pages/country_home.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
// 定义功能和子功能数据模型 class HomePage extends StatelessWidget {
class FunctionItem { // 功能列表数据 - 为交易所项添加路由信息
final String name; final List<Map<String, dynamic>> features = [
final List<SubFunctionItem> subFunctions; {'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},
];
FunctionItem({required this.name, required this.subFunctions}); HomePage({super.key});
}
class SubFunctionItem { // 退出登录方法(保持不变)
final String name; void _logout(BuildContext context) async {
final Widget page; final result = await showDialog(
context: context,
SubFunctionItem({required this.name, required this.page}); builder: (context) => AlertDialog(
} title: const Text('退出登录'),
content: const Text('确定要退出当前账号吗?'),
class HomePage extends StatefulWidget { actions: [
const HomePage({super.key}); TextButton(
onPressed: () => Navigator.pop(context, false),
@override child: const Text('取消'),
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// 功能列表数据 - 初始只包含"系统"功能
final List<FunctionItem> _functions = [
FunctionItem(
name: "系统",
subFunctions: [
SubFunctionItem(name: "国家", page: CountryHomePage()),
SubFunctionItem(
name: "货币",
page: const Center(child: Text("系统参数设置页面内容")),
), ),
SubFunctionItem( TextButton(
name: "交易所", onPressed: () => Navigator.pop(context, true),
page: const Center(child: Text("系统日志管理页面内容")), child: const Text('确定'),
), ),
], ],
), ),
]; );
// 选中状态跟踪 if (result == true && context.mounted) {
int _selectedFunctionIndex = 0; Navigator.pushReplacementNamed(context, '/login');
int _selectedSubFunctionIndex = 0;
Future<void> _logout(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
final hasUserID = prefs.getString('user_id') != null;
if (hasUserID) {
await prefs.remove('user_id');
debugPrint('移除的用户ID: $hasUserID');
} }
await prefs.reload();
if (!context.mounted) return;
Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 获取当前选中的功能和子功能 final theme = Theme.of(context);
final currentFunction = _functions[_selectedFunctionIndex];
final currentSubFunction =
currentFunction.subFunctions[_selectedSubFunctionIndex];
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar(
// 移除顶部导航栏 title: const Text('功能列表'),
body: Row( centerTitle: true,
children: [ elevation: 4,
// 左侧功能导航栏 shadowColor: Colors.black12,
Container( backgroundColor: theme.colorScheme.surfaceContainerHighest,
width: 180, automaticallyImplyLeading: false,
decoration: BoxDecoration( actions: [
color: Theme.of(context).colorScheme.surfaceContainerHighest, TextButton(
border: Border( onPressed: () => _logout(context),
right: BorderSide(
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.1),
),
),
),
child: Column(
children: [
// 功能列表
Expanded(
child: ListView.builder(
itemCount: _functions.length,
itemBuilder: (context, index) {
final function = _functions[index];
return ListTile(
title: Text(
function.name,
style: TextStyle(
color: _selectedFunctionIndex == index
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.onSurface,
fontWeight: _selectedFunctionIndex == index
? FontWeight.bold
: FontWeight.normal,
),
),
selected: _selectedFunctionIndex == index,
selectedTileColor: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
onTap: () {
setState(() {
_selectedFunctionIndex = index;
_selectedSubFunctionIndex = 0; // 切换功能时默认选中第一个子功能
});
},
);
},
),
),
// 底部退出登录功能
const Divider(height: 1), // 分隔线
ListTile(
title: const Text(
'退出登录',
textAlign: TextAlign.center, // 文案居中展示
),
// 设置文本颜色为灰白色
textColor: Theme.of(context).colorScheme.onSurfaceVariant,
onTap: () => _logout(context),
tileColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
// 移除默认内边距,使居中更美观
contentPadding: EdgeInsets.zero,
),
],
),
),
// 右侧内容区域
Expanded(
child: Column(
children: [
// 顶部子功能导航栏
Container(
height: 60,
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
border: Border(
bottom: BorderSide(
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.1),
),
),
),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: currentFunction.subFunctions.length,
itemBuilder: (context, index) {
final subFunction = currentFunction.subFunctions[index];
return InkWell(
onTap: () {
setState(() {
_selectedSubFunctionIndex = index;
});
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: _selectedSubFunctionIndex == index
? Theme.of(context).colorScheme.primary
: Colors.transparent,
width: 2,
),
),
),
child: Text( child: Text(
subFunction.name, '退出登录',
style: TextStyle( style: TextStyle(
color: _selectedSubFunctionIndex == index color: theme.colorScheme.onSurface,
? Theme.of(context).colorScheme.primary
: Theme.of(
context,
).colorScheme.onSurfaceVariant,
fontSize: 16, fontSize: 16,
), ),
), ),
), ),
const SizedBox(width: 8),
],
),
body: SafeArea(
child: Container(
color: theme.colorScheme.surface,
width: double.infinity,
height: double.infinity,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: features.length,
physics: const ScrollPhysics(parent: BouncingScrollPhysics()),
itemBuilder: (context, index) {
Widget item = _buildFeatureItem(
context: context,
icon: features[index]['icon'],
title: features[index]['title'],
route: features[index]['route'], // 传递路由信息
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
item,
Divider(
height: 1,
thickness: 1,
indent: 72,
endIndent: 16,
color: theme.dividerColor,
),
],
); );
}, },
), ),
), ),
),
);
}
// 子功能操作页面区域 // 修改_buildFeatureItem方法添加路由参数和导航逻辑
Expanded(child: currentSubFunction.page), Widget _buildFeatureItem({
Expanded(child: currentSubFunction.page), required BuildContext context,
required IconData icon,
required String title,
String? route, // 新增路由参数
}) {
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// 点击事件处理 - 如果有路由信息则导航
if (route != null && context.mounted) {
// 使用路由路径进行导航
Navigator.pushNamed(context, route);
}
},
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: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 24, color: theme.colorScheme.secondary),
),
const SizedBox(width: 16),
Text(
title,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
const Spacer(),
Icon(Icons.arrow_forward_ios, size: 18, color: theme.hintColor),
], ],
), ),
), ),
],
), ),
); );
} }

View File

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

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';
}
}