Files
st/docs/重构实施方案.md

80 KiB
Raw Permalink Blame History

云酒馆项目重构实施方案

文档说明

本文档详细描述了将云酒馆项目从 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+
  • 向量扩展: pgvectorPostgreSQL扩展
  • 缓存: 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用户, assistantAI, 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 InfoCREATE 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

重要说明

  1. 所有 system/ 目录下的代码保持不变
  2. 所有新增功能都在 app/ 目录下实现
  3. pkg/ 目录下的公共包可以被 systemapp 共同使用
  4. 两个模块使用不同的数据表,互不干扰

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.js
  • src/ 目录(所有后端代码)
  • 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 性能优化

  1. 数据库连接池优化

    • 调整 max-open-connsmax-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
维护者: 开发团队
下次更新: 根据实施进度更新