Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>某某产品 - 客户咨询端</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; padding: 50px; }
.chat-box { width: 100%; max-width: 500px; height: 60vh; background: #fff; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); display: flex; flex-direction: column; margin: 0 auto; }
.chat-header { padding: 15px; border-bottom: 1px solid #eee; background: #198754; color: white; border-radius: 10px 10px 0 0; }
.chat-messages { flex: 1; padding: 20px; overflow-y: auto; background: #fafafa; }
.chat-input-area { padding: 15px; border-top: 1px solid #eee; }
.message { margin-bottom: 15px; }
.message.system { text-align: center; color: #888; font-size: 0.9em; }
.message.me { text-align: right; color: #198754; }
.message.admin { text-align: left; color: #0d6efd; }
</style>
</head>
<body>

<div class="container text-center mb-4">
<h2>欢迎访问我们的产品主页</h2>
<p class="text-muted">遇到问题?请通过下方的窗口联系人工客服</p>
</div>

<div class="chat-box" id="customerChatBox">
<div class="chat-header">
<h5 class="mb-0">在线人工客服</h5>
</div>
<div class="chat-messages" id="messageBox">
<div class="message system">您好,人工客服正在为您服务...</div>
</div>
<div class="chat-input-area d-flex">
<input type="text" class="form-control me-2" id="messageInput" placeholder="输入您的问题...">
<button class="btn btn-success" id="sendBtn">发送</button>
</div>
</div>

<script src="/socket.io/socket.io.js"></script>
<script src="/public/js/client-chat.js"></script>
</body>
</html>
74 changes: 74 additions & 0 deletions client/public/admin/chat.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人工客服工作台 - 智能客服系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f4f6f9; height: 100vh; display: flex; align-items: center; justify-content: center; }
.login-box { width: 100%; max-width: 400px; padding: 30px; background: #ffffff; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }

.chat-container { width: 100%; max-width: 800px; height: 80vh; background: #fff; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); display: flex; flex-direction: column; }
.chat-header { padding: 15px; border-bottom: 1px solid #eee; background: #0d6efd; color: white; border-radius: 10px 10px 0 0; }
.chat-messages { flex: 1; padding: 20px; overflow-y: auto; background: #f8f9fa; }
.chat-input-area { padding: 15px; border-top: 1px solid #eee; }
.message { margin-bottom: 15px; }
.message.system { text-align: center; color: #888; font-size: 0.9em; }
.message.customer { text-align: left; }
.message.admin { text-align: right; color: #0d6efd; }
</style>
</head>
<body>

<div class="login-box" id="loginBox">
<h3 class="text-center mb-4">客服人员登录</h3>
<form id="loginForm">
<div class="mb-3"><label class="form-label">用户名</label><input type="text" class="form-control" id="username" required></div>
<div class="mb-4"><label class="form-label">密码</label><input type="password" class="form-control" id="password" required></div>
<button type="submit" class="btn btn-primary w-100">登录进入工作台</button>
</form>
</div>

<div class="chat-container d-none" id="chatContainer" style="max-width: 1000px;">
<div class="chat-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">人工客服工作台</h5>
<span class="badge bg-success">已上线</span>
</div>

<div class="d-flex" style="flex: 1; overflow: hidden;">

<div class="customer-info-sidebar p-3 border-end" style="width: 260px; background: #f8f9fa; overflow-y: auto;">
<h6 class="border-bottom pb-2 text-secondary">当前接待客户</h6>
<div id="customerInfoPanel">
<p class="text-muted small mt-3 text-center">暂无客户接入,等待咨询...</p>
</div>
</div>

<div class="chat-main d-flex flex-column" style="flex: 1;">
<div class="chat-messages" id="messageBox">
<div class="message system">等待客户发送消息...</div>
</div>

<div class="chat-input-area">
<div class="mb-2">
<select class="form-select form-select-sm text-secondary" id="quickReplySelect">
<option value="">-- 请选择知识库标准回复 (可编辑) --</option>
<option value="您好,我是专属人工客服,请问有什么可以帮您?">👉 [问候] 标准开场白</option>
<option value="非常抱歉给您带来不便,麻烦您提供一下订单编号,我马上为您核实。">👉 [售后] 索要订单号</option>
</select>
</div>
<div class="d-flex">
<input type="text" class="form-control me-2" id="messageInput" placeholder="请输入回复内容...">
<button class="btn btn-primary" id="sendBtn">发送消息</button>
</div>
</div>
</div>

</div>
</div>

<script src="/socket.io/socket.io.js"></script>
<script src="../js/chat.js"></script>
</body>
</html>
89 changes: 89 additions & 0 deletions client/public/js/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
document.addEventListener('DOMContentLoaded', () => {
const loginForm = document.getElementById('loginForm');
let socket = null;

if (loginForm) {
const quickReplySelect = document.getElementById('quickReplySelect');
if (quickReplySelect) {
quickReplySelect.addEventListener('change', function() {
const selectedText = this.value;
if (selectedText) {
document.getElementById('messageInput').value = selectedText;
this.value = '';
}
});
}
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const usernameInput = document.getElementById('username').value;
const passwordInput = document.getElementById('password').value;

try {
const response = await fetch('/api/chat/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: usernameInput, password: passwordInput })
});
const result = await response.json();

if (result.status === 'success') {
document.getElementById('loginBox').classList.add('d-none');
document.getElementById('chatContainer').classList.remove('d-none');

socket = io();

socket.on('receive_message', (msgData) => {
const messageBox = document.getElementById('messageBox');

if (msgData.sender !== '客服') {
messageBox.innerHTML += `<div class="message customer"><strong>${msgData.sender}:</strong> ${msgData.text}</div>`;

const infoPanel = document.getElementById('customerInfoPanel');
const now = new Date();
const timeStr = now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0');

infoPanel.innerHTML = `
<div class="card border-0 shadow-sm mb-3">
<div class="card-body p-2">
<div class="fw-bold text-primary">👤 ${msgData.sender}</div>
<div class="small text-muted mt-1">接入时间: ${timeStr}</div>
<div class="mt-2 d-flex justify-content-between align-items-center">
<span class="badge bg-warning text-dark" id="statusBadge">未处理</span>
<button class="btn btn-sm btn-outline-success py-0" style="font-size: 0.8rem;"
onclick="document.getElementById('statusBadge').className='badge bg-success'; document.getElementById('statusBadge').innerText='已处理'">
标为已处理
</button>
</div>
</div>
</div>
`;
} else {
messageBox.innerHTML += `<div class="message admin"><strong>${msgData.sender}:</strong> ${msgData.text}</div>`;
}

messageBox.scrollTop = messageBox.scrollHeight;
});

document.getElementById('sendBtn').addEventListener('click', () => {
const inputEle = document.getElementById('messageInput');
const content = inputEle.value.trim();
if (content && socket) {
socket.emit('send_message', { sender: '客服', text: content });

const messageBox = document.getElementById('messageBox');
messageBox.innerHTML += `<div class="message admin"><strong>我:</strong> ${content}</div>`;
messageBox.scrollTop = messageBox.scrollHeight;

inputEle.value = '';
}
});

} else {
alert('登录失败:' + result.message);
}
} catch (error) {
console.error('系统错误:', error);
}
});
}
});
30 changes: 30 additions & 0 deletions client/public/js/client-chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// client/public/js/client-chat.js
document.addEventListener('DOMContentLoaded', () => {
const myNickname = '访客_' + Math.floor(Math.random() * 10000);

const socket = io();

socket.on('receive_message', (msgData) => {
const messageBox = document.getElementById('messageBox');

if (msgData.sender === '客服') {
messageBox.innerHTML += `<div class="message admin"><strong>👩‍💻 ${msgData.sender}:</strong> ${msgData.text}</div>`;
messageBox.scrollTop = messageBox.scrollHeight;
}
});

document.getElementById('sendBtn').addEventListener('click', () => {
const inputEle = document.getElementById('messageInput');
const content = inputEle.value.trim();

if (content && socket) {
socket.emit('send_message', { sender: myNickname, text: content });

const messageBox = document.getElementById('messageBox');
messageBox.innerHTML += `<div class="message me"><strong>我 (${myNickname}):</strong> ${content}</div>`;
messageBox.scrollTop = messageBox.scrollHeight;

inputEle.value = '';
}
});
});
137 changes: 137 additions & 0 deletions docs/api/chat-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# 人工客服模块 API 文档

**模块负责人**:刘浚涵
**基础路径**:`/api/chat`

---

## 1. HTTP 接口

### 1.1 客服登录验证
- **接口说明**:验证人工客服的账号和密码,用于进入工作台。
- **请求地址**:`/api/chat/login`
- **请求方式**:`POST`
- **请求头**:`Content-Type: application/json`
- **请求参数 (Body)**:
```json
{
"username": "admin",
"password": "admin123"
}
```
- **成功响应**:
```json
{
"status": "success",
"data": {
"message": "登录成功",
"user": "admin"
}
}
```
- **失败响应**:
```json
{
"status": "error",
"message": "用户名或密码不正确"
}
```

---

## 2. WebSocket 实时通信事件 (Socket.io)

本模块的核心通信基于 Socket.io,客户端连接成功后,通过监听和触发以下事件进行实时聊天。

### 2.1 发送消息 (`send_message`)
- **触发方**:客户端 (客户页面 或 客服工作台)
- **监听方**:服务端
- **事件说明**:将输入的聊天内容发送给服务端,服务端接收后会自动将记录插入 `chat_records` 数据库表。
- **数据格式**:
```json
{
"sender": "客服",
"text": "您好,我是专属人工客服,请问有什么可以帮您?"
}
```
*(注:如果由客户发送,`sender` 字段应为随机生成的客户昵称,例如 "访客_8372" )*

### 2.2 接收消息 (`receive_message`)
- **触发方**:服务端 (广播)
- **监听方**:客户端 (客户页面 和 客服工作台)
- **事件说明**:服务端处理完 `send_message` 并成功入库后,将该消息体广播给当前在线的所有客户端,用于页面渲染。
- **数据格式**:与发送时的格式完全一致。
```json
{
"sender": "访客_8372",
"text": "你好,我的订单一直没有发货"
}
```

# 人工客服模块 - 使用说明与文件结构解析(人话版)



## 💡 1. 怎么跑起来?(测试流程)

为了测试人工客服与客户的实时聊天功能,请按照以下步骤操作:

1. **启动后端大本营**:
在项目根目录下,打开终端运行 `node server/server.js`。看到终端提示“成功连接到 SQLite 数据库”即为启动成功。
2. **扮演客服(右脑互搏准备)**:
打开浏览器,访问客服专属后台:`http://localhost:3000/public/admin/chat.html`
- **测试账号**:`admin`
- **测试密码**:`admin123` (这个账号是咱们在 db.js 里初始化写进数据库的)
- 登录后,你会看到工作台界面,左侧显示等待接入,右侧是聊天框。
3. **扮演客户(发起咨询)**:
新开一个浏览器标签页(或者换个浏览器),访问商城主页:`http://localhost:3000/index.html`
- 客户不需要登录,系统会自动分配一个类似“访客_1234”的马甲。
- 在右下角的聊天框里随便发一句“你好,有人在吗?”。
4. **见证奇迹**:
切回客服的网页,你会立刻看到客户发来的消息,左侧的客户列表也会自动添加这个客户的信息。此时客服和客户就可以开始实时聊天了。聊天的所有内容都会自动存入 SQLite 数据库的 `chat_records` 表里。

---

## 📂 2. 这个模块涉及了哪些文件?都是干嘛的?

这个模块麻雀虽小五脏俱全,打通了前端、后端和数据库。以下是我负责创建和修改的核心文件:

### 🖥️ 前端部分 (客户看什么,客服看什么)

- **`client/index.html`**
- **角色**:客户的咨询入口(主页)。
- **作用**:画了一个简单的商城主页,并在页面里嵌入了一个客户专用的聊天窗口。
- **`client/public/js/client-chat.js`**
- **角色**:客户聊天窗口的大脑。
- **作用**:帮客户随机生成访客马甲,连接 Socket.io 实时通信,把客户在框里敲的字发给服务器,并把客服的回复展示在屏幕上。

- **`client/public/admin/chat.html`**
- **角色**:人工客服的工作台页面。
- **作用**:包含两部分,一开始是“登录框”,登录成功后隐藏登录框,显示包含“左侧客户列表”和“右侧聊天主界面”的工作台。
- **`client/public/js/chat.js`**
- **角色**:客服工作台的大脑。
- **作用**:
1. 拦截登录按钮,把账号密码发给后端验证。
2. 登录成功后连接 Socket.io。
3. 接收客户消息,动态在左侧生成“客户信息卡片”。
4. 实现了“下拉选择快捷回复”,选中后可以直接进输入框进行编辑。

### ⚙️ 后端部分 (大本营怎么处理数据)

- **`server/routes/chat.js`**
- **角色**:客服登录专属 API 通道。
- **作用**:专门接收客服界面发来的账号密码(`POST /api/chat/login`),然后去查 SQLite 数据库里的 `service_agents` 表。对上了就放行,对不上就报错。

### 🔌 跨模块协作修改 (给服务器打的补丁)

为了让聊天能跑通并存进数据库,我还配合团队的基础文件做了一些联动修改:
- **`server/server.js`**:在里面加入了 Socket.io 的“中转站”代码。收到客户消息就转给客服,收到客服消息就转给客户。**最重要的是,每收到一条消息,都会顺手写进数据库的 `chat_records` 表里。**
- **`server/routes/index.js`**:把我的 `chat.js` 路由像插件一样挂载了上去。

---

## ⚠️ 3. 注意事项

1. **环境依赖**:必须先运行 `npm install` 安装 `express`、`socket.io` 和 `sqlite3` 等核心包。
2. **端口冲突**:如果启动报错说端口被占用,请检查是不是已经有别的终端在跑着 `node server/server.js` 了,把它关掉(Ctrl+C)再重新启动。
3. **数据重置**:如果想清空所有的聊天记录重新测试,可以直接去 `server` 目录下把生成的那个 `database.db` 文件删掉。下次启动服务器时,代码会自动帮你建一个全新的、干净的空数据库。
Loading