Compare commits
151 Commits
0f7f692c93
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99741d44b5 | ||
|
|
62adbd70a4 | ||
|
|
6c31b71f9f | ||
|
|
9173a7ac20 | ||
|
|
4a648d53e9 | ||
|
|
e97c8e00c5 | ||
|
|
a2271b4e0d | ||
|
|
4430d77c81 | ||
|
|
2f05b86f74 | ||
|
|
cc63fece65 | ||
|
|
269d9e5857 | ||
|
|
37a6ec63ba | ||
|
|
7f49c0cdc0 | ||
|
|
3833ed68db | ||
|
|
276be30387 | ||
|
|
9b061b8992 | ||
|
|
f5ecc9a151 | ||
|
|
7cd2ea11da | ||
|
|
19f9c84718 | ||
|
|
fede591197 | ||
|
|
075181cc32 | ||
|
|
e41b3a8dbc | ||
|
|
b9e840a2ba | ||
|
|
a1ea55dffa | ||
|
|
1a638eab5e | ||
|
|
76153930dc | ||
|
|
9f3aa79aa5 | ||
|
|
a573993365 | ||
|
|
6817626669 | ||
|
|
94c07397a0 | ||
|
|
87a037616e | ||
|
|
01c63e1b82 | ||
|
|
4191843802 | ||
|
|
a2c758abae | ||
|
|
6ca4489ad7 | ||
|
|
e9474e672a | ||
|
|
e0dcaf4ff6 | ||
|
|
902a6a9b75 | ||
|
|
97740d0447 | ||
|
|
291cf01983 | ||
|
|
1ccbc3c6d3 | ||
|
|
29f134c3e5 | ||
|
|
2293899780 | ||
|
|
5c32d8977c | ||
|
|
175dc327c3 | ||
|
|
5b58186c96 | ||
|
|
590cace08a | ||
|
|
6f8b1d9b2b | ||
|
|
e9945d67aa | ||
|
|
e716663731 | ||
|
|
30cfd98e92 | ||
|
|
261fbd7180 | ||
|
|
e2114845b5 | ||
|
|
abb1c8500c | ||
|
|
edade96d4a | ||
|
|
c8bb3d4ebd | ||
|
|
b2e89bf5bd | ||
|
|
8a8dd48726 | ||
|
|
b2882ed70a | ||
|
|
5fd2c2d38f | ||
|
|
ebe85f99ef | ||
|
|
2abd401b41 | ||
|
|
73c498ea37 | ||
|
|
e33a5e36ad | ||
|
|
01c6d0fda2 | ||
|
|
55df63985a | ||
|
|
088586f126 | ||
|
|
c661084bb8 | ||
|
|
edfc11d198 | ||
|
|
89031f86fe | ||
|
|
d05a4cb7e2 | ||
|
|
8b38fb2bb7 | ||
|
|
a6e2dda7a7 | ||
|
|
6571076ae6 | ||
|
|
fadd3d6ff3 | ||
|
|
6f32e781b6 | ||
|
|
c734d71c36 | ||
|
|
4a86c66b1c | ||
|
|
5870edb938 | ||
|
|
d1bee92563 | ||
|
|
53e55c1123 | ||
|
|
74e87033ed | ||
|
|
d2e8cd3bcc | ||
|
|
cca0e14823 | ||
|
|
fde1929b2c | ||
|
|
bcd87c3b73 | ||
|
|
53c4450d58 | ||
|
|
af33b34237 | ||
|
|
44e2d7c1f6 | ||
|
|
a47e544657 | ||
|
|
2f461a3a95 | ||
|
|
f13a58e116 | ||
|
|
8bedb4681f | ||
|
|
144042595c | ||
|
|
5592dabc62 | ||
|
|
55f10f344a | ||
|
|
3a7d40ae7b | ||
|
|
0e8afddcb7 | ||
|
|
c14ed2bf45 | ||
|
|
fed9de8090 | ||
|
|
79c631d356 | ||
|
|
e57d48568e | ||
|
|
a57a04c448 | ||
|
|
6ff0d6561e | ||
|
|
300d4d257d | ||
|
|
a67e76bcb8 | ||
|
|
1ebc924efb | ||
|
|
fcc758dd32 | ||
|
|
c2439db17f | ||
|
|
0175b0823e | ||
|
|
5c71c93eee | ||
|
|
03f14394e5 | ||
|
|
396f03964a | ||
|
|
a5753f0c47 | ||
|
|
578e1596ef | ||
|
|
c1870aa135 | ||
|
|
9104b740b2 | ||
|
|
59e45b0733 | ||
|
|
8c84f791da | ||
|
|
4b85656b21 | ||
|
|
b501038836 | ||
|
|
713b3b446a | ||
|
|
8c73afd3aa | ||
|
|
d6bb904e64 | ||
|
|
b7cb6b247b | ||
|
|
caa3ca2a81 | ||
|
|
3b3b1a8807 | ||
|
|
8f61c56e9c | ||
|
|
57798cd12c | ||
|
|
11a93e8d65 | ||
|
|
f4473e47d9 | ||
|
|
13450aa87d | ||
|
|
86553ed4ff | ||
|
|
a485a65395 | ||
|
|
9c217e5622 | ||
|
|
a6c8a1c122 | ||
|
|
396afbdb37 | ||
|
|
647e72829b | ||
|
|
99bbd34c43 | ||
|
|
81170f637e | ||
|
|
54e0a5a0c4 | ||
|
|
7322fee989 | ||
|
|
43fd114804 | ||
|
|
ba18a383aa | ||
|
|
7469b4766b | ||
|
|
d2673a2760 | ||
|
|
66db58898c | ||
|
|
e2a6cfc21c | ||
|
|
c5ce461c2d | ||
|
|
6cf728b5b6 | ||
|
|
c0b55c82df |
119
backend/README.md
Normal file
119
backend/README.md
Normal 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
12
backend/chat.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"biz":“新增国家“,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"name": "",
|
||||||
|
"code": "",
|
||||||
|
"flag": "",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
---
|
||||||
@@ -1 +0,0 @@
|
|||||||
docker run -itd --name go_country_dev -v $(pwd)/src:/app -p 20010:80 golang:1.25.0-alpine3.22
|
|
||||||
@@ -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: {}
|
|
||||||
@@ -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: {}
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -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
3
backend/dev-test.sh
Normal 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
|
||||||
@@ -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: {}
|
||||||
@@ -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: {}
|
||||||
@@ -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
|
|
||||||
@@ -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 "===== 构建脚本执行完成 ====="
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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: {}
|
|
||||||
@@ -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: {}
|
|
||||||
@@ -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进程已退出"
|
|
||||||
@@ -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;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -1,66 +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_name = '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
|
|
||||||
variety_name VARCHAR(50) NOT NULL, -- 品种
|
|
||||||
contract VARCHAR(50) NOT NULL, -- 合约(如:2401)
|
|
||||||
direction VARCHAR(20) NOT NULL CHECK (direction IN ('多', '空')), -- 限制合法方向
|
|
||||||
open_year INT NOT NULL,
|
|
||||||
open_month INT NOT NULL,
|
|
||||||
open_day INT NOT NULL,
|
|
||||||
open_price NUMERIC(12, 6),
|
|
||||||
open_commission NUMERIC(10, 6) DEFAULT 0.00,
|
|
||||||
close_year INT NOT NULL DEFAULT 0,
|
|
||||||
close_month INT NOT NULL DEFAULT 0,
|
|
||||||
close_day INT NOT NULL DEFAULT 0,
|
|
||||||
close_price NUMERIC(12, 6) DEFAULT 0.00,
|
|
||||||
close_commission NUMERIC(10, 6) DEFAULT 0.00,
|
|
||||||
variety_tick NUMERIC(12, 6),
|
|
||||||
close_tick NUMERIC(12, 6) DEFAULT 0.00, -- 平仓跳点
|
|
||||||
variety_tick_price NUMERIC(12, 6),
|
|
||||||
close_tick_profit NUMERIC(12, 6) DEFAULT 0.00,
|
|
||||||
total_commission NUMERIC(10, 6) DEFAULT 0.00,
|
|
||||||
close_profit NUMERIC(12, 6) DEFAULT 0.00,
|
|
||||||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
CREATE TRIGGER update_deal_updated_at
|
|
||||||
BEFORE UPDATE ON "deal"
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION update_deal_modified_column();
|
|
||||||
|
|
||||||
-- 1. 开仓日期索引(支持「未删除+开仓日期」查询)
|
|
||||||
CREATE INDEX idx_trade_open_date ON deal (deleted, open_year, open_month, open_day);
|
|
||||||
|
|
||||||
-- 2. 品种+合约+方向组合索引(支持「未删除+品种+合约+方向」高频查询)
|
|
||||||
CREATE INDEX idx_trade_variety_contract_dir ON deal (deleted, variety_name, contract, direction);
|
|
||||||
|
|
||||||
-- 3. 未平仓记录索引(支持「未删除+未平仓」查询)
|
|
||||||
CREATE INDEX idx_trade_unclosed ON deal (deleted, variety_name, contract) WHERE close_year IS NULL;
|
|
||||||
|
|
||||||
-- 4. 平仓日期索引(支持「未删除+已平仓+平仓日期」查询)
|
|
||||||
CREATE INDEX idx_trade_close_date ON deal (deleted, close_year, close_month, close_day) WHERE close_year IS NOT NULL;
|
|
||||||
|
|
||||||
-- 5. 盈亏排序索引(支持「未删除+已平仓+盈亏排序」查询)
|
|
||||||
CREATE INDEX idx_trade_profit ON deal (deleted, close_profit DESC) WHERE close_year IS NOT NULL;
|
|
||||||
|
|
||||||
RAISE NOTICE 'created deal table and trigger';
|
|
||||||
ELSE
|
|
||||||
RAISE NOTICE 'deal table already exists';
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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("✅ 数据库连接验证成功")
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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=
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package logic
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package logic
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package logic
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package logic
|
|
||||||
@@ -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
126
backend/prompt.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
分析这个项目,在 create.go 中完成以下需求:
|
||||||
|
|
||||||
|
1、接收 name,code 两个参数。
|
||||||
|
2、确认提交的 name,code 两个参数不能为空,如果有空,则返回提示。
|
||||||
|
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,name,code 参数。
|
||||||
|
2、确认提交的 country_id 参数有不能为空,如果为空,则返回提示。
|
||||||
|
3、确认提交的 name,code 两个参数,必须有一个不能为空,如果都为空,则返回提示。
|
||||||
|
4、如果 name 不为空,开启事务保存到 name 中。
|
||||||
|
5、如果 code 不为空,开启事务保存到 code 中。
|
||||||
|
6、如果 name,code 都不为空,开启事务保存到 name,code 中。
|
||||||
|
---
|
||||||
|
分析这个项目,在 read.go 中完成以下需求:
|
||||||
|
|
||||||
|
1、接收 country_id,name,code,page,page_size 参数。
|
||||||
|
2、确认提交的 country_id,name,code 必须有一个不能为空,如果都为空,则返回提示。
|
||||||
|
3、确认提交的 page,page_size, 如果为空,则 page 默认为 1,page_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中的信息。
|
||||||
|
---
|
||||||
20
backend/sql/02_create_function.sql
Normal file
20
backend/sql/02_create_function.sql
Normal 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
116
backend/sql/03_user.sql
Normal 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
137
backend/sql/04_country.sql
Normal 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
138
backend/sql/05_exchange.sql
Normal 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
117
backend/sql/06_currency.sql
Normal 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
165
backend/sql/07_variety.sql
Normal 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
286
backend/sql/08_trade.sql
Normal 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 $$;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module user
|
module asset_assistant
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
303
backend/src/logic4country/create.go
Normal file
303
backend/src/logic4country/create.go
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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"` // 每页条数,可选
|
||||||
}
|
}
|
||||||
@@ -26,15 +28,16 @@ type ReadRequest struct {
|
|||||||
type ReadData struct {
|
type ReadData struct {
|
||||||
Total int64 `json:"total"` // 总条数
|
Total int64 `json:"total"` // 总条数
|
||||||
Page int `json:"page"` // 当前页码
|
Page int `json:"page"` // 当前页码
|
||||||
PageSize int `json:"page_size"`// 每页条数
|
PageSize int `json:"page_size"` // 每页条数
|
||||||
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),
|
||||||
232
backend/src/logic4country/update.go
Normal file
232
backend/src/logic4country/update.go
Normal 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: "更新成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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(¤cyID)
|
||||||
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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
168
backend/src/logic4currency/delete.go
Normal file
168
backend/src/logic4currency/delete.go
Normal 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: "删除成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
231
backend/src/logic4currency/read.go
Normal file
231
backend/src/logic4currency/read.go
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
200
backend/src/logic4exchange/create.go
Normal file
200
backend/src/logic4exchange/create.go
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
184
backend/src/logic4exchange/delete.go
Normal file
184
backend/src/logic4exchange/delete.go
Normal 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: "删除成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
240
backend/src/logic4exchange/read.go
Normal file
240
backend/src/logic4exchange/read.go
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
208
backend/src/logic4exchange/update.go
Normal file
208
backend/src/logic4exchange/update.go
Normal 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: "更新成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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: "账号或密码不能为空",
|
||||||
})
|
})
|
||||||
@@ -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("💡 执行密码插入",
|
||||||
235
backend/src/logic4variety/create.go
Normal file
235
backend/src/logic4variety/create.go
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
216
backend/src/logic4variety/delete.go
Normal file
216
backend/src/logic4variety/delete.go
Normal 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: "删除成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
243
backend/src/logic4variety/read.go
Normal file
243
backend/src/logic4variety/read.go
Normal 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
370
backend/src/logic4variety/update.go
Normal file
370
backend/src/logic4variety/update.go
Normal 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
99
backend/src/main.go
Normal 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")
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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 "===== 构建脚本执行完成 ====="
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
docker run -itd --name go_user_dev -v $(pwd)/src:/app -p 20000:80 golang:1.25.0-alpine3.22
|
|
||||||
@@ -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
|
|
||||||
@@ -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进程已退出"
|
|
||||||
@@ -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;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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("✅ 数据库连接验证成功")
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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=
|
|
||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
分析这个项目,在 create.go 中完成以下需求:
|
|
||||||
|
|
||||||
1、接收 name,code 两个参数。
|
|
||||||
2、确认提交的 name,code 两个参数不能为空,如果有空,则返回提示。
|
|
||||||
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,name,code 参数。
|
|
||||||
2、确认提交的 country_id 参数有不能为空,如果为空,则返回提示。
|
|
||||||
3、确认提交的 name,code 两个参数,必须有一个不能为空,如果都为空,则返回提示。
|
|
||||||
4、如果 name 不为空,开启事务保存到 name 中。
|
|
||||||
5、如果 code 不为空,开启事务保存到 code 中。
|
|
||||||
6、如果 name,code 都不为空,开启事务保存到 name,code 中。
|
|
||||||
---
|
|
||||||
分析这个项目,在 read.go 中完成以下需求:
|
|
||||||
|
|
||||||
1、接收 country_id,name,code,page,page_size 参数。
|
|
||||||
2、确认提交的 country_id,name,code 必须有一个不能为空,如果都为空,则返回提示。
|
|
||||||
3、确认提交的 page,page_size, 如果为空,则 page 默认为 1,page_size 默认为20。
|
|
||||||
3、根据参数去 country_info_view 中查找数据,并做分页查询。
|
|
||||||
4、将查找的数据分页返回。
|
|
||||||
---
|
|
||||||
@@ -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
|
||||||
|
|||||||
238
frontend/asset_assistant/lib/pages/country_add_page.dart
Normal file
238
frontend/asset_assistant/lib/pages/country_add_page.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 国旗为可选字段,不添加验证器
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
439
frontend/asset_assistant/lib/pages/country_page.dart
Normal file
439
frontend/asset_assistant/lib/pages/country_page.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
209
frontend/asset_assistant/lib/pages/exchange_add_page.dart
Normal file
209
frontend/asset_assistant/lib/pages/exchange_add_page.dart
Normal 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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
frontend/asset_assistant/lib/pages/exchange_page.dart
Normal file
138
frontend/asset_assistant/lib/pages/exchange_page.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
22
frontend/asset_assistant/lib/utils/host_utils.dart
Normal file
22
frontend/asset_assistant/lib/utils/host_utils.dart
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user