Compare commits

...

96 Commits

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

119
backend/README.md Normal file
View File

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

12
backend/chat.md Normal file
View File

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

126
backend/prompt.md Normal file
View File

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

View File

@@ -1,28 +1,41 @@
-- 切换到目标数据库 -- =========================================================
\c postgres; -- user.sql 👤
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$ DO $$
BEGIN 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 IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user') THEN
CREATE TABLE "user" ( -- user是关键字用双引号包裹 CREATE TABLE "user" (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_user_updated_at CREATE TRIGGER update_user_updated_at
BEFORE UPDATE ON "user" BEFORE UPDATE ON "user"
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '1⃣✅ user 主表已创建';
RAISE NOTICE 'created user table and trigger';
ELSE ELSE
RAISE NOTICE 'user table already exists'; RAISE NOTICE '1⃣⏩ user 主表已存在,跳过';
END IF; END IF;
-- user_account
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_account') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_account') THEN
CREATE TABLE user_account ( CREATE TABLE user_account (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL, user_id UUID NOT NULL,
account VARCHAR NOT NULL, account VARCHAR NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
@@ -30,18 +43,17 @@ BEGIN
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_user_account_updated_at CREATE TRIGGER update_user_account_updated_at
BEFORE UPDATE ON "user_account" BEFORE UPDATE ON user_account
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '2⃣✅ user_account 子表已创建';
RAISE NOTICE 'created user_account table and trigger';
ELSE ELSE
RAISE NOTICE 'user_account table already exists'; RAISE NOTICE '2⃣⏩ user_account 子表已存在,跳过';
END IF; END IF;
-- user_password
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_password') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_password') THEN
CREATE TABLE user_password ( CREATE TABLE user_password (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL, user_id UUID NOT NULL,
password VARCHAR NOT NULL, password VARCHAR NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
@@ -49,49 +61,56 @@ BEGIN
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_user_password_updated_at CREATE TRIGGER update_user_password_updated_at
BEFORE UPDATE ON "user_password" BEFORE UPDATE ON user_password
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '3⃣✅ user_password 子表已创建';
RAISE NOTICE 'created user_password table and trigger';
ELSE ELSE
RAISE NOTICE 'user_password table already exists'; RAISE NOTICE '3⃣⏩ user_password 子表已存在,跳过';
END IF; END IF;
END $$; END $$;
-- 4⃣ 视图 ------------------------------------
DO $$ DO $$
DECLARE DECLARE
view_exists BOOLEAN; view_exists BOOLEAN;
BEGIN BEGIN
-- 检查视图是否已存在
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 FROM information_schema.views SELECT 1 FROM information_schema.views
WHERE table_name = 'user_info_view' WHERE table_name = 'user_info_view'
) INTO view_exists; ) 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 CREATE OR REPLACE VIEW user_info_view AS
SELECT SELECT
u.id AS user_id, u.id AS user_id,
ua.account AS user_account, ua.account,
up.password AS user_password, up.password
u.deleted AS deleted FROM "user" u
FROM JOIN user_account ua ON u.id = ua.user_id AND ua.deleted = FALSE
"user" u JOIN user_password up ON u.id = up.user_id AND up.deleted = FALSE
JOIN WHERE u.deleted = FALSE;
user_account ua ON u.id = ua.user_id
JOIN
user_password up ON u.id = up.user_id
WHERE
u.deleted = FALSE;
-- 根据视图是否已存在输出不同提示 RAISE NOTICE '4⃣✅ user_info_view 已创建/更新';
IF view_exists THEN
RAISE NOTICE '视图 user_info_view 已更新';
ELSE
RAISE NOTICE '视图 user_info_view 已创建';
END IF;
EXCEPTION EXCEPTION
WHEN OTHERS THEN WHEN OTHERS THEN
RAISE NOTICE '处理视图时发生错误: %', SQLERRM; 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 $$; END $$;

View File

@@ -1,97 +1,137 @@
-- 切换到目标数据库 -- =========================================================
\c postgres; -- country.sql 🌍
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$ DO $$
BEGIN 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 IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country') THEN
CREATE TABLE country ( CREATE TABLE country (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_country_updated_at CREATE TRIGGER update_country_updated_at
BEFORE UPDATE ON "country" BEFORE UPDATE ON country
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '1⃣✅ country 主表已创建';
RAISE NOTICE 'created country table and trigger';
ELSE ELSE
RAISE NOTICE 'country table already exists'; RAISE NOTICE '1⃣⏩ country 主表已存在,跳过';
END IF; END IF;
-- country_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_name') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_name') THEN
CREATE TABLE country_name ( CREATE TABLE country_name (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
country_id UUID NOT NULL, country_id UUID NOT NULL,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_name_updated_at CREATE TRIGGER update_country_name_updated_at
BEFORE UPDATE ON "country_name" BEFORE UPDATE ON country_name
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '2⃣✅ country_name 子表已创建';
RAISE NOTICE 'created country_name table and trigger';
ELSE ELSE
RAISE NOTICE 'country_name table already exists'; RAISE NOTICE '2⃣⏩ country_name 子表已存在,跳过';
END IF; END IF;
-- country_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_code') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_code') THEN
CREATE TABLE country_code ( CREATE TABLE country_code (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
country_id UUID NOT NULL, country_id UUID NOT NULL,
code VARCHAR NOT NULL, code VARCHAR NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_code_updated_at CREATE TRIGGER update_country_code_updated_at
BEFORE UPDATE ON "country_code" BEFORE UPDATE ON country_code
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '3⃣✅ country_code 子表已创建';
RAISE NOTICE 'created country_code table and trigger';
ELSE ELSE
RAISE NOTICE 'country_code table already exists'; 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 IF;
END $$; END $$;
-- 4⃣ 视图 ------------------------------------
DO $$ DO $$
DECLARE DECLARE
view_exists BOOLEAN; view_exists BOOLEAN;
BEGIN BEGIN
-- 检查视图是否已存在
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 FROM information_schema.views SELECT 1 FROM information_schema.views
WHERE table_name = 'country_info_view' WHERE table_name = 'country_info_view'
) INTO view_exists; ) 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 CREATE OR REPLACE VIEW country_info_view AS
SELECT SELECT
u.id AS country_id, u.id AS country_id,
n.name AS name, n.name,
c.code AS code, c.code,
u.deleted AS deleted f.flag
FROM FROM country u
country u LEFT JOIN country_name n ON u.id = n.country_id AND n.deleted = FALSE
JOIN LEFT JOIN country_code c ON u.id = c.country_id AND c.deleted = FALSE
country_name n ON u.id = n.country_id LEFT JOIN country_flag f ON u.id = f.country_id AND f.deleted = FALSE
JOIN WHERE u.deleted = FALSE;
country_code c ON u.id = c.country_id
WHERE
u.deleted = FALSE;
-- 根据视图是否已存在输出不同提示 RAISE NOTICE '4⃣✅ country_info_view 已创建/更新';
IF view_exists THEN
RAISE NOTICE '视图 country_info_view 已更新';
ELSE
RAISE NOTICE '视图 country_info_view 已创建';
END IF;
EXCEPTION EXCEPTION
WHEN OTHERS THEN WHEN OTHERS THEN
RAISE NOTICE '处理视图时发生错误: %', SQLERRM; 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 $$; END $$;

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

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

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

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

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

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

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

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

View File

@@ -41,9 +41,9 @@ func Init() {
} }
// 设置连接池参数 // 设置连接池参数
DB.SetMaxOpenConns(100) // 最大打开连接数 DB.SetMaxOpenConns(100) // 最大打开连接数
DB.SetMaxIdleConns(20) // 最大空闲连接数 DB.SetMaxIdleConns(20) // 最大空闲连接数
DB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间 DB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
// 验证数据库连接 // 验证数据库连接
if err := DB.Ping(); err != nil { if err := DB.Ping(); err != nil {

View File

@@ -14,17 +14,17 @@ import (
var shanghaiLoc *time.Location var shanghaiLoc *time.Location
func init() { func init() {
var err error var err error
shanghaiLoc, err = time.LoadLocation("Asia/Shanghai") shanghaiLoc, err = time.LoadLocation("Asia/Shanghai")
if err != nil { if err != nil {
// 尝试备选时区名称 // 尝试备选时区名称
shanghaiLoc, err = time.LoadLocation("PRC") shanghaiLoc, err = time.LoadLocation("PRC")
if err != nil { if err != nil {
// 若仍失败,手动设置东八区偏移 // 若仍失败,手动设置东八区偏移
shanghaiLoc = time.FixedZone("CST", 8*3600) shanghaiLoc = time.FixedZone("CST", 8*3600)
log.Printf("警告:加载时区失败,使用手动东八区偏移: %v", err) log.Printf("警告:加载时区失败,使用手动东八区偏移: %v", err)
} }
} }
} }
// Init 初始化日志(依赖配置文件已加载) // Init 初始化日志(依赖配置文件已加载)
@@ -43,10 +43,10 @@ func Init() {
// 日志轮转配置lumberjack // 日志轮转配置lumberjack
hook := lumberjack.Logger{ hook := lumberjack.Logger{
Filename: viper.GetString("logger.path") + "logs/app.log", // 日志文件路径 Filename: viper.GetString("logger.path") + "logs/app.log", // 日志文件路径
MaxSize: viper.GetInt("logger.max_size"), // 单个文件最大大小MB MaxSize: viper.GetInt("logger.max_size"), // 单个文件最大大小MB
MaxBackups: viper.GetInt("logger.max_backup"), // 最大备份数 MaxBackups: viper.GetInt("logger.max_backup"), // 最大备份数
MaxAge: viper.GetInt("logger.max_age"), // 最大保留天数 MaxAge: viper.GetInt("logger.max_age"), // 最大保留天数
Compress: true, // 是否压缩 Compress: true, // 是否压缩
} }
// 编码器配置 // 编码器配置

View File

@@ -1,48 +1,43 @@
package logic4country package logic4country
import ( import (
"asset_assistant/db" // 数据库操作相关包 "asset_assistant/db"
"net/http" "net/http"
"time" // 时间处理包 "time"
"github.com/gin-gonic/gin" // Gin框架用于处理HTTP请求 "github.com/gin-gonic/gin"
"github.com/google/uuid" // UUID生成工具 "github.com/google/uuid"
"go.uber.org/zap" // 日志库 "go.uber.org/zap"
) )
// CreateRequest 注册请求参数结构 // CreateRequest 注册请求参数结构
// 用于接收客户端发送的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"` // 国家代码,必填
Flag string `json:"flag"` // 国旗信息,可选
} }
// CreateResponse 注册响应结构 // CreateResponse 注册响应结构
// 统一的API响应格式包含成功状态、提示信息和数据
type CreateResponse struct { type CreateResponse struct {
Success bool `json:"success"` // 操作是否成功 Success bool `json:"success"`
Message string `json:"message"` // 提示信息 Message string `json:"message"`
Data CreateData `json:"data"` // 响应数据 Data CreateData `json:"data"`
} }
// CreateData 响应数据结构 // CreateData 响应数据结构
// 包含创建成功后的国家ID
type CreateData struct { type CreateData struct {
CountryID string `json:"country_id"` // 国家唯一标识ID CountryID string `json:"country_id"`
} }
// CreateHandler 处理国家创建逻辑 // CreateHandler 处理创建逻辑
// 接收HTTP请求完成参数验证、数据库事务处理并返回响应
func CreateHandler(c *gin.Context) { func CreateHandler(c *gin.Context) {
startTime := time.Now() // 记录请求开始时间,用于统计耗时 startTime := time.Now()
// 获取或生成请求ID用于追踪整个请求链路
reqID := c.Request.Header.Get("X-RegisterRequest-ID") reqID := c.Request.Header.Get("X-RegisterRequest-ID")
if reqID == "" { if reqID == "" {
reqID = uuid.New().String() reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID)) zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
} }
// 记录请求接收日志,包含关键追踪信息
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),
@@ -50,14 +45,12 @@ func CreateHandler(c *gin.Context) {
) )
var req CreateRequest var req CreateRequest
// 绑定并验证请求参数检查name和code是否存在
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),
zap.Error(err), zap.Error(err),
zap.Any("request_body", c.Request.Body), zap.Any("request_body", c.Request.Body),
) )
// 返回参数错误响应
c.JSON(http.StatusBadRequest, CreateResponse{ c.JSON(http.StatusBadRequest, CreateResponse{
Success: false, Success: false,
Message: "请求参数错误name和code为必填项", Message: "请求参数错误name和code为必填项",
@@ -65,14 +58,14 @@ func CreateHandler(c *gin.Context) {
return return
} }
// 记录通过验证的请求参数
zap.L().Debug("✅ 请求参数验证通过", zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID), zap.String("req_id", reqID),
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),
) )
// 开启数据库事务,确保多表操作原子性(要么全成功,要么全失败) // 开启数据库事务
tx, err := db.DB.Begin() tx, err := db.DB.Begin()
if err != nil { if err != nil {
zap.L().Error("❌ 事务开启失败", zap.L().Error("❌ 事务开启失败",
@@ -85,10 +78,9 @@ func CreateHandler(c *gin.Context) {
}) })
return return
} }
// 延迟执行的恢复函数处理panic情况
defer func() { defer func() {
if r := recover(); r != nil { // 捕获panic if r := recover(); r != nil {
// 回滚事务
if err := tx.Rollback(); err != nil { if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败", zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
@@ -99,7 +91,6 @@ func CreateHandler(c *gin.Context) {
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.Any("recover", r), zap.Any("recover", r),
) )
// 返回系统错误响应
c.JSON(http.StatusInternalServerError, CreateResponse{ c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false, Success: false,
Message: "系统错误,请稍后重试", Message: "系统错误,请稍后重试",
@@ -107,11 +98,109 @@ func CreateHandler(c *gin.Context) {
} }
}() }()
// 1. 在country表中创建记录并获取自动生成的ID // 唯一性校验 - 国家名称(排除已删除数据)
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 var countryID string
err = tx.QueryRow("INSERT INTO country DEFAULT VALUES RETURNING id").Scan(&countryID) err = tx.QueryRow("INSERT INTO country DEFAULT VALUES RETURNING id").Scan(&countryID)
if err != nil { if err != nil {
tx.Rollback() // 操作失败,回滚事务 tx.Rollback()
zap.L().Error("❌ country表插入失败", zap.L().Error("❌ country表插入失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.Error(err), zap.Error(err),
@@ -128,11 +217,11 @@ func CreateHandler(c *gin.Context) {
zap.String("country_id", countryID), zap.String("country_id", countryID),
) )
// 2. 插入国家名称到name表与country_id关联 // 2. 插入国家名称
_, err = tx.Exec("INSERT INTO name (country_id, name) VALUES ($1, $2)", countryID, req.Name) _, err = tx.Exec("INSERT INTO country_name (country_id, name) VALUES ($1, $2)", countryID, req.Name)
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", countryID), zap.String("country_id", countryID),
zap.Error(err), zap.Error(err),
@@ -144,11 +233,11 @@ func CreateHandler(c *gin.Context) {
return return
} }
// 3. 插入国家代码到code表与country_id关联 // 3. 插入国家代码
_, err = tx.Exec("INSERT INTO code (country_id, code) VALUES ($1, $2)", countryID, req.Code) _, err = tx.Exec("INSERT INTO country_code (country_id, code) VALUES ($1, $2)", countryID, req.Code)
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", countryID), zap.String("country_id", countryID),
zap.Error(err), zap.Error(err),
@@ -160,9 +249,31 @@ func CreateHandler(c *gin.Context) {
return 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 { if err := tx.Commit(); err != nil {
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("country_id", countryID),
@@ -175,7 +286,6 @@ func CreateHandler(c *gin.Context) {
return return
} }
// 记录请求处理耗时
duration := time.Since(startTime) duration := time.Since(startTime)
zap.L().Info("✅ 国家创建请求处理完成", zap.L().Info("✅ 国家创建请求处理完成",
zap.String("req_id", reqID), zap.String("req_id", reqID),
@@ -183,7 +293,6 @@ func CreateHandler(c *gin.Context) {
zap.Duration("duration", duration), zap.Duration("duration", duration),
) )
// 返回成功响应包含创建的国家ID
c.JSON(http.StatusOK, CreateResponse{ c.JSON(http.StatusOK, CreateResponse{
Success: true, Success: true,
Message: "创建成功", Message: "创建成功",

View File

@@ -21,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")
@@ -106,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),
@@ -122,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),
@@ -137,6 +137,26 @@ func DeleteHandler(c *gin.Context) {
return return
} }
// 新增3.4 更新flag表软删除国旗信息
_, err = tx.Exec("UPDATE country_flag SET deleted = TRUE WHERE country_id = $1", req.CountryID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ country_flag表更新失败",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除国旗信息失败",
})
return
}
zap.L().Debug("📝 flag表软删除成功",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
)
// 提交事务 // 提交事务
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
tx.Rollback() tx.Rollback()

View File

@@ -19,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"` // 每页条数,可选
} }
@@ -31,11 +32,12 @@ type ReadData struct {
Items []CountryInfoViewItem `json:"items"` // 数据列表 Items []CountryInfoViewItem `json:"items"` // 数据列表
} }
// CountryInfoViewItem 视图数据项结构 // CountryInfoViewItem 视图数据项结构,新增国旗字段
type CountryInfoViewItem struct { type CountryInfoViewItem struct {
CountryID string `json:"country_id"` // 国家ID CountryID string `json:"country_id"` // 国家ID
Name string `json:"name"` // 国家名称 Name string `json:"name"` // 国家名称
Code string `json:"code"` // 国家代码 Code string `json:"code"` // 国家代码
Flag string `json:"flag"` // 国旗信息,新增字段
} }
// ReadResponse 读取响应结构 // ReadResponse 读取响应结构
@@ -45,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
@@ -76,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 {
@@ -104,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),
) )
@@ -128,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 ")
@@ -141,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)
@@ -176,11 +172,11 @@ func ReadHandler(c *gin.Context) {
} }
defer rows.Close() defer rows.Close()
// 处理查询结果 // 处理查询结果新增flag字段扫描
var items []CountryInfoViewItem var items []CountryInfoViewItem
for rows.Next() { for rows.Next() {
var item CountryInfoViewItem var item CountryInfoViewItem
if err := rows.Scan(&item.CountryID, &item.Name, &item.Code); err != nil { if err := rows.Scan(&item.CountryID, &item.Name, &item.Code, &item.Flag); err != nil {
zap.L().Error("❌ 解析查询结果失败", zap.L().Error("❌ 解析查询结果失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.Error(err), zap.Error(err),

View File

@@ -15,6 +15,7 @@ type UpdateRequest struct {
CountryID string `json:"country_id" binding:"required"` // 国家ID必填 CountryID string `json:"country_id" binding:"required"` // 国家ID必填
Name string `json:"name"` // 国家名称,可选 Name string `json:"name"` // 国家名称,可选
Code string `json:"code"` // 国家代码,可选 Code string `json:"code"` // 国家代码,可选
Flag string `json:"flag"` // 国旗信息,可选(新增字段)
} }
// UpdateResponse 更新响应结构 // UpdateResponse 更新响应结构
@@ -23,7 +24,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
@@ -54,16 +55,16 @@ func UpdateHandler(c *gin.Context) {
return return
} }
// 验证namecode不能同时为空 // 验证namecode和flag不能同时为空
if req.Name == "" && req.Code == "" { if req.Name == "" && req.Code == "" && req.Flag == "" {
zap.L().Warn("⚠️ 请求参数验证失败", zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID), zap.String("req_id", reqID),
zap.String("country_id", req.CountryID), zap.String("country_id", req.CountryID),
zap.String("reason", "namecode不能同时为空"), zap.String("reason", "namecode和flag不能同时为空"),
) )
c.JSON(http.StatusBadRequest, UpdateResponse{ c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false, Success: false,
Message: "请求参数错误namecode不能同时为空", Message: "请求参数错误namecode和flag不能同时为空",
}) })
return return
} }
@@ -73,6 +74,7 @@ func UpdateHandler(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), // 新增国旗参数日志
) )
// 开启数据库事务 // 开启数据库事务
@@ -111,10 +113,10 @@ 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 country_name SET name = $1, updated_at = CURRENT_TIMESTAMP WHERE country_id = $2", req.Name, 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),
@@ -133,10 +135,10 @@ func UpdateHandler(c *gin.Context) {
// 如果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 country_code SET code = $1, updated_at = CURRENT_TIMESTAMP WHERE country_id = $2", req.Code, 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),
@@ -153,6 +155,52 @@ func UpdateHandler(c *gin.Context) {
) )
} }
// 新增如果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 { if err := tx.Commit(); err != nil {
tx.Rollback() tx.Rollback()

View File

@@ -0,0 +1,193 @@
package logic4currency
import (
"asset_assistant/db" // 数据库操作相关包
"net/http"
"time" // 时间处理包
"github.com/gin-gonic/gin" // Gin框架用于处理HTTP请求
"github.com/google/uuid" // UUID生成工具
"go.uber.org/zap" // 日志库
)
// CreateRequest 注册请求参数结构
// 用于接收客户端发送的JSON数据绑定并验证必填字段
type CreateRequest struct {
Name string `json:"name" binding:"required"` // 货币名称,必填
Code string `json:"code" binding:"required"` // 货币代码,必填
}
// CreateResponse 注册响应结构
// 统一的API响应格式包含成功状态、提示信息和数据
type CreateResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
Data CreateData `json:"data"` // 响应数据
}
// CreateData 响应数据结构
// 包含创建成功后的货币ID
type CreateData struct {
CurrencyID string `json:"currency_id"` // 货币唯一标识ID
}
// CreateHandler 处理创建逻辑
func CreateHandler(c *gin.Context) {
startTime := time.Now() // 记录请求开始时间,用于统计耗时
// 获取或生成请求ID用于追踪整个请求链路
reqID := c.Request.Header.Get("X-RegisterRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
// 记录请求接收日志,包含关键追踪信息
zap.L().Info("📥 收到货币创建请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req CreateRequest
// 绑定并验证请求参数检查name和code是否存在
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
zap.Any("request_body", c.Request.Body),
)
// 返回参数错误响应
c.JSON(http.StatusBadRequest, CreateResponse{
Success: false,
Message: "请求参数错误name和code为必填项",
})
return
}
// 记录通过验证的请求参数
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("name", req.Name),
zap.String("code", req.Code),
)
// 开启数据库事务,确保多表操作原子性(要么全成功,要么全失败)
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
// 延迟执行的恢复函数处理panic情况
defer func() {
if r := recover(); r != nil { // 捕获panic
// 回滚事务
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
// 返回系统错误响应
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 1. 在currency表中创建记录并获取自动生成的ID
var currencyID string
err = tx.QueryRow("INSERT INTO currency DEFAULT VALUES RETURNING id").Scan(&currencyID)
if err != nil {
tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ currency表插入失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建货币记录失败",
})
return
}
zap.L().Debug("📝 currency表插入成功",
zap.String("req_id", reqID),
zap.String("currency_id", currencyID),
)
// 2. 插入货币名称到name表与currency_id关联
_, err = tx.Exec("INSERT INTO currency_name (currency_id, name) VALUES ($1, $2)", currencyID, req.Name)
if err != nil {
tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ currency_name表插入失败",
zap.String("req_id", reqID),
zap.String("currency_id", currencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存名称信息失败",
})
return
}
// 3. 插入货币代码到code表与currency_id关联
_, err = tx.Exec("INSERT INTO currency_code (currency_id, code) VALUES ($1, $2)", currencyID, req.Code)
if err != nil {
tx.Rollback() // 操作失败,回滚事务
zap.L().Error("❌ currency_code表插入失败",
zap.String("req_id", reqID),
zap.String("currency_id", currencyID),
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("currency_id", currencyID),
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("currency_id", currencyID),
zap.Duration("duration", duration),
)
// 返回成功响应包含创建的货币ID
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
Data: CreateData{
CurrencyID: currencyID,
},
})
}

View File

@@ -0,0 +1,168 @@
package logic4currency
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DeleteRequest 删除请求参数结构
type DeleteRequest struct {
CurrencyID string `json:"currency_id" binding:"required"` // 货币ID必填
}
// DeleteResponse 删除响应结构
type DeleteResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
}
// DeleteHandler 处理删除逻辑(软删除)
func DeleteHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-DeleteRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
zap.L().Info("📥 收到货币删除请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req DeleteRequest
// 绑定并验证请求参数
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, DeleteResponse{
Success: false,
Message: "请求参数错误currency_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
// 延迟处理panic情况
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 3.1 更新currency表
_, err = tx.Exec("UPDATE currency SET deleted = TRUE WHERE id = $1", req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ currency表更新失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除货币记录失败",
})
return
}
// 3.2 更新name表
_, err = tx.Exec("UPDATE currency_name SET deleted = TRUE WHERE currency_id = $1", req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ currency_name表更新失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除名称信息失败",
})
return
}
// 3.3 更新code表
_, err = tx.Exec("UPDATE currency_code SET deleted = TRUE WHERE currency_id = $1", req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ currency_code表更新失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除代码信息失败",
})
return
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 货币删除请求处理完成",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, DeleteResponse{
Success: true,
Message: "删除成功",
})
}

View File

@@ -0,0 +1,231 @@
package logic4currency
import (
"asset_assistant/db"
"net/http"
"strconv"
"strings"
"time"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// ReadRequest 读取请求参数结构
type ReadRequest struct {
CurrencyID string `form:"currency_id"` // 货币ID可选
Name string `form:"name"` // 货币名称,可选
Code string `form:"code"` // 货币代码,可选
Page string `form:"page"` // 页码,可选
PageSize string `form:"page_size"` // 每页条数,可选
}
// ReadData 读取响应数据结构
type ReadData struct {
Total int64 `json:"total"` // 总条数
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页条数
Items []CurrencyInfoViewItem `json:"items"` // 数据列表
}
// CurrencyInfoViewItem 视图数据项结构
type CurrencyInfoViewItem struct {
CurrencyID string `json:"currency_id"` // 货币ID
Name string `json:"name"` // 货币名称
Code string `json:"code"` // 货币代码
}
// ReadResponse 读取响应结构
type ReadResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
Data ReadData `json:"data"` // 响应数据
}
// ReadHandler 处理信息查询逻辑
func ReadHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
reqID := c.Request.Header.Get("X-ReadRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
// 记录请求接收日志
zap.L().Info("📥 收到货币查询请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
// 绑定请求参数
var req ReadRequest
if err := c.ShouldBindQuery(&req); err != nil {
zap.L().Warn("⚠️ 请求参数解析失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数格式错误",
})
return
}
// 验证查询条件至少有一个不为空
if req.CurrencyID == "" && req.Name == "" && req.Code == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("reason", "currency_id、name、code不能同时为空"),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数错误currency_id、name、code不能同时为空",
})
return
}
// 处理分页参数默认值
page, err := strconv.Atoi(req.Page)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(req.PageSize)
if err != nil || pageSize < 1 {
pageSize = 20
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.Int("page", page),
zap.Int("page_size", pageSize),
)
// 构建查询条件和参数
whereClauses := []string{}
args := []interface{}{}
paramIndex := 1
if req.CurrencyID != "" {
whereClauses = append(whereClauses, "currency_id = $"+strconv.Itoa(paramIndex))
args = append(args, req.CurrencyID)
paramIndex++
}
if req.Name != "" {
whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Name+"%")
paramIndex++
}
if req.Code != "" {
whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Code+"%")
paramIndex++
}
// 构建基础SQL
baseSQL := "SELECT currency_id, name, code FROM currency_info_view"
countSQL := "SELECT COUNT(*) FROM currency_info_view"
if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
baseSQL += whereStr
countSQL += whereStr
}
// 计算分页偏移量
offset := (page - 1) * pageSize
// 拼接分页SQL使用fmt.Sprintf更清晰
querySQL := fmt.Sprintf("%s ORDER BY currency_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1)
args = append(args, pageSize, offset)
// 查询总条数(修正参数传递方式)
var total int64
countArgs := args[:len(args)-2] // 排除分页参数
err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total)
if err != nil {
zap.L().Error("❌ 查询总条数失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
// 执行分页查询
rows, err := db.DB.Query(querySQL, args...)
if err != nil {
zap.L().Error("❌ 分页查询失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
defer rows.Close()
// 处理查询结果
var items []CurrencyInfoViewItem
for rows.Next() {
var item CurrencyInfoViewItem
if err := rows.Scan(&item.CurrencyID, &item.Name, &item.Code); err != nil {
zap.L().Error("❌ 解析查询结果失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "数据处理失败,请稍后重试",
})
return
}
items = append(items, item)
}
// 检查行迭代过程中是否发生错误
if err := rows.Err(); err != nil {
zap.L().Error("❌ 行迭代错误",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 货币查询请求处理完成",
zap.String("req_id", reqID),
zap.Int64("total", total),
zap.Int("page", page),
zap.Int("page_size", pageSize),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, ReadResponse{
Success: true,
Message: "查询成功",
Data: ReadData{
Total: total,
Page: page,
PageSize: pageSize,
Items: items,
},
})
}

View File

@@ -0,0 +1,184 @@
package logic4currency
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// UpdateRequest 更新请求参数结构
type UpdateRequest struct {
CurrencyID string `json:"currency_id" binding:"required"` // 货币ID必填
Name string `json:"name"` // 货币名称,可选
Code string `json:"code"` // 货币代码,可选
}
// UpdateResponse 更新响应结构
type UpdateResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
}
// UpdateHandler 处理信息更新逻辑
func UpdateHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
reqID := c.Request.Header.Get("X-UpdateRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
// 记录请求接收日志
zap.L().Info("📥 收到货币更新请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req UpdateRequest
// 绑定并验证请求参数主要验证currency_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: "请求参数错误currency_id为必填项",
})
return
}
// 验证name和code不能同时为空
if req.Name == "" && req.Code == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.String("reason", "name和code不能同时为空"),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误name和code不能同时为空",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.String("name", req.Name),
zap.String("code", req.Code),
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
// 延迟处理panic情况
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 如果name不为空更新name表
if req.Name != "" {
_, err = tx.Exec("UPDATE currency_name SET name = $1 WHERE currency_id = $2", req.Name, 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, UpdateResponse{
Success: false,
Message: "更新名称信息失败",
})
return
}
zap.L().Debug("📝 name表更新成功",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
)
}
// 如果code不为空更新code表
if req.Code != "" {
_, err = tx.Exec("UPDATE currency_code SET code = $1 WHERE currency_id = $2", req.Code, req.CurrencyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ currency_code表更新失败",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新代码信息失败",
})
return
}
zap.L().Debug("📝 code表更新成功",
zap.String("req_id", reqID),
zap.String("currency_id", req.CurrencyID),
)
}
// 提交事务
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, UpdateResponse{
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, UpdateResponse{
Success: true,
Message: "更新成功",
})
}

View File

@@ -0,0 +1,200 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// CreateRequest 注册请求参数结构
// 新增short_name字段保持必填性与其他核心字段一致
type CreateRequest struct {
Name string `json:"name" binding:"required"` // 交易所名称,必填
Code string `json:"code" binding:"required"` // 交易所代码,必填
ShortName string `json:"short_name" binding:"required"` // 交易所短名称,必填
}
// CreateResponse 注册响应结构
type CreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data CreateData `json:"data"`
}
// CreateData 响应数据结构
type CreateData struct {
ExchangeID string `json:"exchange_id"`
}
// CreateHandler 处理创建逻辑
func CreateHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-RegisterRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
zap.L().Info("📥 收到交易所创建请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req CreateRequest
// 绑定参数时会自动验证name、code、short_name三个必填字段
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
zap.Any("request_body", c.Request.Body),
)
c.JSON(http.StatusBadRequest, CreateResponse{
Success: false,
Message: "请求参数错误name、code和short_name为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("short_name", req.ShortName), // 新增短名称日志
)
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 1. 创建exchange主记录
var exchangeID string
err = tx.QueryRow("INSERT INTO exchange DEFAULT VALUES RETURNING id").Scan(&exchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange表插入失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建交易所记录失败",
})
return
}
zap.L().Debug("📝 exchange表插入成功",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
)
// 2. 插入名称信息
_, err = tx.Exec("INSERT INTO exchange_name (exchange_id, name) VALUES ($1, $2)", exchangeID, req.Name)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_name表插入失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存名称信息失败",
})
return
}
// 3. 新增插入短名称信息对应exchange_short_name表
_, err = tx.Exec("INSERT INTO exchange_short_name (exchange_id, short_name) VALUES ($1, $2)", exchangeID, req.ShortName)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_short_name表插入失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存短名称信息失败",
})
return
}
// 4. 插入代码信息
_, err = tx.Exec("INSERT INTO exchange_code (exchange_id, code) VALUES ($1, $2)", exchangeID, req.Code)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_code表插入失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存代码信息失败",
})
return
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
duration := time.Since(startTime)
zap.L().Info("✅ 交易所创建请求处理完成",
zap.String("req_id", reqID),
zap.String("exchange_id", exchangeID),
zap.Duration("duration", duration),
)
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
Data: CreateData{
ExchangeID: exchangeID,
},
})
}

View File

@@ -0,0 +1,184 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DeleteRequest 删除请求参数结构
type DeleteRequest struct {
ExchangeID string `json:"exchange_id" binding:"required"` // 交易所ID必填
}
// DeleteResponse 删除响应结构
type DeleteResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
}
// DeleteHandler 处理删除逻辑(软删除)
func DeleteHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-DeleteRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
zap.L().Info("📥 收到交易所删除请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req DeleteRequest
// 绑定并验证请求参数
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, DeleteResponse{
Success: false,
Message: "请求参数错误exchange_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
// 延迟处理panic情况
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 1. 更新exchange表
_, err = tx.Exec("UPDATE exchange SET deleted = TRUE WHERE id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除交易所记录失败",
})
return
}
// 2. 更新exchange_name表
_, err = tx.Exec("UPDATE exchange_name SET deleted = TRUE WHERE exchange_id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除名称信息失败",
})
return
}
// 3. 新增更新exchange_short_name表软删除短名称记录
_, err = tx.Exec("UPDATE exchange_short_name SET deleted = TRUE WHERE exchange_id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_short_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除短名称信息失败",
})
return
}
// 4. 更新exchange_code表
_, err = tx.Exec("UPDATE exchange_code SET deleted = TRUE WHERE exchange_id = $1", req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_code表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除代码信息失败",
})
return
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 交易所删除请求处理完成",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, DeleteResponse{
Success: true,
Message: "删除成功",
})
}

View File

@@ -0,0 +1,240 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"strconv"
"strings"
"time"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// ReadRequest 读取请求参数结构
type ReadRequest struct {
ExchangeID string `form:"exchange_id"` // 交易所ID可选
Name string `form:"name"` // 交易所名称,可选
Code string `form:"code"` // 交易所代码,可选
ShortName string `form:"short_name"` // 交易所短名称,新增查询条件
Page string `form:"page"` // 页码,可选
PageSize string `form:"page_size"` // 每页条数,可选
}
// ReadData 读取响应数据结构
type ReadData struct {
Total int64 `json:"total"` // 总条数
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页条数
Items []ExchangeInfoViewItem `json:"items"` // 数据列表
}
// ExchangeInfoViewItem 视图数据项结构
type ExchangeInfoViewItem struct {
ExchangeID string `json:"exchange_id"` // 交易所ID
Name string `json:"name"` // 交易所名称
Code string `json:"code"` // 交易所代码
ShortName string `json:"short_name"` // 新增:交易所短名称
}
// ReadResponse 读取响应结构
type ReadResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
Data ReadData `json:"data"` // 响应数据
}
// ReadHandler 处理信息查询逻辑
func ReadHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
reqID := c.Request.Header.Get("X-ReadRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
// 记录请求接收日志
zap.L().Info("📥 收到交易所查询请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
// 绑定请求参数
var req ReadRequest
if err := c.ShouldBindQuery(&req); err != nil {
zap.L().Warn("⚠️ 请求参数解析失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数格式错误",
})
return
}
// 验证查询条件至少有一个不为空新增short_name作为可选条件
if req.ExchangeID == "" && req.Name == "" && req.Code == "" && req.ShortName == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("reason", "exchange_id、name、code、short_name不能同时为空"),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数错误exchange_id、name、code、short_name不能同时为空",
})
return
}
// 处理分页参数默认值
page, err := strconv.Atoi(req.Page)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(req.PageSize)
if err != nil || pageSize < 1 {
pageSize = 20
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("short_name", req.ShortName), // 新增短名称日志
zap.Int("page", page),
zap.Int("page_size", pageSize),
)
// 构建查询条件和参数
whereClauses := []string{}
args := []interface{}{}
paramIndex := 1
if req.ExchangeID != "" {
whereClauses = append(whereClauses, "exchange_id = $"+strconv.Itoa(paramIndex))
args = append(args, req.ExchangeID)
paramIndex++
}
if req.Name != "" {
whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Name+"%")
paramIndex++
}
// 新增:短名称查询条件
if req.ShortName != "" {
whereClauses = append(whereClauses, "short_name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.ShortName+"%")
paramIndex++
}
if req.Code != "" {
whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Code+"%")
paramIndex++
}
// 构建基础SQL新增查询short_name字段
baseSQL := "SELECT exchange_id, name, short_name, code FROM exchange_info_view"
countSQL := "SELECT COUNT(*) FROM exchange_info_view"
if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
baseSQL += whereStr
countSQL += whereStr
}
// 计算分页偏移量
offset := (page - 1) * pageSize
// 拼接分页SQL
querySQL := fmt.Sprintf("%s ORDER BY exchange_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1)
args = append(args, pageSize, offset)
// 查询总条数
var total int64
countArgs := args[:len(args)-2] // 排除分页参数
err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total)
if err != nil {
zap.L().Error("❌ 查询总条数失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
// 执行分页查询
rows, err := db.DB.Query(querySQL, args...)
if err != nil {
zap.L().Error("❌ 分页查询失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
defer rows.Close()
// 处理查询结果新增扫描short_name字段
var items []ExchangeInfoViewItem
for rows.Next() {
var item ExchangeInfoViewItem
if err := rows.Scan(&item.ExchangeID, &item.Name, &item.ShortName, &item.Code); err != nil {
zap.L().Error("❌ 解析查询结果失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "数据处理失败,请稍后重试",
})
return
}
items = append(items, item)
}
// 检查行迭代过程中是否发生错误
if err := rows.Err(); err != nil {
zap.L().Error("❌ 行迭代错误",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 交易所查询请求处理完成",
zap.String("req_id", reqID),
zap.Int64("total", total),
zap.Int("page", page),
zap.Int("page_size", pageSize),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, ReadResponse{
Success: true,
Message: "查询成功",
Data: ReadData{
Total: total,
Page: page,
PageSize: pageSize,
Items: items,
},
})
}

View File

@@ -0,0 +1,208 @@
package logic4exchange
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// UpdateRequest 更新请求参数结构
type UpdateRequest struct {
ExchangeID string `json:"exchange_id" binding:"required"` // 交易所ID必填
Name string `json:"name"` // 交易所名称,可选
Code string `json:"code"` // 交易所代码,可选
ShortName string `json:"short_name"` // 新增:交易所短名称,可选
}
// UpdateResponse 更新响应结构
type UpdateResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
}
// UpdateHandler 处理信息更新逻辑
func UpdateHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
reqID := c.Request.Header.Get("X-UpdateRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
// 记录请求接收日志
zap.L().Info("📥 收到交易所更新请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req UpdateRequest
// 绑定并验证请求参数主要验证exchange_id必填
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误exchange_id为必填项",
})
return
}
// 验证name、code和short_name不能同时为空
if req.Name == "" && req.Code == "" && req.ShortName == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.String("reason", "name、code和short_name不能同时为空"),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误name、code和short_name不能同时为空",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("short_name", req.ShortName), // 新增短名称日志
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
// 延迟处理panic情况
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 如果name不为空更新name表
if req.Name != "" {
_, err = tx.Exec("UPDATE exchange_name SET name = $1 WHERE exchange_id = $2", req.Name, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新名称信息失败",
})
return
}
zap.L().Debug("📝 name表更新成功",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
}
// 如果code不为空更新code表
if req.Code != "" {
_, err = tx.Exec("UPDATE exchange_code SET code = $1 WHERE exchange_id = $2", req.Code, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_code表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新代码信息失败",
})
return
}
zap.L().Debug("📝 code表更新成功",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
}
// 新增如果short_name不为空更新short_name表
if req.ShortName != "" {
_, err = tx.Exec("UPDATE exchange_short_name SET short_name = $1 WHERE exchange_id = $2", req.ShortName, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ exchange_short_name表更新失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新短名称信息失败",
})
return
}
zap.L().Debug("📝 short_name表更新成功",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
)
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 交易所更新请求处理完成",
zap.String("req_id", reqID),
zap.String("exchange_id", req.ExchangeID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, UpdateResponse{
Success: true,
Message: "更新成功",
})
}

View File

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

View File

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

View File

@@ -0,0 +1,235 @@
package logic4variety
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// CreateRequest 创建品种请求参数结构
type CreateRequest struct {
Name string `json:"name" binding:"required"` // 品种名称,必填
Code string `json:"code" binding:"required"` // 品种代码,必填
Tick float64 `json:"tick" binding:"required"` // 品种tick必填
TickPrice float64 `json:"tick_price" binding:"required"` // 品种tick价格必填
ExchangeID string `json:"exchange_id" binding:"required"` // 交易所ID必填
}
// CreateResponse 创建品种响应结构
type CreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data CreateData `json:"data"`
}
// CreateData 响应数据结构
type CreateData struct {
VarietyID string `json:"variety_id"`
}
// CreateHandler 处理创建品种逻辑
func CreateHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-VarietyCreateRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
zap.L().Info("📥 收到品种创建请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req CreateRequest
// 绑定参数时会自动验证所有必填字段
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
zap.Any("request_body", c.Request.Body),
)
c.JSON(http.StatusBadRequest, CreateResponse{
Success: false,
Message: "请求参数错误name、code、tick、tick_price和exchange_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.Float64("tick", req.Tick),
zap.Float64("tick_price", req.TickPrice),
zap.String("exchange_id", req.ExchangeID),
)
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 1. 创建variety主记录
var varietyID string
err = tx.QueryRow("INSERT INTO variety DEFAULT VALUES RETURNING id").Scan(&varietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety表插入失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "创建品种记录失败",
})
return
}
zap.L().Debug("📝 variety表插入成功",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
)
// 2. 插入交易所关联信息
_, err = tx.Exec("INSERT INTO variety_exchange (variety_id, exchange_id) VALUES ($1, $2)", varietyID, req.ExchangeID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存交易所关联信息失败",
})
return
}
// 3. 插入名称信息
_, err = tx.Exec("INSERT INTO variety_name (variety_id, name) VALUES ($1, $2)", varietyID, req.Name)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_name表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存名称信息失败",
})
return
}
// 4. 插入代码信息
_, err = tx.Exec("INSERT INTO variety_code (variety_id, code) VALUES ($1, $2)", varietyID, req.Code)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存代码信息失败",
})
return
}
// 5. 插入tick信息
_, err = tx.Exec("INSERT INTO variety_tick (variety_id, tick) VALUES ($1, $2)", varietyID, req.Tick)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存tick信息失败",
})
return
}
// 6. 插入tick价格信息
_, err = tx.Exec("INSERT INTO variety_tick_price (variety_id, price) VALUES ($1, $2)", varietyID, req.TickPrice)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表插入失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "保存tick价格信息失败",
})
return
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, CreateResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
duration := time.Since(startTime)
zap.L().Info("✅ 品种创建请求处理完成",
zap.String("req_id", reqID),
zap.String("variety_id", varietyID),
zap.Duration("duration", duration),
)
c.JSON(http.StatusOK, CreateResponse{
Success: true,
Message: "创建成功",
Data: CreateData{
VarietyID: varietyID,
},
})
}

View File

@@ -0,0 +1,216 @@
package logic4variety
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DeleteRequest 删除请求参数结构
type DeleteRequest struct {
VarietyID string `json:"variety_id" binding:"required"` // 品种ID必填
}
// DeleteResponse 删除响应结构
type DeleteResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
}
// DeleteHandler 处理删除逻辑(软删除)
func DeleteHandler(c *gin.Context) {
startTime := time.Now()
reqID := c.Request.Header.Get("X-DeleteRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
zap.L().Info("📥 收到品种删除请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req DeleteRequest
// 绑定并验证请求参数
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, DeleteResponse{
Success: false,
Message: "请求参数错误variety_id为必填项",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
// 延迟处理panic情况
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 1. 更新variety表
_, err = tx.Exec("UPDATE variety SET deleted = TRUE WHERE id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除品种记录失败",
})
return
}
// 2. 更新variety_exchange表
_, err = tx.Exec("UPDATE variety_exchange SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除交易所关联信息失败",
})
return
}
// 3. 更新variety_name表
_, err = tx.Exec("UPDATE variety_name SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_name表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除名称信息失败",
})
return
}
// 4. 更新variety_code表
_, err = tx.Exec("UPDATE variety_code SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除代码信息失败",
})
return
}
// 5. 更新variety_tick表
_, err = tx.Exec("UPDATE variety_tick SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除最小变动价位信息失败",
})
return
}
// 6. 更新variety_tick_price表
_, err = tx.Exec("UPDATE variety_tick_price SET deleted = TRUE WHERE variety_id = $1", req.VarietyID)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "删除最小变动价位金额信息失败",
})
return
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, DeleteResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 品种删除请求处理完成",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, DeleteResponse{
Success: true,
Message: "删除成功",
})
}

View File

@@ -0,0 +1,243 @@
package logic4variety
import (
"asset_assistant/db"
"net/http"
"strconv"
"strings"
"time"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// ReadRequest 读取请求参数结构
type ReadRequest struct {
VarietyID string `form:"variety_id"` // 品种ID可选
Name string `form:"name"` // 品种名称,可选
Code string `form:"code"` // 品种代码,可选
ExchangeName string `form:"exchange_name"` // 交易所名称,可选
Page string `form:"page"` // 页码,可选
PageSize string `form:"page_size"` // 每页条数,可选
}
// ReadData 读取响应数据结构
type ReadData struct {
Total int64 `json:"total"` // 总条数
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页条数
Items []VarietyInfoViewItem `json:"items"` // 数据列表
}
// VarietyInfoViewItem 视图数据项结构
type VarietyInfoViewItem struct {
VarietyID string `json:"variety_id"` // 品种ID
Name string `json:"name"` // 品种名称
Code string `json:"code"` // 品种代码
ExchangeName string `json:"exchange_name"` // 交易所名称
Tick string `json:"tick"` // 最小变动价位
TickPrice string `json:"tick_price"` // 最小变动价位对应的价值
TickOriginal float64 `json:"tick_original"` // 最小变动价位(原始值)
TickPriceOriginal float64 `json:"tick_price_original"` // 最小变动价位对应的价值(原始值)
}
// ReadResponse 读取响应结构
type ReadResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
Data ReadData `json:"data"` // 响应数据
}
// ReadHandler 处理信息查询逻辑
func ReadHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
reqID := c.Request.Header.Get("X-ReadRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
// 记录请求接收日志
zap.L().Info("📥 收到品种查询请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
// 绑定请求参数
var req ReadRequest
if err := c.ShouldBindQuery(&req); err != nil {
zap.L().Warn("⚠️ 请求参数解析失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数格式错误",
})
return
}
// 验证查询条件至少有一个不为空
if req.VarietyID == "" && req.Name == "" && req.Code == "" && req.ExchangeName == "" {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("reason", "variety_id、name、code、exchange_name不能同时为空"),
)
c.JSON(http.StatusBadRequest, ReadResponse{
Success: false,
Message: "请求参数错误variety_id、name、code、exchange_name不能同时为空",
})
return
}
// 处理分页参数默认值
page, err := strconv.Atoi(req.Page)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(req.PageSize)
if err != nil || pageSize < 1 {
pageSize = 20
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("exchange_name", req.ExchangeName),
zap.Int("page", page),
zap.Int("page_size", pageSize),
)
// 构建查询条件和参数
whereClauses := []string{}
args := []interface{}{}
paramIndex := 1
if req.VarietyID != "" {
whereClauses = append(whereClauses, "variety_id = $"+strconv.Itoa(paramIndex))
args = append(args, req.VarietyID)
paramIndex++
}
if req.Name != "" {
whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Name+"%")
paramIndex++
}
if req.ExchangeName != "" {
whereClauses = append(whereClauses, "exchange_name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.ExchangeName+"%")
paramIndex++
}
if req.Code != "" {
whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Code+"%")
paramIndex++
}
// 构建基础SQL
baseSQL := "SELECT variety_id, name, code, exchange_name, tick, tick_price, tick_original, tick_price_original FROM variety_info_view"
countSQL := "SELECT COUNT(*) FROM variety_info_view"
if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
baseSQL += whereStr
countSQL += whereStr
}
// 计算分页偏移量
offset := (page - 1) * pageSize
// 拼接分页SQL
querySQL := fmt.Sprintf("%s ORDER BY variety_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1)
args = append(args, pageSize, offset)
// 查询总条数
var total int64
countArgs := args[:len(args)-2] // 排除分页参数
err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total)
if err != nil {
zap.L().Error("❌ 查询总条数失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
// 执行分页查询
rows, err := db.DB.Query(querySQL, args...)
if err != nil {
zap.L().Error("❌ 分页查询失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
defer rows.Close()
// 处理查询结果
var items []VarietyInfoViewItem
for rows.Next() {
var item VarietyInfoViewItem
if err := rows.Scan(&item.VarietyID, &item.Name, &item.Code, &item.ExchangeName, &item.Tick, &item.TickPrice, &item.TickOriginal, &item.TickPriceOriginal); err != nil {
zap.L().Error("❌ 解析查询结果失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "数据处理失败,请稍后重试",
})
return
}
items = append(items, item)
}
// 检查行迭代过程中是否发生错误
if err := rows.Err(); err != nil {
zap.L().Error("❌ 行迭代错误",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, ReadResponse{
Success: false,
Message: "查询数据失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 品种查询请求处理完成",
zap.String("req_id", reqID),
zap.Int64("total", total),
zap.Int("page", page),
zap.Int("page_size", pageSize),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, ReadResponse{
Success: true,
Message: "查询成功",
Data: ReadData{
Total: total,
Page: page,
PageSize: pageSize,
Items: items,
},
})
}

View File

@@ -0,0 +1,370 @@
package logic4variety
import (
"asset_assistant/db"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// UpdateRequest 更新请求参数结构
type UpdateRequest struct {
VarietyID string `json:"variety_id" binding:"required"` // 品种ID必填
ExchangeID string `json:"exchange_id"` // 交易所ID可选
ExchangeName string `json:"exchange_name"` // 交易所名称,可选
Name string `json:"name"` // 品种名称,可选
Code string `json:"code"` // 品种代码,可选
Tick float64 `json:"tick"` // 最小变动价位,可选
TickPrice float64 `json:"tick_price"` // 最小变动价值,可选
}
// UpdateResponse 更新响应结构
type UpdateResponse struct {
Success bool `json:"success"` // 操作是否成功
Message string `json:"message"` // 提示信息
}
// UpdateHandler 处理信息更新逻辑
func UpdateHandler(c *gin.Context) {
startTime := time.Now()
// 获取或生成请求ID
reqID := c.Request.Header.Get("X-UpdateRequest-ID")
if reqID == "" {
reqID = uuid.New().String()
zap.L().Debug("✨ 生成新的请求ID", zap.String("req_id", reqID))
}
// 记录请求接收日志
zap.L().Info("📥 收到品种更新请求",
zap.String("req_id", reqID),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
var req UpdateRequest
// 绑定并验证请求参数主要验证variety_id必填
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误variety_id为必填项",
})
return
}
// 验证所有可选字段不能同时为空
if req.ExchangeID == "" && req.ExchangeName == "" && req.Name == "" && req.Code == "" && req.Tick == 0 && req.TickPrice == 0 {
zap.L().Warn("⚠️ 请求参数验证失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.String("reason", "所有更新字段不能同时为空"),
)
c.JSON(http.StatusBadRequest, UpdateResponse{
Success: false,
Message: "请求参数错误:至少提供一个需要更新的字段",
})
return
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.String("exchange_id", req.ExchangeID),
zap.String("exchange_name", req.ExchangeName),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.Float64("tick", req.Tick),
zap.Float64("tick_price", req.TickPrice),
)
// 开启数据库事务
tx, err := db.DB.Begin()
if err != nil {
zap.L().Error("❌ 事务开启失败",
zap.String("req_id", reqID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
return
}
// 延迟处理panic情况
defer func() {
if r := recover(); r != nil {
if err := tx.Rollback(); err != nil {
zap.L().Error("💥 panic后事务回滚失败",
zap.String("req_id", reqID),
zap.Error(err),
)
}
zap.L().Error("💥 事务处理发生panic",
zap.String("req_id", reqID),
zap.Any("recover", r),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "系统错误,请稍后重试",
})
}
}()
// 如果exchange_id或exchange_name不为空更新variety_exchange表
if req.ExchangeID != "" || req.ExchangeName != "" {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_exchange WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询交易所信息失败",
})
return
}
if count > 0 {
// 更新现有记录
_, err = tx.Exec("UPDATE variety_exchange SET exchange_id = $1, exchange_name = $2 WHERE variety_id = $3", req.ExchangeID, req.ExchangeName, req.VarietyID)
} else {
// 插入新记录
_, err = tx.Exec("INSERT INTO variety_exchange (variety_id, exchange_id, exchange_name) VALUES ($1, $2, $3)", req.VarietyID, req.ExchangeID, req.ExchangeName)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_exchange表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新交易所信息失败",
})
return
}
zap.L().Debug("📝 variety_exchange表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果name不为空更新variety_name表
if req.Name != "" {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_name WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_name表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询品种名称失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_name SET name = $1 WHERE variety_id = $2", req.Name, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_name (variety_id, name) VALUES ($1, $2)", req.VarietyID, req.Name)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_name表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新品种名称失败",
})
return
}
zap.L().Debug("📝 variety_name表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果code不为空更新variety_code表
if req.Code != "" {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_code WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询品种代码失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_code SET code = $1 WHERE variety_id = $2", req.Code, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_code (variety_id, code) VALUES ($1, $2)", req.VarietyID, req.Code)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_code表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新品种代码失败",
})
return
}
zap.L().Debug("📝 variety_code表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果tick不为0更新variety_tick表
if req.Tick != 0 {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_tick WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询最小变动价位失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_tick SET tick = $1 WHERE variety_id = $2", req.Tick, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_tick (variety_id, tick) VALUES ($1, $2)", req.VarietyID, req.Tick)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新最小变动价位失败",
})
return
}
zap.L().Debug("📝 variety_tick表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 如果tick_price不为0更新variety_tick_price表
if req.TickPrice != 0 {
// 先查询是否已存在记录
var count int
err = tx.QueryRow("SELECT COUNT(*) FROM variety_tick_price WHERE variety_id = $1 AND deleted = FALSE", req.VarietyID).Scan(&count)
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表查询失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "查询最小变动价值失败",
})
return
}
if count > 0 {
_, err = tx.Exec("UPDATE variety_tick_price SET price = $1 WHERE variety_id = $2", req.TickPrice, req.VarietyID)
} else {
_, err = tx.Exec("INSERT INTO variety_tick_price (variety_id, price) VALUES ($1, $2)", req.VarietyID, req.TickPrice)
}
if err != nil {
tx.Rollback()
zap.L().Error("❌ variety_tick_price表更新失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "更新最小变动价值失败",
})
return
}
zap.L().Debug("📝 variety_tick_price表更新成功",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
)
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
zap.L().Error("❌ 事务提交失败",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, UpdateResponse{
Success: false,
Message: "数据提交失败,请稍后重试",
})
return
}
// 记录请求处理耗时
duration := time.Since(startTime)
zap.L().Info("✅ 品种更新请求处理完成",
zap.String("req_id", reqID),
zap.String("variety_id", req.VarietyID),
zap.Duration("duration", duration),
)
// 返回成功响应
c.JSON(http.StatusOK, UpdateResponse{
Success: true,
Message: "更新成功",
})
}

View File

@@ -4,6 +4,8 @@ import (
"asset_assistant/db" // 数据库相关操作包 "asset_assistant/db" // 数据库相关操作包
"asset_assistant/logger" // 日志工具包 "asset_assistant/logger" // 日志工具包
"asset_assistant/logic4country" "asset_assistant/logic4country"
"asset_assistant/logic4currency"
"asset_assistant/logic4exchange"
"asset_assistant/logic4user" "asset_assistant/logic4user"
// 业务逻辑处理包 // 业务逻辑处理包
@@ -71,6 +73,26 @@ func main() {
} }
zap.L().Info("✅ 国家接口注册完成") 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端口 // 记录服务启动日志监听80端口
zap.L().Info("✅ 服务启动在80端口") zap.L().Info("✅ 服务启动在80端口")
r.Run(":80") r.Run(":80")

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import 'package:asset_assistant/utils/host_utils.dart';
import 'package:flutter/material.dart'; import 'package: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';
@@ -43,8 +44,7 @@ class _AddExchangePageState extends State<AddExchangePage> {
} }
// 准备请求数据 // 准备请求数据
// const baseUrl = 'https://api.fishestlife.com'; final baseUrl = HostUtils().currentHost;
const baseUrl = 'http://localhost:20010';
const path = '/exchange/create'; const path = '/exchange/create';
final url = '$baseUrl$path'; final url = '$baseUrl$path';

View File

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

View File

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

View File

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