Internal error occurred but is skipped: FindTagsByCommitIDs

refactor: 优化数据库迁移文件约束和注释 #211

Merged
huanghaosheng merged 1 commits from refatcor/table-structure into main 2026-06-23 23:42:40 +08:00
12 changed files with 67 additions and 91 deletions

View File

@@ -208,8 +208,8 @@ func (h *AuthHandler) Logout(c *gin.Context) {
// validateCredentials 校验用户名和密码格式。 // validateCredentials 校验用户名和密码格式。
// 返回空字符串表示校验通过,否则返回错误描述。 // 返回空字符串表示校验通过,否则返回错误描述。
func validateCredentials(username, password string) string { func validateCredentials(username, password string) string {
if len(username) < 3 || len(username) > 64 { if len(username) > 64 {
return "username must be 3-64 characters" return "username must not exceed 64 characters"
} }
if len(password) < 8 || len(password) > 72 { if len(password) < 8 || len(password) > 72 {
return "password must be 8-72 characters" return "password must be 8-72 characters"

View File

@@ -10,6 +10,14 @@ CREATE TABLE IF NOT EXISTS users (
-- 用户名索引(用于登录查询) -- 用户名索引(用于登录查询)
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
-- 表和列注释
COMMENT ON TABLE users IS '用户表,存储系统所有注册用户的基本信息';
COMMENT ON COLUMN users.id IS '用户唯一标识符 (UUID)';
COMMENT ON COLUMN users.username IS '用户名,最大 64 字符,全局唯一';
COMMENT ON COLUMN users.password_hash IS '密码哈希值,使用 bcrypt 算法cost=10';
COMMENT ON COLUMN users.created_at IS '用户注册时间';
COMMENT ON COLUMN users.updated_at IS '用户信息最后更新时间';
-- Refresh Token 表 -- Refresh Token 表
CREATE TABLE IF NOT EXISTS refresh_tokens ( CREATE TABLE IF NOT EXISTS refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -24,3 +32,11 @@ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token_hash ON refresh_tokens(token
-- 用户 ID 索引(用于登出所有设备) -- 用户 ID 索引(用于登出所有设备)
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id);
-- 表和列注释
COMMENT ON TABLE refresh_tokens IS 'Refresh Token 表,用于 JWT 双 token 机制的长期身份验证';
COMMENT ON COLUMN refresh_tokens.id IS 'Token 唯一标识符 (UUID)';
COMMENT ON COLUMN refresh_tokens.user_id IS '所属用户 ID外键关联 users 表,用户删除时级联删除';
COMMENT ON COLUMN refresh_tokens.token_hash IS 'Token 哈希值,使用 SHA-256 算法,十六进制编码 (64 字符)';
COMMENT ON COLUMN refresh_tokens.expires_at IS 'Token 过期时间,默认有效期 7 天';
COMMENT ON COLUMN refresh_tokens.created_at IS 'Token 创建时间';

View File

@@ -2,10 +2,12 @@
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
session_id UUID NOT NULL, session_id UUID NOT NULL,
role VARCHAR(16) NOT NULL, -- "user" | "assistant" | "system" role VARCHAR(10) NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
tokens_used INTEGER NOT NULL DEFAULT 0, tokens_used INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT check_tokens_non_negative CHECK (tokens_used >= 0)
); );
-- 按会话查询消息(分页核心索引) -- 按会话查询消息(分页核心索引)
@@ -15,3 +17,12 @@ CREATE INDEX IF NOT EXISTS idx_messages_session_id_created_at
-- 按会话查询最后一条消息 -- 按会话查询最后一条消息
CREATE INDEX IF NOT EXISTS idx_messages_session_id_id_desc CREATE INDEX IF NOT EXISTS idx_messages_session_id_id_desc
ON messages(session_id, id DESC); ON messages(session_id, id DESC);
-- 表和列注释
COMMENT ON TABLE messages IS '消息表,存储所有会话的消息记录';
COMMENT ON COLUMN messages.id IS '消息唯一标识符,自增序列';
COMMENT ON COLUMN messages.session_id IS '所属会话 ID关联 sessions 表';
COMMENT ON COLUMN messages.role IS '消息角色,可选值: ''user'' (用户), ''assistant'' (AI 助手), ''system'' (系统)';
COMMENT ON COLUMN messages.content IS '消息内容,无长度限制';
COMMENT ON COLUMN messages.tokens_used IS '消息消耗的 token 数量,用于计费统计';
COMMENT ON COLUMN messages.created_at IS '消息创建时间';

View File

@@ -1,11 +1,22 @@
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY, id UUID PRIMARY KEY,
user_id UUID NOT NULL, user_id UUID NOT NULL,
title VARCHAR(256) NOT NULL DEFAULT '新对话', title VARCHAR(100) NOT NULL DEFAULT '新对话',
config JSONB NOT NULL DEFAULT '{}', config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT check_title_length CHECK (char_length(title) >= 1 AND char_length(title) <= 100)
); );
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions (user_id); CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions (user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_user_updated ON sessions (user_id, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_sessions_user_updated ON sessions (user_id, updated_at DESC);
-- 表和列注释
COMMENT ON TABLE sessions IS '会话表,存储用户的对话会话信息';
COMMENT ON COLUMN sessions.id IS '会话唯一标识符 (UUID)';
COMMENT ON COLUMN sessions.user_id IS '所属用户 ID关联 users 表';
COMMENT ON COLUMN sessions.title IS '会话标题,默认为"新对话",长度 1-100 字符';
COMMENT ON COLUMN sessions.config IS '会话配置 (JSONB),包含: tts_enabled (布尔), detail_level (''low''/''high''), language (语言代码), scenario (情景 ID)';
COMMENT ON COLUMN sessions.created_at IS '会话创建时间';
COMMENT ON COLUMN sessions.updated_at IS '会话最后更新时间';

View File

@@ -5,18 +5,19 @@ CREATE TABLE user_scenarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(50) NOT NULL, name VARCHAR(50) NOT NULL,
icon VARCHAR(10) DEFAULT '', icon VARCHAR(20) DEFAULT '',
description VARCHAR(100), description VARCHAR(100),
prompt TEXT NOT NULL, prompt TEXT NOT NULL,
greeting VARCHAR(200), greeting VARCHAR(500),
language VARCHAR(10) DEFAULT 'zh-CN', language VARCHAR(10) DEFAULT 'zh-CN',
created_at TIMESTAMP NOT NULL DEFAULT NOW(), created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT unique_user_scenario UNIQUE(user_id, name), CONSTRAINT unique_user_scenario UNIQUE(user_id, name),
CONSTRAINT check_name_length CHECK (char_length(name) >= 2 AND char_length(name) <= 50), CONSTRAINT check_name_length CHECK (char_length(name) >= 2 AND char_length(name) <= 50),
CONSTRAINT check_description_length CHECK (description IS NULL OR (char_length(description) >= 1 AND char_length(description) <= 100)), CONSTRAINT check_description_length CHECK (description IS NULL OR char_length(description) <= 100),
CONSTRAINT check_prompt_length CHECK (char_length(prompt) >= 10 AND char_length(prompt) <= 2000) CONSTRAINT check_prompt_length CHECK (char_length(prompt) >= 10),
CONSTRAINT check_greeting_length CHECK (greeting IS NULL OR char_length(greeting) <= 500)
); );
-- 为用户 ID 创建索引,加速查询 -- 为用户 ID 创建索引,加速查询
@@ -25,12 +26,15 @@ CREATE INDEX idx_user_scenarios_user_id ON user_scenarios(user_id);
-- 为创建时间创建索引,用于排序 -- 为创建时间创建索引,用于排序
CREATE INDEX idx_user_scenarios_created_at ON user_scenarios(created_at DESC); CREATE INDEX idx_user_scenarios_created_at ON user_scenarios(created_at DESC);
COMMENT ON TABLE user_scenarios IS '用户自建情景表'; -- 表和列注释
COMMENT ON COLUMN user_scenarios.id IS '情景唯一标识'; COMMENT ON TABLE user_scenarios IS '用户自建情景表,存储用户创建的 AI 对话情景配置';
COMMENT ON COLUMN user_scenarios.user_id IS '所属用户 ID外键关联 users 表'; COMMENT ON COLUMN user_scenarios.id IS '情景唯一标识符 (UUID)';
COMMENT ON COLUMN user_scenarios.name IS '情景名称,如"创意写作导师"'; COMMENT ON COLUMN user_scenarios.user_id IS '所属用户 ID外键关联 users 表,用户删除时级联删除';
COMMENT ON COLUMN user_scenarios.icon IS 'Emoji 图标,如"🎨"'; COMMENT ON COLUMN user_scenarios.name IS '情景名称 (2-50 字符),如"创意写作导师"';
COMMENT ON COLUMN user_scenarios.description IS '简短描述,可选,显示在情景卡片上'; COMMENT ON COLUMN user_scenarios.icon IS 'Emoji 图标 (最多 20 字符),支持复合 Emoji如"🎨"';
COMMENT ON COLUMN user_scenarios.prompt IS '角色 System Prompt定义 AI 行为'; COMMENT ON COLUMN user_scenarios.description IS '简短描述 (最多 100 字符),可选,显示在情景卡片上';
COMMENT ON COLUMN user_scenarios.greeting IS '首句引导,可选'; COMMENT ON COLUMN user_scenarios.prompt IS '角色 System Prompt (最少 10 字符,无上限),定义 AI 行为和对话风格';
COMMENT ON COLUMN user_scenarios.language IS '默认语言,如 zh-CN、en-US'; COMMENT ON COLUMN user_scenarios.greeting IS '首句引导 (最多 500 字符)可选AI 的开场白';
COMMENT ON COLUMN user_scenarios.language IS '默认语言代码 (如 zh-CN、en-US、ja-JP)';
COMMENT ON COLUMN user_scenarios.created_at IS '情景创建时间';
COMMENT ON COLUMN user_scenarios.updated_at IS '情景最后更新时间';

View File

@@ -1,15 +0,0 @@
-- 005_fix_user_scenarios_description.down.sql
-- 回滚恢复原约束description 必填prompt 最小长度 50
-- 注意:如果表中存在不符合原约束的数据,此回滚会失败
-- 1. 恢复 description 为必填
ALTER TABLE user_scenarios
ALTER COLUMN description SET NOT NULL;
-- 2. 恢复 prompt 最小长度为 50
ALTER TABLE user_scenarios
DROP CONSTRAINT IF EXISTS check_prompt_length;
ALTER TABLE user_scenarios
ADD CONSTRAINT check_prompt_length CHECK (char_length(prompt) >= 50 AND char_length(prompt) <= 2000);

View File

@@ -1,16 +0,0 @@
-- 005_fix_user_scenarios_description.up.sql
-- 修复 user_scenarios 表约束description 允许为空prompt 最小长度改为 10
-- 1. 允许 description 为空
ALTER TABLE user_scenarios
ALTER COLUMN description DROP NOT NULL;
-- 2. 修复 prompt 长度约束(从 50 改为 10与代码一致
ALTER TABLE user_scenarios
DROP CONSTRAINT IF EXISTS check_prompt_length;
ALTER TABLE user_scenarios
ADD CONSTRAINT check_prompt_length CHECK (char_length(prompt) >= 10 AND char_length(prompt) <= 2000);
COMMENT ON COLUMN user_scenarios.description IS '简短描述,可选,显示在情景卡片上(允许为空)';
COMMENT ON COLUMN user_scenarios.prompt IS '角色 System Prompt定义 AI 行为10-2000 字符)';

View File

@@ -1,9 +0,0 @@
-- 检查 user_scenarios 表的所有约束
SELECT
con.conname AS constraint_name,
con.contype AS constraint_type,
pg_get_constraintdef(con.oid) AS constraint_definition
FROM pg_constraint con
JOIN pg_class rel ON rel.oid = con.conrelid
WHERE rel.relname = 'user_scenarios'
ORDER BY con.conname;

View File

@@ -1,26 +0,0 @@
-- 快速修复脚本:修复 user_scenarios 表约束
-- 在生产 PostgreSQL 容器中执行此文件
-- 1. 允许 description 为空
ALTER TABLE user_scenarios
ALTER COLUMN description DROP NOT NULL;
-- 2. 修复 prompt 长度约束(从 50 改为 10与代码一致
ALTER TABLE user_scenarios
DROP CONSTRAINT IF EXISTS check_prompt_length;
ALTER TABLE user_scenarios
ADD CONSTRAINT check_prompt_length CHECK (char_length(prompt) >= 10 AND char_length(prompt) <= 2000);
-- 更新注释
COMMENT ON COLUMN user_scenarios.description IS '简短描述,可选,显示在情景卡片上(允许为空)';
COMMENT ON COLUMN user_scenarios.prompt IS '角色 System Prompt定义 AI 行为10-2000 字符)';
-- 验证修改
SELECT
con.conname AS constraint_name,
pg_get_constraintdef(con.oid) AS constraint_definition
FROM pg_constraint con
JOIN pg_class rel ON rel.oid = con.conrelid
WHERE rel.relname = 'user_scenarios'
AND con.conname = 'check_prompt_length';

View File

@@ -155,9 +155,9 @@ export const enUS: TranslationMap = {
"auth.login": "Sign In", "auth.login": "Sign In",
"auth.register": "Sign Up", "auth.register": "Sign Up",
"auth.username": "Username", "auth.username": "Username",
"auth.username.placeholder": "3-64 characters", "auth.username.placeholder": "Enter username",
"auth.password": "Password", "auth.password": "Password",
"auth.password.placeholder": "8-72 characters", "auth.password.placeholder": "Enter password",
"auth.submitting": "Please wait...", "auth.submitting": "Please wait...",
"auth.noAccount": "Don't have an account?", "auth.noAccount": "Don't have an account?",
"auth.hasAccount": "Already have an account?", "auth.hasAccount": "Already have an account?",

View File

@@ -155,9 +155,9 @@ export const jaJP: TranslationMap = {
"auth.login": "ログイン", "auth.login": "ログイン",
"auth.register": "新規登録", "auth.register": "新規登録",
"auth.username": "ユーザー名", "auth.username": "ユーザー名",
"auth.username.placeholder": "3〜64文字", "auth.username.placeholder": "ユーザー名を入力",
"auth.password": "パスワード", "auth.password": "パスワード",
"auth.password.placeholder": "8〜72文字", "auth.password.placeholder": "パスワードを入力",
"auth.submitting": "お待ちください...", "auth.submitting": "お待ちください...",
"auth.noAccount": "アカウントをお持ちでないですか?", "auth.noAccount": "アカウントをお持ちでないですか?",
"auth.hasAccount": "すでにアカウントをお持ちですか?", "auth.hasAccount": "すでにアカウントをお持ちですか?",

View File

@@ -155,9 +155,9 @@ export const zhCN: TranslationMap = {
"auth.login": "登录", "auth.login": "登录",
"auth.register": "注册", "auth.register": "注册",
"auth.username": "用户名", "auth.username": "用户名",
"auth.username.placeholder": "3-64 个字符", "auth.username.placeholder": "请输入用户名",
"auth.password": "密码", "auth.password": "密码",
"auth.password.placeholder": "8-72 个字符", "auth.password.placeholder": "请输入密码",
"auth.submitting": "请稍候...", "auth.submitting": "请稍候...",
"auth.noAccount": "还没有账号?", "auth.noAccount": "还没有账号?",
"auth.hasAccount": "已有账号?", "auth.hasAccount": "已有账号?",