# 云酒馆项目重构实施方案 ## 文档说明 本文档详细描述了将云酒馆项目从 Node.js 后端 + 前端一体化架构重构为 Go 后端 + 纯前端架构的完整实施方案。 --- ## 一、重构目标 ### 1.1 目标架构 ``` 云酒馆项目(重构后) ├── web-app/ # 用户前端应用(纯前端 - C端) │ ├── 移除所有 Node.js 后端代码 │ ├── 仅保留静态资源和前端逻辑 │ ├── 所有 API 调用指向 Go 后端 │ └── 使用 Nginx 或其他静态服务器部署 │ ├── web/ # 管理后台前端(B端) │ ├── 基于 Vue 3 + Vite │ ├── 系统管理功能(保持不变) │ ├── AI对话数据管理(新增) │ └── 用户数据统计(新增) │ └── server/ # 统一 Go 后端服务 ├── 基于 Gin + Gorm ├── PostgreSQL 数据库 ├── system/ 模块(保持不变) │ └── 管理后台相关功能 ├── app/ 模块(新增) │ ├── 前台用户认证与授权 │ ├── AI 对话服务集成 │ ├── 角色与对话管理 │ ├── 文件与对象存储 │ ├── 向量数据库集成 │ └── WebSocket 实时通信 └── 双用户体系设计 ├── sys_users(管理员) └── app_users(前台用户) ``` ### 1.2 重构原则 **⚠️ 重要约束**: 1. **不修改现有 system 模块**:所有管理后台相关的代码(api/v1/system、service/system、model/system 等)保持不变 2. **不修改 sys_users 表**:管理员用户体系保持独立 3. **新增 app 模块**:所有前台相关功能在新的 app 模块中实现 4. **并行开发**:system 和 app 模块互不干扰,可以独立开发和部署 5. **共享基础设施**:数据库连接、Redis、对象存储等基础设施共享 ### 1.3 双用户体系设计 本项目采用双用户体系,将管理员和前台用户完全分离: #### 用户体系对比 | 特性 | 管理后台用户(sys_users) | 前台应用用户(app_users) | |------|-------------------------|------------------------| | 用途 | 系统管理、数据管理 | AI对话、角色管理 | | 数据表 | `sys_users` | `app_users` | | 认证方式 | 原有 JWT | 独立 JWT(带UserType标识) | | 中间件 | `JWTAuth()` | `AppJWTAuth()` | | 路由前缀 | `/base`, `/user`, `/authority` 等 | `/app/*` | | 模块目录 | `system/` | `app/` | | 是否修改 | ❌ 不修改 | ✅ 新建 | #### 设计优势 1. **完全隔离**:两套用户体系互不干扰,降低风险 2. **独立扩展**:前台功能可独立开发、测试、部署 3. **权限清晰**:管理员和普通用户权限分离 4. **数据安全**:管理后台数据不受前台影响 ### 1.4 技术栈确定 #### 后端技术栈 - **语言**: Go 1.24+ - **Web框架**: Gin 1.10+ - **ORM**: Gorm 1.25+ - **数据库**: PostgreSQL 14+ - **向量扩展**: pgvector(PostgreSQL扩展) - **缓存**: Redis 7+ - **认证**: JWT (golang-jwt/jwt) - **WebSocket**: gorilla/websocket - **AI SDK**: - go-openai (OpenAI) - anthropic-sdk-go (Claude) - google-cloud-go/ai (Gemini) #### 前端技术栈 - **web-app**: 原生 JavaScript + HTML + CSS(移除 Express) - **web**: Vue 3 + Vite + Element Plus + Pinia --- ## 二、重构阶段规划 ### 阶段概览 | 阶段 | 名称 | 预期成果 | 依赖 | |------|------|----------|------| | 阶段一 | 数据库设计 | 完整的数据库表结构 | - | | 阶段二 | Go后端API开发 | 替代所有 Node.js 接口 | 阶段一 | | 阶段三 | 前端改造 | web-app 纯前端化 | 阶段二 | | 阶段四 | 数据迁移 | 历史数据迁移 | 阶段二、三 | | 阶段五 | 测试与优化 | 功能验证、性能优化 | 阶段一-四 | | 阶段六 | 部署上线 | 生产环境部署 | 阶段五 | --- ## 三、阶段一:数据库设计 ### 3.1 PostgreSQL 数据库设计 #### 3.1.1 安装 pgvector 扩展 ```sql -- 安装 pgvector 扩展(用于向量存储) CREATE EXTENSION IF NOT EXISTS vector; ``` #### 3.1.2 核心表结构设计 **说明**: - `sys_users` 表保持不变,仅用于管理后台用户(管理员) - 新建 `app_users` 表,用于前台用户(普通用户) - 两套用户体系完全独立,互不干扰 ##### 1. 前台用户相关表 ```sql -- 前台用户表(新建,与 sys_users 独立) CREATE TABLE IF NOT EXISTS app_users ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 基本信息 uuid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(), username VARCHAR(191) UNIQUE NOT NULL, password VARCHAR(191) NOT NULL, -- bcrypt 加密 nick_name VARCHAR(191) DEFAULT '', email VARCHAR(191), phone VARCHAR(191), avatar VARCHAR(1024), -- 头像 URL -- 账户状态 status VARCHAR(50) DEFAULT 'active', -- active, suspended, deleted enable BOOLEAN DEFAULT TRUE, -- 认证信息 last_login_at TIMESTAMP WITH TIME ZONE, last_login_ip VARCHAR(100), -- AI 相关配置(JSONB 存储) ai_settings JSONB DEFAULT '{}'::jsonb, -- 用户偏好设置 preferences JSONB DEFAULT '{}'::jsonb, -- 统计信息 chat_count INTEGER DEFAULT 0, -- 对话数量 message_count INTEGER DEFAULT 0, -- 消息数量 INDEX idx_username (username), INDEX idx_uuid (uuid), INDEX idx_email (email), INDEX idx_deleted_at (deleted_at) ); COMMENT ON TABLE app_users IS '前台用户表(与管理后台 sys_users 独立)'; COMMENT ON COLUMN app_users.ai_settings IS 'AI配置,如:默认模型、参数等'; COMMENT ON COLUMN app_users.preferences IS '用户偏好,如:主题、语言等'; -- 前台用户会话表 CREATE TABLE IF NOT EXISTS app_user_sessions ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, session_token VARCHAR(500) UNIQUE NOT NULL, refresh_token VARCHAR(500), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, refresh_expires_at TIMESTAMP WITH TIME ZONE, ip_address VARCHAR(100), user_agent TEXT, device_info JSONB DEFAULT '{}'::jsonb, INDEX idx_user_id (user_id), INDEX idx_session_token (session_token), INDEX idx_expires_at (expires_at) ); COMMENT ON TABLE app_user_sessions IS '前台用户会话表(支持多设备登录)'; ``` ##### 2. AI 角色相关表 ```sql -- AI 角色表 CREATE TABLE IF NOT EXISTS ai_characters ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 角色基本信息 name VARCHAR(500) NOT NULL, description TEXT, personality TEXT, scenario TEXT, -- 角色头像 avatar VARCHAR(1024), -- 创建者(前台用户) creator_id BIGINT REFERENCES app_users(id) ON DELETE SET NULL, -- 角色卡片数据(完整的 Character Card 格式) card_data JSONB NOT NULL, -- 角色标签 tags TEXT[], -- 可见性:public(公开), private(私有), shared(共享) visibility VARCHAR(50) DEFAULT 'private', -- 角色版本 version INTEGER DEFAULT 1, -- 第一条消息(开场白) first_message TEXT, -- 消息示例 example_messages JSONB DEFAULT '[]'::jsonb, -- 统计信息 usage_count INTEGER DEFAULT 0, favorite_count INTEGER DEFAULT 0, INDEX idx_creator_id (creator_id), INDEX idx_visibility (visibility), INDEX idx_tags (tags) USING GIN, INDEX idx_deleted_at (deleted_at) ); -- 用户收藏的角色 CREATE TABLE IF NOT EXISTS app_user_favorite_characters ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, character_id BIGINT NOT NULL REFERENCES ai_characters(id) ON DELETE CASCADE, UNIQUE(user_id, character_id), INDEX idx_user_id (user_id), INDEX idx_character_id (character_id) ); COMMENT ON TABLE app_user_favorite_characters IS '前台用户收藏的角色'; ``` ##### 3. 对话相关表 ```sql -- 对话表 CREATE TABLE IF NOT EXISTS ai_chats ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 对话标题 title VARCHAR(500) DEFAULT '新对话', -- 所属用户(前台用户) user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, -- 关联角色 character_id BIGINT REFERENCES ai_characters(id) ON DELETE SET NULL, -- 对话类型:single(单角色), group(群聊) chat_type VARCHAR(50) DEFAULT 'single', -- 对话设置 settings JSONB DEFAULT '{}'::jsonb, -- 最后一条消息时间 last_message_at TIMESTAMP WITH TIME ZONE, -- 消息数量 message_count INTEGER DEFAULT 0, -- 是否固定 is_pinned BOOLEAN DEFAULT FALSE, INDEX idx_user_id (user_id), INDEX idx_character_id (character_id), INDEX idx_last_message_at (last_message_at), INDEX idx_deleted_at (deleted_at) ); -- 消息表 CREATE TABLE IF NOT EXISTS ai_messages ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 所属对话 chat_id BIGINT NOT NULL REFERENCES ai_chats(id) ON DELETE CASCADE, -- 消息内容 content TEXT NOT NULL, -- 发送者类型:user(用户), assistant(AI), system(系统) role VARCHAR(50) NOT NULL, -- 发送者ID(如果是用户消息,关联前台用户) sender_id BIGINT REFERENCES app_users(id) ON DELETE SET NULL, -- AI角色ID(如果是AI消息) character_id BIGINT REFERENCES ai_characters(id) ON DELETE SET NULL, -- 消息序号(在对话中的位置) sequence_number INTEGER NOT NULL, -- AI 模型信息 model VARCHAR(200), -- Token 使用量 prompt_tokens INTEGER DEFAULT 0, completion_tokens INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, -- 生成参数 generation_params JSONB DEFAULT '{}'::jsonb, -- 消息元数据(如:swipe变体、编辑历史等) metadata JSONB DEFAULT '{}'::jsonb, -- 是否被用户删除 is_deleted BOOLEAN DEFAULT FALSE, INDEX idx_chat_id (chat_id), INDEX idx_role (role), INDEX idx_sequence_number (chat_id, sequence_number), INDEX idx_deleted_at (deleted_at) ); -- 消息变体表(swipe 功能) CREATE TABLE IF NOT EXISTS ai_message_swipes ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, message_id BIGINT NOT NULL REFERENCES ai_messages(id) ON DELETE CASCADE, -- 变体内容 content TEXT NOT NULL, -- 变体序号 swipe_index INTEGER NOT NULL, -- 是否为当前选中的变体 is_active BOOLEAN DEFAULT FALSE, -- 生成参数 generation_params JSONB DEFAULT '{}'::jsonb, UNIQUE(message_id, swipe_index), INDEX idx_message_id (message_id) ); ``` ##### 4. 群聊相关表 ```sql -- 群聊成员表 CREATE TABLE IF NOT EXISTS ai_chat_members ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, chat_id BIGINT NOT NULL REFERENCES ai_chats(id) ON DELETE CASCADE, character_id BIGINT NOT NULL REFERENCES ai_characters(id) ON DELETE CASCADE, -- 成员在群聊中的排序 display_order INTEGER DEFAULT 0, -- 成员设置 settings JSONB DEFAULT '{}'::jsonb, UNIQUE(chat_id, character_id), INDEX idx_chat_id (chat_id), INDEX idx_character_id (character_id) ); ``` ##### 5. 向量记忆表 ```sql -- 向量记忆表(使用 pgvector) CREATE TABLE IF NOT EXISTS ai_memory_vectors ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 所属用户(前台用户) user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, -- 所属角色(可选) character_id BIGINT REFERENCES ai_characters(id) ON DELETE CASCADE, -- 所属对话(可选) chat_id BIGINT REFERENCES ai_chats(id) ON DELETE CASCADE, -- 文本内容 content TEXT NOT NULL, -- 向量嵌入(1536维,OpenAI text-embedding-ada-002) embedding vector(1536), -- 元数据 metadata JSONB DEFAULT '{}'::jsonb, -- 重要性评分 importance FLOAT DEFAULT 0.5, INDEX idx_user_id (user_id), INDEX idx_character_id (character_id), INDEX idx_chat_id (chat_id), INDEX idx_deleted_at (deleted_at) ); -- 创建向量索引(使用 HNSW 算法,余弦相似度) CREATE INDEX idx_memory_vectors_embedding ON ai_memory_vectors USING hnsw (embedding vector_cosine_ops); ``` ##### 6. AI 服务配置表 ```sql -- AI 服务提供商配置 CREATE TABLE IF NOT EXISTS ai_providers ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 用户ID(每个前台用户可以有自己的配置,NULL表示系统默认配置) user_id BIGINT REFERENCES app_users(id) ON DELETE CASCADE, -- 提供商名称:openai, anthropic, google, azure, etc. provider_name VARCHAR(100) NOT NULL, -- API 配置(加密存储) api_config JSONB NOT NULL, -- 是否启用 is_enabled BOOLEAN DEFAULT TRUE, -- 是否为默认提供商 is_default BOOLEAN DEFAULT FALSE, INDEX idx_user_id (user_id), INDEX idx_provider_name (provider_name), INDEX idx_deleted_at (deleted_at) ); -- AI 模型配置 CREATE TABLE IF NOT EXISTS ai_models ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, provider_id BIGINT NOT NULL REFERENCES ai_providers(id) ON DELETE CASCADE, -- 模型名称 model_name VARCHAR(200) NOT NULL, -- 模型显示名称 display_name VARCHAR(200), -- 模型参数配置 config JSONB DEFAULT '{}'::jsonb, -- 是否启用 is_enabled BOOLEAN DEFAULT TRUE, INDEX idx_provider_id (provider_id), INDEX idx_model_name (model_name) ); ``` ##### 7. 文件管理表 ```sql -- 文件表 CREATE TABLE IF NOT EXISTS ai_files ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 上传者(前台用户) user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, -- 文件名 filename VARCHAR(500) NOT NULL, -- 原始文件名 original_filename VARCHAR(500) NOT NULL, -- 文件类型:avatar, background, attachment, export, etc. file_type VARCHAR(100) NOT NULL, -- MIME 类型 mime_type VARCHAR(200), -- 文件大小(字节) file_size BIGINT, -- 存储路径 storage_path VARCHAR(1024) NOT NULL, -- 对象存储 URL(如果使用OSS) url VARCHAR(1024), -- 关联对象(JSON格式,如:{"chat_id": 123, "character_id": 456}) related_to JSONB DEFAULT '{}'::jsonb, -- 元数据 metadata JSONB DEFAULT '{}'::jsonb, INDEX idx_user_id (user_id), INDEX idx_file_type (file_type), INDEX idx_deleted_at (deleted_at) ); ``` ##### 8. 预设与设置表 ```sql -- 对话预设表 CREATE TABLE IF NOT EXISTS ai_presets ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 预设名称 name VARCHAR(200) NOT NULL, -- 所属用户(NULL表示系统预设,非NULL表示前台用户的自定义预设) user_id BIGINT REFERENCES app_users(id) ON DELETE CASCADE, -- 预设类型:generation, instruction, etc. preset_type VARCHAR(100) NOT NULL, -- 预设配置 config JSONB NOT NULL, -- 是否为系统预设 is_system BOOLEAN DEFAULT FALSE, -- 是否为默认预设 is_default BOOLEAN DEFAULT FALSE, INDEX idx_user_id (user_id), INDEX idx_preset_type (preset_type), INDEX idx_deleted_at (deleted_at) ); -- 世界书(World Info)表 CREATE TABLE IF NOT EXISTS ai_world_info ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP WITH TIME ZONE, -- 所属用户(前台用户) user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, -- 关联角色(可选) character_id BIGINT REFERENCES ai_characters(id) ON DELETE CASCADE, -- 世界书名称 name VARCHAR(500) NOT NULL, -- 触发关键词 keywords TEXT[], -- 内容 content TEXT NOT NULL, -- 优先级 priority INTEGER DEFAULT 0, -- 是否启用 is_enabled BOOLEAN DEFAULT TRUE, -- 触发条件配置 trigger_config JSONB DEFAULT '{}'::jsonb, INDEX idx_user_id (user_id), INDEX idx_character_id (character_id), INDEX idx_keywords (keywords) USING GIN, INDEX idx_deleted_at (deleted_at) ); ``` ##### 9. 系统设置与日志表 ```sql -- 系统配置表(扩展现有) CREATE TABLE IF NOT EXISTS sys_configs ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- 配置键 config_key VARCHAR(200) UNIQUE NOT NULL, -- 配置值 config_value JSONB NOT NULL, -- 配置描述 description TEXT, -- 配置分组 config_group VARCHAR(100), INDEX idx_config_key (config_key), INDEX idx_config_group (config_group) ); -- AI 使用统计表 CREATE TABLE IF NOT EXISTS ai_usage_stats ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, -- 统计日期 stat_date DATE NOT NULL, -- AI 提供商 provider_name VARCHAR(100), -- 模型名称 model_name VARCHAR(200), -- 请求次数 request_count INTEGER DEFAULT 0, -- Token 使用量 total_tokens BIGINT DEFAULT 0, prompt_tokens BIGINT DEFAULT 0, completion_tokens BIGINT DEFAULT 0, -- 费用(如果有) cost DECIMAL(10, 4) DEFAULT 0, UNIQUE(user_id, stat_date, provider_name, model_name), INDEX idx_user_id (user_id), INDEX idx_stat_date (stat_date) ); ``` ### 3.2 数据库迁移脚本 创建 Gorm 迁移文件:`server/model/app/` 目录下创建对应的 Go 模型文件。 **重要说明**: - 不要修改 `server/model/system/` 下的任何文件 - 所有前台相关模型都在 `server/model/app/` 下创建 #### 示例:前台用户模型 ```go // server/model/app/user.go package app import ( "git.echol.cn/loser/st/server/global" "gorm.io/datatypes" "time" ) // AppUser 前台用户模型(与 sys_users 独立) type AppUser struct { global.GVA_MODEL UUID string `json:"uuid" gorm:"type:uuid;uniqueIndex;comment:用户UUID"` Username string `json:"username" gorm:"uniqueIndex;comment:用户登录名"` Password string `json:"-" gorm:"comment:用户登录密码"` NickName string `json:"nickName" gorm:"comment:用户昵称"` Email string `json:"email" gorm:"index;comment:用户邮箱"` Phone string `json:"phone" gorm:"comment:用户手机号"` Avatar string `json:"avatar" gorm:"type:varchar(1024);comment:用户头像"` Status string `json:"status" gorm:"type:varchar(50);default:active;comment:账户状态"` Enable bool `json:"enable" gorm:"default:true;comment:用户是否启用"` LastLoginAt *time.Time `json:"lastLoginAt" gorm:"comment:最后登录时间"` LastLoginIP string `json:"lastLoginIp" gorm:"type:varchar(100);comment:最后登录IP"` AISettings datatypes.JSON `json:"aiSettings" gorm:"type:jsonb;comment:AI配置"` Preferences datatypes.JSON `json:"preferences" gorm:"type:jsonb;comment:用户偏好"` ChatCount int `json:"chatCount" gorm:"default:0;comment:对话数量"` MessageCount int `json:"messageCount" gorm:"default:0;comment:消息数量"` } func (AppUser) TableName() string { return "app_users" } // AppUserSession 前台用户会话 type AppUserSession struct { global.GVA_MODEL UserID uint `json:"userId" gorm:"index;comment:用户ID"` SessionToken string `json:"sessionToken" gorm:"type:varchar(500);uniqueIndex;comment:会话Token"` RefreshToken string `json:"refreshToken" gorm:"type:varchar(500);comment:刷新Token"` ExpiresAt time.Time `json:"expiresAt" gorm:"index;comment:过期时间"` RefreshExpiresAt *time.Time `json:"refreshExpiresAt" gorm:"comment:刷新Token过期时间"` IPAddress string `json:"ipAddress" gorm:"type:varchar(100);comment:IP地址"` UserAgent string `json:"userAgent" gorm:"type:text;comment:用户代理"` DeviceInfo datatypes.JSON `json:"deviceInfo" gorm:"type:jsonb;comment:设备信息"` } func (AppUserSession) TableName() string { return "app_user_sessions" } ``` --- ## 四、阶段二:Go 后端 API 开发 ### 4.1 项目结构调整 **重要原则**: - ✅ 保持现有 `system/` 模块完全不变 - ✅ 新增 `app/` 模块,与 `system/` 并列 - ✅ 两个模块完全独立,互不干扰 在 `server/` 目录下创建新的模块结构: ``` server/ ├── api/ │ └── v1/ │ ├── system/ # 现有系统接口(保持不变) │ │ ├── sys_user.go │ │ ├── sys_authority.go │ │ └── ... │ └── app/ # 新增:前台应用接口 │ ├── character.go # 角色管理 │ ├── chat.go # 对话管理 │ ├── message.go # 消息管理 │ ├── provider.go # AI提供商配置 │ ├── memory.go # 向量记忆 │ ├── preset.go # 预设管理 │ └── file.go # 文件管理 ├── model/ │ └── app/ # 新增:应用模型 │ ├── character.go │ ├── chat.go │ ├── message.go │ ├── provider.go │ ├── memory.go │ └── ... ├── service/ │ └── app/ # 新增:应用服务 │ ├── character.go │ ├── chat.go │ ├── ai_service.go # AI服务集成 │ ├── embedding.go # 向量嵌入服务 │ └── ... ├── router/ │ └── app/ # 新增:应用路由 │ ├── character.go │ ├── chat.go │ └── ... └── pkg/ # 新增:公共包(system 和 app 共享) ├── ai/ # AI SDK 封装 │ ├── openai.go │ ├── anthropic.go │ └── google.go ├── embedding/ # 向量嵌入 │ └── openai.go └── websocket/ # WebSocket 服务 └── hub.go ``` **重要说明**: 1. 所有 `system/` 目录下的代码保持不变 2. 所有新增功能都在 `app/` 目录下实现 3. `pkg/` 目录下的公共包可以被 `system` 和 `app` 共同使用 4. 两个模块使用不同的数据表,互不干扰 ### 4.2 双用户体系设计 #### 4.2.1 用户类型区分 ```go // server/global/constants.go (新增常量) package global const ( UserTypeSystem = "system" // 管理后台用户 UserTypeApp = "app" // 前台应用用户 ) ``` #### 4.2.2 前台用户 JWT 中间件 ```go // server/middleware/app_jwt.go (新增) package middleware import ( "github.com/gin-gonic/gin" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/app" "git.echol.cn/loser/st/server/utils" ) // AppJWTAuth 前台用户JWT认证中间件 func AppJWTAuth() gin.HandlerFunc { return func(c *gin.Context) { token := utils.GetToken(c) if token == "" { response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c) c.Abort() return } // 解析 JWT claims, err := utils.ParseToken(token) if err != nil { response.FailWithDetailed(gin.H{"reload": true}, "Token已过期", c) c.Abort() return } // 验证用户类型(确保是前台用户) if claims.UserType != global.UserTypeApp { response.FailWithMessage("无效的用户类型", c) c.Abort() return } // 查询用户是否存在 var user app.AppUser err = global.GVA_DB.Where("id = ?", claims.UserID).First(&user).Error if err != nil { response.FailWithMessage("用户不存在", c) c.Abort() return } // 检查用户状态 if !user.Enable { response.FailWithMessage("用户已被禁用", c) c.Abort() return } // 将用户信息存入上下文 c.Set("appUserID", user.ID) c.Set("appUser", user) c.Next() } } // GetAppUserID 从上下文获取前台用户ID func GetAppUserID(c *gin.Context) uint { if userID, exists := c.Get("appUserID"); exists { return userID.(uint) } return 0 } // GetAppUser 从上下文获取前台用户信息 func GetAppUser(c *gin.Context) *app.AppUser { if user, exists := c.Get("appUser"); exists { return user.(*app.AppUser) } return nil } ``` **说明**: - 管理后台继续使用原有的 `JWTAuth()` 中间件(不修改) - 前台应用使用新的 `AppJWTAuth()` 中间件 - JWT Claims 中添加 `UserType` 字段区分用户类型 - 两套中间件完全独立,互不干扰 ### 4.3 核心依赖添加 在 `server/go.mod` 中添加以下依赖: ```bash cd server go get github.com/sashabaranov/go-openai@latest go get github.com/gorilla/websocket@latest go get github.com/pgvector/pgvector-go@latest go get github.com/pkoukk/tiktoken-go@latest ``` ### 4.3 核心 API 接口开发 **重要提醒**: - 所有前台 API 都在 `api/v1/app/` 下实现 - 不要修改 `api/v1/system/` 下的任何文件 - 前台用户使用 `app_users` 表,管理员使用 `sys_users` 表 #### 4.3.1 前台用户认证 API ```go // server/api/v1/app/auth.go package app import ( "github.com/gin-gonic/gin" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/common/response" "git.echol.cn/loser/st/server/service" ) type AuthApi struct{} // Register 前台用户注册 // @Tags App.Auth // @Summary 前台用户注册 // @accept application/json // @Produce application/json // @Param data body request.RegisterRequest true "用户注册信息" // @Success 200 {object} response.Response{msg=string} // @Router /app/auth/register [post] func (a *AuthApi) Register(ctx *gin.Context) { // 实现逻辑 } // Login 前台用户登录 // @Tags App.Auth // @Summary 前台用户登录 // @accept application/json // @Produce application/json // @Param data body request.LoginRequest true "用户登录信息" // @Success 200 {object} response.Response{data=response.LoginResponse,msg=string} // @Router /app/auth/login [post] func (a *AuthApi) Login(ctx *gin.Context) { // 实现逻辑 } // Logout 前台用户登出 // @Tags App.Auth // @Summary 前台用户登出 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{msg=string} // @Router /app/auth/logout [post] func (a *AuthApi) Logout(ctx *gin.Context) { // 实现逻辑 } // RefreshToken 刷新Token // @Tags App.Auth // @Summary 刷新Token // @accept application/json // @Produce application/json // @Param data body request.RefreshTokenRequest true "刷新Token" // @Success 200 {object} response.Response{data=response.LoginResponse,msg=string} // @Router /app/auth/refresh [post] func (a *AuthApi) RefreshToken(ctx *gin.Context) { // 实现逻辑 } // GetUserInfo 获取当前登录用户信息 // @Tags App.Auth // @Summary 获取当前登录用户信息 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Success 200 {object} response.Response{data=model.AppUser,msg=string} // @Router /app/auth/userinfo [get] func (a *AuthApi) GetUserInfo(ctx *gin.Context) { // 实现逻辑 } ``` **说明**: - 前台用户认证完全独立于管理后台 - 使用独立的 JWT Token,避免混淆 - 建议使用不同的 Token 密钥或添加类型标识 #### 4.3.2 角色管理 API ```go // server/api/v1/app/character.go package app import ( "github.com/gin-gonic/gin" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/common/response" "git.echol.cn/loser/st/server/service" ) type CharacterApi struct{} // GetCharacterList 获取角色列表 // @Tags Character // @Summary 获取角色列表 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data query request.CharacterSearch true "分页查询" // @Success 200 {object} response.Response{data=response.PageResult,msg=string} // @Router /character/list [get] func (c *CharacterApi) GetCharacterList(ctx *gin.Context) { // 实现逻辑 } // CreateCharacter 创建角色 // @Tags Character // @Summary 创建角色 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param data body request.CharacterCreate true "角色信息" // @Success 200 {object} response.Response{msg=string} // @Router /character/create [post] func (c *CharacterApi) CreateCharacter(ctx *gin.Context) { // 实现逻辑 } // GetCharacter 获取角色详情 // @Tags Character // @Summary 获取角色详情 // @Security ApiKeyAuth // @accept application/json // @Produce application/json // @Param id path int true "角色ID" // @Success 200 {object} response.Response{data=model.Character,msg=string} // @Router /character/:id [get] func (c *CharacterApi) GetCharacter(ctx *gin.Context) { // 实现逻辑 } // UpdateCharacter 更新角色 func (c *CharacterApi) UpdateCharacter(ctx *gin.Context) { // 实现逻辑 } // DeleteCharacter 删除角色 func (c *CharacterApi) DeleteCharacter(ctx *gin.Context) { // 实现逻辑 } // ImportCharacter 导入角色(支持 Character Card 格式) func (c *CharacterApi) ImportCharacter(ctx *gin.Context) { // 实现逻辑 } // ExportCharacter 导出角色 func (c *CharacterApi) ExportCharacter(ctx *gin.Context) { // 实现逻辑 } ``` #### 4.3.2 对话管理 API ```go // server/api/v1/app/chat.go package app type ChatApi struct{} // CreateChat 创建对话 func (c *ChatApi) CreateChat(ctx *gin.Context) {} // GetChatList 获取对话列表 func (c *ChatApi) GetChatList(ctx *gin.Context) {} // GetChat 获取对话详情 func (c *ChatApi) GetChat(ctx *gin.Context) {} // UpdateChat 更新对话 func (c *ChatApi) UpdateChat(ctx *gin.Context) {} // DeleteChat 删除对话 func (c *ChatApi) DeleteChat(ctx *gin.Context) {} // GetChatMessages 获取对话消息历史 func (c *ChatApi) GetChatMessages(ctx *gin.Context) {} // SendMessage 发送消息(调用 AI) func (c *ChatApi) SendMessage(ctx *gin.Context) {} // RegenerateMessage 重新生成消息 func (c *ChatApi) RegenerateMessage(ctx *gin.Context) {} // EditMessage 编辑消息 func (c *ChatApi) EditMessage(ctx *gin.Context) {} // DeleteMessage 删除消息 func (c *ChatApi) DeleteMessage(ctx *gin.Context) {} // SwipeMessage 切换消息变体 func (c *ChatApi) SwipeMessage(ctx *gin.Context) {} ``` #### 4.3.3 AI 服务 API ```go // server/api/v1/app/provider.go package app type ProviderApi struct{} // GetProviders 获取 AI 提供商列表 func (p *ProviderApi) GetProviders(ctx *gin.Context) {} // CreateProvider 创建 AI 提供商配置 func (p *ProviderApi) CreateProvider(ctx *gin.Context) {} // UpdateProvider 更新 AI 提供商配置 func (p *ProviderApi) UpdateProvider(ctx *gin.Context) {} // DeleteProvider 删除 AI 提供商配置 func (p *ProviderApi) DeleteProvider(ctx *gin.Context) {} // TestProvider 测试 AI 提供商连接 func (p *ProviderApi) TestProvider(ctx *gin.Context) {} // GetModels 获取可用模型列表 func (p *ProviderApi) GetModels(ctx *gin.Context) {} ``` ### 4.4 AI 服务集成 #### 4.4.1 OpenAI 集成 ```go // server/pkg/ai/openai.go package ai import ( "context" "github.com/sashabaranov/go-openai" ) type OpenAIClient struct { client *openai.Client } func NewOpenAIClient(apiKey string, baseURL ...string) *OpenAIClient { config := openai.DefaultConfig(apiKey) if len(baseURL) > 0 { config.BaseURL = baseURL[0] } return &OpenAIClient{ client: openai.NewClientWithConfig(config), } } // ChatCompletion 对话补全 func (c *OpenAIClient) ChatCompletion(ctx context.Context, messages []openai.ChatCompletionMessage, opts ...Option) (*openai.ChatCompletionResponse, error) { req := openai.ChatCompletionRequest{ Model: openai.GPT4, Messages: messages, } // 应用选项 for _, opt := range opts { opt(&req) } return c.client.CreateChatCompletion(ctx, req) } // StreamChatCompletion 流式对话补全 func (c *OpenAIClient) StreamChatCompletion(ctx context.Context, messages []openai.ChatCompletionMessage, opts ...Option) (*openai.ChatCompletionStream, error) { req := openai.ChatCompletionRequest{ Model: openai.GPT4, Messages: messages, Stream: true, } for _, opt := range opts { opt(&req) } return c.client.CreateChatCompletionStream(ctx, req) } // CreateEmbedding 创建向量嵌入 func (c *OpenAIClient) CreateEmbedding(ctx context.Context, input string) ([]float32, error) { resp, err := c.client.CreateEmbeddings(ctx, openai.EmbeddingRequest{ Model: openai.AdaEmbeddingV2, Input: input, }) if err != nil { return nil, err } return resp.Data[0].Embedding, nil } type Option func(*openai.ChatCompletionRequest) func WithModel(model string) Option { return func(req *openai.ChatCompletionRequest) { req.Model = model } } func WithTemperature(temp float32) Option { return func(req *openai.ChatCompletionRequest) { req.Temperature = temp } } func WithMaxTokens(max int) Option { return func(req *openai.ChatCompletionRequest) { req.MaxTokens = max } } ``` #### 4.4.2 向量嵌入服务 ```go // server/service/app/embedding.go package app import ( "context" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/app" "git.echol.cn/loser/st/server/pkg/ai" "github.com/pgvector/pgvector-go" ) type EmbeddingService struct{} // CreateMemory 创建记忆(带向量嵌入) func (e *EmbeddingService) CreateMemory(ctx context.Context, userID uint, content string, metadata map[string]interface{}) error { // 1. 创建向量嵌入 aiClient := ai.NewOpenAIClient(global.GVA_CONFIG.AI.OpenAI.APIKey) embedding, err := aiClient.CreateEmbedding(ctx, content) if err != nil { return err } // 2. 保存到数据库 memory := app.MemoryVector{ UserID: userID, Content: content, Embedding: pgvector.NewVector(embedding), Metadata: metadata, } return global.GVA_DB.Create(&memory).Error } // SearchSimilarMemories 搜索相似记忆 func (e *EmbeddingService) SearchSimilarMemories(ctx context.Context, userID uint, query string, limit int) ([]app.MemoryVector, error) { // 1. 创建查询向量 aiClient := ai.NewOpenAIClient(global.GVA_CONFIG.AI.OpenAI.APIKey) embedding, err := aiClient.CreateEmbedding(ctx, query) if err != nil { return nil, err } // 2. 向量相似度搜索 var memories []app.MemoryVector err = global.GVA_DB. Where("user_id = ?", userID). Order(fmt.Sprintf("embedding <=> '%s'", pgvector.NewVector(embedding).String())). Limit(limit). Find(&memories).Error return memories, err } ``` ### 4.5 WebSocket 实现 ```go // server/pkg/websocket/hub.go package websocket import ( "sync" "github.com/gorilla/websocket" ) // Hub WebSocket 连接中心 type Hub struct { // 客户端连接映射 map[userID]map[connectionID]*Client clients map[uint]map[string]*Client broadcast chan *Message register chan *Client unregister chan *Client mu sync.RWMutex } type Client struct { hub *Hub conn *websocket.Conn userID uint id string send chan []byte } type Message struct { UserID uint `json:"userId"` Type string `json:"type"` Payload interface{} `json:"payload"` } func NewHub() *Hub { return &Hub{ clients: make(map[uint]map[string]*Client), broadcast: make(chan *Message, 256), register: make(chan *Client), unregister: make(chan *Client), } } func (h *Hub) Run() { for { select { case client := <-h.register: h.mu.Lock() if _, ok := h.clients[client.userID]; !ok { h.clients[client.userID] = make(map[string]*Client) } h.clients[client.userID][client.id] = client h.mu.Unlock() case client := <-h.unregister: h.mu.Lock() if clients, ok := h.clients[client.userID]; ok { if _, ok := clients[client.id]; ok { delete(clients, client.id) close(client.send) if len(clients) == 0 { delete(h.clients, client.userID) } } } h.mu.Unlock() case message := <-h.broadcast: h.mu.RLock() if clients, ok := h.clients[message.UserID]; ok { for _, client := range clients { select { case client.send <- marshal(message): default: close(client.send) delete(clients, client.id) } } } h.mu.RUnlock() } } } // BroadcastToUser 向指定用户广播消息 func (h *Hub) BroadcastToUser(userID uint, msgType string, payload interface{}) { h.broadcast <- &Message{ UserID: userID, Type: msgType, Payload: payload, } } ``` ### 4.6 路由注册 **重要说明**: - 前台路由统一使用 `/app` 前缀,与管理后台的路由区分 - 管理后台路由保持原有的路径不变 #### 4.6.1 前台用户认证路由 ```go // server/router/app/auth.go package app import ( "github.com/gin-gonic/gin" v1 "git.echol.cn/loser/st/server/api/v1" ) type AuthRouter struct{} func (r *AuthRouter) InitAuthRouter(Router *gin.RouterGroup) { // 公开路由(无需认证) publicRouter := Router.Group("auth") authApi := v1.ApiGroupApp.AppApiGroup.AuthApi { publicRouter.POST("register", authApi.Register) // 注册 publicRouter.POST("login", authApi.Login) // 登录 publicRouter.POST("refresh", authApi.RefreshToken) // 刷新Token } // 需要认证的路由 privateRouter := Router.Group("auth") // privateRouter.Use(middleware.AppJWTAuth()) // 使用前台用户JWT中间件 { privateRouter.POST("logout", authApi.Logout) privateRouter.GET("userinfo", authApi.GetUserInfo) } } ``` #### 4.6.2 角色管理路由 ```go // server/router/app/character.go package app import ( "github.com/gin-gonic/gin" v1 "git.echol.cn/loser/st/server/api/v1" ) type CharacterRouter struct{} func (r *CharacterRouter) InitCharacterRouter(Router *gin.RouterGroup) { characterRouter := Router.Group("character") // characterRouter.Use(middleware.AppJWTAuth()) // 使用前台用户JWT中间件 characterApi := v1.ApiGroupApp.AppApiGroup.CharacterApi { characterRouter.GET("list", characterApi.GetCharacterList) characterRouter.POST("create", characterApi.CreateCharacter) characterRouter.GET(":id", characterApi.GetCharacter) characterRouter.PUT(":id", characterApi.UpdateCharacter) characterRouter.DELETE(":id", characterApi.DeleteCharacter) characterRouter.POST("import", characterApi.ImportCharacter) characterRouter.GET("export/:id", characterApi.ExportCharacter) } } ``` #### 4.6.3 路由入口注册 ```go // server/router/app/enter.go package app type RouterGroup struct { AuthRouter CharacterRouter ChatRouter MessageRouter ProviderRouter FileRouter } ``` #### 4.6.4 主路由文件集成 ```go // server/initialize/router.go package initialize import ( "git.echol.cn/loser/st/server/router" "github.com/gin-gonic/gin" ) func Routers() *gin.Engine { Router := gin.Default() // ... 其他中间件配置 PublicGroup := Router.Group("") { // 管理后台路由(保持不变) systemRouter := router.RouterGroupApp.System systemRouter.InitBaseRouter(PublicGroup) // 原有的管理后台路由 // ... 其他 system 路由 // 前台应用路由(新增) appRouter := router.RouterGroupApp.App appGroup := PublicGroup.Group("app") // 统一使用 /app 前缀 { appRouter.InitAuthRouter(appGroup) // /app/auth/* appRouter.InitCharacterRouter(appGroup) // /app/character/* appRouter.InitChatRouter(appGroup) // /app/chat/* // ... 其他 app 路由 } } return Router } ``` **路由示例**: - 管理后台登录:`POST /base/login` (保持不变) - 前台用户登录:`POST /app/auth/login` (新增) - 管理后台用户列表:`GET /user/getUserList` (保持不变) - 前台角色列表:`GET /app/character/list` (新增) --- ## 五、阶段三:前端改造 ### 5.1 web-app 改造计划 #### 5.1.1 目录结构调整 ``` web-app/ ├── public/ # 静态资源(保留) │ ├── index.html # 主页面 │ ├── css/ # 样式 │ ├── scripts/ # JavaScript │ │ ├── main.js # 主入口 │ │ ├── api/ # API 调用层(新增) │ │ │ ├── client.js # HTTP 客户端 │ │ │ ├── character.js │ │ │ ├── chat.js │ │ │ └── ... │ │ ├── services/ # 业务逻辑层 │ │ ├── components/ # UI 组件 │ │ └── utils/ # 工具函数 │ ├── lib/ # 第三方库(保留) │ └── ... ├── nginx.conf # Nginx 配置(新增) ├── Dockerfile # Docker 配置(新增) └── package.json # 仅用于开发工具(可选) ``` #### 5.1.2 移除的文件 删除以下 Node.js 后端相关文件: - `server.js` - `src/` 目录(所有后端代码) - `webpack.config.js`(如果不需要打包) #### 5.1.3 API 客户端实现 ```javascript // public/scripts/api/client.js class APIClient { constructor(baseURL) { this.baseURL = baseURL || 'http://localhost:8888'; this.token = localStorage.getItem('token'); } async request(method, endpoint, data = null, options = {}) { const url = `${this.baseURL}${endpoint}`; const headers = { 'Content-Type': 'application/json', ...options.headers }; if (this.token) { headers['x-token'] = this.token; } const config = { method, headers, ...options }; if (data && (method === 'POST' || method === 'PUT')) { config.body = JSON.stringify(data); } try { const response = await fetch(url, config); const result = await response.json(); if (result.code !== 0) { throw new Error(result.msg || '请求失败'); } return result.data; } catch (error) { console.error('API请求失败:', error); throw error; } } get(endpoint, options) { return this.request('GET', endpoint, null, options); } post(endpoint, data, options) { return this.request('POST', endpoint, data, options); } put(endpoint, data, options) { return this.request('PUT', endpoint, data, options); } delete(endpoint, options) { return this.request('DELETE', endpoint, null, options); } setToken(token) { this.token = token; localStorage.setItem('token', token); } clearToken() { this.token = null; localStorage.removeItem('token'); } } // 导出单例 const apiClient = new APIClient(); ``` ```javascript // public/scripts/api/character.js class CharacterAPI { constructor(client) { this.client = client; } // 获取角色列表 async getList(params = {}) { const query = new URLSearchParams(params).toString(); return this.client.get(`/character/list?${query}`); } // 创建角色 async create(characterData) { return this.client.post('/character/create', characterData); } // 获取角色详情 async get(id) { return this.client.get(`/character/${id}`); } // 更新角色 async update(id, characterData) { return this.client.put(`/character/${id}`, characterData); } // 删除角色 async delete(id) { return this.client.delete(`/character/${id}`); } // 导入角色 async import(file) { const formData = new FormData(); formData.append('file', file); return this.client.post('/character/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); } // 导出角色 async export(id) { window.location.href = `${this.client.baseURL}/character/export/${id}`; } } const characterAPI = new CharacterAPI(apiClient); ``` ```javascript // public/scripts/api/chat.js class ChatAPI { constructor(client) { this.client = client; } async getList(params = {}) { const query = new URLSearchParams(params).toString(); return this.client.get(`/chat/list?${query}`); } async create(chatData) { return this.client.post('/chat/create', chatData); } async get(id) { return this.client.get(`/chat/${id}`); } async update(id, chatData) { return this.client.put(`/chat/${id}`, chatData); } async delete(id) { return this.client.delete(`/chat/${id}`); } async getMessages(chatId, params = {}) { const query = new URLSearchParams(params).toString(); return this.client.get(`/chat/${chatId}/messages?${query}`); } async sendMessage(chatId, content, options = {}) { return this.client.post(`/chat/${chatId}/send`, { content, ...options }); } async regenerateMessage(chatId, messageId) { return this.client.post(`/chat/${chatId}/regenerate/${messageId}`); } async editMessage(chatId, messageId, content) { return this.client.put(`/chat/${chatId}/message/${messageId}`, { content }); } async deleteMessage(chatId, messageId) { return this.client.delete(`/chat/${chatId}/message/${messageId}`); } } const chatAPI = new ChatAPI(apiClient); ``` #### 5.1.4 WebSocket 客户端 ```javascript // public/scripts/utils/websocket.js class WebSocketClient { constructor(url) { this.url = url || 'ws://localhost:8888/ws'; this.ws = null; this.reconnectInterval = 5000; this.listeners = new Map(); } connect(token) { const wsUrl = `${this.url}?token=${token}`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket 连接成功'); this.emit('connected'); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.emit(message.type, message.payload); } catch (error) { console.error('WebSocket 消息解析失败:', error); } }; this.ws.onerror = (error) => { console.error('WebSocket 错误:', error); this.emit('error', error); }; this.ws.onclose = () => { console.log('WebSocket 连接关闭'); this.emit('disconnected'); // 自动重连 setTimeout(() => { this.connect(token); }, this.reconnectInterval); }; } send(type, payload) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, payload })); } } on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); } off(event, callback) { if (this.listeners.has(event)) { const callbacks = this.listeners.get(event); const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } } } emit(event, data) { if (this.listeners.has(event)) { this.listeners.get(event).forEach(callback => { callback(data); }); } } disconnect() { if (this.ws) { this.ws.close(); } } } const wsClient = new WebSocketClient(); ``` #### 5.1.5 环境配置 ```javascript // public/scripts/config.js const CONFIG = { // API 基础地址 API_BASE_URL: window.location.origin.includes('localhost') ? 'http://localhost:8888' : window.location.origin + '/api', // WebSocket 地址 WS_URL: window.location.origin.includes('localhost') ? 'ws://localhost:8888/ws' : (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + '/ws', // 其他配置... }; ``` ### 5.2 web 管理后台扩展 #### 5.2.1 添加 AI 管理模块 在 `web/src/view/` 下创建新目录: ``` web/src/view/ └── aiManage/ # AI 管理模块 ├── character/ # 角色管理 │ ├── index.vue │ └── components/ │ ├── CharacterForm.vue │ └── CharacterCard.vue ├── chat/ # 对话管理 │ ├── index.vue │ └── components/ │ └── ChatHistory.vue ├── user/ # 用户管理 │ └── index.vue ├── statistics/ # 数据统计 │ └── index.vue └── provider/ # AI提供商配置 └── index.vue ``` #### 5.2.2 添加 API 接口 ```javascript // web/src/api/ai.js import service from '@/utils/request' // 角色管理 export const getCharacterList = (params) => { return service({ url: '/character/list', method: 'get', params }) } export const createCharacter = (data) => { return service({ url: '/character/create', method: 'post', data }) } export const updateCharacter = (id, data) => { return service({ url: `/character/${id}`, method: 'put', data }) } export const deleteCharacter = (id) => { return service({ url: `/character/${id}`, method: 'delete' }) } // 对话管理 export const getChatList = (params) => { return service({ url: '/chat/list', method: 'get', params }) } export const getChatMessages = (chatId, params) => { return service({ url: `/chat/${chatId}/messages`, method: 'get', params }) } // 统计数据 export const getUsageStats = (params) => { return service({ url: '/stats/usage', method: 'get', params }) } ``` #### 5.2.3 添加路由 ```javascript // web/src/router/index.js const aiManageRouter = { path: 'aiManage', name: 'aiManage', component: () => import('@/view/routerHolder.vue'), meta: { title: 'AI管理', icon: 'ai-gva' }, children: [ { path: 'character', name: 'character', component: () => import('@/view/aiManage/character/index.vue'), meta: { title: '角色管理', icon: 'customer-gva' } }, { path: 'chat', name: 'chat', component: () => import('@/view/aiManage/chat/index.vue'), meta: { title: '对话管理', icon: 'customer-gva' } }, { path: 'statistics', name: 'statistics', component: () => import('@/view/aiManage/statistics/index.vue'), meta: { title: '数据统计', icon: 'customer-gva' } } ] } ``` --- ## 六、阶段四:数据迁移 ### 6.1 数据迁移策略 **重要说明**: - 数据迁移目标用户为 `app_users` 表中的前台用户 - 不涉及 `sys_users` 表 - 建议先创建一个测试用的前台用户,然后将数据迁移到该用户下 #### 6.1.1 从文件系统迁移到 PostgreSQL 现有 web-app 使用文件系统存储数据,需要编写迁移脚本。 ```go // server/utils/migrate/migrate_sillytavern.go package migrate import ( "encoding/json" "io/ioutil" "path/filepath" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/app" ) type SillyTavernMigrator struct { dataPath string // SillyTavern 数据目录路径 } func NewSillyTavernMigrator(dataPath string) *SillyTavernMigrator { return &SillyTavernMigrator{dataPath: dataPath} } // MigrateCharacters 迁移角色数据 func (m *SillyTavernMigrator) MigrateCharacters(userID uint) error { charactersPath := filepath.Join(m.dataPath, "characters") files, err := ioutil.ReadDir(charactersPath) if err != nil { return err } for _, file := range files { if filepath.Ext(file.Name()) == ".json" { // 读取角色文件 data, err := ioutil.ReadFile(filepath.Join(charactersPath, file.Name())) if err != nil { global.GVA_LOG.Error("读取角色文件失败: " + err.Error()) continue } // 解析角色数据 var cardData map[string]interface{} if err := json.Unmarshal(data, &cardData); err != nil { global.GVA_LOG.Error("解析角色数据失败: " + err.Error()) continue } // 创建角色记录 character := app.Character{ Name: cardData["name"].(string), Description: cardData["description"].(string), Personality: cardData["personality"].(string), Scenario: cardData["scenario"].(string), FirstMessage: cardData["first_mes"].(string), CreatorID: &userID, CardData: cardData, Visibility: "private", } if err := global.GVA_DB.Create(&character).Error; err != nil { global.GVA_LOG.Error("创建角色失败: " + err.Error()) continue } global.GVA_LOG.Info("迁移角色成功: " + character.Name) } } return nil } // MigrateChats 迁移对话数据 func (m *SillyTavernMigrator) MigrateChats(userID uint) error { chatsPath := filepath.Join(m.dataPath, "chats") files, err := ioutil.ReadDir(chatsPath) if err != nil { return err } for _, file := range files { if filepath.Ext(file.Name()) == ".jsonl" { // 读取对话文件 data, err := ioutil.ReadFile(filepath.Join(chatsPath, file.Name())) if err != nil { global.GVA_LOG.Error("读取对话文件失败: " + err.Error()) continue } // 解析对话数据(JSONL 格式,每行一个消息) lines := strings.Split(string(data), "\n") // 创建对话记录 chat := app.Chat{ Title: "迁移的对话", UserID: userID, ChatType: "single", } if err := global.GVA_DB.Create(&chat).Error; err != nil { global.GVA_LOG.Error("创建对话失败: " + err.Error()) continue } // 迁移消息 for i, line := range lines { if line == "" { continue } var msgData map[string]interface{} if err := json.Unmarshal([]byte(line), &msgData); err != nil { continue } message := app.Message{ ChatID: chat.ID, Content: msgData["mes"].(string), Role: msgData["is_user"].(bool) ? "user" : "assistant", SequenceNumber: i + 1, } if err := global.GVA_DB.Create(&message).Error; err != nil { global.GVA_LOG.Error("创建消息失败: " + err.Error()) } } global.GVA_LOG.Info("迁移对话成功: " + file.Name()) } } return nil } ``` #### 6.1.2 迁移命令 ```go // server/cmd/migrate/main.go package main import ( "flag" "git.echol.cn/loser/st/server/core" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/initialize" "git.echol.cn/loser/st/server/utils/migrate" ) func main() { dataPath := flag.String("data", "", "SillyTavern 数据目录路径") userID := flag.Uint("user", 1, "目标用户ID") flag.Parse() if *dataPath == "" { panic("请指定数据目录路径") } // 初始化系统 global.GVA_VP = core.Viper() global.GVA_LOG = core.Zap() global.GVA_DB = initialize.Gorm() // 执行迁移 migrator := migrate.NewSillyTavernMigrator(*dataPath) if err := migrator.MigrateCharacters(*userID); err != nil { global.GVA_LOG.Error("迁移角色失败: " + err.Error()) } if err := migrator.MigrateChats(*userID); err != nil { global.GVA_LOG.Error("迁移对话失败: " + err.Error()) } global.GVA_LOG.Info("数据迁移完成") } ``` 使用方式: ```bash cd server go run cmd/migrate/main.go --data=/path/to/sillytavern/data --user=1 ``` ### 6.2 文件迁移到对象存储 ```go // server/utils/migrate/migrate_files.go package migrate import ( "io/ioutil" "path/filepath" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/app" "git.echol.cn/loser/st/server/utils/upload" ) type FileMigrator struct { sourcePath string uploader upload.OSS } func NewFileMigrator(sourcePath string) *FileMigrator { return &FileMigrator{ sourcePath: sourcePath, uploader: upload.NewOss(), // 根据配置创建 OSS 上传器 } } // MigrateAvatars 迁移头像文件 func (m *FileMigrator) MigrateAvatars(userID uint) error { avatarsPath := filepath.Join(m.sourcePath, "User Avatars") files, err := ioutil.ReadDir(avatarsPath) if err != nil { return err } for _, file := range files { if file.IsDir() { continue } // 读取文件 filePath := filepath.Join(avatarsPath, file.Name()) data, err := ioutil.ReadFile(filePath) if err != nil { global.GVA_LOG.Error("读取文件失败: " + err.Error()) continue } // 上传到对象存储 uploadPath, uploadURL, err := m.uploader.UploadFile(file.Name(), data) if err != nil { global.GVA_LOG.Error("上传文件失败: " + err.Error()) continue } // 保存文件记录 fileRecord := app.File{ UserID: userID, Filename: file.Name(), OriginalFilename: file.Name(), FileType: "avatar", FileSize: file.Size(), StoragePath: uploadPath, URL: uploadURL, } if err := global.GVA_DB.Create(&fileRecord).Error; err != nil { global.GVA_LOG.Error("保存文件记录失败: " + err.Error()) } global.GVA_LOG.Info("迁移文件成功: " + file.Name()) } return nil } ``` --- ## 七、阶段五:测试与优化 ### 7.1 功能测试清单 #### 7.1.1 用户认证测试 - [ ] 用户注册 - [ ] 用户登录 - [ ] JWT Token 验证 - [ ] 登出功能 - [ ] 密码修改 #### 7.1.2 角色管理测试 - [ ] 创建角色 - [ ] 编辑角色 - [ ] 删除角色 - [ ] 角色列表查询 - [ ] 角色详情查看 - [ ] 导入 Character Card - [ ] 导出 Character Card - [ ] 角色收藏 #### 7.1.3 对话功能测试 - [ ] 创建对话 - [ ] 发送消息 - [ ] 接收 AI 回复 - [ ] 消息流式输出 - [ ] 重新生成消息 - [ ] 编辑消息 - [ ] 删除消息 - [ ] Swipe 功能(消息变体) - [ ] 对话历史加载 - [ ] 群聊功能 #### 7.1.4 AI 服务测试 - [ ] OpenAI 集成 - [ ] Claude 集成 - [ ] Gemini 集成 - [ ] 模型切换 - [ ] 参数配置 - [ ] 错误处理 #### 7.1.5 向量记忆测试 - [ ] 创建记忆 - [ ] 向量搜索 - [ ] 记忆召回 - [ ] 记忆管理 #### 7.1.6 WebSocket 测试 - [ ] 连接建立 - [ ] 消息推送 - [ ] 断线重连 - [ ] 多客户端支持 #### 7.1.7 文件管理测试 - [ ] 文件上传 - [ ] 文件下载 - [ ] 文件删除 - [ ] 对象存储集成 ### 7.2 性能测试 #### 7.2.1 压力测试 使用工具:Apache Bench (ab)、wrk、或 K6 ```bash # 测试登录接口 ab -n 1000 -c 100 -p login.json -T application/json http://localhost:8888/base/login # 测试对话接口 ab -n 500 -c 50 -H "x-token: YOUR_TOKEN" http://localhost:8888/chat/list ``` #### 7.2.2 数据库优化 ```sql -- 创建必要的索引 CREATE INDEX CONCURRENTLY idx_messages_chat_id_sequence ON ai_messages(chat_id, sequence_number); CREATE INDEX CONCURRENTLY idx_characters_user_visibility ON ai_characters(creator_id, visibility) WHERE deleted_at IS NULL; -- 分析表统计信息 ANALYZE ai_messages; ANALYZE ai_characters; ANALYZE ai_chats; -- 查看慢查询 SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10; ``` #### 7.2.3 缓存优化 使用 Redis 缓存热点数据: ```go // server/service/app/character.go func (s *CharacterService) GetCharacter(id uint) (*app.Character, error) { cacheKey := fmt.Sprintf("character:%d", id) // 尝试从缓存获取 if global.GVA_REDIS != nil { cached, err := global.GVA_REDIS.Get(context.Background(), cacheKey).Result() if err == nil { var character app.Character if err := json.Unmarshal([]byte(cached), &character); err == nil { return &character, nil } } } // 从数据库查询 var character app.Character err := global.GVA_DB.First(&character, id).Error if err != nil { return nil, err } // 写入缓存 if global.GVA_REDIS != nil { data, _ := json.Marshal(character) global.GVA_REDIS.Set(context.Background(), cacheKey, data, 30*time.Minute) } return &character, nil } ``` ### 7.3 安全加固 #### 7.3.1 API 限流 ```go // server/middleware/limiter.go package middleware import ( "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "golang.org/x/time/rate" ) // RateLimiter 基于令牌桶的限流中间件 func RateLimiter(r rate.Limit, b int) gin.HandlerFunc { limiter := rate.NewLimiter(r, b) return func(c *gin.Context) { if !limiter.Allow() { c.JSON(429, gin.H{ "code": 429, "msg": "请求过于频繁,请稍后再试", }) c.Abort() return } c.Next() } } // UserRateLimiter 基于用户的限流 func UserRateLimiter(rdb *redis.Client) gin.HandlerFunc { return func(c *gin.Context) { userID := c.GetUint("userID") key := fmt.Sprintf("rate_limit:user:%d", userID) // 使用 Redis 实现滑动窗口限流 // ... c.Next() } } ``` #### 7.3.2 敏感数据加密 ```go // server/utils/crypto/encrypt.go package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "io" ) // EncryptAPIKey 加密 API Key func EncryptAPIKey(plaintext, key string) (string, error) { block, err := aes.NewCipher([]byte(key)) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } // DecryptAPIKey 解密 API Key func DecryptAPIKey(ciphertext, key string) (string, error) { data, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return "", err } block, err := aes.NewCipher([]byte(key)) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonceSize := gcm.NonceSize() nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil) if err != nil { return "", err } return string(plaintext), nil } ``` --- ## 八、阶段六:部署上线 ### 8.1 Docker 部署 #### 8.1.1 Dockerfile ##### Go 后端 Dockerfile ```dockerfile # server/Dockerfile FROM golang:1.24-alpine AS builder WORKDIR /app # 安装依赖 RUN apk add --no-cache git gcc musl-dev # 复制 go.mod 和 go.sum COPY go.mod go.sum ./ RUN go mod download # 复制源代码 COPY . . # 编译 RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server . # 运行阶段 FROM alpine:latest RUN apk --no-cache add ca-certificates tzdata WORKDIR /app # 从构建阶段复制编译好的二进制文件 COPY --from=builder /app/server . COPY --from=builder /app/config.yaml . COPY --from=builder /app/resource ./resource # 设置时区 ENV TZ=Asia/Shanghai EXPOSE 8888 CMD ["./server"] ``` ##### web-app Dockerfile ```dockerfile # web-app/Dockerfile FROM nginx:alpine # 复制静态文件 COPY public /usr/share/nginx/html # 复制 nginx 配置 COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` ##### web 管理后台 Dockerfile ```dockerfile # web/Dockerfile FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段 FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` #### 8.1.2 docker-compose.yml ```yaml # docker-compose.yml version: '3.8' services: postgres: image: pgvector/pgvector:pg16 container_name: st-postgres environment: POSTGRES_DB: st_db POSTGRES_USER: postgres POSTGRES_PASSWORD: your_password TZ: Asia/Shanghai volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: st-redis command: redis-server --appendonly yes volumes: - redis_data:/data ports: - "6379:6379" restart: unless-stopped healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 3 server: build: context: ./server dockerfile: Dockerfile container_name: st-server environment: - TZ=Asia/Shanghai volumes: - ./server/config.yaml:/app/config.yaml - ./server/log:/app/log - ./server/uploads:/app/uploads ports: - "8888:8888" depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped web: build: context: ./web dockerfile: Dockerfile container_name: st-web-admin ports: - "8080:80" restart: unless-stopped web-app: build: context: ./web-app dockerfile: Dockerfile container_name: st-web-app ports: - "8000:80" restart: unless-stopped nginx: image: nginx:alpine container_name: st-nginx volumes: - ./deploy/nginx/nginx.conf:/etc/nginx/nginx.conf - ./deploy/nginx/ssl:/etc/nginx/ssl ports: - "80:80" - "443:443" depends_on: - server - web - web-app restart: unless-stopped volumes: postgres_data: redis_data: ``` #### 8.1.3 Nginx 配置 ```nginx # deploy/nginx/nginx.conf upstream api_server { server server:8888; } upstream admin_web { server web:80; } upstream user_app { server web-app:80; } server { listen 80; server_name yourdomain.com; # 用户前端应用 location / { proxy_pass http://user_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 管理后台 location /admin { proxy_pass http://admin_web; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # API 接口 location /api { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://api_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 超时设置(AI 请求可能较慢) proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; } # WebSocket location /ws { proxy_pass http://api_server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket 超时 proxy_read_timeout 86400s; proxy_send_timeout 86400s; } # 文件上传大小限制 client_max_body_size 100M; } ``` ### 8.2 启动与验证 #### 8.2.1 启动所有服务 ```bash # 构建并启动 docker-compose up -d --build # 查看日志 docker-compose logs -f # 查看服务状态 docker-compose ps ``` #### 8.2.2 初始化数据库 ```bash # 进入 server 容器 docker exec -it st-server sh # 运行数据库初始化(如果有初始化脚本) ./server --init-db ``` #### 8.2.3 验证服务 ```bash # 检查 API curl http://localhost/api/base/captcha # 检查管理后台 curl http://localhost/admin # 检查用户前端 curl http://localhost/ ``` ### 8.3 生产环境配置 #### 8.3.1 环境变量配置 创建 `.env` 文件: ```bash # .env # 数据库配置 POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=st_db POSTGRES_USER=postgres POSTGRES_PASSWORD=your_secure_password # Redis 配置 REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD= # JWT 配置 JWT_SECRET=your_jwt_secret_key_at_least_32_chars # AI API Keys(加密存储) OPENAI_API_KEY= ANTHROPIC_API_KEY= GOOGLE_API_KEY= # 对象存储配置 OSS_TYPE=aliyun # aliyun, tencent, qiniu, minio, local OSS_ENDPOINT= OSS_ACCESS_KEY= OSS_SECRET_KEY= OSS_BUCKET= # 日志级别 LOG_LEVEL=info # 环境 ENVIRONMENT=production ``` #### 8.3.2 SSL 证书配置 ```nginx # HTTPS 配置 server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # ... 其他配置同上 } # HTTP 重定向到 HTTPS server { listen 80; server_name yourdomain.com; return 301 https://$server_name$request_uri; } ``` --- ## 九、后续优化建议 ### 9.1 性能优化 1. **数据库连接池优化** - 调整 `max-open-conns` 和 `max-idle-conns` - 监控连接池使用情况 2. **Redis 缓存策略** - 角色数据缓存 - 对话历史缓存 - 用户配置缓存 3. **CDN 加速** - 静态资源使用 CDN - 图片压缩与优化 4. **数据库分表分库** - 消息表按月分表 - 使用分区表优化查询 ### 9.2 功能扩展 1. **移动端支持** - 响应式设计优化 - PWA 支持 - 移动端 APP 2. **高级功能** - 语音输入输出 - 图片生成集成 - 多模态支持 3. **社交功能** - 角色分享 - 社区讨论 - 排行榜 ### 9.3 监控与运维 1. **应用监控** - Prometheus + Grafana - 日志聚合(ELK) - 告警系统 2. **备份策略** - 数据库定时备份 - 对象存储备份 - 灾难恢复计划 --- ## 十、时间规划与里程碑 ### 10.1 预估时间表 | 阶段 | 任务 | 预估时间 | 负责人 | |------|------|----------|--------| | 阶段一 | 数据库设计 | 3-5天 | 后端开发 | | 阶段二 | Go后端API开发 | 15-20天 | 后端开发 | | 阶段三 | 前端改造 | 10-15天 | 前端开发 | | 阶段四 | 数据迁移 | 3-5天 | 后端开发 | | 阶段五 | 测试与优化 | 7-10天 | 全体 | | 阶段六 | 部署上线 | 2-3天 | 运维 | | **总计** | | **40-58天** | | ### 10.2 关键里程碑 - **M1**: 数据库设计完成并评审通过 - **M2**: 核心 API(角色、对话)开发完成 - **M3**: web-app 前端改造完成 - **M4**: 数据迁移脚本测试通过 - **M5**: 全功能测试通过 - **M6**: 生产环境部署成功 --- ## 十一、风险与应对 ### 11.1 技术风险 | 风险 | 影响 | 概率 | 应对措施 | |------|------|------|----------| | AI SDK 兼容性问题 | 高 | 中 | 提前调研,准备备选方案 | | 向量数据库性能 | 中 | 低 | 性能测试,优化索引 | | WebSocket 稳定性 | 高 | 低 | 完善重连机制,监控 | | 数据迁移数据丢失 | 高 | 低 | 充分备份,分批迁移 | ### 11.2 进度风险 | 风险 | 应对措施 | |------|----------| | 开发人员不足 | 优先开发核心功能,延后次要功能 | | 技术难点耗时 | 预留 buffer 时间,及时调整计划 | | 测试不充分 | 增加自动化测试,提前介入测试 | --- ## 十二、重要提醒:不修改清单 ### 12.1 后端 - 保持不变的部分 #### 目录结构(不修改) ``` server/ ├── api/v1/system/ ❌ 不修改 ├── model/system/ ❌ 不修改 ├── service/system/ ❌ 不修改 ├── router/system/ ❌ 不修改 ├── initialize/ ⚠️ 仅添加新的初始化逻辑,不修改现有代码 ├── middleware/ ⚠️ 仅添加 AppJWTAuth,不修改现有中间件 └── config/ ⚠️ 可能需要添加配置,但不修改现有配置 ``` #### 数据表(不修改) - `sys_users` - 管理员用户表 - `sys_authorities` - 角色表 - `sys_base_menus` - 菜单表 - `sys_apis` - API表 - `casbin_rule` - 权限规则表 - `sys_operation_records` - 操作日志表 - 其他所有 `sys_*` 开头的表 #### API路由(不修改) - `/base/*` - 基础接口 - `/user/*` - 用户管理 - `/authority/*` - 角色管理 - `/menu/*` - 菜单管理 - `/api/*` - API管理 - 其他所有管理后台相关路由 ### 12.2 新增的部分 #### 目录结构(新增) ``` server/ ├── api/v1/app/ ✅ 新增 - 前台应用接口 ├── model/app/ ✅ 新增 - 前台应用模型 ├── service/app/ ✅ 新增 - 前台应用服务 ├── router/app/ ✅ 新增 - 前台应用路由 ├── middleware/ │ └── app_jwt.go ✅ 新增 - 前台JWT中间件 └── pkg/ ✅ 新增 - 公共包(AI SDK等) ``` #### 数据表(新增) - `app_users` - 前台用户表 - `app_user_sessions` - 前台用户会话表 - `ai_characters` - AI角色表 - `ai_chats` - 对话表 - `ai_messages` - 消息表 - `ai_memory_vectors` - 向量记忆表 - 其他所有 `app_*` 和 `ai_*` 开头的表 #### API路由(新增) - `/app/auth/*` - 前台用户认证 - `/app/character/*` - 角色管理 - `/app/chat/*` - 对话管理 - `/app/provider/*` - AI提供商配置 - 其他所有 `/app/*` 开头的路由 ### 12.3 开发检查清单 在开发过程中,请确保: - [ ] 没有修改 `server/api/v1/system/` 下的任何文件 - [ ] 没有修改 `server/model/system/` 下的任何文件 - [ ] 没有修改 `server/service/system/` 下的任何文件 - [ ] 没有修改 `server/router/system/` 下的任何文件 - [ ] 没有修改 `sys_users` 表结构 - [ ] 所有新功能都在 `app/` 目录下实现 - [ ] 所有新数据表都使用 `app_` 或 `ai_` 前缀 - [ ] 所有新路由都使用 `/app/` 前缀 - [ ] 前台用户使用独立的 JWT 认证 --- ## 十三、总结 本重构方案将云酒馆项目从 Node.js 一体化架构重构为 Go + PostgreSQL 的前后端分离架构,主要收益包括: 1. **性能提升**: Go 的高并发能力,PostgreSQL 的稳定性 2. **架构清晰**: 前后端分离,职责明确,双用户体系隔离 3. **易于维护**: 统一的后端服务,便于扩展 4. **功能增强**: 向量数据库支持长期记忆,WebSocket 实时通信 5. **部署灵活**: Docker 容器化,易于部署和扩展 6. **风险可控**: 不修改现有 system 模块,新功能独立开发 **关键设计特点**: - ✅ 双用户体系:`sys_users`(管理员)和 `app_users`(前台用户)完全独立 - ✅ 模块隔离:`system/` 和 `app/` 模块并行开发,互不影响 - ✅ 路由分离:管理后台和前台应用使用不同的路由前缀 - ✅ 数据隔离:使用不同的数据表前缀,避免冲突 通过分阶段实施,可以降低风险,确保项目平稳过渡。 --- **文档版本**: v1.0.0 **创建日期**: 2026-02-10 **维护者**: 开发团队 **下次更新**: 根据实施进度更新