From 109e92715253c0dac148819c33f19d3ea2e3c039 Mon Sep 17 00:00:00 2001 From: 537yaha <2930134478@qq.com> Date: Tue, 2 Dec 2025 20:03:39 +0800 Subject: [PATCH] v1.0.0 --- .gitignore | 53 + README.md | 337 +++-- backend/controller/admin_controller.go | 268 +++- backend/controller/ai_config_controller.go | 157 +++ backend/controller/conversation_controller.go | 118 +- backend/controller/faq_controller.go | 149 +++ backend/controller/message_controller.go | 128 +- backend/controller/profile_controller.go | 12 +- backend/controller/visitor_controller.go | 35 + backend/infra/storage.go | 47 + backend/main.go | 106 +- backend/models/ai_config.go | 26 + backend/models/faq.go | 17 + backend/models/user.go | 30 +- backend/package-lock.json | 305 +++++ backend/package.json | 6 + backend/repository/ai_config_repository.go | 79 ++ backend/repository/faq_repository.go | 74 ++ backend/repository/message_repository.go | 47 +- backend/repository/user_repository.go | 41 + backend/router/router.go | 33 +- backend/service/ai_config_service.go | 228 ++++ backend/service/ai_provider.go | 262 ++++ backend/service/ai_service.go | 154 +++ backend/service/conversation_service.go | 153 ++- backend/service/faq_service.go | 168 +++ backend/service/message_service.go | 117 +- backend/service/profile_service.go | 16 +- backend/service/types.go | 118 +- backend/service/user_service.go | 257 ++++ backend/service/visitor_service.go | 60 + ...4_1763359157.png => user_4_1764663058.png} | Bin ...5_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes ...2_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes .../23/1763991961_委托服务合同-5000.pdf | Bin 0 -> 214826 bytes .../23/1763992488_委托服务合同-5000.pdf | Bin 0 -> 214826 bytes ...3992539_微信图片_20251120123549_93_228.png | Bin 0 -> 70273 bytes ...7_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes ...6_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes ...5_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes .../uploads/messages/23/1763993203_112048.png | Bin 0 -> 4027 bytes ...5_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes ...8_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes .../uploads/messages/23/1763993260_112048.png | Bin 0 -> 4027 bytes ...5_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes ...039057_c248590134d469ed61a3d722a2e733a.png | Bin 0 -> 531 bytes ...6_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png | Bin 0 -> 21155 bytes ...039089_c248590134d469ed61a3d722a2e733a.png | Bin 0 -> 531 bytes ...039200_c248590134d469ed61a3d722a2e733a.png | Bin 0 -> 531 bytes ...ocial media account featuring the correctly | Bin 0 -> 165376 bytes ...039231_c248590134d469ed61a3d722a2e733a.png | Bin 0 -> 531 bytes .../uploads/messages/23/1764577446_image.png | Bin 0 -> 19798 bytes backend/utils/crypto.go | 118 ++ backend/websocket/client.go | 6 +- backend/websocket/handler.go | 13 +- backend/websocket/hub.go | 42 +- frontend/README-配置说明.md | 65 + .../app/agent/chat/[conversationId]/page.tsx | 16 +- frontend/app/agent/conversations/page.tsx | 14 +- frontend/app/agent/faqs/page.tsx | 476 ++++++++ frontend/app/agent/login/page.tsx | 108 ++ frontend/app/agent/settings/page.tsx | 513 ++++++++ frontend/app/agent/users/page.tsx | 667 ++++++++++ frontend/app/chat/page.tsx | 296 +---- frontend/app/globals.css | 147 ++- frontend/app/layout.tsx | 4 +- frontend/app/page.tsx | 557 +++++++-- frontend/components.json | 21 + frontend/components/ScreenshotDisplay.tsx | 64 + frontend/components/dashboard/ChatHeader.tsx | 65 +- .../dashboard/ConversationHeader.tsx | 60 +- .../components/dashboard/ConversationList.tsx | 6 +- .../dashboard/ConversationListItem.tsx | 35 +- .../dashboard/ConversationSearch.tsx | 10 +- .../dashboard/ConversationSidebar.tsx | 8 +- .../components/dashboard/DashboardHeader.tsx | 47 +- .../components/dashboard/DashboardShell.tsx | 224 ++-- .../components/dashboard/MessageInput.tsx | 282 ++++- frontend/components/dashboard/MessageList.tsx | 410 +++++-- .../dashboard/NavigationSidebar.tsx | 262 +++- frontend/components/dashboard/PageWrapper.tsx | 20 + .../components/dashboard/ProfileModal.tsx | 116 +- .../dashboard/VisitorDetailPanel.tsx | 171 +-- frontend/components/layout/Footer.tsx | 153 +++ frontend/components/layout/Header.tsx | 99 ++ .../components/layout/ResponsiveLayout.tsx | 121 ++ frontend/components/layout/index.ts | 6 + frontend/components/ui/badge.tsx | 37 + frontend/components/ui/button.tsx | 57 + frontend/components/ui/card.tsx | 80 ++ frontend/components/ui/checkbox.tsx | 31 + frontend/components/ui/dialog.tsx | 121 ++ frontend/components/ui/input.tsx | 26 + frontend/components/ui/label.tsx | 27 + frontend/components/ui/separator.tsx | 30 + frontend/components/ui/sheet.tsx | 145 +++ frontend/components/ui/tabs.tsx | 120 ++ frontend/components/ui/textarea.tsx | 25 + .../components/visitor/ChatModeSelector.tsx | 156 +++ frontend/components/visitor/ChatWidget.tsx | 611 +++++++++ .../components/visitor/FloatingButton.tsx | 67 + .../components/visitor/OnlineAgentsList.tsx | 72 ++ .../features/agent/hooks/useConversations.ts | 120 +- frontend/features/agent/hooks/useMessages.ts | 150 ++- frontend/features/agent/hooks/useWebSocket.ts | 7 +- .../features/agent/services/aiConfigApi.ts | 131 ++ .../agent/services/conversationApi.ts | 24 +- frontend/features/agent/services/faqApi.ts | 115 ++ .../features/agent/services/messageApi.ts | 77 +- .../features/agent/services/profileApi.ts | 4 + frontend/features/agent/services/userApi.ts | 189 +++ frontend/features/agent/types.ts | 9 + .../visitor/services/conversationApi.ts | 4 + .../features/visitor/services/visitorApi.ts | 35 + frontend/lib/config.ts | 11 + frontend/lib/constants/breakpoints.ts | 33 + frontend/lib/utils.ts | 11 + frontend/lib/website-config.ts | 35 + frontend/lib/websocket.ts | 9 +- frontend/next.config.ts | 23 +- frontend/package-lock.json | 1087 ++++++++++++++++- frontend/package.json | 22 +- .../public/images/screenshots/ai-config.png | Bin 0 -> 82286 bytes .../public/images/screenshots/dashboard.png | Bin 0 -> 127411 bytes frontend/public/images/screenshots/faq.png | Bin 0 -> 73744 bytes frontend/public/images/screenshots/users.png | Bin 0 -> 69730 bytes .../public/images/screenshots/visitor.png | Bin 0 -> 196117 bytes .../public/images/screenshots/如何添加截图.md | 43 + frontend/public/widget.js | 189 +++ scripts/deploy-server.sh | 116 ++ 130 files changed, 11596 insertions(+), 1276 deletions(-) create mode 100644 backend/controller/ai_config_controller.go create mode 100644 backend/controller/faq_controller.go create mode 100644 backend/controller/visitor_controller.go create mode 100644 backend/models/ai_config.go create mode 100644 backend/models/faq.go create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/repository/ai_config_repository.go create mode 100644 backend/repository/faq_repository.go create mode 100644 backend/service/ai_config_service.go create mode 100644 backend/service/ai_provider.go create mode 100644 backend/service/ai_service.go create mode 100644 backend/service/faq_service.go create mode 100644 backend/service/user_service.go create mode 100644 backend/service/visitor_service.go rename backend/uploads/avatars/{user_4_1763359157.png => user_4_1764663058.png} (100%) create mode 100644 backend/uploads/messages/23/1763991925_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1763991942_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1763991961_委托服务合同-5000.pdf create mode 100644 backend/uploads/messages/23/1763992488_委托服务合同-5000.pdf create mode 100644 backend/uploads/messages/23/1763992539_微信图片_20251120123549_93_228.png create mode 100644 backend/uploads/messages/23/1763992817_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1763992836_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1763992845_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1763993203_112048.png create mode 100644 backend/uploads/messages/23/1763993225_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1763993238_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1763993260_112048.png create mode 100644 backend/uploads/messages/23/1764039045_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1764039057_c248590134d469ed61a3d722a2e733a.png create mode 100644 backend/uploads/messages/23/1764039076_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png create mode 100644 backend/uploads/messages/23/1764039089_c248590134d469ed61a3d722a2e733a.png create mode 100644 backend/uploads/messages/23/1764039200_c248590134d469ed61a3d722a2e733a.png create mode 100644 backend/uploads/messages/23/1764039214_DALL·E 2024-06-29 16.49.18 - A minimalist avatar for a social media account featuring the correctly create mode 100644 backend/uploads/messages/23/1764039231_c248590134d469ed61a3d722a2e733a.png create mode 100644 backend/uploads/messages/23/1764577446_image.png create mode 100644 backend/utils/crypto.go create mode 100644 frontend/README-配置说明.md create mode 100644 frontend/app/agent/faqs/page.tsx create mode 100644 frontend/app/agent/login/page.tsx create mode 100644 frontend/app/agent/settings/page.tsx create mode 100644 frontend/app/agent/users/page.tsx create mode 100644 frontend/components.json create mode 100644 frontend/components/ScreenshotDisplay.tsx create mode 100644 frontend/components/dashboard/PageWrapper.tsx create mode 100644 frontend/components/layout/Footer.tsx create mode 100644 frontend/components/layout/Header.tsx create mode 100644 frontend/components/layout/ResponsiveLayout.tsx create mode 100644 frontend/components/layout/index.ts create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/checkbox.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/separator.tsx create mode 100644 frontend/components/ui/sheet.tsx create mode 100644 frontend/components/ui/tabs.tsx create mode 100644 frontend/components/ui/textarea.tsx create mode 100644 frontend/components/visitor/ChatModeSelector.tsx create mode 100644 frontend/components/visitor/ChatWidget.tsx create mode 100644 frontend/components/visitor/FloatingButton.tsx create mode 100644 frontend/components/visitor/OnlineAgentsList.tsx create mode 100644 frontend/features/agent/services/aiConfigApi.ts create mode 100644 frontend/features/agent/services/faqApi.ts create mode 100644 frontend/features/agent/services/userApi.ts create mode 100644 frontend/features/visitor/services/visitorApi.ts create mode 100644 frontend/lib/constants/breakpoints.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/lib/website-config.ts create mode 100644 frontend/public/images/screenshots/ai-config.png create mode 100644 frontend/public/images/screenshots/dashboard.png create mode 100644 frontend/public/images/screenshots/faq.png create mode 100644 frontend/public/images/screenshots/users.png create mode 100644 frontend/public/images/screenshots/visitor.png create mode 100644 frontend/public/images/screenshots/如何添加截图.md create mode 100644 frontend/public/widget.js create mode 100644 scripts/deploy-server.sh diff --git a/.gitignore b/.gitignore index e69de29..2206c6b 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,53 @@ +# ---- Go ---- +# 编译后的二进制文件 +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test + +# go build / install 产生的可执行文件 +bin/ +build/ +dist/ + +# Go modules 缓存(不要上传) +vendor/ + +# IDE 临时文件 +*.out + +# ---- Node / Next.js ---- +# 依赖包 +node_modules/ + +# 构建输出目录 +.next/ +out/ + +# 调试日志 +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 环境变量文件(敏感信息) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# 覆盖率结果 +coverage/ + +# Mac / Windows / Linux 系统文件 +.DS_Store +Thumbs.db + +# 编辑器配置 +.vscode/ +.idea/ + +# 文档目录不上传 +doc/ diff --git a/README.md b/README.md index 4b36160..77d2917 100644 --- a/README.md +++ b/README.md @@ -1,209 +1,194 @@ # AI-CS 智能客服系统 -## 项目简介 +> 一个融合 AI 技术与人工客服的现代化智能客服解决方案 -这是一个基于Go后端和Next.js前端的智能客服系统,用于处理访客与客服之间的对话交流。 +## ✨ 核心特性 -## 项目结构 +- 🤖 **AI 客服支持**:支持多厂商 AI 模型,可配置 API 和模型选择 +- 👥 **人工客服**:实时在线状态显示,支持多客服协作 +- 💬 **实时通信**:基于 WebSocket 的双向实时消息推送 +- 📁 **文件传输**:支持图片、文档上传和预览 +- 📚 **FAQ 管理**:知识库管理,关键词搜索 +- 👤 **用户管理**:完整的用户权限管理系统 +- 🎨 **现代化 UI**:基于 Shadcn UI 的响应式设计 +- 🔌 **访客小窗插件**:可嵌入任何网站的客服小窗组件 +- 🌐 **产品官网**:内置产品展示页面 + +## 🏗️ 技术栈 + +### 后端 +- **语言**: Go 1.21+ +- **框架**: Gin (Web 框架) +- **ORM**: GORM +- **数据库**: MySQL 8.0+ +- **实时通信**: WebSocket (gorilla/websocket) +- **密码加密**: bcrypt +- **文件存储**: 本地存储(可扩展为云存储) + +### 前端 +- **框架**: Next.js 14+ (App Router) +- **语言**: TypeScript +- **UI 组件**: Shadcn UI +- **样式**: Tailwind CSS +- **状态管理**: React Hooks +- **实时通信**: WebSocket Client + +## 📁 项目结构 ``` AI-CS/ -├── backend/ # Go后端服务 -│ ├── controller/ # 控制器层 -│ ├── models/ # 数据模型 -│ ├── service/ # 业务逻辑层 -│ ├── repository/ # 数据访问层 -│ ├── middleware/ # 中间件 -│ ├── router/ # 路由配置 -│ ├── infra/ # 基础设施(数据库等) -│ └── utils/ # 工具函数 -└── frontend/ # Next.js前端应用 - ├── app/ # 应用页面 - ├── public/ # 静态资源 - └── ... +├── backend/ # Go 后端服务 +│ ├── controller/ # 控制器层(HTTP 处理) +│ ├── service/ # 业务逻辑层 +│ ├── repository/ # 数据访问层 +│ ├── models/ # 数据模型 +│ ├── router/ # 路由配置 +│ ├── middleware/ # 中间件(认证、CORS、日志) +│ ├── websocket/ # WebSocket Hub +│ ├── infra/ # 基础设施(数据库、存储) +│ ├── utils/ # 工具函数(加密、验证等) +│ └── main.go # 入口文件 +├── frontend/ # Next.js 前端应用 +│ ├── app/ # 页面和路由 +│ │ ├── page.tsx # 官网首页 +│ │ ├── chat/ # 访客聊天页面 +│ │ └── agent/ # 客服工作台 +│ ├── components/ # React 组件 +│ │ ├── ui/ # Shadcn UI 基础组件 +│ │ ├── dashboard/ # 客服端组件 +│ │ ├── visitor/ # 访客端组件 +│ │ └── layout/ # 布局组件 +│ ├── features/ # 功能模块 +│ │ ├── agent/ # 客服端功能 +│ │ └── visitor/ # 访客端功能 +│ └── lib/ # 工具库和配置 +├── doc/ # 项目文档 +│ ├── CHANGELOG.md # 更新日志 +│ ├── 测试指南.md # 测试文档 +│ ├── 后端学习笔记.md # 后端架构说明 +│ └── 前端学习笔记.md # 前端架构说明 +└── README.md # 本文件 ``` -## 核心功能 +## 🚀 快速开始 -### 1. 用户管理 -- **用户注册** (`Register`): 创建新用户账户 -- **用户登录** (`Login`): 验证用户身份 +### 环境要求 -### 2. 对话管理 -- **初始化对话** (`InitConversation`): 为访客创建或获取现有对话 -- **发送消息**: 处理消息发送 -- **拉取消息**: 获取对话历史 +- Go 1.21 或更高版本 +- Node.js 18+ 和 npm/yarn +- MySQL 8.0 或更高版本 -## 数据模型 +### 1. 克隆项目 -### User (用户) -```go -type User struct { - ID uint `json:"id" gorm:"primarykey"` - Username string `json:"username" gorm:"unique"` - Password string `json:"password"` - Role string `json:"role"` -} +```bash +git clone +cd AI-CS ``` -### Conversation (对话) -```go -type Conversation struct { - ID uint `json:"id" gorm:"primaryKey"` - VisitorID uint `json:"visiter_id"` - AgentID uint `json:"agent_id"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} -``` +### 2. 配置后端 -### Message (消息) -```go -type Message struct { - ID uint `json:"id" gorm:"primarykey"` - ConversationID uint `json:"conversation_id"` - SenderID uint `json:"sender_id"` - SenderIsAgent bool `json:"sender_is_agent"` - Content string `json:"content"` - CreatedAt time.Time `json:"created_at"` -} -``` - -## API接口说明 - -### 用户相关接口 - -#### 用户注册 -- **路径**: `POST /register` -- **参数**: - ```json - { - "username": "用户名", - "password": "密码", - "role": "角色" - } - ``` -- **返回**: 注册成功或失败信息 - -#### 用户登录 -- **路径**: `POST /login` -- **参数**: - ```json - { - "username": "用户名", - "password": "密码" - } - ``` -- **返回**: 登录成功或失败信息 - -### 对话相关接口 - -#### 初始化对话 -- **路径**: `POST /conversation/init` -- **参数**: - ```json - { - "visitor_id": 访客ID - } - ``` -- **返回**: - ```json - { - "conversation_id": 对话ID, - "status": "对话状态" - } - ``` - -#### 发送消息 -- **路径**: `POST /messages` -- **参数**: - ```json - { - "conversation_id": 对话ID, - "content": "消息内容", - "sender_is_agent": 是否客服, - "sender_id": 发送者ID(客服必填,访客可省略或传0) - } - ``` -- **返回**: { "message": "创建消息成功" } - -#### 拉取消息 -- **路径**: `GET /messages?conversation_id=对话ID` -- **返回**: 消息数组,按创建时间升序 - -## 对话初始化逻辑详解 - -当你调用对话初始化接口时,系统会执行以下步骤: - -1. **检查现有对话**: 系统会查找该访客是否已有未关闭的对话 -2. **复用或创建**: - - 如果找到现有对话,直接返回该对话信息 - - 如果没有找到,创建一个新的对话并返回 - -这就像你去银行办事: -- 如果之前有没办完的业务,继续办理 -- 如果没有,开一个新的业务单 - -## 技术栈 - -### 后端 -- **语言**: Go -- **框架**: Gin (Web框架) -- **数据库**: GORM (ORM) -- **密码加密**: bcrypt - -### 前端 -- **框架**: Next.js -- **语言**: TypeScript -- **样式**: CSS - -## 开发环境设置 - -### 后端启动 ```bash cd backend -cp .env.example .env # 按需修改数据库配置 -go mod tidy -go run main.go -``` -### 前端启动 -```bash -cd frontend -cp .env.local.example .env.local # 可选:如需自定义 API 地址 -npm install -npm run dev -``` - -## 注意事项 - -1. 确保数据库连接配置正确(后端从环境变量读取) -2. 用户密码会自动加密存储 -3. 对话状态包括: "open"(开放), "closed"(关闭) -4. 消息发送者通过 `SenderIsAgent` 字段区分是访客还是客服 - -### 后端环境变量 -在 `backend/.env` 中配置以下变量(主进程会自动加载): - -``` +# 创建 .env 文件 +cat > .env << EOF DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=your_password -DB_NAME=CS +DB_NAME=ai_cs +EOF + +# 安装依赖 +go mod tidy + +# 启动服务(默认端口 8080) +go run main.go +``` + +### 3. 配置前端 + +```bash +cd frontend + +# 安装依赖 +npm install + +# 启动开发服务器(默认端口 3000) +npm run dev +``` + +### 4. 访问应用 + +- **官网首页**: http://localhost:3000 +- **访客聊天**: http://localhost:3000/chat +- **客服登录**: http://localhost:3000/agent/login + +### 5. 默认账号 + +系统会自动创建默认管理员账号: +- **用户名**: `admin` +- **密码**: `admin123` + +> ⚠️ 生产环境请务必修改默认密码! + +## 📖 主要功能 + +### 访客端 +- 人工/AI 客服模式切换 +- 实时消息收发 +- 文件/图片上传 +- 在线客服列表查看 +- 访客小窗插件(可嵌入第三方网站) + +### 客服端 +- 对话列表管理(全部/我的/他人的对话) +- 实时消息推送 +- 访客信息查看和编辑 +- 在线状态显示 +- 消息已读状态同步 +- AI 配置管理(多厂商支持) +- FAQ 知识库管理 +- 用户权限管理 +- 个人资料管理 + +## ⚙️ 配置说明 + +### 后端环境变量 + +在 `backend/.env` 中配置: + +```env +DB_HOST=localhost # 数据库主机 +DB_PORT=3306 # 数据库端口 +DB_USER=root # 数据库用户名 +DB_PASSWORD=your_password # 数据库密码 +DB_NAME=ai_cs # 数据库名称 ``` ### 前端环境变量(可选) -在 `frontend/.env.local` 中配置以下变量(不配置则使用默认值): -``` +在 `frontend/.env.local` 中配置(不配置则使用默认值): + +```env NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8080 ``` -说明:本地开发无需配置,已默认 `http://127.0.0.1:8080`。部署到生产环境时修改为实际后端地址(如 `https://api.yourdomain.com`)。 +> 本地开发无需配置,已默认 `http://127.0.0.1:8080`。生产环境请修改为实际后端地址。 -## 更新日志 -详见 `doc/CHANGELOG.md` 文件 +## 🤝 贡献 +欢迎提交 Issue 和 Pull Request! + +## 📄 许可证 + +[待添加] + +## 🙏 致谢 + +感谢所有为这个项目做出贡献的开发者! + +--- + +**最后更新**: 2025-01-XX diff --git a/backend/controller/admin_controller.go b/backend/controller/admin_controller.go index c9a4fcd..fde77e0 100644 --- a/backend/controller/admin_controller.go +++ b/backend/controller/admin_controller.go @@ -1,7 +1,9 @@ package controller import ( + "log" "net/http" + "strconv" "github.com/2930134478/AI-CS/backend/service" "github.com/gin-gonic/gin" @@ -10,11 +12,49 @@ import ( // AdminController 负责处理管理员相关的 HTTP 请求。 type AdminController struct { authService *service.AuthService + userService *service.UserService } // NewAdminController 创建 AdminController 实例。 -func NewAdminController(authService *service.AuthService) *AdminController { - return &AdminController{authService: authService} +func NewAdminController(authService *service.AuthService, userService *service.UserService) *AdminController { + return &AdminController{ + authService: authService, + userService: userService, + } +} + +// checkAdminPermission 检查当前用户是否是管理员。 +// 暂时从 query 参数获取 current_user_id,后续可以改为从 JWT token 获取。 +func (a *AdminController) checkAdminPermission(c *gin.Context) (uint, bool) { + userIDStr := c.Query("current_user_id") + if userIDStr == "" { + // 也可以从请求头获取 + userIDStr = c.GetHeader("X-Current-User-ID") + } + if userIDStr == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供当前用户ID"}) + return 0, false + } + + userID, err := strconv.ParseUint(userIDStr, 10, 64) + if err != nil || userID == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "用户ID不合法"}) + return 0, false + } + + // 检查用户是否是管理员 + user, err := a.userService.GetUser(uint(userID)) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return 0, false + } + + if user.Role != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "权限不足,只有管理员才能执行此操作"}) + return 0, false + } + + return uint(userID), true } type createAgentRequest struct { @@ -53,3 +93,227 @@ func (a *AdminController) CreateAgent(c *gin.Context) { "role": user.Role, }) } + +// ListUsers 获取所有用户列表。 +func (a *AdminController) ListUsers(c *gin.Context) { + // 检查权限 + currentUserID, ok := a.checkAdminPermission(c) + if !ok { + return + } + _ = currentUserID // 暂时不使用,但保留用于后续日志记录 + + users, err := a.userService.ListUsers() + if err != nil { + log.Printf("❌ 获取用户列表失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户列表失败"}) + return + } + + c.JSON(http.StatusOK, users) +} + +// GetUser 获取用户详情。 +func (a *AdminController) GetUser(c *gin.Context) { + // 检查权限 + currentUserID, ok := a.checkAdminPermission(c) + if !ok { + return + } + _ = currentUserID + + // 获取用户ID + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "用户ID不合法"}) + return + } + + user, err := a.userService.GetUser(uint(id)) + if err != nil { + if err.Error() == "用户不存在" { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + } else { + log.Printf("❌ 获取用户详情失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户详情失败"}) + } + return + } + + c.JSON(http.StatusOK, user) +} + +// CreateUser 处理创建新用户的请求。 +func (a *AdminController) CreateUser(c *gin.Context) { + // 检查权限 + currentUserID, ok := a.checkAdminPermission(c) + if !ok { + return + } + _ = currentUserID + + var req struct { + Username string `json:"username"` + Password string `json:"password"` + Role string `json:"role"` + Nickname *string `json:"nickname"` + Email *string `json:"email"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + user, err := a.userService.CreateUser(service.CreateUserInput{ + Username: req.Username, + Password: req.Password, + Role: req.Role, + Nickname: req.Nickname, + Email: req.Email, + }) + if err != nil { + switch err { + case service.ErrUsernameExists: + c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"}) + default: + log.Printf("❌ 创建用户失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "创建成功", + "user": user, + }) +} + +// UpdateUser 处理更新用户信息的请求。 +func (a *AdminController) UpdateUser(c *gin.Context) { + // 检查权限 + currentUserID, ok := a.checkAdminPermission(c) + if !ok { + return + } + _ = currentUserID + + // 获取用户ID + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "用户ID不合法"}) + return + } + + var req struct { + Role *string `json:"role"` + Nickname *string `json:"nickname"` + Email *string `json:"email"` + ReceiveAIConversations *bool `json:"receive_ai_conversations"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + user, err := a.userService.UpdateUser(service.UpdateUserInput{ + UserID: uint(id), + Role: req.Role, + Nickname: req.Nickname, + Email: req.Email, + ReceiveAIConversations: req.ReceiveAIConversations, + }) + if err != nil { + if err.Error() == "用户不存在" { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + } else { + log.Printf("❌ 更新用户失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "更新成功", + "user": user, + }) +} + +// DeleteUser 处理删除用户的请求。 +func (a *AdminController) DeleteUser(c *gin.Context) { + // 检查权限 + currentUserID, ok := a.checkAdminPermission(c) + if !ok { + return + } + + // 获取用户ID + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "用户ID不合法"}) + return + } + + if err := a.userService.DeleteUser(uint(id), currentUserID); err != nil { + if err.Error() == "用户不存在" { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + } else { + log.Printf("❌ 删除用户失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} + +// UpdateUserPassword 处理更新用户密码的请求。 +func (a *AdminController) UpdateUserPassword(c *gin.Context) { + // 检查权限 + currentUserID, ok := a.checkAdminPermission(c) + if !ok { + return + } + + // 获取用户ID + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "用户ID不合法"}) + return + } + + var req struct { + OldPassword *string `json:"old_password"` + NewPassword string `json:"new_password"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + // 判断是否是管理员修改其他用户密码 + isAdmin := uint(id) != currentUserID + + if err := a.userService.UpdateUserPassword(service.UpdatePasswordInput{ + UserID: uint(id), + OldPassword: req.OldPassword, + NewPassword: req.NewPassword, + IsAdmin: isAdmin, + }); err != nil { + if err.Error() == "用户不存在" { + c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"}) + } else { + log.Printf("❌ 更新密码失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + return + } + + c.JSON(http.StatusOK, gin.H{"message": "密码更新成功"}) +} diff --git a/backend/controller/ai_config_controller.go b/backend/controller/ai_config_controller.go new file mode 100644 index 0000000..1c3c492 --- /dev/null +++ b/backend/controller/ai_config_controller.go @@ -0,0 +1,157 @@ +package controller + +import ( + "net/http" + + "github.com/2930134478/AI-CS/backend/service" + "github.com/gin-gonic/gin" +) + +// AIConfigController 负责处理 AI 配置相关的 HTTP 请求。 +type AIConfigController struct { + aiConfigService *service.AIConfigService +} + +// NewAIConfigController 创建 AI 配置控制器实例。 +func NewAIConfigController(aiConfigService *service.AIConfigService) *AIConfigController { + return &AIConfigController{aiConfigService: aiConfigService} +} + +type createAIConfigRequest struct { + Provider string `json:"provider" binding:"required"` + APIURL string `json:"api_url" binding:"required"` + APIKey string `json:"api_key" binding:"required"` + Model string `json:"model" binding:"required"` + ModelType string `json:"model_type"` + IsActive bool `json:"is_active"` + IsPublic bool `json:"is_public"` // 是否开放给访客使用 + Description string `json:"description"` +} + +type updateAIConfigRequest struct { + Provider *string `json:"provider"` + APIURL *string `json:"api_url"` + APIKey *string `json:"api_key"` + Model *string `json:"model"` + ModelType *string `json:"model_type"` + IsActive *bool `json:"is_active"` + IsPublic *bool `json:"is_public"` // 是否开放给访客使用 + Description *string `json:"description"` +} + +// CreateAIConfig 创建 AI 配置。 +func (a *AIConfigController) CreateAIConfig(c *gin.Context) { + userID, err := parseUintParam(c, "user_id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"}) + return + } + + var req createAIConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + config, err := a.aiConfigService.CreateAIConfig(service.CreateAIConfigInput{ + UserID: uint(userID), + Provider: req.Provider, + APIURL: req.APIURL, + APIKey: req.APIKey, + Model: req.Model, + ModelType: req.ModelType, + IsActive: req.IsActive, + IsPublic: req.IsPublic, + Description: req.Description, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, config) +} + +// GetAIConfig 获取 AI 配置。 +func (a *AIConfigController) GetAIConfig(c *gin.Context) { + id, err := parseUintParam(c, "id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "id 不合法"}) + return + } + + config, err := a.aiConfigService.GetAIConfig(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "AI 配置不存在"}) + return + } + + c.JSON(http.StatusOK, config) +} + +// ListAIConfigs 获取指定用户的所有 AI 配置。 +func (a *AIConfigController) ListAIConfigs(c *gin.Context) { + userID, err := parseUintParam(c, "user_id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"}) + return + } + + configs, err := a.aiConfigService.ListAIConfigs(uint(userID)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"}) + return + } + + c.JSON(http.StatusOK, configs) +} + +// UpdateAIConfig 更新 AI 配置。 +func (a *AIConfigController) UpdateAIConfig(c *gin.Context) { + id, err := parseUintParam(c, "id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "id 不合法"}) + return + } + + var req updateAIConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) + return + } + + config, err := a.aiConfigService.UpdateAIConfig(service.UpdateAIConfigInput{ + ID: uint(id), + Provider: req.Provider, + APIURL: req.APIURL, + APIKey: req.APIKey, + Model: req.Model, + ModelType: req.ModelType, + IsActive: req.IsActive, + IsPublic: req.IsPublic, + Description: req.Description, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, config) +} + +// DeleteAIConfig 删除 AI 配置。 +func (a *AIConfigController) DeleteAIConfig(c *gin.Context) { + id, err := parseUintParam(c, "id") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "id 不合法"}) + return + } + + if err := a.aiConfigService.DeleteAIConfig(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} + diff --git a/backend/controller/conversation_controller.go b/backend/controller/conversation_controller.go index 584ab0b..30f0137 100644 --- a/backend/controller/conversation_controller.go +++ b/backend/controller/conversation_controller.go @@ -2,6 +2,7 @@ package controller import ( "net/http" + "strconv" "github.com/2930134478/AI-CS/backend/service" "github.com/2930134478/AI-CS/backend/utils" @@ -11,20 +12,29 @@ import ( // ConversationController 负责处理会话相关的 HTTP 请求。 type ConversationController struct { conversationService *service.ConversationService + aiConfigService *service.AIConfigService // 用于获取开放的模型列表 } // NewConversationController 创建 ConversationController 实例。 -func NewConversationController(conversationService *service.ConversationService) *ConversationController { - return &ConversationController{conversationService: conversationService} +func NewConversationController( + conversationService *service.ConversationService, + aiConfigService *service.AIConfigService, +) *ConversationController { + return &ConversationController{ + conversationService: conversationService, + aiConfigService: aiConfigService, + } } type initConversationRequest struct { - VisitorID uint `json:"visitor_id"` - Website string `json:"website"` - Referrer string `json:"referrer"` - Browser string `json:"browser"` - OS string `json:"os"` - Language string `json:"language"` + VisitorID uint `json:"visitor_id"` + Website string `json:"website"` + Referrer string `json:"referrer"` + Browser string `json:"browser"` + OS string `json:"os"` + Language string `json:"language"` + ChatMode string `json:"chat_mode"` // 对话模式:human(人工客服)、ai(AI客服) + AIConfigID *uint `json:"ai_config_id"` // AI 配置 ID(访客选择的模型配置,AI 模式时必需) } type updateContactRequest struct { @@ -54,17 +64,19 @@ func (cc *ConversationController) InitConversation(c *gin.Context) { } result, err := cc.conversationService.InitConversation(service.InitConversationInput{ - VisitorID: req.VisitorID, - Website: req.Website, - Referrer: req.Referrer, - Browser: browser, - OS: os, - Language: req.Language, - IPAddress: utils.GetClientIP(c), + VisitorID: req.VisitorID, + Website: req.Website, + Referrer: req.Referrer, + Browser: browser, + OS: os, + Language: req.Language, + IPAddress: utils.GetClientIP(c), + ChatMode: req.ChatMode, + AIConfigID: req.AIConfigID, }) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "创建对话失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -74,6 +86,17 @@ func (cc *ConversationController) InitConversation(c *gin.Context) { }) } +// GetPublicAIModels 获取所有开放的模型配置(供访客选择)。 +func (cc *ConversationController) GetPublicAIModels(c *gin.Context) { + modelType := c.DefaultQuery("model_type", "text") + models, err := cc.aiConfigService.GetPublicModels(modelType) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"models": models}) +} + // UpdateContactInfo 用于更新访客的联系信息。 func (cc *ConversationController) UpdateContactInfo(c *gin.Context) { id, err := parseUintParam(c, "id") @@ -117,7 +140,16 @@ func (cc *ConversationController) UpdateContactInfo(c *gin.Context) { // ListConversations 返回当前活跃会话的列表。 func (cc *ConversationController) ListConversations(c *gin.Context) { - conversations, err := cc.conversationService.ListConversations() + // 从查询参数获取 user_id(可选) + var userID uint + if userIDStr := c.Query("user_id"); userIDStr != "" { + // 使用 strconv 解析查询参数(不是路径参数) + if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil { + userID = uint(parsed) + } + } + + conversations, err := cc.conversationService.ListConversations(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询对话列表失败"}) return @@ -126,13 +158,14 @@ func (cc *ConversationController) ListConversations(c *gin.Context) { items := make([]gin.H, 0, len(conversations)) for _, conv := range conversations { item := gin.H{ - "id": conv.ID, - "visitor_id": conv.VisitorID, - "agent_id": conv.AgentID, - "status": conv.Status, - "created_at": formatTimeValue(conv.CreatedAt), - "updated_at": formatTimeValue(conv.UpdatedAt), - "unread_count": conv.UnreadCount, + "id": conv.ID, + "visitor_id": conv.VisitorID, + "agent_id": conv.AgentID, + "status": conv.Status, + "created_at": formatTimeValue(conv.CreatedAt), + "updated_at": formatTimeValue(conv.UpdatedAt), + "unread_count": conv.UnreadCount, + "has_participated": conv.HasParticipated, // 当前用户是否参与过该会话 } // 添加 last_seen_at 字段(用于判断在线状态) @@ -165,7 +198,16 @@ func (cc *ConversationController) GetConversationDetail(c *gin.Context) { return } - detail, err := cc.conversationService.GetConversationDetail(uint(id)) + // 从查询参数获取 user_id(可选,用于检查参与状态) + var userID uint + if userIDStr := c.Query("user_id"); userIDStr != "" { + // 使用 strconv 解析查询参数(不是路径参数) + if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil { + userID = uint(parsed) + } + } + + detail, err := cc.conversationService.GetConversationDetail(uint(id), userID) if err != nil { if err == service.ErrConversationNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"}) @@ -220,7 +262,16 @@ func (cc *ConversationController) SearchConversations(c *gin.Context) { return } - conversations, err := cc.conversationService.SearchConversations(query) + // 从查询参数获取 user_id(可选,用于检查参与状态) + var userID uint + if userIDStr := c.Query("user_id"); userIDStr != "" { + // 使用 strconv 解析查询参数(不是路径参数) + if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil { + userID = uint(parsed) + } + } + + conversations, err := cc.conversationService.SearchConversations(query, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "搜索失败"}) return @@ -229,13 +280,14 @@ func (cc *ConversationController) SearchConversations(c *gin.Context) { items := make([]gin.H, 0, len(conversations)) for _, conv := range conversations { item := gin.H{ - "id": conv.ID, - "visitor_id": conv.VisitorID, - "agent_id": conv.AgentID, - "status": conv.Status, - "created_at": formatTimeValue(conv.CreatedAt), - "updated_at": formatTimeValue(conv.UpdatedAt), - "unread_count": conv.UnreadCount, + "id": conv.ID, + "visitor_id": conv.VisitorID, + "agent_id": conv.AgentID, + "status": conv.Status, + "created_at": formatTimeValue(conv.CreatedAt), + "updated_at": formatTimeValue(conv.UpdatedAt), + "unread_count": conv.UnreadCount, + "has_participated": conv.HasParticipated, // 当前用户是否参与过该会话 } // 添加 last_seen_at 字段(用于判断在线状态) diff --git a/backend/controller/faq_controller.go b/backend/controller/faq_controller.go new file mode 100644 index 0000000..372d55c --- /dev/null +++ b/backend/controller/faq_controller.go @@ -0,0 +1,149 @@ +package controller + +import ( + "log" + "net/http" + "strconv" + + "github.com/2930134478/AI-CS/backend/service" + "github.com/gin-gonic/gin" +) + +// FAQController 负责处理 FAQ(常见问题)相关的 HTTP 请求。 +type FAQController struct { + faqService *service.FAQService +} + +// NewFAQController 创建 FAQController 实例。 +func NewFAQController(faqService *service.FAQService) *FAQController { + return &FAQController{faqService: faqService} +} + +// ListFAQs 获取 FAQ 列表,支持关键词搜索。 +// GET /faqs?query=openai%api%调用 +func (f *FAQController) ListFAQs(c *gin.Context) { + // 获取查询参数 + query := c.Query("query") + + // 查询 FAQ 列表 + faqs, err := f.faqService.ListFAQs(query) + if err != nil { + log.Printf("查询 FAQ 列表失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "查询 FAQ 列表失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "faqs": faqs, + }) +} + +// GetFAQ 获取 FAQ 详情。 +// GET /faqs/:id +func (f *FAQController) GetFAQ(c *gin.Context) { + // 获取 ID + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "FAQ ID 不合法"}) + return + } + + // 查询 FAQ + faq, err := f.faqService.GetFAQ(uint(id)) + if err != nil { + log.Printf("查询 FAQ 失败: %v", err) + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, faq) +} + +// CreateFAQ 创建新的 FAQ 记录。 +// POST /faqs +func (f *FAQController) CreateFAQ(c *gin.Context) { + var req struct { + Question string `json:"question" binding:"required"` + Answer string `json:"answer" binding:"required"` + Keywords string `json:"keywords"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 创建 FAQ + faq, err := f.faqService.CreateFAQ(service.CreateFAQInput{ + Question: req.Question, + Answer: req.Answer, + Keywords: req.Keywords, + }) + if err != nil { + log.Printf("创建 FAQ 失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, faq) +} + +// UpdateFAQ 更新 FAQ 记录。 +// PUT /faqs/:id +func (f *FAQController) UpdateFAQ(c *gin.Context) { + // 获取 ID + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "FAQ ID 不合法"}) + return + } + + var req struct { + Question *string `json:"question"` + Answer *string `json:"answer"` + Keywords *string `json:"keywords"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 更新 FAQ + faq, err := f.faqService.UpdateFAQ(uint(id), service.UpdateFAQInput{ + Question: req.Question, + Answer: req.Answer, + Keywords: req.Keywords, + }) + if err != nil { + log.Printf("更新 FAQ 失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, faq) +} + +// DeleteFAQ 删除 FAQ 记录。 +// DELETE /faqs/:id +func (f *FAQController) DeleteFAQ(c *gin.Context) { + // 获取 ID + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "FAQ ID 不合法"}) + return + } + + // 删除 FAQ + if err := f.faqService.DeleteFAQ(uint(id)); err != nil { + log.Printf("删除 FAQ 失败: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} + diff --git a/backend/controller/message_controller.go b/backend/controller/message_controller.go index 68e1626..eba80ac 100644 --- a/backend/controller/message_controller.go +++ b/backend/controller/message_controller.go @@ -3,8 +3,11 @@ package controller import ( "log" "net/http" + "path/filepath" "strconv" + "strings" + "github.com/2930134478/AI-CS/backend/infra" "github.com/2930134478/AI-CS/backend/service" "github.com/gin-gonic/gin" ) @@ -12,33 +15,54 @@ import ( // MessageController 负责处理消息相关的 HTTP 请求。 type MessageController struct { messageService *service.MessageService + storageService infra.StorageService } // NewMessageController 创建 MessageController 实例。 -func NewMessageController(messageService *service.MessageService) *MessageController { - return &MessageController{messageService: messageService} +func NewMessageController(messageService *service.MessageService, storageService infra.StorageService) *MessageController { + return &MessageController{ + messageService: messageService, + storageService: storageService, + } } type createMessageRequest struct { - ConversationID uint `json:"conversation_id"` - Content string `json:"content"` - SenderIsAgent bool `json:"sender_is_agent"` - SenderID uint `json:"sender_id"` + ConversationID uint `json:"conversation_id"` + Content string `json:"content"` + SenderIsAgent bool `json:"sender_is_agent"` + SenderID uint `json:"sender_id"` + // 文件相关字段(可选) + FileURL *string `json:"file_url"` + FileType *string `json:"file_type"` + FileName *string `json:"file_name"` + FileSize *int64 `json:"file_size"` + MimeType *string `json:"mime_type"` } // CreateMessage 处理发送消息的请求。 func (mc *MessageController) CreateMessage(c *gin.Context) { var req createMessageRequest - if err := c.ShouldBindJSON(&req); err != nil || req.ConversationID == 0 || req.Content == "" { + if err := c.ShouldBindJSON(&req); err != nil || req.ConversationID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) return } + // 验证:必须有内容或文件 + if req.Content == "" && req.FileURL == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "消息内容或文件不能同时为空"}) + return + } + _, err := mc.messageService.CreateMessage(service.CreateMessageInput{ ConversationID: req.ConversationID, Content: req.Content, SenderID: req.SenderID, SenderIsAgent: req.SenderIsAgent, + FileURL: req.FileURL, + FileType: req.FileType, + FileName: req.FileName, + FileSize: req.FileSize, + MimeType: req.MimeType, }) if err != nil { log.Printf("❌ 创建消息失败: 对话ID=%d, 错误=%v", req.ConversationID, err) @@ -57,6 +81,9 @@ func (mc *MessageController) CreateMessage(c *gin.Context) { } // ListMessages 返回指定会话的消息列表。 +// 查询参数: +// - conversation_id: 会话ID(必需) +// - include_ai_messages: 是否包含 AI 消息(可选,默认 false) func (mc *MessageController) ListMessages(c *gin.Context) { conversationIDStr := c.Query("conversation_id") if conversationIDStr == "" { @@ -70,7 +97,10 @@ func (mc *MessageController) ListMessages(c *gin.Context) { return } - messages, err := mc.messageService.ListMessages(uint(conversationID)) + // 解析 include_ai_messages 参数(默认 false) + includeAIMessages := c.DefaultQuery("include_ai_messages", "false") == "true" + + messages, err := mc.messageService.ListMessages(uint(conversationID), includeAIMessages) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败"}) return @@ -106,3 +136,85 @@ func (mc *MessageController) MarkMessagesRead(c *gin.Context) { "read_at": formatTimeValue(result.ReadAt), }) } + +// UploadFile 处理文件上传请求。 +// 请求格式:multipart/form-data +// - file: 文件内容(必需) +// - conversation_id: 对话ID(可选,用于组织目录) +func (mc *MessageController) UploadFile(c *gin.Context) { + // 解析文件 + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "文件不能为空"}) + return + } + + // 验证文件大小(10MB) + const maxFileSize = 10 * 1024 * 1024 // 10MB + if file.Size > maxFileSize { + c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小超过限制(最大10MB)"}) + return + } + + // 验证文件类型 + ext := strings.ToLower(filepath.Ext(file.Filename)) + allowedExts := map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".png": true, + ".gif": true, + ".webp": true, + ".pdf": true, + ".doc": true, + ".docx": true, + ".txt": true, + } + if !allowedExts[ext] { + c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"}) + return + } + + // 获取对话ID(可选) + var conversationID uint + if conversationIDStr := c.PostForm("conversation_id"); conversationIDStr != "" { + if id, err := strconv.ParseUint(conversationIDStr, 10, 64); err == nil { + conversationID = uint(id) + } + } + + // 打开文件 + src, err := file.Open() + if err != nil { + log.Printf("❌ 打开文件失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "打开文件失败"}) + return + } + defer src.Close() + + // 保存文件 + fileURL, err := mc.storageService.SaveMessageFile(conversationID, src, file.Filename) + if err != nil { + log.Printf("❌ 保存文件失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"}) + return + } + + // 判断文件类型 + fileType := "document" + mimeType := file.Header.Get("Content-Type") + if strings.HasPrefix(mimeType, "image/") { + fileType = "image" + } + + // 返回文件信息 + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "file_url": fileURL, + "file_type": fileType, + "file_name": file.Filename, + "file_size": file.Size, + "mime_type": mimeType, + }, + }) +} diff --git a/backend/controller/profile_controller.go b/backend/controller/profile_controller.go index a050bfd..31f2e00 100644 --- a/backend/controller/profile_controller.go +++ b/backend/controller/profile_controller.go @@ -18,8 +18,9 @@ func NewProfileController(profileService *service.ProfileService) *ProfileContro } type updateProfileRequest struct { - Nickname *string `json:"nickname"` - Email *string `json:"email"` + Nickname *string `json:"nickname"` + Email *string `json:"email"` + ReceiveAIConversations *bool `json:"receive_ai_conversations"` // 是否接收 AI 对话(可选) } // GetProfile 获取当前用户的个人资料。 @@ -56,9 +57,10 @@ func (p *ProfileController) UpdateProfile(c *gin.Context) { } profile, err := p.profileService.UpdateProfile(service.UpdateProfileInput{ - UserID: uint(userID), - Nickname: req.Nickname, - Email: req.Email, + UserID: uint(userID), + Nickname: req.Nickname, + Email: req.Email, + ReceiveAIConversations: req.ReceiveAIConversations, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) diff --git a/backend/controller/visitor_controller.go b/backend/controller/visitor_controller.go new file mode 100644 index 0000000..361ec16 --- /dev/null +++ b/backend/controller/visitor_controller.go @@ -0,0 +1,35 @@ +package controller + +import ( + "net/http" + + "github.com/2930134478/AI-CS/backend/service" + "github.com/gin-gonic/gin" +) + +// VisitorController 负责处理访客相关的 HTTP 请求。 +type VisitorController struct { + visitorService *service.VisitorService +} + +// NewVisitorController 创建 VisitorController 实例。 +func NewVisitorController(visitorService *service.VisitorService) *VisitorController { + return &VisitorController{ + visitorService: visitorService, + } +} + +// GetOnlineAgents 获取在线客服列表(供访客查看)。 +// GET /visitor/online-agents +func (v *VisitorController) GetOnlineAgents(c *gin.Context) { + agents, err := v.visitorService.GetOnlineAgents() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "agents": agents, + }) +} + diff --git a/backend/infra/storage.go b/backend/infra/storage.go index 5eea893..84808ba 100644 --- a/backend/infra/storage.go +++ b/backend/infra/storage.go @@ -12,6 +12,11 @@ import ( type StorageService interface { // SaveAvatar 保存头像文件,返回文件URL SaveAvatar(userID uint, file io.Reader, filename string) (string, error) + // SaveMessageFile 保存消息文件,返回文件URL + // conversationID: 对话ID,用于组织文件目录 + // file: 文件内容 + // filename: 原始文件名 + SaveMessageFile(conversationID uint, file io.Reader, filename string) (string, error) // DeleteFile 删除文件 DeleteFile(fileURL string) error // GetFileURL 获取文件的完整URL @@ -99,6 +104,48 @@ func (s *LocalStorageService) DeleteFile(fileURL string) error { return nil } +// SaveMessageFile 保存消息文件 +func (s *LocalStorageService) SaveMessageFile(conversationID uint, file io.Reader, filename string) (string, error) { + // 获取文件扩展名 + ext := filepath.Ext(filename) + if ext == "" { + ext = ".bin" // 默认扩展名 + } + // 生成唯一文件名:{timestamp}_{原始文件名} + timestamp := time.Now().Unix() + // 清理文件名,移除特殊字符 + safeFilename := filepath.Base(filename) + if len(safeFilename) > 100 { + // 文件名过长,截断 + safeFilename = safeFilename[:100] + } + newFilename := fmt.Sprintf("%d_%s", timestamp, safeFilename) + + // 按对话ID组织目录:messages/{conversationID}/ + messageDir := filepath.Join(s.baseDir, "messages", fmt.Sprintf("%d", conversationID)) + if err := os.MkdirAll(messageDir, 0755); err != nil { + return "", fmt.Errorf("创建消息文件目录失败: %w", err) + } + + filePath := filepath.Join(messageDir, newFilename) + + // 创建文件 + dst, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("创建文件失败: %w", err) + } + defer dst.Close() + + // 复制文件内容 + if _, err := io.Copy(dst, file); err != nil { + return "", fmt.Errorf("保存文件失败: %w", err) + } + + // 返回相对路径(用于构建URL) + relativePath := filepath.Join("messages", fmt.Sprintf("%d", conversationID), newFilename) + return s.GetFileURL(relativePath), nil +} + // GetFileURL 获取文件的完整URL func (s *LocalStorageService) GetFileURL(filePath string) string { // 确保路径使用正斜杠(用于URL) diff --git a/backend/main.go b/backend/main.go index 0a81de7..0aca461 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,6 +4,7 @@ import ( "log" "os" "path/filepath" + "time" "github.com/2930134478/AI-CS/backend/controller" "github.com/2930134478/AI-CS/backend/infra" @@ -81,13 +82,15 @@ func main() { } //根据结构体定义自动创建更新表 - if err := db.AutoMigrate(&models.User{}, &models.Conversation{}, &models.Message{}); err != nil { + if err := db.AutoMigrate(&models.User{}, &models.Conversation{}, &models.Message{}, &models.AIConfig{}, &models.FAQ{}); err != nil { log.Fatalf("自动创建表失败: %v", err) } userRepo := repository.NewUserRepository(db) conversationRepo := repository.NewConversationRepository(db) messageRepo := repository.NewMessageRepository(db) + aiConfigRepo := repository.NewAIConfigRepository(db) + faqRepo := repository.NewFAQRepository(db) // 初始化默认管理员账号(如果不存在) initDefaultAdmin(userRepo) @@ -111,15 +114,19 @@ func main() { // 初始化服务层 authService := service.NewAuthService(userRepo) - conversationService := service.NewConversationService(conversationRepo, messageRepo) + conversationService := service.NewConversationService(conversationRepo, messageRepo, aiConfigRepo, userRepo) profileService := service.NewProfileService(userRepo, storageService) + aiConfigService := service.NewAIConfigService(aiConfigRepo, userRepo) + aiService := service.NewAIService(aiConfigRepo, messageRepo, conversationRepo) + userService := service.NewUserService(userRepo) // 用户管理服务 + faqService := service.NewFAQService(faqRepo) // FAQ 管理服务 // 声明 Hub 变量(用于在回调函数中访问) var wsHub *websocket.Hub // 创建 WebSocket Hub,设置回调函数来处理客户端连接/断开事件 - // 使用闭包来访问 conversationService 和 wsHub - onConnect := func(conversationID uint, isVisitor bool, visitorCount int) { + // 使用闭包来访问 conversationService、messageService、userRepo 和 wsHub + onConnect := func(conversationID uint, isVisitor bool, visitorCount int, agentID uint) { if isVisitor { if err := conversationService.UpdateVisitorOnlineStatus(conversationID, true); err != nil { log.Printf("更新访客在线状态失败: %v", err) @@ -131,6 +138,64 @@ func main() { "is_online": true, "visitor_count": visitorCount, }) + } else if agentID > 0 { + // 客服连接:创建系统消息 "{客服名}加入了会话" + // 但需要检查是否已经存在该客服的加入消息,避免重复创建 + // 获取客服信息 + agent, err := userRepo.GetByID(agentID) + if err != nil { + log.Printf("获取客服信息失败: %v", err) + return + } + // 确定显示名称:优先使用昵称,如果没有则使用用户名 + agentName := agent.Nickname + if agentName == "" { + agentName = agent.Username + } + // 检查是否已经存在该客服的加入消息 + hasJoinMessage, err := messageRepo.HasAgentJoinMessage(conversationID, agentID, agentName) + if err != nil { + log.Printf("检查客服加入消息失败: %v", err) + return + } + // 如果已经存在加入消息,不再创建 + if hasJoinMessage { + log.Printf("客服 %s 已经加入过对话 %d,跳过创建系统消息", agentName, conversationID) + return + } + // 创建系统消息 + // 需要获取对话信息以确定当前模式 + conv, err := conversationRepo.GetByID(conversationID) + if err != nil { + log.Printf("获取对话信息失败: %v", err) + return + } + now := time.Now() + chatMode := conv.ChatMode + if chatMode == "" { + chatMode = "human" // 默认人工模式 + } + systemMessage := &models.Message{ + ConversationID: conversationID, + SenderID: agentID, + SenderIsAgent: true, + Content: agentName + "加入了会话", + MessageType: "system_message", + ChatMode: chatMode, // 记录系统消息发送时的对话模式 + IsRead: true, // 系统消息默认已读 + ReadAt: &now, + } + if err := messageRepo.Create(systemMessage); err != nil { + log.Printf("创建客服加入系统消息失败: %v", err) + return + } + // 延迟一小段时间后广播系统消息,确保客服的 WebSocket 连接已经完全建立 + // 这样可以确保系统消息能够被客服接收到 + go func() { + time.Sleep(100 * time.Millisecond) + wsHub.BroadcastMessage(conversationID, "new_message", systemMessage) + log.Printf("✅ 客服加入系统消息已创建并广播: 对话ID=%d, 客服=%s", conversationID, agentName) + }() } } @@ -161,14 +226,18 @@ func main() { wsHub = websocket.NewHub(onConnect, onDisconnect) go wsHub.Run() // 启动 Hub(在后台运行) - messageService := service.NewMessageService(conversationRepo, messageRepo, wsHub) + messageService := service.NewMessageService(conversationRepo, messageRepo, wsHub, aiService) + visitorService := service.NewVisitorService(userRepo, wsHub) // 初始化控制器 authController := controller.NewAuthController(authService) - conversationController := controller.NewConversationController(conversationService) - messageController := controller.NewMessageController(messageService) - adminController := controller.NewAdminController(authService) + conversationController := controller.NewConversationController(conversationService, aiConfigService) + messageController := controller.NewMessageController(messageService, storageService) + adminController := controller.NewAdminController(authService, userService) profileController := controller.NewProfileController(profileService) + aiConfigController := controller.NewAIConfigController(aiConfigService) + faqController := controller.NewFAQController(faqService) + visitorController := controller.NewVisitorController(visitorService) appRouter.RegisterRoutes( r, @@ -178,6 +247,9 @@ func main() { Message: messageController, Admin: adminController, Profile: profileController, + AIConfig: aiConfigController, + FAQ: faqController, + Visitor: visitorController, }, websocket.HandleWebSocket(wsHub), ) @@ -186,8 +258,20 @@ func main() { // 静态文件路径:/uploads -> backend/uploads r.Static("/uploads", uploadDir) - //启动服务器) - log.Println("🚀 服务器启动成功,监听 :8080") + //启动服务器 + // 监听所有网络接口(0.0.0.0),允许外部设备访问 + // 如果只想本地访问,可以改为 "127.0.0.1:8080" 或 ":8080" + host := os.Getenv("SERVER_HOST") + if host == "" { + host = "0.0.0.0" // 默认监听所有网络接口,允许外部访问 + } + port := os.Getenv("SERVER_PORT") + if port == "" { + port = "8080" + } + addr := host + ":" + port + log.Println("🚀 服务器启动成功,监听 " + addr) log.Println("📡 WebSocket 服务已启动,路径: /ws?conversation_id=<对话ID>") - r.Run(":8080") + log.Println("💡 提示:如需限制为仅本地访问,请设置环境变量 SERVER_HOST=127.0.0.1") + r.Run(addr) } diff --git a/backend/models/ai_config.go b/backend/models/ai_config.go new file mode 100644 index 0000000..e7ed62a --- /dev/null +++ b/backend/models/ai_config.go @@ -0,0 +1,26 @@ +package models + +import ( + "time" +) + +// AIConfig AI 配置模型 +// 支持多种模型类型(文本、图片、语音、视频)和不同的协议路径 +type AIConfig struct { + ID uint `json:"id" gorm:"primaryKey"` + UserID uint `json:"user_id"` // 配置所属的用户(管理员) + Provider string `json:"provider" gorm:"type:varchar(50)"` // 服务提供商(如:openai、claude、custom,仅用于标识) + APIURL string `json:"api_url" gorm:"type:varchar(500)"` // API 地址(支持不同的协议路径) + APIKey string `json:"api_key" gorm:"type:varchar(1000)"` // API Key(加密存储) + Model string `json:"model" gorm:"type:varchar(100)"` // 模型名称(如:gpt-3.5-turbo、gpt-4) + ModelType string `json:"model_type" gorm:"type:varchar(20);default:'text'"` // 模型类型:text、image、audio、video + IsActive bool `json:"is_active" gorm:"default:true"` // 是否启用(服务商级别) + IsPublic bool `json:"is_public" gorm:"default:false"` // 是否开放给访客使用(模型级别) + Description string `json:"description" gorm:"type:varchar(500)"` // 配置描述 + // 可选的适配参数(JSON 格式,用于适配不同服务商的细微差异) + // 例如:{"auth_header": "X-API-Key", "response_path": "data.choices[0].message.content"} + AdapterConfig string `json:"adapter_config" gorm:"type:text"` // 适配器配置(JSON 格式) + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + diff --git a/backend/models/faq.go b/backend/models/faq.go new file mode 100644 index 0000000..322a23c --- /dev/null +++ b/backend/models/faq.go @@ -0,0 +1,17 @@ +package models + +import ( + "time" +) + +// FAQ 常见问题/事件记录模型 +// 用于存储客服常见问题的问答记录 +type FAQ struct { + ID uint `json:"id" gorm:"primarykey"` + Question string `json:"question" gorm:"type:text;not null"` // 问题 + Answer string `json:"answer" gorm:"type:text;not null"` // 答案 + Keywords string `json:"keywords" gorm:"type:varchar(500)"` // 关键词,用逗号或空格分隔,用于搜索 + CreatedAt time.Time `json:"created_at"` // 创建时间 + UpdatedAt time.Time `json:"updated_at"` // 更新时间 +} + diff --git a/backend/models/user.go b/backend/models/user.go index 743dbd8..c9c4c7d 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -5,15 +5,17 @@ import ( ) type User struct { - ID uint `json:"id" gorm:"primarykey"` - Username string `json:"username" gorm:"unique"` - Password string `json:"password"` - Role string `json:"role"` - AvatarURL string `json:"avatar_url" gorm:"type:varchar(500)"` // 头像URL - Nickname string `json:"nickname" gorm:"type:varchar(100)"` // 昵称 - Email string `json:"email" gorm:"type:varchar(255)"` // 邮箱 - CreatedAt time.Time `json:"created_at"` // 创建时间 - UpdatedAt time.Time `json:"updated_at"` // 更新时间 + ID uint `json:"id" gorm:"primarykey"` + Username string `json:"username" gorm:"unique"` + Password string `json:"password"` + Role string `json:"role"` + AvatarURL string `json:"avatar_url" gorm:"type:varchar(500)"` // 头像URL + Nickname string `json:"nickname" gorm:"type:varchar(100)"` // 昵称 + Email string `json:"email" gorm:"type:varchar(255)"` // 邮箱 + // AI 对话接收设置 + ReceiveAIConversations bool `json:"receive_ai_conversations" gorm:"default:true"` // 是否接收 AI 对话(默认接收) + CreatedAt time.Time `json:"created_at"` // 创建时间 + UpdatedAt time.Time `json:"updated_at"` // 更新时间 } type Conversation struct { @@ -37,6 +39,9 @@ type Conversation struct { Notes string `json:"notes" gorm:"type:text"` // 备注 // 在线状态 LastSeenAt *time.Time `json:"last_seen_at"` // 最后活跃时间 + // AI 客服相关 + ChatMode string `json:"chat_mode" gorm:"type:varchar(20);default:'human'"` // 对话模式:human(人工客服)、ai(AI客服) + AIConfigID *uint `json:"ai_config_id"` // AI 配置 ID(访客选择的模型配置) } type Message struct { @@ -46,7 +51,14 @@ type Message struct { SenderIsAgent bool `json:"sender_is_agent"` Content string `json:"content" gorm:"type:text"` MessageType string `json:"message_type" gorm:"type:varchar(20);default:'user_message'"` // 消息类型:user_message, system_message + ChatMode string `json:"chat_mode" gorm:"type:varchar(20);default:'human'"` // 消息发送时的对话模式:human(人工客服)、ai(AI客服) IsRead bool `json:"is_read"` ReadAt *time.Time `json:"read_at"` CreatedAt time.Time `json:"created_at"` + // 文件相关字段(可选) + FileURL *string `json:"file_url" gorm:"type:varchar(500)"` // 文件URL(相对路径或完整URL) + FileType *string `json:"file_type" gorm:"type:varchar(50)"` // 文件类型:image, document + FileName *string `json:"file_name" gorm:"type:varchar(255)"` // 原始文件名 + FileSize *int64 `json:"file_size"` // 文件大小(字节) + MimeType *string `json:"mime_type" gorm:"type:varchar(100)"` // MIME类型(如 image/jpeg) } diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..1d893b1 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,305 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.8" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "peer": true + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..09c65a4 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.8" + } +} diff --git a/backend/repository/ai_config_repository.go b/backend/repository/ai_config_repository.go new file mode 100644 index 0000000..284f649 --- /dev/null +++ b/backend/repository/ai_config_repository.go @@ -0,0 +1,79 @@ +package repository + +import ( + "github.com/2930134478/AI-CS/backend/models" + "gorm.io/gorm" +) + +// AIConfigRepository 封装与 AI 配置相关的数据库操作。 +type AIConfigRepository struct { + db *gorm.DB +} + +// NewAIConfigRepository 创建 AI 配置仓库实例。 +func NewAIConfigRepository(db *gorm.DB) *AIConfigRepository { + return &AIConfigRepository{db: db} +} + +// Create 创建新的 AI 配置记录。 +func (r *AIConfigRepository) Create(config *models.AIConfig) error { + return r.db.Create(config).Error +} + +// GetByID 根据主键查询 AI 配置。 +func (r *AIConfigRepository) GetByID(id uint) (*models.AIConfig, error) { + var config models.AIConfig + if err := r.db.First(&config, id).Error; err != nil { + return nil, err + } + return &config, nil +} + +// GetActiveByUserID 查询指定用户的活跃 AI 配置(按模型类型筛选)。 +func (r *AIConfigRepository) GetActiveByUserID(userID uint, modelType string) (*models.AIConfig, error) { + var config models.AIConfig + query := r.db.Where("user_id = ? AND is_active = ?", userID, true) + if modelType != "" { + query = query.Where("model_type = ?", modelType) + } + if err := query.Order("created_at desc").First(&config).Error; err != nil { + return nil, err + } + return &config, nil +} + +// ListByUserID 查询指定用户的所有 AI 配置。 +func (r *AIConfigRepository) ListByUserID(userID uint) ([]models.AIConfig, error) { + var configs []models.AIConfig + if err := r.db.Where("user_id = ?", userID).Order("created_at desc").Find(&configs).Error; err != nil { + return nil, err + } + return configs, nil +} + +// UpdateFields 更新 AI 配置的指定字段。 +func (r *AIConfigRepository) UpdateFields(id uint, values map[string]interface{}) error { + if len(values) == 0 { + return nil + } + return r.db.Model(&models.AIConfig{}).Where("id = ?", id).Updates(values).Error +} + +// Delete 删除 AI 配置。 +func (r *AIConfigRepository) Delete(id uint) error { + return r.db.Delete(&models.AIConfig{}, id).Error +} + +// ListPublic 查询所有开放的模型配置(供访客选择)。 +func (r *AIConfigRepository) ListPublic(modelType string) ([]models.AIConfig, error) { + var configs []models.AIConfig + query := r.db.Where("is_active = ? AND is_public = ?", true, true) + if modelType != "" { + query = query.Where("model_type = ?", modelType) + } + if err := query.Order("provider, model").Find(&configs).Error; err != nil { + return nil, err + } + return configs, nil +} + diff --git a/backend/repository/faq_repository.go b/backend/repository/faq_repository.go new file mode 100644 index 0000000..cbeee74 --- /dev/null +++ b/backend/repository/faq_repository.go @@ -0,0 +1,74 @@ +package repository + +import ( + "github.com/2930134478/AI-CS/backend/models" + "gorm.io/gorm" +) + +// FAQRepository 封装与 FAQ(常见问题)相关的数据库操作。 +type FAQRepository struct { + db *gorm.DB +} + +// NewFAQRepository 创建 FAQ 仓库实例。 +func NewFAQRepository(db *gorm.DB) *FAQRepository { + return &FAQRepository{db: db} +} + +// Create 创建新的 FAQ 记录。 +func (r *FAQRepository) Create(faq *models.FAQ) error { + return r.db.Create(faq).Error +} + +// GetByID 根据ID查询 FAQ 记录。 +func (r *FAQRepository) GetByID(id uint) (*models.FAQ, error) { + var faq models.FAQ + if err := r.db.Where("id = ?", id).First(&faq).Error; err != nil { + return nil, err + } + return &faq, nil +} + +// List 获取所有 FAQ 列表,支持关键词搜索。 +// 如果 keywords 不为空,会按关键词搜索(AND 查询,所有关键词都要包含)。 +// 搜索范围:问题、答案、关键词字段。 +func (r *FAQRepository) List(keywords []string) ([]models.FAQ, error) { + var faqs []models.FAQ + query := r.db.Model(&models.FAQ{}) + + // 如果有关键词,进行 AND 查询 + // 每个关键词都必须在 question、answer、keywords 字段中至少有一个匹配 + // 但所有关键词都必须被满足(AND 逻辑) + if len(keywords) > 0 { + for _, keyword := range keywords { + if keyword != "" { + // 对于每个关键词,要求在问题、答案、关键词字段中至少有一个包含该关键词(OR) + // 但所有关键词都必须满足(通过链式 Where 实现 AND) + query = query.Where( + "(question LIKE ? OR answer LIKE ? OR keywords LIKE ?)", + "%"+keyword+"%", + "%"+keyword+"%", + "%"+keyword+"%", + ) + } + } + } + + // 按创建时间倒序排列 + if err := query.Order("created_at DESC").Find(&faqs).Error; err != nil { + return nil, err + } + + return faqs, nil +} + +// Update 更新 FAQ 记录。 +func (r *FAQRepository) Update(faq *models.FAQ) error { + return r.db.Save(faq).Error +} + +// Delete 删除 FAQ 记录。 +func (r *FAQRepository) Delete(id uint) error { + return r.db.Delete(&models.FAQ{}, id).Error +} + diff --git a/backend/repository/message_repository.go b/backend/repository/message_repository.go index fb1a632..8d243c7 100644 --- a/backend/repository/message_repository.go +++ b/backend/repository/message_repository.go @@ -90,7 +90,52 @@ func (r *MessageRepository) MarkMessagesRead(conversationID uint, senderIsAgent } remaining, err := r.CountUnreadBySender(conversationID, senderIsAgent) if err != nil { - return nil, 0, time.Time{}, err + return nil, 0, time.Time{}, nil } return messageIDs, remaining, now, nil } + +// HasAgentJoinMessage 检查该对话中是否已经存在该客服的加入消息。 +// 用于避免重复创建"xxx加入了会话"的系统消息。 +func (r *MessageRepository) HasAgentJoinMessage(conversationID uint, agentID uint, agentName string) (bool, error) { + var count int64 + joinMessageContent := agentName + "加入了会话" + if err := r.db.Model(&models.Message{}). + Where("conversation_id = ? AND sender_id = ? AND sender_is_agent = ? AND message_type = ? AND content = ?", + conversationID, agentID, true, "system_message", joinMessageContent). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +// HasVisitorMessageInHumanMode 检查对话中是否有访客在人工模式下发送的消息。 +// 用于判断对话是否应该显示在客服列表中。 +// 只有当 ChatMode == "human" 且存在访客发送的消息时,才应该显示。 +func (r *MessageRepository) HasVisitorMessageInHumanMode(conversationID uint) (bool, error) { + var count int64 + // 查询是否有访客发送的消息(sender_is_agent = false) + // 注意:这里不检查 ChatMode,因为 ChatMode 在 Conversation 表中 + // 这个方法只检查消息是否存在,ChatMode 的检查在 Service 层 + if err := r.db.Model(&models.Message{}). + Where("conversation_id = ? AND sender_is_agent = ?", conversationID, false). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +// HasAgentParticipated 检查指定客服是否在指定会话中发送过消息。 +// 用于判断该会话是否应该出现在该客服的"My chats"列表中。 +func (r *MessageRepository) HasAgentParticipated(conversationID uint, agentID uint) (bool, error) { + var count int64 + // 查询是否有该客服发送的消息(sender_is_agent = true AND sender_id = agentID) + // 注意:系统消息(message_type = 'system_message')也应该算作参与 + // 所以不限制 message_type,包括所有类型的消息 + if err := r.db.Model(&models.Message{}). + Where("conversation_id = ? AND sender_is_agent = ? AND sender_id = ?", conversationID, true, agentID). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} diff --git a/backend/repository/user_repository.go b/backend/repository/user_repository.go index 0bfbe6a..578281d 100644 --- a/backend/repository/user_repository.go +++ b/backend/repository/user_repository.go @@ -42,3 +42,44 @@ func (r *UserRepository) GetByID(id uint) (*models.User, error) { func (r *UserRepository) UpdateFields(id uint, updates map[string]interface{}) error { return r.db.Model(&models.User{}).Where("id = ?", id).Updates(updates).Error } + +// ListUsers 获取所有用户列表。 +func (r *UserRepository) ListUsers() ([]models.User, error) { + var users []models.User + if err := r.db.Order("created_at DESC").Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +// Delete 删除用户。 +func (r *UserRepository) Delete(id uint) error { + return r.db.Delete(&models.User{}, id).Error +} + +// CountByRole 统计指定角色的用户数量。 +func (r *UserRepository) CountByRole(role string) (int64, error) { + var count int64 + if err := r.db.Model(&models.User{}).Where("role = ?", role).Count(&count).Error; err != nil { + return 0, err + } + return count, nil +} + +// FindByIDsAndRole 根据ID列表和角色查询用户。 +func (r *UserRepository) FindByIDsAndRole(ids []uint, role string) ([]models.User, error) { + var users []models.User + if err := r.db.Where("id IN ? AND role = ?", ids, role).Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +// FindByIDsAndRoles 根据ID列表和多个角色查询用户(支持查询多个角色,如 admin 和 agent)。 +func (r *UserRepository) FindByIDsAndRoles(ids []uint, roles []string) ([]models.User, error) { + var users []models.User + if err := r.db.Where("id IN ? AND role IN ?", ids, roles).Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} diff --git a/backend/router/router.go b/backend/router/router.go index cfd7673..5edae75 100644 --- a/backend/router/router.go +++ b/backend/router/router.go @@ -12,6 +12,9 @@ type ControllerSet struct { Message *controller.MessageController Admin *controller.AdminController Profile *controller.ProfileController + AIConfig *controller.AIConfigController + FAQ *controller.FAQController + Visitor *controller.VisitorController } // RegisterRoutes 注册 HTTP 路由及对应的处理函数。 @@ -26,20 +29,46 @@ func RegisterRoutes(r *gin.Engine, controllers ControllerSet, wsHandler gin.Hand r.GET("/conversations/:id", controllers.Conversation.GetConversationDetail) r.PUT("/conversations/:id/contact", controllers.Conversation.UpdateContactInfo) r.GET("/conversations/search", controllers.Conversation.SearchConversations) + r.GET("/conversations/ai-models", controllers.Conversation.GetPublicAIModels) // 获取开放的模型列表(供访客选择) // Message r.POST("/messages", controllers.Message.CreateMessage) + r.POST("/messages/upload", controllers.Message.UploadFile) // 文件上传接口 r.GET("/messages", controllers.Message.ListMessages) r.PUT("/messages/read", controllers.Message.MarkMessagesRead) - // Admin - r.POST("/admin/users", controllers.Admin.CreateAgent) + // Admin(用户管理) + r.GET("/admin/users", controllers.Admin.ListUsers) // 获取所有用户列表 + r.GET("/admin/users/:id", controllers.Admin.GetUser) // 获取用户详情 + r.POST("/admin/users", controllers.Admin.CreateUser) // 创建新用户 + r.PUT("/admin/users/:id", controllers.Admin.UpdateUser) // 更新用户信息 + r.DELETE("/admin/users/:id", controllers.Admin.DeleteUser) // 删除用户 + r.PUT("/admin/users/:id/password", controllers.Admin.UpdateUserPassword) // 更新用户密码 + // 兼容旧接口 + r.POST("/admin/agents", controllers.Admin.CreateAgent) // 创建客服(兼容旧接口) // Profile(个人资料) r.GET("/agent/profile/:user_id", controllers.Profile.GetProfile) r.PUT("/agent/profile/:user_id", controllers.Profile.UpdateProfile) r.POST("/agent/avatar/:user_id", controllers.Profile.UploadAvatar) + // AI Config(AI 配置) + r.POST("/agent/ai-config/:user_id", controllers.AIConfig.CreateAIConfig) + r.GET("/agent/ai-config/:user_id", controllers.AIConfig.ListAIConfigs) + r.GET("/agent/ai-config/:user_id/:id", controllers.AIConfig.GetAIConfig) + r.PUT("/agent/ai-config/:user_id/:id", controllers.AIConfig.UpdateAIConfig) + r.DELETE("/agent/ai-config/:user_id/:id", controllers.AIConfig.DeleteAIConfig) + + // FAQ(事件管理/常见问题) + r.GET("/faqs", controllers.FAQ.ListFAQs) // 获取 FAQ 列表(支持关键词搜索) + r.GET("/faqs/:id", controllers.FAQ.GetFAQ) // 获取 FAQ 详情 + r.POST("/faqs", controllers.FAQ.CreateFAQ) // 创建 FAQ + r.PUT("/faqs/:id", controllers.FAQ.UpdateFAQ) // 更新 FAQ + r.DELETE("/faqs/:id", controllers.FAQ.DeleteFAQ) // 删除 FAQ + + // Visitor(访客相关) + r.GET("/visitor/online-agents", controllers.Visitor.GetOnlineAgents) // 获取在线客服列表 + // WebSocket r.GET("/ws", wsHandler) } diff --git a/backend/service/ai_config_service.go b/backend/service/ai_config_service.go new file mode 100644 index 0000000..d359021 --- /dev/null +++ b/backend/service/ai_config_service.go @@ -0,0 +1,228 @@ +package service + +import ( + "errors" + "fmt" + + "github.com/2930134478/AI-CS/backend/models" + "github.com/2930134478/AI-CS/backend/repository" + "github.com/2930134478/AI-CS/backend/utils" +) + +// AIConfigService AI 配置服务(负责管理 AI 配置) +type AIConfigService struct { + aiConfigRepo *repository.AIConfigRepository + userRepo *repository.UserRepository +} + +// NewAIConfigService 创建 AI 配置服务实例。 +func NewAIConfigService( + aiConfigRepo *repository.AIConfigRepository, + userRepo *repository.UserRepository, +) *AIConfigService { + return &AIConfigService{ + aiConfigRepo: aiConfigRepo, + userRepo: userRepo, + } +} + +// CreateAIConfigInput 创建 AI 配置的输入参数。 +type CreateAIConfigInput struct { + UserID uint + Provider string + APIURL string + APIKey string // 明文 API Key(会被加密存储) + Model string + ModelType string + IsActive bool + IsPublic bool // 是否开放给访客使用 + Description string +} + +// UpdateAIConfigInput 更新 AI 配置的输入参数。 +type UpdateAIConfigInput struct { + ID uint + Provider *string + APIURL *string + APIKey *string // 明文 API Key(如果提供,会被加密存储) + Model *string + ModelType *string + IsActive *bool + IsPublic *bool // 是否开放给访客使用 + Description *string +} + +// AIConfigResult AI 配置返回结果(不包含加密的 API Key)。 +type AIConfigResult struct { + ID uint `json:"id"` + UserID uint `json:"user_id"` + Provider string `json:"provider"` + APIURL string `json:"api_url"` + Model string `json:"model"` + ModelType string `json:"model_type"` + Protocol string `json:"protocol"` + IsActive bool `json:"is_active"` + IsPublic bool `json:"is_public"` + Description string `json:"description"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// CreateAIConfig 创建 AI 配置。 +func (s *AIConfigService) CreateAIConfig(input CreateAIConfigInput) (*AIConfigResult, error) { + // 验证用户是否存在 + _, err := s.userRepo.GetByID(input.UserID) + if err != nil { + return nil, errors.New("用户不存在") + } + + // 验证 API Key 不能为空 + if input.APIKey == "" { + return nil, errors.New("API Key 不能为空") + } + + // 加密 API Key + encryptedKey, err := utils.EncryptAPIKey(input.APIKey) + if err != nil { + return nil, fmt.Errorf("加密 API Key 失败: %v", err) + } + + // 设置默认值 + modelType := input.ModelType + if modelType == "" { + modelType = "text" + } + + // 创建配置 + config := &models.AIConfig{ + UserID: input.UserID, + Provider: input.Provider, + APIURL: input.APIURL, + APIKey: encryptedKey, + Model: input.Model, + ModelType: modelType, + IsActive: input.IsActive, + IsPublic: input.IsPublic, + Description: input.Description, + } + + if err := s.aiConfigRepo.Create(config); err != nil { + return nil, err + } + + return s.toResult(config), nil +} + +// GetAIConfig 获取 AI 配置(不返回加密的 API Key)。 +func (s *AIConfigService) GetAIConfig(id uint) (*AIConfigResult, error) { + config, err := s.aiConfigRepo.GetByID(id) + if err != nil { + return nil, err + } + return s.toResult(config), nil +} + +// ListAIConfigs 获取指定用户的所有 AI 配置。 +func (s *AIConfigService) ListAIConfigs(userID uint) ([]AIConfigResult, error) { + configs, err := s.aiConfigRepo.ListByUserID(userID) + if err != nil { + return nil, err + } + + results := make([]AIConfigResult, 0, len(configs)) + for _, config := range configs { + results = append(results, *s.toResult(&config)) + } + + return results, nil +} + +// UpdateAIConfig 更新 AI 配置。 +func (s *AIConfigService) UpdateAIConfig(input UpdateAIConfigInput) (*AIConfigResult, error) { + // 检查配置是否存在 + _, err := s.aiConfigRepo.GetByID(input.ID) + if err != nil { + return nil, errors.New("AI 配置不存在") + } + + // 构建更新字段 + updates := make(map[string]interface{}) + if input.Provider != nil { + updates["provider"] = *input.Provider + } + if input.APIURL != nil { + updates["api_url"] = *input.APIURL + } + if input.APIKey != nil { + // 验证 API Key 不能为空 + if *input.APIKey == "" { + return nil, errors.New("API Key 不能为空") + } + // 如果提供了新的 API Key,需要加密 + encryptedKey, err := utils.EncryptAPIKey(*input.APIKey) + if err != nil { + return nil, fmt.Errorf("加密 API Key 失败: %v", err) + } + updates["api_key"] = encryptedKey + } + if input.Model != nil { + updates["model"] = *input.Model + } + if input.ModelType != nil { + updates["model_type"] = *input.ModelType + } + if input.IsActive != nil { + updates["is_active"] = *input.IsActive + } + if input.IsPublic != nil { + updates["is_public"] = *input.IsPublic + } + if input.Description != nil { + updates["description"] = *input.Description + } + + if err := s.aiConfigRepo.UpdateFields(input.ID, updates); err != nil { + return nil, err + } + + // 返回更新后的配置 + return s.GetAIConfig(input.ID) +} + +// DeleteAIConfig 删除 AI 配置。 +func (s *AIConfigService) DeleteAIConfig(id uint) error { + return s.aiConfigRepo.Delete(id) +} + +// GetPublicModels 获取所有开放的模型配置(供访客选择)。 +func (s *AIConfigService) GetPublicModels(modelType string) ([]AIConfigResult, error) { + configs, err := s.aiConfigRepo.ListPublic(modelType) + if err != nil { + return nil, err + } + + results := make([]AIConfigResult, 0, len(configs)) + for _, config := range configs { + results = append(results, *s.toResult(&config)) + } + + return results, nil +} + +// toResult 将模型转换为返回结果(不包含加密的 API Key)。 +func (s *AIConfigService) toResult(config *models.AIConfig) *AIConfigResult { + return &AIConfigResult{ + ID: config.ID, + UserID: config.UserID, + Provider: config.Provider, + APIURL: config.APIURL, + Model: config.Model, + ModelType: config.ModelType, + IsActive: config.IsActive, + IsPublic: config.IsPublic, + Description: config.Description, + CreatedAt: config.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: config.UpdatedAt.Format("2006-01-02 15:04:05"), + } +} + diff --git a/backend/service/ai_provider.go b/backend/service/ai_provider.go new file mode 100644 index 0000000..131a89b --- /dev/null +++ b/backend/service/ai_provider.go @@ -0,0 +1,262 @@ +package service + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" +) + +// AIProvider AI 服务提供商接口(可扩展设计) +// 不同的 AI 服务提供商需要实现这个接口 +type AIProvider interface { + // GenerateResponse 生成 AI 回复 + // conversationHistory: 对话历史(用于上下文) + // userMessage: 用户当前消息 + // 返回: AI 回复内容 + GenerateResponse(conversationHistory []MessageHistory, userMessage string) (string, error) +} + +// AdapterConfig 适配器配置(用于适配不同服务商的 API 格式差异) +type AdapterConfig struct { + // 认证头格式(默认:Bearer) + AuthHeader string `json:"auth_header"` // 例如:"Bearer"、"X-API-Key"、"Authorization" + // 响应解析路径(默认:choices[0].message.content) + ResponsePath string `json:"response_path"` // 例如:"choices[0].message.content"、"data.text"、"result.content" + // 请求格式自定义(可选) + RequestFormat map[string]interface{} `json:"request_format"` // 用于覆盖默认的请求格式 +} + +// MessageHistory 对话历史记录 +type MessageHistory struct { + Role string `json:"role"` // "user" 或 "assistant" + Content string `json:"content"` // 消息内容 +} + +// AIConfig 用于 AI 调用的配置信息 +type AIConfig struct { + APIURL string + APIKey string + Model string + ModelType string + Provider string + AdapterConfig *AdapterConfig // 适配器配置(用于适配不同服务商的差异) +} + +// UniversalAIProvider 通用 AI 服务提供商(支持所有 OpenAI 兼容格式) +// 通过适配器配置来适配不同服务商的细微差异 +// 这样 90% 的服务商都可以用同一个 Provider,无需单独实现 +type UniversalAIProvider struct { + config AIConfig + client *http.Client + adapter *AdapterConfig +} + +// NewUniversalAIProvider 创建通用 AI 提供商实例。 +func NewUniversalAIProvider(config AIConfig) *UniversalAIProvider { + // 设置默认适配器配置 + adapter := config.AdapterConfig + if adapter == nil { + adapter = &AdapterConfig{ + AuthHeader: "Bearer", // 默认使用 Bearer Token + ResponsePath: "choices[0].message.content", // 默认 OpenAI 格式 + } + } else { + // 设置默认值 + if adapter.AuthHeader == "" { + adapter.AuthHeader = "Bearer" + } + if adapter.ResponsePath == "" { + adapter.ResponsePath = "choices[0].message.content" + } + } + + return &UniversalAIProvider{ + config: config, + client: &http.Client{ + Timeout: 30 * time.Second, // 30 秒超时 + }, + adapter: adapter, + } +} + +// GenerateResponse 生成 AI 回复(支持 OpenAI 兼容格式,通过适配器适配不同服务商)。 +func (p *UniversalAIProvider) GenerateResponse(conversationHistory []MessageHistory, userMessage string) (string, error) { + // 根据模型类型选择不同的处理逻辑 + switch p.config.ModelType { + case "text": + return p.generateTextResponse(conversationHistory, userMessage) + case "image": + // 图片生成(未来扩展) + return "", fmt.Errorf("图片模型暂未支持") + case "audio": + // 语音识别/合成(未来扩展) + return "", fmt.Errorf("语音模型暂未支持") + case "video": + // 视频生成(未来扩展) + return "", fmt.Errorf("视频模型暂未支持") + default: + return "", fmt.Errorf("不支持的模型类型: %s", p.config.ModelType) + } +} + +// generateTextResponse 生成文本回复(通用实现,支持所有 OpenAI 兼容格式)。 +func (p *UniversalAIProvider) generateTextResponse(conversationHistory []MessageHistory, userMessage string) (string, error) { + // 构建消息列表(包含历史对话和当前消息) + messages := make([]map[string]string, 0) + + // 添加历史对话 + for _, history := range conversationHistory { + messages = append(messages, map[string]string{ + "role": history.Role, + "content": history.Content, + }) + } + + // 添加当前用户消息 + messages = append(messages, map[string]string{ + "role": "user", + "content": userMessage, + }) + + // 构建请求体(OpenAI 兼容格式) + requestBody := map[string]interface{}{ + "model": p.config.Model, + "messages": messages, + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return "", fmt.Errorf("序列化请求失败: %v", err) + } + + // 创建 HTTP 请求 + req, err := http.NewRequest("POST", p.config.APIURL, bytes.NewBuffer(jsonData)) + if err != nil { + return "", fmt.Errorf("创建请求失败: %v", err) + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/json") + + // 根据适配器配置设置认证头 + authValue := p.config.APIKey + if p.adapter.AuthHeader == "Bearer" { + authValue = "Bearer " + p.config.APIKey + req.Header.Set("Authorization", authValue) + } else if p.adapter.AuthHeader == "X-API-Key" { + req.Header.Set("X-API-Key", p.config.APIKey) + } else { + // 默认使用 Authorization: Bearer + req.Header.Set("Authorization", "Bearer "+p.config.APIKey) + } + + // 发送请求 + resp, err := p.client.Do(req) + if err != nil { + return "", fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + // 读取响应 + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %v", err) + } + + // 检查 HTTP 状态码 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("API 返回错误: %s (状态码: %d)", string(body), resp.StatusCode) + } + + // 解析响应(支持灵活的响应路径) + var responseData map[string]interface{} + if err := json.Unmarshal(body, &responseData); err != nil { + return "", fmt.Errorf("解析响应失败: %v", err) + } + + // 检查是否有错误字段 + if errorMsg, ok := responseData["error"].(map[string]interface{}); ok { + if msg, ok := errorMsg["message"].(string); ok { + return "", fmt.Errorf("API 错误: %s", msg) + } + } + + // 根据适配器配置的响应路径提取内容 + content, err := p.extractResponseContent(responseData, p.adapter.ResponsePath) + if err != nil { + return "", err + } + + if content == "" { + return "", errors.New("API 返回空内容") + } + + return content, nil +} + +// extractResponseContent 根据响应路径提取内容(支持灵活的路径配置)。 +// 例如:"choices[0].message.content" 或 "data.text" 或 "result.content" +func (p *UniversalAIProvider) extractResponseContent(data map[string]interface{}, path string) (string, error) { + // 默认路径:choices[0].message.content(OpenAI 格式) + if path == "" || path == "choices[0].message.content" { + // 尝试 OpenAI 格式 + if choices, ok := data["choices"].([]interface{}); ok && len(choices) > 0 { + if choice, ok := choices[0].(map[string]interface{}); ok { + if message, ok := choice["message"].(map[string]interface{}); ok { + if content, ok := message["content"].(string); ok { + return content, nil + } + } + } + } + } + + // 尝试其他常见格式 + // 格式1: data.text + if dataObj, ok := data["data"].(map[string]interface{}); ok { + if text, ok := dataObj["text"].(string); ok { + return text, nil + } + } + + // 格式2: result.content + if result, ok := data["result"].(map[string]interface{}); ok { + if content, ok := result["content"].(string); ok { + return content, nil + } + } + + // 格式3: content(直接字段) + if content, ok := data["content"].(string); ok { + return content, nil + } + + // 格式4: text(直接字段) + if text, ok := data["text"].(string); ok { + return text, nil + } + + return "", errors.New("无法从响应中提取内容,请检查响应格式或配置适配器") +} + +// AIProviderFactory AI 提供商工厂(用于创建不同类型的提供商) +type AIProviderFactory struct{} + +// NewAIProviderFactory 创建 AI 提供商工厂实例。 +func NewAIProviderFactory() *AIProviderFactory { + return &AIProviderFactory{} +} + +// CreateProvider 根据配置创建对应的 AI 提供商。 +// 设计理念: +// 所有主流 AI 服务商都使用 REST API(HTTP/HTTPS),统一使用 UniversalAIProvider 处理 +// 通过 AdapterConfig 适配不同服务商的细微差异(认证头、响应路径等) +func (f *AIProviderFactory) CreateProvider(config AIConfig) (AIProvider, error) { + // 所有服务商都使用 REST API,统一处理 + return NewUniversalAIProvider(config), nil +} + diff --git a/backend/service/ai_service.go b/backend/service/ai_service.go new file mode 100644 index 0000000..ff2d123 --- /dev/null +++ b/backend/service/ai_service.go @@ -0,0 +1,154 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "log" + + "github.com/2930134478/AI-CS/backend/models" + "github.com/2930134478/AI-CS/backend/repository" + "github.com/2930134478/AI-CS/backend/utils" + "gorm.io/gorm" +) + +// AIService AI 服务(负责调用 AI 生成回复) +type AIService struct { + aiConfigRepo *repository.AIConfigRepository + messageRepo *repository.MessageRepository + conversationRepo *repository.ConversationRepository + providerFactory *AIProviderFactory +} + +// NewAIService 创建 AI 服务实例。 +func NewAIService( + aiConfigRepo *repository.AIConfigRepository, + messageRepo *repository.MessageRepository, + conversationRepo *repository.ConversationRepository, +) *AIService { + return &AIService{ + aiConfigRepo: aiConfigRepo, + messageRepo: messageRepo, + conversationRepo: conversationRepo, + providerFactory: NewAIProviderFactory(), + } +} + +// GenerateAIResponse 为对话生成 AI 回复。 +// conversationID: 对话ID +// userMessage: 用户消息 +// userID: 用户ID(用于回退查找 AI 配置) +// 返回: AI 回复内容,如果失败返回错误 +func (s *AIService) GenerateAIResponse(conversationID uint, userMessage string, userID uint) (string, error) { + // 1. 获取对话信息,优先使用对话绑定的 AI 配置 + conversation, err := s.conversationRepo.GetByID(conversationID) + if err != nil { + return "", fmt.Errorf("获取对话失败: %v", err) + } + + var config *models.AIConfig + if conversation.AIConfigID != nil { + // 使用对话绑定的配置(多厂商支持) + config, err = s.aiConfigRepo.GetByID(*conversation.AIConfigID) + if err != nil { + return "", fmt.Errorf("获取 AI 配置失败: %v", err) + } + // 验证配置是否启用 + if !config.IsActive { + return "", errors.New("该模型配置已禁用") + } + } else { + // 回退:使用用户默认配置(向后兼容) + config, err = s.aiConfigRepo.GetActiveByUserID(userID, "text") + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", errors.New("未找到 AI 配置,请先在设置中配置 AI 服务") + } + return "", fmt.Errorf("获取 AI 配置失败: %v", err) + } + } + + // 2. 解密 API Key + apiKey, err := utils.DecryptAPIKey(config.APIKey) + if err != nil { + return "", fmt.Errorf("解密 API Key 失败: %v", err) + } + + // 3. 获取对话历史(用于上下文) + history, err := s.buildConversationHistory(conversationID) + if err != nil { + log.Printf("⚠️ 获取对话历史失败: %v", err) + // 即使获取历史失败,也继续处理(使用空历史) + history = []MessageHistory{} + } + + // 4. 解析适配器配置(如果有) + var adapterConfig *AdapterConfig + if config.AdapterConfig != "" { + if err := json.Unmarshal([]byte(config.AdapterConfig), &adapterConfig); err != nil { + log.Printf("⚠️ 解析适配器配置失败: %v,使用默认配置", err) + } + } + + // 5. 创建 AI 提供商 + aiConfig := AIConfig{ + APIURL: config.APIURL, + APIKey: apiKey, + Model: config.Model, + ModelType: config.ModelType, + Provider: config.Provider, + AdapterConfig: adapterConfig, + } + + provider, err := s.providerFactory.CreateProvider(aiConfig) + if err != nil { + return "", fmt.Errorf("创建 AI 提供商失败: %v", err) + } + + // 6. 调用 AI 生成回复 + response, err := provider.GenerateResponse(history, userMessage) + if err != nil { + // AI 调用失败,返回友好的错误消息 + log.Printf("❌ AI 调用失败: %v", err) + return "AI客服好像出了点差错,请联系人工客服解决", nil + } + + return response, nil +} + +// buildConversationHistory 构建对话历史(用于 AI 上下文)。 +func (s *AIService) buildConversationHistory(conversationID uint) ([]MessageHistory, error) { + // 获取最近的对话消息(最多 10 条,避免上下文过长) + messages, err := s.messageRepo.ListByConversationID(conversationID) + if err != nil { + return nil, err + } + + // 只取最近 10 条消息 + startIdx := 0 + if len(messages) > 10 { + startIdx = len(messages) - 10 + } + + history := make([]MessageHistory, 0) + for i := startIdx; i < len(messages); i++ { + msg := messages[i] + // 跳过系统消息 + if msg.MessageType == "system_message" { + continue + } + + role := "user" + if msg.SenderIsAgent { + role = "assistant" + } + + history = append(history, MessageHistory{ + Role: role, + Content: msg.Content, + }) + } + + return history, nil +} + diff --git a/backend/service/conversation_service.go b/backend/service/conversation_service.go index 73b897e..741aa7e 100644 --- a/backend/service/conversation_service.go +++ b/backend/service/conversation_service.go @@ -14,16 +14,22 @@ import ( type ConversationService struct { conversations *repository.ConversationRepository messages *repository.MessageRepository + aiConfigRepo *repository.AIConfigRepository // 用于验证 AI 配置 + userRepo *repository.UserRepository // 用于查询用户设置 } // NewConversationService 创建 ConversationService 实例。 func NewConversationService( conversations *repository.ConversationRepository, messages *repository.MessageRepository, + aiConfigRepo *repository.AIConfigRepository, + userRepo *repository.UserRepository, ) *ConversationService { return &ConversationService{ conversations: conversations, messages: messages, + aiConfigRepo: aiConfigRepo, + userRepo: userRepo, } } @@ -40,6 +46,31 @@ func (s *ConversationService) InitConversation(input InitConversationInput) (*In if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { now := time.Now() + chatMode := input.ChatMode + if chatMode == "" { + chatMode = "human" // 默认人工客服 + } + + // 如果是 AI 模式,验证 AI 配置 + var aiConfigID *uint + if chatMode == "ai" { + if input.AIConfigID == nil || *input.AIConfigID == 0 { + return nil, errors.New("AI 模式需要选择模型配置") + } + // 验证配置是否存在且开放 + config, err := s.aiConfigRepo.GetByID(*input.AIConfigID) + if err != nil { + return nil, errors.New("模型配置不存在") + } + if !config.IsPublic { + return nil, errors.New("该模型未开放给访客使用") + } + if !config.IsActive { + return nil, errors.New("该模型配置已禁用") + } + aiConfigID = input.AIConfigID + } + conv = &models.Conversation{ VisitorID: input.VisitorID, Status: "open", @@ -50,6 +81,8 @@ func (s *ConversationService) InitConversation(input InitConversationInput) (*In Language: input.Language, IPAddress: input.IPAddress, LastSeenAt: &now, + ChatMode: chatMode, + AIConfigID: aiConfigID, } if err := s.conversations.Create(conv); err != nil { return nil, err @@ -59,10 +92,13 @@ func (s *ConversationService) InitConversation(input InitConversationInput) (*In return nil, err } } else { + // 恢复已存在的对话 now := time.Now() updates := map[string]interface{}{ "last_seen_at": &now, } + + // 更新访客信息(如果之前没有) if input.Website != "" && conv.Website == "" { updates["website"] = input.Website } @@ -81,19 +117,60 @@ func (s *ConversationService) InitConversation(input InitConversationInput) (*In if input.IPAddress != "" && conv.IPAddress == "" { updates["ip_address"] = input.IPAddress } + + // 重要:如果用户选择了新的 ChatMode,更新对话模式 + // 这样访客可以在人工客服和 AI 客服之间切换 + if input.ChatMode != "" && input.ChatMode != conv.ChatMode { + chatMode := input.ChatMode + updates["chat_mode"] = chatMode + + // 如果是 AI 模式,验证并更新 AI 配置 + if chatMode == "ai" { + if input.AIConfigID == nil || *input.AIConfigID == 0 { + return nil, errors.New("AI 模式需要选择模型配置") + } + // 验证配置是否存在且开放 + config, err := s.aiConfigRepo.GetByID(*input.AIConfigID) + if err != nil { + return nil, errors.New("模型配置不存在") + } + if !config.IsPublic { + return nil, errors.New("该模型未开放给访客使用") + } + if !config.IsActive { + return nil, errors.New("该模型配置已禁用") + } + updates["ai_config_id"] = input.AIConfigID + } else { + // 切换到人工客服模式,清除 AI 配置 + updates["ai_config_id"] = nil + } + } + if err := s.conversations.UpdateFields(conv.ID, updates); err != nil { return nil, err } + + // 重新获取更新后的对话信息 + conv, err = s.conversations.GetByID(conv.ID) + if err != nil { + return nil, err + } } if isNewConversation { now := time.Now() + chatMode := input.ChatMode + if chatMode == "" { + chatMode = "human" // 默认人工模式 + } message := &models.Message{ ConversationID: conv.ID, SenderID: 0, SenderIsAgent: false, Content: "Visitor opened the page", MessageType: "system_message", + ChatMode: chatMode, // 记录系统消息发送时的对话模式 IsRead: true, ReadAt: &now, } @@ -106,12 +183,17 @@ func (s *ConversationService) InitConversation(input InitConversationInput) (*In if input.Referrer != "" { readTime := time.Now() + chatMode := input.ChatMode + if chatMode == "" { + chatMode = "human" // 默认人工模式 + } referrerMsg := &models.Message{ ConversationID: conv.ID, SenderID: 0, SenderIsAgent: false, Content: "Visitor came from [" + input.Referrer + "]", MessageType: "system_message", + ChatMode: chatMode, // 记录系统消息发送时的对话模式 IsRead: true, ReadAt: &readTime, } @@ -152,22 +234,34 @@ func (s *ConversationService) UpdateConversationContact(input UpdateConversation return nil, err } - return s.GetConversationDetail(input.ConversationID) + // UpdateConversationContact 不传递 userID,因为更新联系信息时不需要检查参与状态 + return s.GetConversationDetail(input.ConversationID, 0) } -func (s *ConversationService) buildSummary(conv models.Conversation) (ConversationSummary, error) { +func (s *ConversationService) buildSummary(conv models.Conversation, userID uint) (ConversationSummary, error) { var lastSeen *time.Time if conv.LastSeenAt != nil { lastSeen = conv.LastSeenAt } + + // 检查当前用户是否参与过该会话(是否发送过消息) + hasParticipated := false + if userID > 0 { + if participated, err := s.messages.HasAgentParticipated(conv.ID, userID); err == nil { + hasParticipated = participated + } + // 错误时静默处理,不影响流程 + } + summary := ConversationSummary{ - ID: conv.ID, - VisitorID: conv.VisitorID, - AgentID: conv.AgentID, - Status: conv.Status, - CreatedAt: conv.CreatedAt, - UpdatedAt: conv.UpdatedAt, - LastSeenAt: lastSeen, // 添加 last_seen_at 字段 + ID: conv.ID, + VisitorID: conv.VisitorID, + AgentID: conv.AgentID, + Status: conv.Status, + CreatedAt: conv.CreatedAt, + UpdatedAt: conv.UpdatedAt, + LastSeenAt: lastSeen, // 添加 last_seen_at 字段 + HasParticipated: hasParticipated, // 当前用户是否参与过该会话 } if message, err := s.messages.LatestByConversationID(conv.ID); err == nil && message != nil { @@ -194,7 +288,12 @@ func (s *ConversationService) buildSummary(conv models.Conversation) (Conversati } // ListConversations 返回当前活跃会话的摘要信息。 -func (s *ConversationService) ListConversations() ([]ConversationSummary, error) { +// userID: 当前登录的客服ID(可选,如果为0则使用默认过滤规则) +// 过滤规则: +// 1. 默认不显示 ChatMode == "ai" 的对话 +// 2. 如果 userID > 0 且该用户的 ReceiveAIConversations == false,则不显示 AI 对话 +// 3. 只显示 ChatMode == "human" 且存在访客消息的对话(访客切换到人工并发送消息后) +func (s *ConversationService) ListConversations(userID uint) ([]ConversationSummary, error) { conversations, err := s.conversations.ListActive() if err != nil { return nil, err @@ -202,9 +301,30 @@ func (s *ConversationService) ListConversations() ([]ConversationSummary, error) result := make([]ConversationSummary, 0, len(conversations)) for _, conv := range conversations { - summary, err := s.buildSummary(conv) + // 过滤规则 1: 默认不显示 AI 对话 + // 只有在会话页面手动开启"显示 AI 对话"时才显示 + if conv.ChatMode == "ai" { + continue + } + + // 过滤规则 2: 如果是人工对话,检查是否有访客发送的消息 + // 只有当访客切换到人工并发送消息后,才显示在列表中 + if conv.ChatMode == "human" { + hasVisitorMessage, err := s.messages.HasVisitorMessageInHumanMode(conv.ID) + if err != nil { + // 如果查询失败,为了安全起见,不显示该对话 + continue + } + if !hasVisitorMessage { + // 没有访客消息,不显示(访客只是切换了模式,但还没发送消息) + continue + } + } + + // 通过过滤,添加到结果列表 + summary, err := s.buildSummary(conv, userID) if err != nil { - return nil, err + continue // 如果构建摘要失败,跳过该对话 } result = append(result, summary) } @@ -212,13 +332,13 @@ func (s *ConversationService) ListConversations() ([]ConversationSummary, error) } // GetConversationDetail 获取指定会话的详细信息。 -func (s *ConversationService) GetConversationDetail(id uint) (*ConversationDetail, error) { +func (s *ConversationService) GetConversationDetail(id uint, userID uint) (*ConversationDetail, error) { conv, err := s.conversations.GetByID(id) if err != nil { return nil, err } - summary, err := s.buildSummary(*conv) + summary, err := s.buildSummary(*conv, userID) if err != nil { return nil, err } @@ -245,7 +365,8 @@ func (s *ConversationService) GetConversationDetail(id uint) (*ConversationDetai } // SearchConversations 根据关键字检索会话摘要。 -func (s *ConversationService) SearchConversations(query string) ([]ConversationSummary, error) { +// userID: 当前登录的客服ID(可选,用于检查参与状态) +func (s *ConversationService) SearchConversations(query string, userID uint) ([]ConversationSummary, error) { pattern := "%" + query + "%" idSet := map[uint]struct{}{} @@ -282,7 +403,7 @@ func (s *ConversationService) SearchConversations(query string) ([]ConversationS result := make([]ConversationSummary, 0, len(conversations)) for _, conv := range conversations { - summary, err := s.buildSummary(conv) + summary, err := s.buildSummary(conv, userID) if err != nil { return nil, err } diff --git a/backend/service/faq_service.go b/backend/service/faq_service.go new file mode 100644 index 0000000..5f9ada9 --- /dev/null +++ b/backend/service/faq_service.go @@ -0,0 +1,168 @@ +package service + +import ( + "errors" + "strings" + + "github.com/2930134478/AI-CS/backend/models" + "github.com/2930134478/AI-CS/backend/repository" + "gorm.io/gorm" +) + +// FAQService 负责 FAQ(常见问题)管理领域的业务编排。 +type FAQService struct { + faqs *repository.FAQRepository +} + +// NewFAQService 创建 FAQService 实例。 +func NewFAQService(faqs *repository.FAQRepository) *FAQService { + return &FAQService{faqs: faqs} +} + +// CreateFAQ 创建新的 FAQ 记录。 +func (s *FAQService) CreateFAQ(input CreateFAQInput) (*FAQSummary, error) { + // 验证必填字段 + if input.Question == "" { + return nil, errors.New("问题不能为空") + } + if input.Answer == "" { + return nil, errors.New("答案不能为空") + } + + // 创建 FAQ 记录 + faq := &models.FAQ{ + Question: input.Question, + Answer: input.Answer, + Keywords: input.Keywords, + } + + if err := s.faqs.Create(faq); err != nil { + return nil, err + } + + return s.toSummary(faq), nil +} + +// GetFAQ 获取 FAQ 详情。 +func (s *FAQService) GetFAQ(id uint) (*FAQSummary, error) { + faq, err := s.faqs.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("FAQ 不存在") + } + return nil, err + } + + return s.toSummary(faq), nil +} + +// ListFAQs 获取 FAQ 列表,支持关键词搜索。 +// query 格式:关键词之间用 % 分隔,例如 "openai%api%调用" +// 搜索逻辑:所有关键词都要包含(AND 查询) +func (s *FAQService) ListFAQs(query string) ([]FAQSummary, error) { + // 解析关键词 + keywords := s.parseKeywords(query) + + // 查询 FAQ 列表 + faqs, err := s.faqs.List(keywords) + if err != nil { + return nil, err + } + + // 转换为 Summary + summaries := make([]FAQSummary, 0, len(faqs)) + for _, faq := range faqs { + summaries = append(summaries, *s.toSummary(&faq)) + } + + return summaries, nil +} + +// UpdateFAQ 更新 FAQ 记录。 +func (s *FAQService) UpdateFAQ(id uint, input UpdateFAQInput) (*FAQSummary, error) { + // 获取现有记录 + faq, err := s.faqs.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("FAQ 不存在") + } + return nil, err + } + + // 验证必填字段 + if input.Question != nil && *input.Question == "" { + return nil, errors.New("问题不能为空") + } + if input.Answer != nil && *input.Answer == "" { + return nil, errors.New("答案不能为空") + } + + // 更新字段 + if input.Question != nil { + faq.Question = *input.Question + } + if input.Answer != nil { + faq.Answer = *input.Answer + } + if input.Keywords != nil { + faq.Keywords = *input.Keywords + } + + // 保存更新 + if err := s.faqs.Update(faq); err != nil { + return nil, err + } + + return s.toSummary(faq), nil +} + +// DeleteFAQ 删除 FAQ 记录。 +func (s *FAQService) DeleteFAQ(id uint) error { + // 检查记录是否存在 + _, err := s.faqs.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("FAQ 不存在") + } + return err + } + + // 删除记录 + return s.faqs.Delete(id) +} + +// parseKeywords 解析关键词查询字符串。 +// 输入格式:关键词之间用 % 分隔,例如 "openai%api%调用" +// 返回:关键词数组 +func (s *FAQService) parseKeywords(query string) []string { + if query == "" { + return nil + } + + // 按 % 分隔 + parts := strings.Split(query, "%") + keywords := make([]string, 0, len(parts)) + + for _, part := range parts { + // 去除首尾空格 + keyword := strings.TrimSpace(part) + if keyword != "" { + keywords = append(keywords, keyword) + } + } + + return keywords +} + +// toSummary 将 FAQ 模型转换为 Summary。 +func (s *FAQService) toSummary(faq *models.FAQ) *FAQSummary { + return &FAQSummary{ + ID: faq.ID, + Question: faq.Question, + Answer: faq.Answer, + Keywords: faq.Keywords, + CreatedAt: faq.CreatedAt, + UpdatedAt: faq.UpdatedAt, + } +} + diff --git a/backend/service/message_service.go b/backend/service/message_service.go index 7ab2eb2..b8a4c6c 100644 --- a/backend/service/message_service.go +++ b/backend/service/message_service.go @@ -22,6 +22,7 @@ type MessageService struct { conversations *repository.ConversationRepository messages *repository.MessageRepository hub BroadcastHub + aiService *AIService // AI 服务(用于 AI 自动回复) } // NewMessageService 创建 MessageService 实例。 @@ -29,11 +30,13 @@ func NewMessageService( conversations *repository.ConversationRepository, messages *repository.MessageRepository, hub BroadcastHub, + aiService *AIService, ) *MessageService { return &MessageService{ conversations: conversations, messages: messages, hub: hub, + aiService: aiService, } } @@ -58,31 +61,135 @@ func (s *MessageService) CreateMessage(input CreateMessageInput) (*models.Messag SenderIsAgent: input.SenderIsAgent, Content: input.Content, MessageType: "user_message", + ChatMode: conv.ChatMode, // 记录消息发送时的对话模式 IsRead: false, + // 文件相关字段(可选) + FileURL: input.FileURL, + FileType: input.FileType, + FileName: input.FileName, + FileSize: input.FileSize, + MimeType: input.MimeType, } if err := s.messages.Create(message); err != nil { return nil, err } - if err := s.conversations.UpdateFields(conv.ID, map[string]interface{}{ + // 如果客服发送消息,且会话的 agent_id 为 0,则更新为当前客服的 ID + updateFields := map[string]interface{}{ "updated_at": message.CreatedAt, - }); err != nil { + } + if input.SenderIsAgent && input.SenderID > 0 && conv.AgentID == 0 { + updateFields["agent_id"] = input.SenderID + } + + if err := s.conversations.UpdateFields(conv.ID, updateFields); err != nil { return nil, err } if s.hub != nil { + // 1. 先广播到该对话的所有客户端(访客和已连接该对话的客服) s.hub.BroadcastMessage(message.ConversationID, "new_message", message) + // 2. 如果是访客发送的消息,且对话模式是人工客服,才广播到所有客服 + // 这样即使客服没有连接到这个对话,也能收到新消息的通知 + // 注意:AI 模式下的访客消息不广播给客服(避免干扰) + if !input.SenderIsAgent && conv.ChatMode == "human" { + s.hub.BroadcastToAllAgents("new_message", message) + } } else { log.Printf("⚠️ WebSocket Hub 为空,无法广播消息: 消息ID=%d, 对话ID=%d", message.ID, message.ConversationID) } + // 3. 如果是 AI 客服模式,且是访客发送的消息,自动调用 AI 生成回复 + if conv.ChatMode == "ai" && !input.SenderIsAgent && s.aiService != nil { + // 异步调用 AI 生成回复(避免阻塞) + go func() { + // 获取对话的 AgentID(用于查找 AI 配置) + // 如果 AgentID 为 0,使用默认管理员 ID(1) + userID := conv.AgentID + if userID == 0 { + userID = 1 // 默认使用管理员 ID + } + + aiResponse, err := s.aiService.GenerateAIResponse(message.ConversationID, input.Content, userID) + if err != nil { + log.Printf("❌ AI 生成回复失败: %v", err) + // 使用友好的错误消息 + aiResponse = "AI客服好像出了点差错,请联系人工客服解决" + } + + // 创建 AI 回复消息 + aiMessage := &models.Message{ + ConversationID: message.ConversationID, + SenderID: 0, // AI 消息的 SenderID 为 0 + SenderIsAgent: true, // AI 回复视为客服消息 + Content: aiResponse, + MessageType: "user_message", + ChatMode: "ai", // AI 回复消息的模式为 "ai" + IsRead: false, + } + + if err := s.messages.Create(aiMessage); err != nil { + log.Printf("❌ 创建 AI 回复消息失败: %v", err) + return + } + + // 更新对话的更新时间 + if err := s.conversations.UpdateFields(conv.ID, map[string]interface{}{ + "updated_at": aiMessage.CreatedAt, + }); err != nil { + log.Printf("⚠️ 更新对话时间失败: %v", err) + } + + // 广播 AI 回复消息 + if s.hub != nil { + // AI 回复只广播给访客,不广播给客服(避免干扰) + // 客服可以在会话页面手动开启"显示 AI 消息"来查看 + s.hub.BroadcastMessage(aiMessage.ConversationID, "new_message", aiMessage) + // 不再广播到所有客服 + // s.hub.BroadcastToAllAgents("new_message", aiMessage) + } + }() + } + return message, nil } -// ListMessages 返回会话内的全部消息。 -func (s *MessageService) ListMessages(conversationID uint) ([]models.Message, error) { - return s.messages.ListByConversationID(conversationID) +// ListMessages 返回会话内的消息列表。 +// includeAIMessages: 是否包含 AI 消息(默认 false,不包含) +// 如果 includeAIMessages == false,过滤掉所有 chat_mode == "ai" 的消息 +// 这样就能准确区分 AI 模式下的消息和人工模式下的消息,即使对话模式切换了也能正确过滤 +func (s *MessageService) ListMessages(conversationID uint, includeAIMessages bool) ([]models.Message, error) { + messages, err := s.messages.ListByConversationID(conversationID) + if err != nil { + return nil, err + } + + // 如果不包含 AI 消息,过滤掉所有 chat_mode == "ai" 的消息 + // 这样,无论对话当前是什么模式,都能准确过滤掉 AI 模式下的所有消息 + // 包括:访客在 AI 模式下发送的消息、AI 回复消息 + if !includeAIMessages { + filtered := make([]models.Message, 0, len(messages)) + for _, msg := range messages { + // 只显示 chat_mode != "ai" 的消息(人工模式下的消息) + // 如果 chat_mode 为空(兼容历史数据),则根据 SenderID 和 SenderIsAgent 判断 + if msg.ChatMode != "" { + // 有 chat_mode 字段,直接根据字段过滤 + if msg.ChatMode != "ai" { + filtered = append(filtered, msg) + } + } else { + // 兼容历史数据:chat_mode 为空时,使用旧逻辑 + // 过滤掉 AI 回复消息(SenderID == 0 && SenderIsAgent == true) + if msg.SenderID != 0 || !msg.SenderIsAgent { + filtered = append(filtered, msg) + } + } + } + return filtered, nil + } + + return messages, nil } // MarkMessagesRead 将消息标记为已读并通知监听方。 diff --git a/backend/service/profile_service.go b/backend/service/profile_service.go index 8906b57..40c31ca 100644 --- a/backend/service/profile_service.go +++ b/backend/service/profile_service.go @@ -34,12 +34,13 @@ func (s *ProfileService) GetProfile(userID uint) (*ProfileResult, error) { } return &ProfileResult{ - ID: user.ID, - Username: user.Username, - Role: user.Role, - AvatarURL: user.AvatarURL, - Nickname: user.Nickname, - Email: user.Email, + ID: user.ID, + Username: user.Username, + Role: user.Role, + AvatarURL: user.AvatarURL, + Nickname: user.Nickname, + Email: user.Email, + ReceiveAIConversations: user.ReceiveAIConversations, }, nil } @@ -60,6 +61,9 @@ func (s *ProfileService) UpdateProfile(input UpdateProfileInput) (*ProfileResult if input.Email != nil { updates["email"] = *input.Email } + if input.ReceiveAIConversations != nil { + updates["receive_ai_conversations"] = *input.ReceiveAIConversations + } if len(updates) > 0 { if err := s.users.UpdateFields(input.UserID, updates); err != nil { diff --git a/backend/service/types.go b/backend/service/types.go index baf64ab..113d5a9 100644 --- a/backend/service/types.go +++ b/backend/service/types.go @@ -5,6 +5,7 @@ import "time" // BroadcastHub 描述 WebSocket Hub 的广播能力。 type BroadcastHub interface { BroadcastMessage(conversationID uint, messageType string, data interface{}) + BroadcastToAllAgents(messageType string, data interface{}) } // InitConversationInput 对话初始化需要的输入数据。 @@ -16,6 +17,8 @@ type InitConversationInput struct { OS string Language string IPAddress string + ChatMode string // 对话模式:human(人工客服)、ai(AI客服) + AIConfigID *uint // AI 配置 ID(访客选择的模型配置,AI 模式时必需) } // InitConversationResult 对话初始化后的返回结果。 @@ -34,15 +37,16 @@ type UpdateConversationContactInput struct { // ConversationSummary 用于会话列表展示的概要信息。 type ConversationSummary struct { - ID uint - VisitorID uint - AgentID uint - Status string - CreatedAt time.Time - UpdatedAt time.Time - LastMessage *LastMessageSummary - UnreadCount int64 - LastSeenAt *time.Time // 最后活跃时间,用于判断在线状态 + ID uint + VisitorID uint + AgentID uint + Status string + CreatedAt time.Time + UpdatedAt time.Time + LastMessage *LastMessageSummary + UnreadCount int64 + LastSeenAt *time.Time // 最后活跃时间,用于判断在线状态 + HasParticipated bool // 当前用户是否参与过该会话(是否发送过消息) } // LastMessageSummary 会话最后一条消息的摘要信息。 @@ -78,6 +82,12 @@ type CreateMessageInput struct { Content string SenderID uint SenderIsAgent bool + // 文件相关字段(可选) + FileURL *string // 文件URL + FileType *string // 文件类型:image, document + FileName *string // 原始文件名 + FileSize *int64 // 文件大小(字节) + MimeType *string // MIME类型 } // CreateAgentInput 创建客服或管理员账号需要的参数。 @@ -97,17 +107,89 @@ type MarkMessagesReadResult struct { // UpdateProfileInput 更新个人资料时需要的参数。 type UpdateProfileInput struct { - UserID uint - Nickname *string - Email *string + UserID uint + Nickname *string + Email *string + ReceiveAIConversations *bool // 是否接收 AI 对话(可选) } // ProfileResult 个人资料信息。 type ProfileResult struct { - ID uint `json:"id"` - Username string `json:"username"` - Role string `json:"role"` - AvatarURL string `json:"avatar_url"` - Nickname string `json:"nickname"` - Email string `json:"email"` + ID uint `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + AvatarURL string `json:"avatar_url"` + Nickname string `json:"nickname"` + Email string `json:"email"` + ReceiveAIConversations bool `json:"receive_ai_conversations"` // 是否接收 AI 对话 +} + +// UserSummary 用户列表摘要信息(不包含密码)。 +type UserSummary struct { + ID uint `json:"id"` + Username string `json:"username"` + Role string `json:"role"` + Nickname string `json:"nickname"` + Email string `json:"email"` + AvatarURL string `json:"avatar_url"` + ReceiveAIConversations bool `json:"receive_ai_conversations"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// CreateUserInput 创建用户输入。 +type CreateUserInput struct { + Username string // 用户名(必需) + Password string // 密码(必需) + Role string // 角色:"admin" 或 "agent"(必需) + Nickname *string // 昵称(可选) + Email *string // 邮箱(可选) +} + +// UpdateUserInput 更新用户输入。 +type UpdateUserInput struct { + UserID uint // 用户ID(必需) + Role *string // 角色(可选) + Nickname *string // 昵称(可选) + Email *string // 邮箱(可选) + ReceiveAIConversations *bool // 是否接收 AI 对话(可选) +} + +// UpdatePasswordInput 更新密码输入。 +type UpdatePasswordInput struct { + UserID uint // 用户ID(必需) + OldPassword *string // 旧密码(可选,管理员修改其他用户密码时不需要) + NewPassword string // 新密码(必需) + IsAdmin bool // 是否是管理员操作(必需) +} + +// FAQSummary FAQ(常见问题)摘要信息。 +type FAQSummary struct { + ID uint `json:"id"` + Question string `json:"question"` // 问题 + Answer string `json:"answer"` // 答案 + Keywords string `json:"keywords"` // 关键词(用于搜索) + CreatedAt time.Time `json:"created_at"` // 创建时间 + UpdatedAt time.Time `json:"updated_at"` // 更新时间 +} + +// CreateFAQInput 创建 FAQ 输入。 +type CreateFAQInput struct { + Question string // 问题(必需) + Answer string // 答案(必需) + Keywords string // 关键词(可选,用逗号或空格分隔) +} + +// UpdateFAQInput 更新 FAQ 输入。 +type UpdateFAQInput struct { + Question *string // 问题(可选) + Answer *string // 答案(可选) + Keywords *string // 关键词(可选) +} + +// OnlineAgent 在线客服信息(供访客查看)。 +type OnlineAgent struct { + ID uint `json:"id"` // 客服ID + Nickname string `json:"nickname"` // 昵称 + AvatarURL string `json:"avatar_url"` // 头像URL } diff --git a/backend/service/user_service.go b/backend/service/user_service.go new file mode 100644 index 0000000..55c48a3 --- /dev/null +++ b/backend/service/user_service.go @@ -0,0 +1,257 @@ +package service + +import ( + "errors" + "strings" + + "github.com/2930134478/AI-CS/backend/models" + "github.com/2930134478/AI-CS/backend/repository" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// UserService 负责用户管理领域的业务编排。 +type UserService struct { + users *repository.UserRepository +} + +// NewUserService 创建 UserService 实例。 +func NewUserService(users *repository.UserRepository) *UserService { + return &UserService{users: users} +} + +// ListUsers 获取所有用户列表。 +func (s *UserService) ListUsers() ([]UserSummary, error) { + users, err := s.users.ListUsers() + if err != nil { + return nil, err + } + + summaries := make([]UserSummary, 0, len(users)) + for _, user := range users { + summaries = append(summaries, UserSummary{ + ID: user.ID, + Username: user.Username, + Role: user.Role, + Nickname: user.Nickname, + Email: user.Email, + AvatarURL: user.AvatarURL, + ReceiveAIConversations: user.ReceiveAIConversations, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + }) + } + + return summaries, nil +} + +// GetUser 获取用户详情。 +func (s *UserService) GetUser(id uint) (*UserSummary, error) { + user, err := s.users.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户不存在") + } + return nil, err + } + + return &UserSummary{ + ID: user.ID, + Username: user.Username, + Role: user.Role, + Nickname: user.Nickname, + Email: user.Email, + AvatarURL: user.AvatarURL, + ReceiveAIConversations: user.ReceiveAIConversations, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + }, nil +} + +// CreateUser 创建新用户。 +func (s *UserService) CreateUser(input CreateUserInput) (*UserSummary, error) { + // 验证必填字段 + if input.Username == "" || input.Password == "" { + return nil, errors.New("用户名和密码不能为空") + } + + // 验证角色 + if input.Role != "admin" && input.Role != "agent" { + return nil, errors.New("角色只能是 admin 或 agent") + } + + // 检查用户名是否已存在 + if _, err := s.users.FindByUsername(input.Username); err == nil { + return nil, ErrUsernameExists + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + // 加密密码 + hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) + if err != nil { + return nil, errors.New("密码加密失败") + } + + // 创建用户 + user := &models.User{ + Username: input.Username, + Password: string(hash), + Role: input.Role, + ReceiveAIConversations: true, // 默认接收 AI 对话 + } + + // 设置可选字段 + if input.Nickname != nil { + user.Nickname = strings.TrimSpace(*input.Nickname) + } + if input.Email != nil { + user.Email = strings.TrimSpace(*input.Email) + } + + if err := s.users.Create(user); err != nil { + return nil, err + } + + return &UserSummary{ + ID: user.ID, + Username: user.Username, + Role: user.Role, + Nickname: user.Nickname, + Email: user.Email, + AvatarURL: user.AvatarURL, + ReceiveAIConversations: user.ReceiveAIConversations, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + }, nil +} + +// UpdateUser 更新用户信息。 +func (s *UserService) UpdateUser(input UpdateUserInput) (*UserSummary, error) { + // 检查用户是否存在 + _, err := s.users.GetByID(input.UserID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户不存在") + } + return nil, err + } + + // 构建更新字段 + updates := make(map[string]interface{}) + + // 更新角色 + if input.Role != nil { + role := strings.TrimSpace(*input.Role) + if role != "admin" && role != "agent" { + return nil, errors.New("角色只能是 admin 或 agent") + } + updates["role"] = role + } + + // 更新昵称 + if input.Nickname != nil { + updates["nickname"] = strings.TrimSpace(*input.Nickname) + } + + // 更新邮箱 + if input.Email != nil { + updates["email"] = strings.TrimSpace(*input.Email) + } + + // 更新 AI 对话接收设置 + if input.ReceiveAIConversations != nil { + updates["receive_ai_conversations"] = *input.ReceiveAIConversations + } + + // 如果没有需要更新的字段,直接返回 + if len(updates) == 0 { + return s.GetUser(input.UserID) + } + + // 执行更新 + if err := s.users.UpdateFields(input.UserID, updates); err != nil { + return nil, err + } + + // 返回更新后的用户信息 + return s.GetUser(input.UserID) +} + +// DeleteUser 删除用户。 +func (s *UserService) DeleteUser(id uint, currentUserID uint) error { + // 防止删除当前登录用户 + if id == currentUserID { + return errors.New("不能删除当前登录用户") + } + + // 检查用户是否存在并获取用户信息 + user, err := s.users.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("用户不存在") + } + return err + } + + // 防止删除最后一个管理员 + if user.Role == "admin" { + count, err := s.users.CountByRole("admin") + if err != nil { + return err + } + if count <= 1 { + return errors.New("不能删除最后一个管理员") + } + } + + // 执行删除 + if err := s.users.Delete(id); err != nil { + return err + } + + return nil +} + +// UpdateUserPassword 更新用户密码。 +func (s *UserService) UpdateUserPassword(input UpdatePasswordInput) error { + // 检查用户是否存在 + user, err := s.users.GetByID(input.UserID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("用户不存在") + } + return err + } + + // 验证新密码 + if input.NewPassword == "" { + return errors.New("新密码不能为空") + } + + // 如果不是管理员操作,需要验证旧密码 + if !input.IsAdmin { + if input.OldPassword == nil || *input.OldPassword == "" { + return errors.New("需要提供旧密码") + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(*input.OldPassword)); err != nil { + return errors.New("旧密码不正确") + } + } + + // 加密新密码 + hash, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), bcrypt.DefaultCost) + if err != nil { + return errors.New("密码加密失败") + } + + // 更新密码 + if err := s.users.UpdateFields(input.UserID, map[string]interface{}{ + "password": string(hash), + }); err != nil { + return err + } + + return nil +} + diff --git a/backend/service/visitor_service.go b/backend/service/visitor_service.go new file mode 100644 index 0000000..af6b20a --- /dev/null +++ b/backend/service/visitor_service.go @@ -0,0 +1,60 @@ +package service + +import ( + _ "github.com/2930134478/AI-CS/backend/models" // 用于访问 models.User 类型(通过 repository 返回) + "github.com/2930134478/AI-CS/backend/repository" +) + +// OnlineAgentHub 描述获取在线客服ID列表的能力。 +type OnlineAgentHub interface { + GetOnlineAgentIDs() map[uint]bool +} + +// VisitorService 负责访客相关的业务逻辑。 +type VisitorService struct { + userRepo *repository.UserRepository + hub OnlineAgentHub +} + +// NewVisitorService 创建 VisitorService 实例。 +func NewVisitorService(userRepo *repository.UserRepository, hub OnlineAgentHub) *VisitorService { + return &VisitorService{ + userRepo: userRepo, + hub: hub, + } +} + +// GetOnlineAgents 获取所有在线客服列表。 +// 返回在线客服的基本信息(ID、昵称、头像)。 +func (s *VisitorService) GetOnlineAgents() ([]OnlineAgent, error) { + // 从 WebSocket Hub 获取在线客服ID列表 + onlineAgentIDs := s.hub.GetOnlineAgentIDs() + + if len(onlineAgentIDs) == 0 { + return []OnlineAgent{}, nil + } + + // 将 map 转换为 ID 列表 + ids := make([]uint, 0, len(onlineAgentIDs)) + for id := range onlineAgentIDs { + ids = append(ids, id) + } + + // 从数据库查询这些客服的详细信息(包含 admin 和 agent 角色) + users, err := s.userRepo.FindByIDsAndRoles(ids, []string{"admin", "agent"}) + if err != nil { + return nil, err + } + + // 转换为 OnlineAgent 列表 + agents := make([]OnlineAgent, 0, len(users)) + for _, user := range users { + agents = append(agents, OnlineAgent{ + ID: user.ID, + Nickname: user.Nickname, + AvatarURL: user.AvatarURL, + }) + } + + return agents, nil +} diff --git a/backend/uploads/avatars/user_4_1763359157.png b/backend/uploads/avatars/user_4_1764663058.png similarity index 100% rename from backend/uploads/avatars/user_4_1763359157.png rename to backend/uploads/avatars/user_4_1764663058.png diff --git a/backend/uploads/messages/23/1763991925_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png b/backend/uploads/messages/23/1763991925_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png new file mode 100644 index 0000000000000000000000000000000000000000..78f98ac2bb21fa524ce1de3d2a8cb6bf100e8efd GIT binary patch literal 21155 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 literal 0 HcmV?d00001 diff --git a/backend/uploads/messages/23/1763991942_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png b/backend/uploads/messages/23/1763991942_16e6a139-4afa-47d8-9736-ff3e5ed7636e.png new file mode 100644 index 0000000000000000000000000000000000000000..78f98ac2bb21fa524ce1de3d2a8cb6bf100e8efd GIT binary patch literal 21155 zcmV)9K*hg_P)PyyT}ebiRCwCVy?4A`Rh2is%HHRkr`?`*bCVDVp(G&?YUqLp3fQ}X9cI)Sdqc<3 z5es&7d{u_|4Wn2YdqW9TK%^svp5AYA`%}(2d+)X0KhAS+Zs4LQ`g><*e@H&heR!U8 z)>-?z+IOvszu9;;03nhJ0w4e=ser18s3IZ&004)`h!IYOQYUF8sxZW&wv}|^#8@Xp zd0Ktbd%pJRD}O)Ai)P(($G3j=jyJyhu%_EpwhLmJ+nIFf#O^vsv#;diQ5oBuG|`WB zQmHeimH{>ZsDk7Kyi>1QDZUvI02Dw0q2jGaZ6SEW!W=|EFhO7mB#IIUK|lhCBMfXv zn1xsoz#)2o0IJ39z|@Ta0J`FD1p!q+1pv^!KQG=asu~1UjWGZON&o;r02;XczR3-s z1V8|wsv?4j2DJU7y&ns}fQm#Qh}0G|3$rJ}fTbwvat(66G_+@;dm$R7xJIM(J*$s9 zXzi`n&e`v@JJ$W-#IsJT)bgm=q%ovX`R;eW^Vr%QtzGWW#Roq7Vgn?Cs0;uxvHfYwvqx{YHQ6>e_|>_zDkswKnhm zwY$ImwX4o}-pMDQyr69M^qJl@SKfcszu&xc?!ikATDkr9O-Gdv9Glp7fB6p={?~c+ zo*ngkS8R-dgsBn&2jsv62vv2~MgYY*BMBe^J`MnisA+v7VdM}bPhHt~Q1AdjK~Tsj zvDG~CnIL=QfFy{D0sywHhM+owpdY8`k9$3cpsK1eu_}ltA`=oIJDGeug{%QUk$A6~ zA^@)0a5exTpthfe^0db(D$|}fDnLM1H7rRr=>cabuRgqf{pH{M@Atg_t^IQQ)({?u2`dDb&ue9lSa zhXZ;8vwril`~Uktf4X~5;}f5KIBBGLY17?d&A(m0(k&Y2p%Xs(oM?G<^CORL ze|X!r`?l@?Uc@|XFL^Lu!Vk;KQWI7{>fh)Fm@qz;m zIbdK>U(ejq?EPyYN%u5%=IT?Id&nEvIH9jthAbB!Ac+A$MGEXMkeAwbOR5A~2%S{@ zl%N5DkQ55E1w>Q<1!OV+q@Y0*5es=v$(mTFqcL8;;oM0GCsj~Y#F>tj5&#LRkO%}; zK@A8#nDHcEPLk7pb zsVP*r9DCMnaV#4nMNQ!A3xU3ZPy?tJO@B#25xpL_~yo zh!Pt~mZoVlsl;M5l{`4;?C#oKA8y&qchE34c4uQlG`4o+frYCE4nKG0k!K$coxYxr zI-rE%gFeIU;Whzf;uZo{X@UNxFro*1-!GF3$Z6qSJh zP(g(>AOuoD$SjT<$x8m=iFLo*eOGg!b@p4%J@t7f49v6lZTX#)8X7!0&K+oE8(5Fk z*g!0Z03L&*KmkdqK<6pTscj)lH>X(O@jmXIA^<9Q1Vk((j(E+6bEj}*X8G=2G{cXT zK`kNzWk?DdN48hYD5sD_$v6m7ZR_k2=04|r+lv`EJ9F~OvIIFyp-;WhVC7@f42^;c>eO$uR10<$Tn$0BpV+cRFfe` zB*-37NdP>nXCN=ESU4g3xF}PCo_@8+({-`f3Jw4X3j<4d&H8ixglB_9e^SjA06B+m_>&M+PlF(dFWq zE=Kn8x2BYTChd?kA;1u!#(7X@v;k=vxyL#NAC5K;&YyVY2VU8`IL7keNOOaslJm|x zQJ{b%WQ5p+3^hFd!X`c78653>)foU3!ajmtGpW%tE}AZAlo^ZzDfkLVgqVwtnFv&X zY;Mef+wS3Sz3sctDXl(Wd|r>AT>`SIc{8`nfqbw&r_>0QTnvBtno^4yJz{TsFoC>f zXVkM6zCuJ`X-{Q*vmLy9=>D+Ko&M(I_dj)RRvB;RjWjd`L_AP16c|YqL{&j&{)&C| zQupx=+tBgU1dT-a_}h4b6-HGS3>6K=W%cgvIBmA9EmvwwZ~o4Gzxn9(uj)PP;EA42 z?Z_a?X_Uy58VroUs90PMD*Vk?APuCTSOANlC<-c6vvQz3up5JHusi)t^Y`ZH>hnKz zO4LoJN7ikBu(zvfa>S;nv7vqcs-lI}6A_x>s7;UBvB+mkfx9ZGA}I3tZ$IeCROSgE zIf?67d=>$s6i-YgiFsnQ(N!5J*@54EYs+;XyXKAkFIuv@tJfzQykdwXjYN#12^t3~ zJF4W&ijHTHv`#yVQyZVMpsirZVmr1NjQ=7Bpdy6w5WCmyt?FKLqPj75FLaRBm+!sI z$|R&CDUuK=p`zoEMXf-=xH#65*N~fhXD8l1^80Q5o6oxFjDyZxvSVVKcUeRT8bXjF zAXF7Z3=(igLxo{FKo=SvLJ{epDuE)u=ADN;?tc~qMMA*hA^{1IW;B%w;Cybasu8fI zDi5r?cGpiX{@Lr3XAd+wI((9pN;w81GD03r;Xx^)AP7Mq5PT9%rNW@J4ga*X*i%|U z>Ino3WFkdYU`0|4o*)pBv66>GIdM>JRr(WNDf|YwJ{1RVPE`zs=I($stb9n339GTvqm5%-F5zALST$ z<;PzP9lKjRw4=Vg9CycgCwk0iBw)s3itC!@>h0A51yE6JXYBBC$FWd#p4vn}1c0*7 zZl_cC!DvK|Oew;ya+n(pc7ON7-#@kM_@&K-sDGt!rxyJGxEj`g=gtwkWOw%o%|iwr znf;+tKf2@5M0LTUIZG3lCVqs%m@2A900hdQ1j+!#-iv)AqT0?zA>h=WZr}DUB19}Q z9}qV&s5V zc0BL>Cwyw_uZJtGS#hZX6&K=6fe@l;@?}tN3w_G{XJ+Q&`bD+a#o6K4y#3HZe8dLY z&^%e#P?$nUR6-ymZkIcdL4gcUK-!ti`{Q*RZ@%o77cM+=*6z5=Sw~??aMO=16prO| zf#Tld{5bhO@wKV6)Ki+l-|D=q0X8TRsmN zP#~lt4s4fykcfyxeEaU6L{aty4Il&pU{C{U5DbD<;r1?SGlHNbMU_F(d8t*p-3a{n z(jT2Qa7uNX%!$g)F!of?vb}Yt7pHC2C#27c2Q;~#{)#;yXlnC%lBPB%4V4_`4caq% zPX6g9e|yap50vr&pPD$1$pE4N1OPvounvVoGO3Y|%U&=FMt}bHWp7YB(~HvtJiV+W+O8h$dr+RP zpmXd1sh3@*0ICv5Cye+wfgE~<7To@Y`{L2@=f3rXE%_#mggu1d0Z}yoh>EHrGiw`^ zpEPtNW2`aO7_%28+R+LkAQ2ba5HXXoWsVGB5T~eRTZPf;FTZ-jDf3QR5N6|qt3)Lc z!>RcrjipWG%ctdya^xJvIalHY zx_|nu+xB;d9GD#tdC9sO!#Ea6N8|%ZP}X47qkt+4Rc$SG0;qA`3fOA(QL6@dEE)JR zS3IyY5(OUx%qR+$%7Ia^MWjMDRYt>1jaIXfW=@LCJEB5y&Yq$P)tPol2ZbB;h+4*K zkOT=mOdt)8Z0s0Xchqq5s_M(W|Bjm;ym8n@of(AOYhpVht)e$xLcoAhQtwn+0lZTK z!BiO)?is4byQ4RSSvNee@sR$5 z6IU&{j@fKZntRd@F8bkHzVW7LVL2<0?i$~g1nw|p8!!tl@2pc+VkxsYHjTi3SQJAI zi3?Z|0e=0v4#Rzys;U4&5wo>-H$ESK+$G=B`&hW9|!1y6w?>tEunGO@$1pPl5}Ctutd|l1%(QRn#YsA={;=CnizU zAP^}iNrUr74R71Dc1~^9(!Tk%+$IgnnMEY=VbjJ9$DVYAw-YTGE5#-DowdrMIGzw=yabdGS)kpq$;JGJ{ z=B;IoGk1OAr^}*FX?kYVg1&)5<>cXL{CfZB129w*?2RhB##*O-_N_}#TRpOA$CWR5 z$I;N$OPy43Po7YuQ26szjA?0ZB2Yz<1ji+RY}YPfTr#V#lx2~|iNUhhbOKN4TmJLw z*KEIKSKp~Ujl40xy0pGC`s3Q2*WYkMbJMP|aa)dS0}JElzV_(;V-DI*V@AfZRG8WO zR<$G9>()+TH9(QvU_xdx#U^8ojVz)8Riwt4d+_EB$Ie-{KxWrch}8p%M;0JOje=(l zf)EJDLw?te^~e9iOVgtl?piTBe#uF1zu|l4^uva`O-A`0cihrht3aBUI0`^HlK`S( zX7!KJc*mQcb>8j&{l+`LQ$Kg*()YavPFcR5o8y{dOi(c)F{yX}EJ#5#nRG>+S)lqP z+o1;oIk_=PC)jV@vvt|r#od9*f}kOX6u4E9Fgs0-TX5pPyzQ$)>v*9a^__QI{pn|Z zapQkJa#(ZeQa)_u{FNrRoBuHUzVokm;JO`sW}w=^%$BL<95tI0{F-+xFOsH3c@Ytd z^dJ=h`=q~$2!a3sZ15c0RxAFWPyF(r(ftn%1J&F|AUSDnSOJYFYnB@nWFr_?;y;XU z35U#{efY8o%M$=CRjfvjJ^bLE-}=F(|M|@crhl31(}|EpEU2gy07C_SwBhy>FTP-C zQFTP!5obJ;k33|I!=8sWe*MDtoz=Pjz=WygmhvK#_#}ze8InyiIUXc|LY~NOXx_f= z4<{UQ%mVDFwhRXAm6cF67Z3@YIb+M^2kw2~KR$ZpA?eEF=Ny-|c0aKFwvOes6JN9J z*cU8|qwSO1c8wf z#H5;R+t&KsH||?)R?TZxD-bYbo;hO!Kvn}_mKj!}#3KcX=XYIk?T;VZv^A-8^bQO# za`3K_<$UkDWbM`yYWvL|cB-&MPN zQ|E(&hnD74bK8k!2|@Ak>1)NA1dSjwxp%@M?3?Uwd+h%HMf=Y-9o=J?xFipeqZlI$ zK0~Acaj)wKhC8 zFPS;?>!08ElONn>P5Hos2KaUFTm?!93{b#@0R^H0QfMlybj%(&3E~NYcKz|E8@leT zF7r$J>!3=3A^?g+ZNUu!Dm%-;3S*u%>LvF`Z?^os7Y?jGGJrHN8-}!R{(>WqKK|BU zT))&;=H)S@fyl;z5QDRMERf9ZyyAP`S+{-Ltb>-=o~}5Rnh$!+!ed@|+M2Iiu{@bo zwN(lXDgvSaN+<}$c1#&l;2`XPa@L~YKt>@T1|n97JXldC+*59Bi}GJRe8(aE2d_xx z&2otVW$b_*V~!ypS^)+S7C-{qXYbr~&pq;o6FY-P0FndLHCY)htvis^A=p>9XC|eEU}&<22jPT<%Mm za)z9tMUr%P2tt3~+yhn~D3o=DAR9I>jcb0#;OL$`wb^}}_v~&O$YTnm?SeqS&Qfh_ zeeBrtUht;Ret6-L2iJ#om4=4_}i?d2vm; z4>{uClV5y1n6xoCUeD7EeUm6PT^oj5Y0WKMUHaAccFZoFIH$7mwBw$);o&VrgtSlH z<;gN!!3GDUm0I`UhVh99;n33JXm>i}n>~Xd2}vLrZ#AP}cHO%kw+{}q{N}5#`TBb= zdc(=j?A|!swPhSOk1UzJ$Y-I_*`c7QN(!Xt>S-W4eD$%;yDp?;_t1yWKmU=RT|a+w zBVN0EuIXwu@(7Jp#kNV?Ctwj0&mjC4aDuzf??IbP3(%fSK_HyiC7k+mE5yy|VQ zn&^1&{MVfDfs0=Lv!8zUCqMl67d`h3GnuZI>CA#j$4&thizpHSECuC2$?vYc>)`GK zIixoqFtwTEm@4! zD1#A66-h!7Cr+6~O$~TLR%};vkb)|avM`9tOg zN-mj3yiXEk@(DV1CPZD*H99u3XJqGn_uaX8dE8U(9CeCF2_++=Y3|(drydb^-+bl^ z{1KQ%93$C?seI=(e^}MEDj7?HIN+Jo zU%&`3sE|fVvn4qsm^qx3cTi>4p$gR5*>!Y0@66_`-ZUyVq>@X+I%XgR512Dm;;28v zb9rF=d-qc%Thz3b-p+&K&X>$xv2+KQ_CPI;a+O8`R)B#4K%mVIRRvOr6l_2nkOh3m zOvFt|$7`kW`iS(|-wr*vC8Vnjp27 zId#9)cm4RzIrEPZ)C2-ZLQXp>+T8`^NE;&|!l<)WF4rm{YgsfDybmE@fk#~YhlrRa zvj;(fsxU_?Y{c|I=fv8Pd7-a{36acP?Jp{82!S9d#GYqm@u980Ih&v_tvD0Aq||Au zm2pj)NMamPYA_Ixwn^SzrAwpzCz4g;9f$kAiLE1JgpAlR>Uzg%PHnbAWY{vHXed0< z>6Es}YbDiE4%RDxcP8HhBiU@d(`?(gzkX}{4*SBx{{8nqe#SYc4@A|PvvCU|PhKT8 zkn^O2JrgZUP=)Jd=a;4n_N*BW<8eI_;2>gxN>mx^>xXNvyJp9x(v80!{lrJU^S*a} zVQ5ziAu_~Vq$RZ|h5}^HApkKUQWQmq5r9HqpXO1(2Y+{OWf*&tN|d973^;n?1!V@E zS7FUq(SXg=I<7auoX!FEqp9G8$nIt{? z$?S3UyoB1_vBBq%P>-jw(_GMNn9f}SfPf&Rikh=FqEN5l4sAR(^2o!Ozx2FUe)5eU z8t=8^%D-K+=IAW3EwNrnu_=;8hyYPgY?ojkFK9*arV%OiH+vh8HCsb4KG%sl&~Emr z!O?er^u<5FYVEpB9fut}_todWqTCf<_J6+o-gm#&APJyDB^84}xo;Au8Y-*{>chNs z&rt3r7A>4VYffb>Y_*ljx(6QY$BwQRd+(bitTw^11OtIoA&@F5lz?5N3TOqrfpN<@ zhWXgh@-2>~vUz*Y!0c9+$pL3K(P;q7CLppIF9m}{!A~Ifwz%psOKcYD?Cy?%R#-A; zVI!kPnpJWZV>7{uloF%PJ5aT3ylAEYtVw;tmUYbdJ&AibAG~?v1N}!Wec6Zp>D|vf zvsA81tO#i-_=oO)_{i>KEl`FbV?g305Nc834o^%~2?kYSjSXbd%$LnSe#6~6=N>3g zTnaSKV7xDt%F*lo@eJ=I_YJZVWa3>Pctay?n5i&|*@$QomqAM6dp>vZHy6%3V)3HI z&PCt8{F{po=sV}-Cw5lnUVr`lM=d_Wjzr{rxl$SxQJ_iE2LmW7B;FtZ3m`xc38*nq z=IY}nuXH5iLnEynyM~&h!=>iDsDjQhNXaV%Bn1&yGVEhf=jv8>bGj?a9$mNjE1&=T zt{o55$}Kgsy1S|&idCRPs9p9E74L;H)esmrH3r9%@W}Q}i;g__883VJt4=$vbI}|% zWFdwe2q*%MB;?~wtRbQb0f=JJ%AuftnpFz~03{O-R2Mz^;C)MBp+Ep|s$@9H@;uLX z5CIz2T%GfXprQr3cA;tH<706W2Pg%$0Z}O+1_TmIDy)DIK!RZez@4S$o^rlqX z*4iz1{pk9KCK_7@*NbyWGk2LdRYr`WNC+#P9bL2fd|&@jFFfmvxpP+@akv*DCIQZT z3t1yJ8a#5i_1YV{lX3+OP2(x5>{HsAF|rS!gj&W@&CA*wAAT+#Jp$WRHK>H(Mb$8= zggo(+%8>t!a3)FH4UGIR>L%!ArNvzOn_)2OV-AvL=5Jr77Tb_rU)EDz{FHj$N)fn zFpO5wIEh@*Pa#DVFME4haK?1D0C)lhfM)2x3O6k`%31~i1r=rsB0<`W15qKR5CS2# zH5ySwT^v`_3HsTOZ}|CDw-F^wEWr^I1_0&#<}La3M?QPSx4!X~FMgo)&NqGb{U5&j zx3}m}>zIRBka>wfTytH1rD)}H$KL}O0RqVhn;>N8$@=1ZTK% z0EkjF{UL(r-r{{bBSAo>iJnms2jviTac=^h3IK#EAgaVv9E&19Cn`ilR0tZ3YQ_w~ z%cGBMTfO=WV{42icTHvwNWkcwn1ojxwEX1bPs**Q1G_%-gUiy1bjOy>kNxo>>t%4$ zwyhgC4G)j(+_h^-r|s>VJ7-b<8A}#*E|}A^xW8w?Jk}`9a-W!{%MywNL?iZ5bJzC~^ycetboSP*sr#74`AWmW3TsPa@#NJ5|`Vd#JjfRZ*V>RB96<9`~85 zq=}Wv+YA2Q_nWzeYf+R=+Rv;3HM6JMNB@2+uoc$JHWS9+O zte(A~Q}l{jFfonKV>gOv3c)DG0Z~CH1j>a$fk6xc2gvJg96-f;m!_mrbiNk7w);@R zJOsgjs4CuoF6f_i&)v5Y<|=`JB3oXuY~@$J^p)O@j!QrLnfv~DPa|&%C@EM7mYtGk zRzqL_P*ITt$pmjTq>Tw{Ks6AnYG5`5WRQqh36+haRb(&-jE2z^NpB=U1oX;IVhjWU z6;+G~s%2qREMiVYFw$lskf0GP2%|-%MJCA(Q@PVNw?PpV1hi*%Zx)=W=7>%e6&cZ# zOr5fNS+@4WJ+;xDb8#qkipYYX;4ye~poqbOMouKxY|cQBNFanHvaC?>^6&|zNdon# zB*A!xUR(Pu?RaeSUG;1SasV{WK@=rq@!aR0eatbdfANc}_v{$~HEjx7L8_Uo!Az}I z+RU=_H`iR3Kn+5K5Fr?x zmID6E3lLRyATa%Je8%t!=w0t(MjV4ay&?tx*&NkwcB`2*XW@az3Lh7fBn*vjvgH!`|c;c{NoRQ=^4?Y<$3okpAcX` z1w{>n#D=_B+F5GtIe2jYb2`O%4ai_rh>!rWaAdsq#3Z7GgtzQ??c{x`sIw7P?c!hl z@SX?nt$*^Pmv?v05wC_5@u?5>%m3r@UhBc;tI}RluieG=^XhNfAT^6-A^-Gc&OP0l*d_ zi&_=ya&7p*XFg};*yv5?UwHES-~Ex3jyfy%ad2Wd5QC}%DBK2pvOrbE=2>Q=xU(Z! zzP#hG!;U}%MM={%h|luYLl6D&=9_PN&wJl{!37t*;q`BdY_(GAQq@Ua*1sXkf8U0U zcKA_zN8h1~=bg0Tbsv8Fjz>4&{@~Ef=-4V!Q}U&fs8XQ7M?Ac+0leDYaBVLd2W|k;wSqs2t5{ zjN%W!_p2MP{ljS|y=c*bB_kulH{W>2zy0gSk3IHSPy{fbR!R4rpnw3Nfr&u{R09$E z;Fw93h&coY7!ZX}x9!;So$p?G%{9Ni;)*Nh&0eTLGdAb`4K+G|CyhW6DGQL*L;W?c zeZ%;ck+HirJbUiS6S@wp4og3En7SiT%|f+Y8*y1$Apotqz@jZAVn%Kb)$;$?`RfaA zcdle(0e90W=0lfD|WZ+Cb6R*KVXyKrN8G;D;b7KtL*LPK;=3 zweBNV9n;@G@5BH4ud9zgrmM3PfYO!+HN<}dK^InWpQ2x(4ycG2ysMa~IX=Age{O&4 z!DpT@Hhb<+T+)(;5J^y?5OIbc8BCB!+C-hem>ID_>!tDg`X5>R!hwjKkun67Ac_j4 zMZvdb=uqf!(nBH?MP_RPiR!tDShP-QB$TJ@7oeCAE~O+PWw70)v* zNI_H5F{>q+ff1>sil}NE2DU+~valOU!5N2XB%+fI5_`)J#l$55Pz^NMX(6hjAVrm> z$a{PAInm_q$Vw5~;7QdRnz7LlRiXeU z*oNhMhSx7VVnI|Eg?x(fJiS*>K>$QGs0Jwn-S*OO>g+qkTtI?QfH-$7P|BUna{{2P z+qSD%z$id2*x1_#Jy~Pac7Oa6_v5RbfJnrKBj)Jvl}D^vb<_>l-;iY)J#FB4GWWZE zlM2?bVS{K0?Tg>@4|k4kTpxy7)^YIFfF(!M$16Cy@$_GUP)c`09Pgq%7b-=C>9nGcI4AxPV ztm7HRmneeIa}&8i*=7%h@;q_gN1>EYBWFx0oP?>#RY=4%dB)nUT3UEYSd)A8xs2Wrg$Cba2lyUFCCg< z+BdxKpVvHg*Q5FFi6rExf-`fhGXm}?jkrDAdVv3kz zq6pE_B$O08sEWnNsD*&yGtmi!VZ~hm27yOyd2HFrIfHxFse;Q5K=tW2vrat*RVCyQ z;P2UGh|`9E1%LpMN1#U>d(_GkkKUe-O+=C<)G{+Sqvs4`luIZ7Zu1?>&t5S?BR(?1 zl}S14bl{q7M>Iu8BA@@xLyDZ13aKzCt2%ABcLAyaiBL+c33!c6DQlUB*AA{e<}`*} z!UR!_d$~8${irAIC*|RTS^z^BAtDhV>hx6pWE9FU@UtKDEMJ>Ml1-h_&KF zq&ifAdL(5>Mu>xLaDIcitNh&RL1)gFDAPt(&NQ&lWD|Iy*F+p^#tH8R8TbEq&>9fvy_VN`gK5)?oZo2740Lb%}F$CznZzUzO zPgH|6$tG==3>?Lkvn7$a;fHo+y;KUUoNax%2rKJfAc* z*zac`vN!$iL|82_<0Z|p& z{q^lOWmIg3unGu4oQ;eG)L>#;9of@7_`p@hQ~?AL@UO?!DeF|YUZN_1f_uj;Dk4pe zZP*)3TVq6(kjtgAs>V^Qst6QW>%8mf?mgke6VHCm*`NK~XVr?>&GYpXs5H-ooJN%&G@!>mfzGq?Wz{4VI~Ey8Wd!*gQB=cDTc1Kn=3$>$$2jt6kCTJHt+ZM z{BpcCn7h0kYd#rU?I}-p0MHOzmbCzbh%1IaNv zkx?LP698t47cAKC$}7KPZG7>?9~<1WQ`EJxF(e-(%kpOXp~7U<2@ug5O3r)Fn@@lH zdEZ@o?PGR$qDxwwTccx*p;nST7#@p`F!4gadu%fR166{mN;W<6q)BhJD7+z~wI;F= zpZ|`fg+@>6OF{PE+i6|=LyO0+M5KVeS0b0(XpX> z@4IK?#`Omud{77;i>ZJpT*Qlt1|YWafvXPdKX|{-ef^6Aix-*LLbDmH+uYf_I=%J9 z7oS;O=tjo3$JC`#_2h|Co@^_fGyuhjK_UZ&iHX!Bpa0G!SPTr($$3^VHK0Rvs`-6# z?g~N>$@+Tc{{H4iR;_$S5|#fJ#!wNp-WZP})7RT`;)y4$Tetr1yY8MlckZfHM z2$RhFC-w=>(1ev|5=RlX$H2&uN3BYd=-uyr*V$)3Yw6Mhk6(RUp1U|o5J4mmVn75S z&XqONJkQHX?~3`yoOJ9bKXpm9TJ4DwQ{#JZ2b@qo=4A)e8ym~9P4jXl31J#@OEH=h zG!QWpGZPXSLqv!Q1a|L$!Rc*!I72^^Qv~!K1R8Npq4po2|Ls3t_?5$!ohrd-fH(#K z2g;zaQus>*Jv~;`r_dgVe({*F8|Rq=##_#1!8!2^#z-Qm z+Q=w?VS~g)7K7Lzw*X3rB|@S=ce zaDe0tdr$_Al;r6^_TP4i2qH-cNJ^Bovef0FTJ4Yk#>Cz`V`d2VF=URT45Dfi?)~u( z-t~@GopRoZr@#KlG1wSYYGaKNPEv+cohjK4A3Pv3F%cEhQ4rZiLDV8xLX4u&}0(?T{bQuR-p<@MKk9e-BPea9O_LWyv z%e{HZ!AIaDqB5ujpl!Q0z4{Gj{@1rJ=qA_YTF^L)8&-Y%(PV!8fq?5EppGiA)YxnatO^k}DQISboQ6>>6Whkim zj^~`c*Xlpcxwmc=gE_$u<*EKU9J>#StAIZSklS`pvc1{;w2#{A#|Noq0g*%_O+8vI87f$ ztTCjrDT9wl5J`wIkl&kaZ%nIifB%{X?*DN%*SX-VD_wUK2s_#dO{kRv|6g7r3IPlh z_vf}O{l?{1mv(h;2=NP^2cZPTVj)-PUitF!wd-E#%)XBi z!&1<5+MMI&oPX5FG2>OT)SK-JtrD(5NFoJ51(bv$EdzF0k9xSs@k$ch5%~6oR0}25Aq#`y*iWq`|7%RQ=7mqsn#M(Q)zxW$B{Pg7G z&l3qqOBh-rqFAXS@&!{d^)QwbhuB*1Ghi{`V5Tb|MSqvs8Ubs=fCyKwUHhfGo(WyJn>A=U#LG5X1@_1^^Ba(E}h*zLfw4NzwwsY>3D~(L^MQ$W`bd zCjaGMUpjmCf3c$U@TWV()$;*Qor6W!Q9crX1rqg42o}?sWa#f7H=*g;rC<8rzr5+p z(_g&evagX%C%r~vG;mY~v>0>nqM2m}>W80pHY)%J;tECxSSoYD+=yji8I_i7Y5%j& z|H}(+{OzUJ&u?x^?b^Q+9gBo)`+|_7dXiFVz*$H{K$KFEHg4SIi4klS6mXsUG1IYkC+5797tFtcupc995I~_mKB?b$0uOohZwd&X!4F1mTU*5_T6~fxkY?y z$yKMn`Q8f#`UC^PitZd%M8ySIAQT8i!q14spvn<_xzZ1jWI3Z_wzjXo`CFIlFRZzA z=_y`I_Jh3}Rl;RZ0HDZ#7)6SZjIqK|Yb?bPMMbgW=m?mojZ#wrZ35Uce#4)Zt=hla{yCK4`-6)|P$bwTi=1WBsa(;>rWiL2%n&_XHH<#^BvSW*lioxQss z`uqD{Tld^r^GiJOGAaG3U4c z;_vSM(uGG)I$nM4&IxR!5@XgNE{Hb-Y^tuH4QJmGyH->zD~OcumsY>N ze#UV#)9G4(P!Fe*USZr297mXns36QM4Q{Gu!{(L$@btrPZd|r- z(b@O^?nYlaRU+`MvBuTLA%%==xd@1YByx}{2$u6>z+qu;1Oh=sCQp;l zpc4!;&;vwTsW|4~!KN6-c&=2haiAc^AtP4-WB@ApHkon-^W>!iQ>d-0S#t4}Pd@oi z`+IgSx#Y6RQ;trjG8I~_fE@O4L9IJBfYYNN7DpBVFSQ8w4LzXjwospfkMHU3m+^0|o4={K^nfYrTEjwqmi^ z+Byai01dv_z8GMrmG7s94+_9ZU{BBh`a~$~gnq8EzPZ0!?b>dCbk`r2KeOt{u_qc+ z3WAQ+udLdH{4nB}OXQx( zEX;z#xp*O9gzE!DY&)g2Mo_L!uvJK;6xU}m#U5^Lob>zOEe}iSJ8%DAAPy31kxRV*Ubu_St9oX8y8g*W7UJ zQ>mJfXP$ZLxUsDbbxkMEm^t={Bfx|Kl^iG8+t=4nSIfZ0SnXI;Ityy9@+pkbCA zNXL*k$?!Zko6R@YjAo0y-KB}+r?J%_s7VbB^k)h}R%jK`>>(mA$k#mek1N|+=G^}E zA7AyArMZ6JY3K*=j72}cmhBtzIoS%Ce9kv$w9;u75O$TL#X@L7fgz%(SE}qb zCRO1?6~Hj0r!;;#sEiiTZ6l5jqKL4UaBU#uxa~VSfYXf)t&WqjHY5d~6F&f};##m4 z6nZP3fBlB9-cpwy!G=e+G~IUFjW56Q?ArBDoH(oJ#+#;$YTc=^y^!6l6_f%%?0I_G zFFX3)*|hyFAHWwVSmr>NvOUKh)BNb)|1=D`NJ${Z_e*TuyYBeOFMs|hh#Q1O!+BI; zL}s)$P%2Wn52k64**Z%jdF3#{(@0%5~H0R&TuE%S#9%4K3f%?p0s8y1#Ev>!`f&dp3OM z%jZtG=YL$j@%=x%x9!)z_|@X6Q}*2V^Cgcy_Fcp6ez@Otb7D3>`NX%DT-rLeZP#xf zyt8EX`!@KeXP#=PA31y0!jq?;zy6I6Y7a~nG#x1DAw3SB0aKs;?DOwGR}AaHsQWGb558#y|pL*%D6Ul^VDNP-EIU=`GRxl zqUl#(bBA(Et6zKcwr|a=t=)9xHB;VrP0u}Jddt{ZPdxHqjo0M+`ljo@d-ct;PMmMfr)=^M731#?$iU=i2Nf3Y|t7{p;(IG?zwrq;Ul325D?b}yhk#=1Q zf|BEqW%IcVG?_Zl{fr2b4okUY%17UAX>Ji9Ylt+uNeE4rl#@!2?eT*&>ntWxBCY5vJccMD55sw2ltnDoW@bML!l`+oz>Zqb{bsNc?3ct5g=62J{tG-f!IjKlqj-+Byu8~@VF^08hkl0Q4)}-X)uyP zjnD!HWOR%`lG$+*;y|cF-8y<|Yt#5J2t8K=SW^0P+M^?u0gIs44zmSIqehJe5K=m} zm?I^E0$va(*Ga2H1|cNOD<~-_mD__tNA(hIKl&J+FtJFgAJ8Y%MW0_>`NnCdo>`l& zj~IynfZ%-j+>7_@?oOsN9JAJA=5g%N8?F6#4;b29Th&!1<*M0GCWe4SZ9T@C3V$9z zf-n#k6et#owYBvfd-o~jCes1~7-b$C_G(B0Xg~%qfFU4j_OxgXmN2KNOW;G#+e1{0EZ>c(U9;x>GvtDSCD@RvA4*w2Y&f!x49dVG)53vY0wRK-0hp9{ZpsE~^&9_o zWZRLh>nTz(I05+i4YZA*IBP5d8DkNNIo2MI@cuBgt)q_Y=`;mDc2WZf%Ke=N#DcIw z1&o?Jwn$VGFhtFpHucV!zMv*G#?=Xc5`Y15fZa`-Hl;F782UrF=~9*X4jgM$^ECc8 z1~pL+R|v+~CNSbNmcI`~p)f326Nn*4)xY!RmPLy$v0#t@1x#Rw+=^xlR65-bL_S0y zS~AQ$&>wn8A{mMRD}_Yhy2{a-NE>4#DBX1vuImwa`}Y-6UOiX~Xe)3OKnXAe0kDq3 zdNB3GEee<|g<)#>@_)N<;q?G%Yb=Y2ymuLbQrOwl?0N|m2H_A9#^_>ZLehGelPUVI z%J2>5RZs;fEvJ+5u^D&Tiy~kU3!-~F{mt*SH#dw_T7w86IgTrQs2N?og39h$B8Mcw z#b%)hAfzA&i>^l-H*U0+i4@zo8B173k8bPhEe6Kez>itgqCm7D14`656S>5QkYS%N z5%`&WK9f$jSytLr!e|VVLMwf*t$xN$6_t{NH+;iK1lu1hj61ARh?+Ix7EF$Rz(gnr zwyvhJYhQsR={lYabWR`u>O=hrfU4zl;HNyuq*j%_HfsaV)#;3@HATiQq<{qkSOgjy zN3aFI6r^=I{SpP>Nc4@O5h{TM07Vd`NP~4h_+a1YQBxC%CISF92-L-b$ssI>43jl+tV$PpD>6pg)rt-W201^Qz zD;Y^y140;tF+OyocC4uDGYaa5I3QIurd8Q$VkqDM2vP}&KqV+C0c4AQR*@15vEU!M zNw2QZ?>LFZeAolxAlC_nR^`jLhkyWNfkIG46rJ58=c0%VW84h{zGl*dqnHJlL5M+vfOdwBqX+woOI+>#WSo@PVHglVj49;` zy>reuY5iNT3WTMSU%8n~OrJ!I7f|kIN5Dv;$D|Jf(06=88Qh zAZD|DG<>bF!n#p}L_mb}NkG)t)0q&6tuR}_aQuYEWq*8vrI1LvWgJzMQb1bRVM_p! z+W_Do8IXvTp+U5T)Yh%N)2A%}s3Xw;Mkr)XlavZJ@NLlv5tu@^FLw zEJ!&AcPXBx#yR|5c1tU*)lELoPKjvHEs zepg&BIch+P7?L1P7ic~pE8MyJNAL6ho zlY?H5EEdUFYpuZ#cXo^$JF&K=9*Hb66GuyNPe%t;7Qm1hUE#4v#e??~BrH+h5l0b< z93oXyPZKBCZQS%00GA~=1i(9@ZANz|wlq!x0Dyo1Nc7;spo+e;Y+wEO@u!byJW3G& z1OP_FB?sPm`{jlfrF4Kup&NM9@u$4Q?hS45{b?&>o{&rrs2c(ePc#X@Vt~( zx=f#C0NAp1D^-&IVfW}b^7zD=p&!jnk?4lCHIU19&0lcZ%dfl;hC$WG)pJfiZ~fYB zB|j36Hy}cwtLk-Bj`?Wa`aMTaoolV96aWj~qJf9LIXwkZYz41p{l3xrW!M$iKK7R5ql z-TIv;%{bd75cUBB03`w&-dVGD&1OVp#J9o-35A-)l#?csDs|dHcTyFFVAYcY?)hMx zmNKRUkt-pyHM@51J?`ie0Z1S~h_+?RVpp$SOG;@%C4#Y}0w5zW#U6{5kfvI-cyMhn zA_a>G3tM1}yOc!Br0h zQK*W_dc;BySoZgH4764pOwN^sxe_pbnyXL>4fu2C&G`LezYa}cAuzOu-7c&0m;dF> zk39UUqH&fJBq<3Jnp4DxU|9sAHW$>i@4$IyU7{086bNN}KWVx2>Z-@TaH@0RqT|=E zS)I@=2nZ@GXaV$Kd=n31+IhIU1Pk+E24Y1GEYj#OA#`_UT_*{MAdH|Q<|2{~Lt~k# zES$s#1qUk5v!CwD)Mc#7w@s)q+_QSs8bZ%vXrM2U;^Zl3yuVd#-)0@EK0g$X5>J3CrRqx9cx|CDO)Z#HykNBIP|D>a%Tf+#1T16?r`fI=X!(aVoSt#jZ z&~3332tbM~Ib(&edFS@hg%{o6sAfP*XaRsZ_xO|lbJ3zH3HO81t@)Nl)wwIDAT8)C z7c7_4RFs%gi>ks~9J&F<%KslPwGL6;rGnl8Vnp6$VPh;ps&jv-uC^J#qCyZQ07Q-j z+u7CSn~*9t_lJZ%52y9>V_tyD6mq$IPj>f*?@pVRes{wP5SMM8SRGnEYyL$IjVEu} ztic({4uMLrR;bf~sm=A*{p8+9PdVv42NML5t&Dkp+jDQd`Ov&M<5XC5uxIhbvtL>H ziiU<{B2)CUKov9yxw8 z5gB8INkkB}Z-b9M+O^=EbLjwIhr?(-I~ZFwhWv(qfenFddi|<*=gvO9v3b`$_n!5g zyRQH#60qkO--J^po;~}N+kf`gFKyqE$Oehs-S&;uA1zH)y^So~TBZcC_=bzKc7Hld2 zucri^uG@Rr!V~}DHxE&7b5do{EgBHZE?5V_3it+WD8z`2$XMx@WJUvMtqqL{C9dxf zh+-Rq43*9u#5nD)hzeIuq?ArTpoyI^tt~_aF9D=AL&(PO?d>fJHcyyvNZ@~%CYFkx z`yqq|%5zE>iBxT3)C(`XJZn~4eO=%JG4OTQows!959&uW9Y1}Z(v4SNbJfh5$8LE0 z)r%HhdD-QE**4~=AOymc)=4%|ji=X`?cclm#z!9c3Q8wp(S%?fR;XP^oiJnUeLr7O zmu`UCBRcv&hy?iv3BbT1JqjaUMuqr`v7XoAvV~Y@`-{RVl}aK;3;+`?jqdL5rsk&5 zn8Ph)!C=l=w3Q8h!SH8iH2t-9i@nYtpV{yie=%L_JsME_63fk+2d`j+;P4&r=K~E5(QY?x)}T z{^CsSc9vaA<$$x`BtZzkMKLUUrX4$acf0vu^FCxpDZ)XOiowlZGPF|w%d-5@q@n7_ z?tuAIQ6!AsU0>ha+nWbbM9~gmBhW@Wdf&c%J`uc literal 0 HcmV?d00001 diff --git a/backend/uploads/messages/23/1763991961_委托服务合同-5000.pdf b/backend/uploads/messages/23/1763991961_委托服务合同-5000.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a1d9383564dad5e2e4267a0ed561746765a3930e GIT binary patch literal 214826 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