3078 lines
80 KiB
Markdown
3078 lines
80 KiB
Markdown
# 云酒馆项目重构实施方案
|
||
|
||
## 文档说明
|
||
|
||
本文档详细描述了将云酒馆项目从 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
|
||
**维护者**: 开发团队
|
||
**下次更新**: 根据实施进度更新
|