80 KiB
80 KiB
云酒馆项目重构实施方案
文档说明
本文档详细描述了将云酒馆项目从 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 重构原则
⚠️ 重要约束:
- 不修改现有 system 模块:所有管理后台相关的代码(api/v1/system、service/system、model/system 等)保持不变
- 不修改 sys_users 表:管理员用户体系保持独立
- 新增 app 模块:所有前台相关功能在新的 app 模块中实现
- 并行开发:system 和 app 模块互不干扰,可以独立开发和部署
- 共享基础设施:数据库连接、Redis、对象存储等基础设施共享
1.3 双用户体系设计
本项目采用双用户体系,将管理员和前台用户完全分离:
用户体系对比
| 特性 | 管理后台用户(sys_users) | 前台应用用户(app_users) |
|---|---|---|
| 用途 | 系统管理、数据管理 | AI对话、角色管理 |
| 数据表 | sys_users |
app_users |
| 认证方式 | 原有 JWT | 独立 JWT(带UserType标识) |
| 中间件 | JWTAuth() |
AppJWTAuth() |
| 路由前缀 | /base, /user, /authority 等 |
/app/* |
| 模块目录 | system/ |
app/ |
| 是否修改 | ❌ 不修改 | ✅ 新建 |
设计优势
- 完全隔离:两套用户体系互不干扰,降低风险
- 独立扩展:前台功能可独立开发、测试、部署
- 权限清晰:管理员和普通用户权限分离
- 数据安全:管理后台数据不受前台影响
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 扩展
-- 安装 pgvector 扩展(用于向量存储)
CREATE EXTENSION IF NOT EXISTS vector;
3.1.2 核心表结构设计
说明:
sys_users表保持不变,仅用于管理后台用户(管理员)- 新建
app_users表,用于前台用户(普通用户) - 两套用户体系完全独立,互不干扰
1. 前台用户相关表
-- 前台用户表(新建,与 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 角色相关表
-- 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. 对话相关表
-- 对话表
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. 群聊相关表
-- 群聊成员表
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. 向量记忆表
-- 向量记忆表(使用 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 服务配置表
-- 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. 文件管理表
-- 文件表
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. 预设与设置表
-- 对话预设表
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. 系统设置与日志表
-- 系统配置表(扩展现有)
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/下创建
示例:前台用户模型
// 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
重要说明:
- 所有
system/目录下的代码保持不变 - 所有新增功能都在
app/目录下实现 pkg/目录下的公共包可以被system和app共同使用- 两个模块使用不同的数据表,互不干扰
4.2 双用户体系设计
4.2.1 用户类型区分
// server/global/constants.go (新增常量)
package global
const (
UserTypeSystem = "system" // 管理后台用户
UserTypeApp = "app" // 前台应用用户
)
4.2.2 前台用户 JWT 中间件
// 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 中添加以下依赖:
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
// 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
// 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
// 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
// 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 集成
// 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 向量嵌入服务
// 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 实现
// 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 前台用户认证路由
// 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 角色管理路由
// 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 路由入口注册
// server/router/app/enter.go
package app
type RouterGroup struct {
AuthRouter
CharacterRouter
ChatRouter
MessageRouter
ProviderRouter
FileRouter
}
4.6.4 主路由文件集成
// 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.jssrc/目录(所有后端代码)webpack.config.js(如果不需要打包)
5.1.3 API 客户端实现
// 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();
// 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);
// 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 客户端
// 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 环境配置
// 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 接口
// 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 添加路由
// 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 使用文件系统存储数据,需要编写迁移脚本。
// 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 迁移命令
// 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("数据迁移完成")
}
使用方式:
cd server
go run cmd/migrate/main.go --data=/path/to/sillytavern/data --user=1
6.2 文件迁移到对象存储
// 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
# 测试登录接口
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 数据库优化
-- 创建必要的索引
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 缓存热点数据:
// 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 限流
// 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 敏感数据加密
// 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
# 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
# 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
# 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
# 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 配置
# 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 启动所有服务
# 构建并启动
docker-compose up -d --build
# 查看日志
docker-compose logs -f
# 查看服务状态
docker-compose ps
8.2.2 初始化数据库
# 进入 server 容器
docker exec -it st-server sh
# 运行数据库初始化(如果有初始化脚本)
./server --init-db
8.2.3 验证服务
# 检查 API
curl http://localhost/api/base/captcha
# 检查管理后台
curl http://localhost/admin
# 检查用户前端
curl http://localhost/
8.3 生产环境配置
8.3.1 环境变量配置
创建 .env 文件:
# .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 证书配置
# 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 性能优化
-
数据库连接池优化
- 调整
max-open-conns和max-idle-conns - 监控连接池使用情况
- 调整
-
Redis 缓存策略
- 角色数据缓存
- 对话历史缓存
- 用户配置缓存
-
CDN 加速
- 静态资源使用 CDN
- 图片压缩与优化
-
数据库分表分库
- 消息表按月分表
- 使用分区表优化查询
9.2 功能扩展
-
移动端支持
- 响应式设计优化
- PWA 支持
- 移动端 APP
-
高级功能
- 语音输入输出
- 图片生成集成
- 多模态支持
-
社交功能
- 角色分享
- 社区讨论
- 排行榜
9.3 监控与运维
-
应用监控
- Prometheus + Grafana
- 日志聚合(ELK)
- 告警系统
-
备份策略
- 数据库定时备份
- 对象存储备份
- 灾难恢复计划
十、时间规划与里程碑
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 的前后端分离架构,主要收益包括:
- 性能提升: Go 的高并发能力,PostgreSQL 的稳定性
- 架构清晰: 前后端分离,职责明确,双用户体系隔离
- 易于维护: 统一的后端服务,便于扩展
- 功能增强: 向量数据库支持长期记忆,WebSocket 实时通信
- 部署灵活: Docker 容器化,易于部署和扩展
- 风险可控: 不修改现有 system 模块,新功能独立开发
关键设计特点:
- ✅ 双用户体系:
sys_users(管理员)和app_users(前台用户)完全独立 - ✅ 模块隔离:
system/和app/模块并行开发,互不影响 - ✅ 路由分离:管理后台和前台应用使用不同的路由前缀
- ✅ 数据隔离:使用不同的数据表前缀,避免冲突
通过分阶段实施,可以降低风险,确保项目平稳过渡。
文档版本: v1.0.0
创建日期: 2026-02-10
维护者: 开发团队
下次更新: 根据实施进度更新