Compare commits

...

55 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
31 changed files with 2041 additions and 652 deletions

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,55 +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 IF view_exists THEN
DROP VIEW user_info_view; DROP VIEW user_info_view;
RAISE NOTICE '已删除旧视图 user_info_view'; RAISE NOTICE '4⃣♻ 已删除旧视图 user_info_view';
END IF; 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 account, ua.account,
up.password AS 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 $$; 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 $$;

View File

@@ -1,124 +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; END IF;
-- country_flag
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_flag') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'country_flag') THEN
CREATE TABLE country_flag ( CREATE TABLE country_flag (
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,
flag VARCHAR NOT NULL, flag 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_flag_updated_at CREATE TRIGGER update_country_flag_updated_at
BEFORE UPDATE ON "country_flag" BEFORE UPDATE ON country_flag
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '4⃣✅ country_flag 子表已创建';
RAISE NOTICE 'created country_flag table and trigger';
ELSE ELSE
RAISE NOTICE 'country_flag table already exists'; 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 IF view_exists THEN
DROP VIEW country_info_view; DROP VIEW country_info_view;
RAISE NOTICE '已删除旧视图 country_info_view'; RAISE NOTICE '4⃣♻ 已删除旧视图 country_info_view';
END IF; 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,
f.flag AS flag -- 国旗信息 f.flag
FROM FROM country u
country u LEFT JOIN country_name n ON u.id = n.country_id AND n.deleted = FALSE
LEFT 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 AND n.deleted = FALSE -- 过滤已删除名称 LEFT JOIN country_flag f ON u.id = f.country_id AND f.deleted = FALSE
LEFT JOIN WHERE u.deleted = FALSE;
country_code c ON u.id = c.country_id AND c.deleted = FALSE -- 过滤已删除代码
LEFT JOIN
country_flag f ON u.id = f.country_id AND f.deleted = FALSE -- 左连接:允许无国旗数据
WHERE
u.deleted = FALSE; -- 只查询未删除的国家
-- 根据视图是否已存在输出不同提示 RAISE NOTICE '4⃣✅ country_info_view 已创建/更新';
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 $$; 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 $$;

View File

@@ -1,124 +1,138 @@
-- 切换到目标数据库 -- =========================================================
\c postgres; -- exchange.sql 🏛️
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$ DO $$
BEGIN 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 IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange') THEN
CREATE TABLE exchange ( CREATE TABLE exchange (
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_exchange_updated_at CREATE TRIGGER update_exchange_updated_at
BEFORE UPDATE ON "exchange" BEFORE UPDATE ON exchange
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '1⃣✅ exchange 主表已创建';
RAISE NOTICE 'created exchange table and trigger';
ELSE ELSE
RAISE NOTICE 'exchange table already exists'; RAISE NOTICE '1⃣⏩ exchange 主表已存在,跳过';
END IF; END IF;
-- exchange_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_name') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_name') THEN
CREATE TABLE exchange_name ( CREATE TABLE exchange_name (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
exchange_id UUID NOT NULL, exchange_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_exchange_name_updated_at
BEFORE UPDATE ON "exchange_name" BEFORE UPDATE ON exchange_name
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '2⃣✅ exchange_name 子表已创建';
RAISE NOTICE 'created exchange_name table and trigger';
ELSE ELSE
RAISE NOTICE 'exchange_name table already exists'; RAISE NOTICE '2⃣⏩ exchange_name 子表已存在,跳过';
END IF; END IF;
-- exchange_short_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_short_name') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_short_name') THEN
CREATE TABLE exchange_short_name ( CREATE TABLE exchange_short_name (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
exchange_id UUID NOT NULL, exchange_id UUID NOT NULL,
short_name VARCHAR NOT NULL, short_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_short_name_updated_at CREATE TRIGGER update_exchange_short_name_updated_at
BEFORE UPDATE ON "exchange_short_name" BEFORE UPDATE ON exchange_short_name
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '3⃣✅ exchange_short_name 子表已创建';
RAISE NOTICE 'created exchange_short_name table and trigger';
ELSE ELSE
RAISE NOTICE 'exchange_short_name table already exists'; RAISE NOTICE '3⃣⏩ exchange_short_name 子表已存在,跳过';
END IF; END IF;
-- exchange_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_code') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'exchange_code') THEN
CREATE TABLE exchange_code ( CREATE TABLE exchange_code (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
exchange_id UUID NOT NULL, exchange_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_exchange_code_updated_at
BEFORE UPDATE ON "exchange_code" BEFORE UPDATE ON exchange_code
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '4⃣✅ exchange_code 子表已创建';
RAISE NOTICE 'created exchange_code table and trigger';
ELSE ELSE
RAISE NOTICE 'exchange_code table already exists'; RAISE NOTICE '4⃣⏩ exchange_code 子表已存在,跳过';
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 = 'exchange_info_view' WHERE table_name = 'exchange_info_view'
) INTO view_exists; ) INTO view_exists;
-- 若视图存在,先删除(避免字段名冲突)
IF view_exists THEN IF view_exists THEN
DROP VIEW exchange_info_view; DROP VIEW exchange_info_view;
RAISE NOTICE '已删除旧视图 exchange_info_view'; RAISE NOTICE '4⃣♻ 已删除旧视图 exchange_info_view';
END IF; END IF;
-- 重新创建视图(无冲突风险) CREATE OR REPLACE VIEW exchange_info_view AS
CREATE VIEW exchange_info_view AS SELECT
SELECT
u.id AS exchange_id, u.id AS exchange_id,
n.name AS name, n.name,
sn.short_name AS short_name, sn.short_name,
c.code AS code, c.code,
u.deleted AS deleted u.deleted
FROM FROM exchange u
exchange u JOIN exchange_name n ON u.id = n.exchange_id AND n.deleted = FALSE
JOIN JOIN exchange_short_name sn ON u.id = sn.exchange_id AND sn.deleted = FALSE
exchange_name n ON u.id = n.exchange_id AND n.deleted = FALSE JOIN exchange_code c ON u.id = c.exchange_id AND c.deleted = FALSE
JOIN WHERE u.deleted = FALSE;
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;
IF view_exists THEN RAISE NOTICE '4⃣✅ exchange_info_view 已创建/更新';
RAISE NOTICE '视图 exchange_info_view 已更新(删除旧视图后重建)';
ELSE
RAISE NOTICE '视图 exchange_info_view 已创建';
END IF;
EXCEPTION EXCEPTION
WHEN OTHERS THEN WHEN OTHERS THEN
RAISE NOTICE '处理视图时发生错误: %', SQLERRM; RAISE NOTICE '4⃣❌ 处理视图时发生错误: %', SQLERRM;
END $$; 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 $$;

View File

@@ -1,103 +1,117 @@
-- 切换到目标数据库 -- =========================================================
\c postgres; -- currency.sql 💰
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$ DO $$
BEGIN 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 IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency') THEN
CREATE TABLE currency ( CREATE TABLE currency (
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_currency_updated_at CREATE TRIGGER update_currency_updated_at
BEFORE UPDATE ON "currency" BEFORE UPDATE ON currency
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '1⃣✅ currency 主表已创建';
RAISE NOTICE 'created currency table and trigger';
ELSE ELSE
RAISE NOTICE 'currency table already exists'; RAISE NOTICE '1⃣⏩ currency 主表已存在,跳过';
END IF; END IF;
-- currency_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency_name') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency_name') THEN
CREATE TABLE currency_name ( CREATE TABLE currency_name (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
currency_id UUID NOT NULL, currency_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_currency_name_updated_at
BEFORE UPDATE ON "currency_name" BEFORE UPDATE ON currency_name
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '2⃣✅ currency_name 子表已创建';
RAISE NOTICE 'created currency_name table and trigger';
ELSE ELSE
RAISE NOTICE 'currency_name table already exists'; RAISE NOTICE '2⃣⏩ currency_name 子表已存在,跳过';
END IF; END IF;
-- currency_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency_code') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'currency_code') THEN
CREATE TABLE currency_code ( CREATE TABLE currency_code (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
currency_id UUID NOT NULL, currency_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_currency_code_updated_at
BEFORE UPDATE ON "currency_code" BEFORE UPDATE ON currency_code
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '3⃣✅ currency_code 子表已创建';
RAISE NOTICE 'created currency_code table and trigger';
ELSE ELSE
RAISE NOTICE 'currency_code table already exists'; RAISE NOTICE '3⃣⏩ currency_code 子表已存在,跳过';
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 = 'currency_info_view' WHERE table_name = 'currency_info_view'
) INTO view_exists; ) INTO view_exists;
-- 若视图存在,先删除(避免字段名冲突)
IF view_exists THEN IF view_exists THEN
DROP VIEW currency_info_view; DROP VIEW currency_info_view;
RAISE NOTICE '已删除旧视图 currency_info_view'; RAISE NOTICE '4⃣♻ 已删除旧视图 currency_info_view';
END IF; END IF;
-- 创建或更新视图
CREATE OR REPLACE VIEW currency_info_view AS CREATE OR REPLACE VIEW currency_info_view AS
SELECT SELECT
u.id AS currency_id, u.id AS currency_id,
n.name AS name, n.name,
c.code AS code, c.code,
u.deleted AS deleted u.deleted
FROM FROM currency u
currency u JOIN currency_name n ON u.id = n.currency_id AND n.deleted = FALSE
JOIN JOIN currency_code c ON u.id = c.currency_id AND c.deleted = FALSE
currency_name n ON u.id = n.currency_id WHERE u.deleted = FALSE;
JOIN
currency_code c ON u.id = c.currency_id
WHERE
u.deleted = FALSE;
-- 根据视图是否已存在输出不同提示 RAISE NOTICE '4⃣✅ currency_info_view 已创建/更新';
IF view_exists THEN
RAISE NOTICE '视图 currency_info_view 已更新(删除旧视图后重建)';
ELSE
RAISE NOTICE '视图 currency_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_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 $$; END $$;

View File

@@ -1,46 +1,57 @@
-- 切换到目标数据库 -- =========================================================
\c postgres; -- variety.sql 🎉
-- 无物化视图 | 超可视提示 | 可重复执行
-- PostgreSQL 17.4+ 👍
-- =========================================================
-- 1⃣ 开始 🚀
DO $$ DO $$
BEGIN BEGIN
-- 创建主表 variety 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 IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety') THEN
CREATE TABLE variety ( CREATE TABLE variety (
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
); );
RAISE NOTICE '1⃣✅ variety 主表已创建';
RAISE NOTICE 'created variety table, trigger and indexes';
ELSE ELSE
RAISE NOTICE 'variety table already exists'; RAISE NOTICE '1⃣⏩ variety 主表已存在,跳过';
END IF; END IF;
-- 创建子表 variety_exchange -- variety_exchange
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_exchange') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_exchange') THEN
CREATE TABLE variety_exchange ( CREATE TABLE variety_exchange (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL, variety_id UUID NOT NULL,
exchange_id VARCHAR(50) NOT NULL, exchange_id VARCHAR(50) NOT NULL,
exchange_name VARCHAR(50) 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_variety_exchange_updated_at CREATE TRIGGER update_variety_exchange_updated_at
BEFORE UPDATE ON "variety_exchange" BEFORE UPDATE ON variety_exchange
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '2⃣✅ variety_exchange 子表已创建';
RAISE NOTICE 'created variety_exchange table, trigger, foreign key and indexes';
ELSE ELSE
RAISE NOTICE 'variety_exchange table already exists'; RAISE NOTICE '2⃣⏩ variety_exchange 子表已存在,跳过';
END IF; END IF;
-- 创建子表 variety_name -- variety_name
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_name') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_name') THEN
CREATE TABLE variety_name ( CREATE TABLE variety_name (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL, variety_id UUID NOT NULL,
name VARCHAR(50) NOT NULL, name VARCHAR(50) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
@@ -48,19 +59,17 @@ BEGIN
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_variety_name_updated_at CREATE TRIGGER update_variety_name_updated_at
BEFORE UPDATE ON "variety_name" BEFORE UPDATE ON variety_name
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '3⃣✅ variety_name 子表已创建';
RAISE NOTICE 'created variety_name table, trigger, foreign key and indexes';
ELSE ELSE
RAISE NOTICE 'variety_name table already exists'; RAISE NOTICE '3⃣⏩ variety_name 子表已存在,跳过';
END IF; END IF;
-- 创建子表 variety_code -- variety_code
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_code') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_code') THEN
CREATE TABLE variety_code ( CREATE TABLE variety_code (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL, variety_id UUID NOT NULL,
code VARCHAR(50) NOT NULL, code VARCHAR(50) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
@@ -68,108 +77,89 @@ BEGIN
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TRIGGER update_variety_code_updated_at CREATE TRIGGER update_variety_code_updated_at
BEFORE UPDATE ON "variety_code" BEFORE UPDATE ON variety_code
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '4⃣✅ variety_code 子表已创建';
RAISE NOTICE 'created variety_code table, trigger, foreign key and indexes';
ELSE ELSE
RAISE NOTICE 'variety_code table already exists'; RAISE NOTICE '4⃣⏩ variety_code 子表已存在,跳过';
END IF; END IF;
-- 创建子表 variety_tick -- variety_tick
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick') THEN
CREATE TABLE variety_tick ( CREATE TABLE variety_tick (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL, variety_id UUID NOT NULL,
tick NUMERIC(12, 6), tick NUMERIC(12,6),
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_variety_tick_updated_at CREATE TRIGGER update_variety_tick_updated_at
BEFORE UPDATE ON "variety_tick" BEFORE UPDATE ON variety_tick
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '5⃣✅ variety_tick 子表已创建';
RAISE NOTICE 'created variety_tick table, trigger, foreign key and indexes';
ELSE ELSE
RAISE NOTICE 'variety_tick table already exists'; RAISE NOTICE '5⃣⏩ variety_tick 子表已存在,跳过';
END IF; END IF;
-- 创建子表 variety_tick_price -- variety_tick_price
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick_price') THEN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'variety_tick_price') THEN
CREATE TABLE variety_tick_price ( CREATE TABLE variety_tick_price (
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY NOT NULL, id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
variety_id UUID NOT NULL, variety_id UUID NOT NULL,
price NUMERIC(12, 6), price NUMERIC(12,6),
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_variety_tick_price_updated_at CREATE TRIGGER update_variety_tick_price_updated_at
BEFORE UPDATE ON "variety_tick_price" BEFORE UPDATE ON variety_tick_price
FOR EACH ROW FOR EACH ROW EXECUTE FUNCTION update_data_modified_column();
EXECUTE FUNCTION update_data_modified_column(); RAISE NOTICE '6⃣✅ variety_tick_price 子表已创建';
RAISE NOTICE 'created variety_tick_price table, trigger, foreign key and indexes';
ELSE ELSE
RAISE NOTICE 'variety_tick_price table already exists'; RAISE NOTICE '6⃣⏩ variety_tick_price 子表已存在,跳过';
END IF; END IF;
END $$; 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 $$ DO $$
DECLARE
view_exists BOOLEAN;
BEGIN BEGIN
-- 检查视图是否已存在 RAISE NOTICE '7⃣✅ variety_info_view 已创建/更新';
SELECT EXISTS ( END $$;
SELECT 1 FROM information_schema.views
WHERE table_name = 'variety_info_view'
) INTO view_exists;
-- 若视图存在,先删除(避免字段名冲突) -- 5⃣ 性能索引 ------------------------------------------------
IF view_exists THEN CREATE INDEX IF NOT EXISTS idx_variety_exchange_variety_id_deleted ON variety_exchange(variety_id, deleted);
DROP VIEW variety_info_view; CREATE INDEX IF NOT EXISTS idx_variety_name_variety_id_deleted ON variety_name(variety_id, deleted);
RAISE NOTICE '已删除旧视图 variety_info_view'; CREATE INDEX IF NOT EXISTS idx_variety_code_variety_id_deleted ON variety_code(variety_id, deleted);
END IF; 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 $$
CREATE OR REPLACE VIEW variety_info_view AS BEGIN
SELECT RAISE NOTICE '8⃣✅ 全部索引已确保存在';
v.id AS variety_id, END $$;
vn.name AS name,
vc.code AS code,
-- 调用格式化函数自动去除尾部多余0
format_numeric_to_original(vt.tick) AS tick,
format_numeric_to_original(vtp.price) AS tick_price,
-- 可选:保留原始精度字段(供计算使用)
vt.tick AS tick_original,
vtp.price AS tick_price_original
FROM
variety v
LEFT JOIN variety_name vn
ON v.id = vn.variety_id
AND vn.deleted = FALSE
LEFT JOIN variety_code vc
ON v.id = vc.variety_id
AND vc.deleted = FALSE
LEFT JOIN variety_tick vt
ON v.id = vt.variety_id
AND vt.deleted = FALSE
LEFT JOIN variety_tick_price vtp
ON v.id = vtp.variety_id
AND vtp.deleted = FALSE
WHERE
v.deleted = FALSE;
-- 根据视图是否已存在输出不同提示 -- 6⃣ 完成 🎉
IF view_exists THEN DO $$
RAISE NOTICE '视图 variety_info_view 已更新(删除旧视图后重建)'; BEGIN
ELSE RAISE NOTICE '🎉============ variety 部署完成 ============🎉';
RAISE NOTICE '视图 variety_info_view 已创建';
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '处理视图时发生错误: %', SQLERRM;
END $$; 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

@@ -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, // 是否压缩
} }
// 编码器配置 // 编码器配置
@@ -83,4 +83,4 @@ func customTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
beijingTime := t.In(shanghaiLoc) beijingTime := t.In(shanghaiLoc)
// 格式化输出 // 格式化输出
enc.AppendString(beijingTime.Format("2006-01-02 15:04:05.000")) enc.AppendString(beijingTime.Format("2006-01-02 15:04:05.000"))
} }

View File

@@ -29,7 +29,7 @@ type CreateData struct {
CountryID string `json:"country_id"` CountryID string `json:"country_id"`
} }
// CreateHandler 处理国家创建逻辑 // CreateHandler 处理创建逻辑
func CreateHandler(c *gin.Context) { func CreateHandler(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")
@@ -62,7 +62,7 @@ func CreateHandler(c *gin.Context) {
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), // 新增国旗参数日志 zap.String("flag", req.Flag),
) )
// 开启数据库事务 // 开启数据库事务
@@ -98,6 +98,104 @@ func CreateHandler(c *gin.Context) {
} }
}() }()
// 唯一性校验 - 国家名称(排除已删除数据)
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主表记录 // 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)
@@ -151,7 +249,7 @@ func CreateHandler(c *gin.Context) {
return return
} }
// 4. 新增:插入国旗信息(如果提供) // 4. 插入国旗信息(如果提供)
if req.Flag != "" { if req.Flag != "" {
_, err = tx.Exec("INSERT INTO country_flag (country_id, flag) VALUES ($1, $2)", countryID, req.Flag) _, err = tx.Exec("INSERT INTO country_flag (country_id, flag) VALUES ($1, $2)", countryID, req.Flag)
if err != nil { if err != nil {
@@ -202,4 +300,4 @@ func CreateHandler(c *gin.Context) {
CountryID: countryID, CountryID: countryID,
}, },
}) })
} }

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")

View File

@@ -47,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
@@ -224,4 +224,4 @@ func ReadHandler(c *gin.Context) {
Items: items, Items: items,
}, },
}) })
} }

View File

@@ -24,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
@@ -229,4 +229,4 @@ func UpdateHandler(c *gin.Context) {
Success: true, Success: true,
Message: "更新成功", Message: "更新成功",
}) })
} }

View File

@@ -31,8 +31,7 @@ type CreateData struct {
CurrencyID string `json:"currency_id"` // 货币唯一标识ID CurrencyID string `json:"currency_id"` // 货币唯一标识ID
} }
// CreateHandler 处理货币创建逻辑 // CreateHandler 处理创建逻辑
// 接收HTTP请求完成参数验证、数据库事务处理并返回响应
func CreateHandler(c *gin.Context) { func CreateHandler(c *gin.Context) {
startTime := time.Now() // 记录请求开始时间,用于统计耗时 startTime := time.Now() // 记录请求开始时间,用于统计耗时
// 获取或生成请求ID用于追踪整个请求链路 // 获取或生成请求ID用于追踪整个请求链路

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")

View File

@@ -45,7 +45,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

View File

@@ -23,7 +23,7 @@ type UpdateResponse struct {
Message string `json:"message"` // 提示信息 Message string `json:"message"` // 提示信息
} }
// UpdateHandler 处理货币信息更新逻辑 // UpdateHandler 处理信息更新逻辑
func UpdateHandler(c *gin.Context) { func UpdateHandler(c *gin.Context) {
startTime := time.Now() startTime := time.Now()
// 获取或生成请求ID // 获取或生成请求ID

View File

@@ -30,7 +30,7 @@ type CreateData struct {
ExchangeID string `json:"exchange_id"` ExchangeID string `json:"exchange_id"`
} }
// CreateHandler 处理交易所创建逻辑 // CreateHandler 处理创建逻辑
func CreateHandler(c *gin.Context) { func CreateHandler(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")

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")

View File

@@ -47,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

View File

@@ -24,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

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")

View File

@@ -1 +1,235 @@
package logic4variety 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

@@ -1 +1,216 @@
package logic4variety 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

@@ -1 +1,243 @@
package logic4variety 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

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

View File

@@ -1,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

@@ -11,9 +11,10 @@ class AddCountryPage extends StatefulWidget {
} }
class _AddCountryPageState extends State<AddCountryPage> { class _AddCountryPageState extends State<AddCountryPage> {
// 输入控制器 // 输入控制器 - 新增国旗控制器
final TextEditingController _nameController = TextEditingController(); final TextEditingController _nameController = TextEditingController();
final TextEditingController _codeController = TextEditingController(); final TextEditingController _codeController = TextEditingController();
final TextEditingController _flagController = TextEditingController();
// 加载状态 // 加载状态
bool _isLoading = false; bool _isLoading = false;
@@ -21,7 +22,7 @@ class _AddCountryPageState extends State<AddCountryPage> {
// 表单验证键 // 表单验证键
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
// 创建国家 // 创建国家 - 调整请求数据
Future<void> _createCountry() async { Future<void> _createCountry() async {
if (!_formKey.currentState!.validate()) { if (!_formKey.currentState!.validate()) {
return; return;
@@ -32,7 +33,7 @@ class _AddCountryPageState extends State<AddCountryPage> {
}); });
try { try {
// 获取用户ID // 获取用户ID(保持不变)
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('user_id'); final userId = prefs.getString('user_id');
if (userId == null) { if (userId == null) {
@@ -43,7 +44,7 @@ class _AddCountryPageState extends State<AddCountryPage> {
return; return;
} }
// 准备请求数据 // 准备请求数据 - 新增flag字段
final baseUrl = HostUtils().currentHost; final baseUrl = HostUtils().currentHost;
const path = '/country/create'; const path = '/country/create';
final url = '$baseUrl$path'; final url = '$baseUrl$path';
@@ -51,9 +52,10 @@ class _AddCountryPageState extends State<AddCountryPage> {
final requestData = { final requestData = {
'name': _nameController.text.trim(), 'name': _nameController.text.trim(),
'code': _codeController.text.trim(), 'code': _codeController.text.trim(),
'flag': _flagController.text.trim(), // 新增国旗参数
}; };
// 发送请求 // 发送请求(保持不变)
final dio = Dio(); final dio = Dio();
final response = await dio.post( final response = await dio.post(
url, url,
@@ -61,7 +63,7 @@ class _AddCountryPageState extends State<AddCountryPage> {
options: Options(headers: {'Content-Type': 'application/json'}), options: Options(headers: {'Content-Type': 'application/json'}),
); );
// 处理响应 // 处理响应(保持不变)
if (response.statusCode == 200) { if (response.statusCode == 200) {
final result = response.data; final result = response.data;
if (result['success'] == true) { if (result['success'] == true) {
@@ -77,13 +79,24 @@ class _AddCountryPageState extends State<AddCountryPage> {
} }
} else { } else {
if (mounted) { if (mounted) {
_showDialog('错误', '服务器响应异常: ${response.statusCode}'); // 处理400错误时获取服务器返回的具体消息
String errorMessage = '服务器响应异常: ${response.statusCode}';
if (response.statusCode == 400 && response.data != null) {
errorMessage = response.data['message'] ?? errorMessage;
}
_showDialog('错误', errorMessage);
} }
} }
} on DioException catch (e) { } on DioException catch (e) {
// 异常处理(优化错误信息提取)
String errorMessage = '网络请求失败'; String errorMessage = '网络请求失败';
if (e.response != null) { if (e.response != null) {
errorMessage = '请求失败: ${e.response?.statusCode}'; // 从响应数据中提取错误信息
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) { } else if (e.type == DioExceptionType.connectionTimeout) {
errorMessage = '连接超时,请检查网络'; errorMessage = '连接超时,请检查网络';
} else if (e.type == DioExceptionType.connectionError) { } else if (e.type == DioExceptionType.connectionError) {
@@ -106,7 +119,7 @@ class _AddCountryPageState extends State<AddCountryPage> {
} }
} }
// 显示对话框 // 显示对话框(保持不变)
void _showDialog(String title, String content, [VoidCallback? onConfirm]) { void _showDialog(String title, String content, [VoidCallback? onConfirm]) {
showDialog( showDialog(
context: context, context: context,
@@ -159,7 +172,7 @@ class _AddCountryPageState extends State<AddCountryPage> {
key: _formKey, key: _formKey,
child: Column( child: Column(
children: [ children: [
// 国家名称输入框 // 国家名称输入框(保持不变)
TextFormField( TextFormField(
controller: _nameController, controller: _nameController,
style: TextStyle(color: theme.colorScheme.onSurface), style: TextStyle(color: theme.colorScheme.onSurface),
@@ -180,7 +193,7 @@ class _AddCountryPageState extends State<AddCountryPage> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// 国家代码输入框 // 国家代码输入框(保持不变)
TextFormField( TextFormField(
controller: _codeController, controller: _codeController,
style: TextStyle(color: theme.colorScheme.onSurface), style: TextStyle(color: theme.colorScheme.onSurface),
@@ -199,6 +212,22 @@ class _AddCountryPageState extends State<AddCountryPage> {
return null; 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

@@ -3,27 +3,27 @@ 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';
// 国家数据模型(新增flag字段 // 国家数据模型flag字段存储emoji
class Country { class Country {
final String countryId; final String countryId;
final String name; final String name;
final String code; final String code;
final String? flag; // 新增国旗字段 final String? flag; // 国旗字段存储emoji
Country({ Country({
required this.countryId, required this.countryId,
required this.name, required this.name,
required this.code, required this.code,
this.flag, // 新增参数 this.flag,
}); });
// 从JSON构建对象添加flag字段解析 // 从JSON构建对象
factory Country.fromJson(Map<String, dynamic> json) { factory Country.fromJson(Map<String, dynamic> json) {
return Country( return Country(
countryId: json['country_id'], countryId: json['country_id'],
name: json['name'], name: json['name'],
code: json['code'], code: json['code'],
flag: json['flag'], // 解析国旗字段 flag: json['flag'], // 解析emoji字段
); );
} }
} }
@@ -64,7 +64,6 @@ class CountryData {
}); });
factory CountryData.fromJson(Map<String, dynamic> json) { factory CountryData.fromJson(Map<String, dynamic> json) {
// 关键修复处理items为null的情况转为空列表
var itemsList = json['items'] as List? ?? []; var itemsList = json['items'] as List? ?? [];
List<Country> items = itemsList.map((i) => Country.fromJson(i)).toList(); List<Country> items = itemsList.map((i) => Country.fromJson(i)).toList();
@@ -376,26 +375,23 @@ class _CountryPageState extends State<CountryPage> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row( child: Row(
children: [ children: [
// 国旗Emoji展示区域
Container( Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest, color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
image: country.flag != null && country.flag!.isNotEmpty
? DecorationImage(
image: NetworkImage(country.flag!),
fit: BoxFit.cover,
)
: null,
), ),
child: country.flag == null || country.flag!.isEmpty child: Center(
? Icon( child: Text(
Icons.flag, // 显示国旗emoji如果没有则显示默认图标
size: 24, country.flag != null && country.flag!.isNotEmpty
color: theme.colorScheme.secondary, ? country.flag!
) : '',
: null, style: const TextStyle(fontSize: 24), // 适当调整emoji大小
),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(

View File

@@ -1,227 +0,0 @@
package logic4country
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 {
CountryID string `form:"country_id"` // 国家ID可选
Name string `form:"name"` // 国家名称,可选
Code string `form:"code"` // 国家代码,可选
Flag string `form:"flag"` // 国旗信息,新增可选参数
Page string `form:"page"` // 页码,可选
PageSize string `form:"page_size"` // 每页条数,可选
}
// ReadData 读取响应数据结构
type ReadData struct {
Total int64 `json:"total"` // 总条数
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页条数
Items []CountryInfoViewItem `json:"items"` // 数据列表
}
// CountryInfoViewItem 视图数据项结构,新增国旗字段
type CountryInfoViewItem struct {
CountryID string `json:"country_id"` // 国家ID
Name string `json:"name"` // 国家名称
Code string `json:"code"` // 国家代码
Flag string `json:"flag"` // 国旗信息,新增字段
}
// 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
}
// 处理分页参数默认值
page, err := strconv.Atoi(req.Page)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(req.PageSize)
if err != nil || pageSize < 1 {
pageSize = 20
}
zap.L().Debug("✅ 请求参数验证通过",
zap.String("req_id", reqID),
zap.String("country_id", req.CountryID),
zap.String("name", req.Name),
zap.String("code", req.Code),
zap.String("flag", req.Flag), // 新增国旗查询参数日志
zap.Int("page", page),
zap.Int("page_size", pageSize),
)
// 构建查询条件和参数
whereClauses := []string{}
args := []interface{}{}
paramIndex := 1
if req.CountryID != "" {
whereClauses = append(whereClauses, "country_id = $"+strconv.Itoa(paramIndex))
args = append(args, req.CountryID)
paramIndex++
}
if req.Name != "" {
whereClauses = append(whereClauses, "name LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Name+"%")
paramIndex++
}
if req.Code != "" {
whereClauses = append(whereClauses, "code LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Code+"%")
paramIndex++
}
// 新增国旗查询条件
if req.Flag != "" {
whereClauses = append(whereClauses, "flag LIKE $"+strconv.Itoa(paramIndex))
args = append(args, "%"+req.Flag+"%")
paramIndex++
}
// 构建基础SQL新增flag字段查询
baseSQL := "SELECT country_id, name, code, flag FROM country_info_view"
countSQL := "SELECT COUNT(*) FROM country_info_view"
if len(whereClauses) > 0 {
whereStr := " WHERE " + strings.Join(whereClauses, " AND ")
baseSQL += whereStr
countSQL += whereStr
}
// 计算分页偏移量
offset := (page - 1) * pageSize
// 拼接分页SQL
querySQL := fmt.Sprintf("%s ORDER BY country_id LIMIT $%d OFFSET $%d", baseSQL, paramIndex, paramIndex+1)
args = append(args, pageSize, offset)
// 查询总条数
var total int64
countArgs := args[:len(args)-2] // 排除分页参数
err = db.DB.QueryRow(countSQL, countArgs...).Scan(&total)
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()
// 处理查询结果新增flag字段扫描
var items []CountryInfoViewItem
for rows.Next() {
var item CountryInfoViewItem
if err := rows.Scan(&item.CountryID, &item.Name, &item.Code, &item.Flag); err != nil {
zap.L().Error("❌ 解析查询结果失败",
zap.String("req_id", reqID),
zap.Error(err),
)
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,
},
})
}