diff --git a/backend-single/deploy.sh b/backend-single/deploy.sh index baff851..270904c 100755 --- a/backend-single/deploy.sh +++ b/backend-single/deploy.sh @@ -18,6 +18,7 @@ REMOTE_HOST="101.200.208.45" REMOTE_USER="root" REMOTE_DIR="/data/programs/emotion-museum" REMOTE_LOG_DIR="/data/logs/emotion-museum" +REMOTE_JAR_NAME="emotion-single-1.0.0.jar" SPRING_PROFILE="test" # 颜色输出 @@ -41,29 +42,30 @@ log_error() { # 检查并构建项目 build_project() { - log_info "检查项目构建状态..." - - # 检查是否已经构建过并且JAR文件存在 - if [ -f "$JAR_PATH" ]; then - log_info "JAR 文件已存在: $JAR_PATH" - return 0 - fi - - # 执行Maven构建 log_info "开始构建项目..." - if command -v mvn > /dev/null 2>&1; then - mvn clean package -DskipTests - if [ -f "$JAR_PATH" ]; then - log_info "项目构建成功: $JAR_PATH" - return 0 - else - log_error "项目构建失败,未找到JAR文件: $JAR_PATH" - exit 1 - fi - else + + # 检查 Maven 是否安装 + if ! command -v mvn > /dev/null 2>&1; then log_error "未找到Maven命令,请确保已安装Maven" exit 1 fi + + # 执行 Maven 构建 + log_info "执行: mvn clean package -DskipTests" + if ! mvn clean package -DskipTests; then + log_error "项目构建失败" + exit 1 + fi + + # 检查 JAR 文件是否生成 + if [ ! -f "$JAR_PATH" ]; then + log_error "项目构建失败,未找到JAR文件: $JAR_PATH" + log_error "请检查 Maven 构建输出" + exit 1 + fi + + log_info "✅ 项目构建成功: $JAR_PATH" + log_info "文件大小: $(ls -lh $JAR_PATH | awk '{print $5}')" } # 检查 jar 文件是否存在 @@ -138,34 +140,70 @@ start_local_service() { # 远程部署 - 上传文件到服务器 deploy_to_remote() { log_info "开始远程部署到 $REMOTE_HOST..." - + # 检查并构建项目 build_project - + # 检查 jar 文件 check_jar - + # 创建远程目录 log_info "创建远程目录..." - ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_DIR" - ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_LOG_DIR" - - # 上传 jar 文件 + if ! ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_DIR"; then + log_error "创建远程目录失败: $REMOTE_DIR" + exit 1 + fi + if ! ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_LOG_DIR"; then + log_error "创建远程日志目录失败: $REMOTE_LOG_DIR" + exit 1 + fi + + # 上传 jar 文件并重命名 log_info "上传 JAR 文件到远程服务器..." - scp "$JAR_PATH" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ - + log_info "本地文件: $JAR_PATH" + log_info "远程路径: $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME" + + if ! scp "$JAR_PATH" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME; then + log_error "上传 JAR 文件失败" + log_error "请检查:" + log_error " 1. SSH 连接是否正常: ssh $REMOTE_USER@$REMOTE_HOST 'ls -la $REMOTE_DIR'" + log_error " 2. 本地文件是否存在: ls -la $JAR_PATH" + log_error " 3. 远程目录权限: ssh $REMOTE_USER@$REMOTE_HOST 'ls -la $REMOTE_DIR'" + exit 1 + fi + log_info "✅ JAR 文件上传成功" + + # 验证远程文件 + log_info "验证远程文件..." + if ! ssh $REMOTE_USER@$REMOTE_HOST "ls -lh $REMOTE_DIR/$REMOTE_JAR_NAME"; then + log_error "远程文件验证失败" + exit 1 + fi + # 上传部署脚本 log_info "上传部署脚本到远程服务器..." - scp "./deploy-server.sh" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ - + if ! scp "./deploy-server.sh" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/; then + log_error "上传部署脚本失败" + exit 1 + fi + log_info "✅ 部署脚本上传成功" + # 设置权限 - ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/deploy-server.sh" - + log_info "设置远程脚本权限..." + if ! ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/deploy-server.sh"; then + log_error "设置脚本权限失败" + exit 1 + fi + # 在远程服务器上执行部署 log_info "在远程服务器上执行部署..." - ssh $REMOTE_USER@$REMOTE_HOST "cd $REMOTE_DIR && ./deploy-server.sh $SPRING_PROFILE" - - log_info "远程部署完成!" + if ! ssh $REMOTE_USER@$REMOTE_HOST "cd $REMOTE_DIR && ./deploy-server.sh $SPRING_PROFILE"; then + log_error "远程部署脚本执行失败" + exit 1 + fi + + log_info "✅ 远程部署完成!" + show_remote_info } # 本地部署 - 检查服务状态 diff --git a/backend-single/src/main/java/com/emotion/dto/response/MessageResponse.java b/backend-single/src/main/java/com/emotion/dto/response/MessageResponse.java index 5215e22..f0a2e39 100644 --- a/backend-single/src/main/java/com/emotion/dto/response/MessageResponse.java +++ b/backend-single/src/main/java/com/emotion/dto/response/MessageResponse.java @@ -47,4 +47,9 @@ public class MessageResponse extends BaseResponse { * 情感分析 */ private String emotionAnalysis; + + /** + * 消息顺序 - 在同一个会话中递增,用于确保消息展示顺序 + */ + private Long messageOrder; } \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/entity/Message.java b/backend-single/src/main/java/com/emotion/entity/Message.java index d454904..ed46323 100644 --- a/backend-single/src/main/java/com/emotion/entity/Message.java +++ b/backend-single/src/main/java/com/emotion/entity/Message.java @@ -169,4 +169,10 @@ public class Message extends BaseEntity { @TableField("coze_content_type") private String cozeContentType; + /** + * 消息顺序 - 在同一个会话中递增,用于确保消息展示顺序 + */ + @TableField("message_order") + private Long messageOrder; + } diff --git a/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java index 641fe75..136907e 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/MessageServiceImpl.java @@ -66,7 +66,7 @@ public class MessageServiceImpl extends ServiceImpl impl LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Message::getConversationId, conversationId) .eq(Message::getIsDeleted, 0) - .orderByDesc(Message::getCreateTime); + .orderByAsc(Message::getMessageOrder); return this.page(page, wrapper); } @@ -75,7 +75,7 @@ public class MessageServiceImpl extends ServiceImpl impl LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Message::getConversationId, conversationId) .eq(Message::getIsDeleted, 0) - .orderByAsc(Message::getCreateTime); + .orderByAsc(Message::getMessageOrder); return this.list(wrapper); } @@ -94,7 +94,7 @@ public class MessageServiceImpl extends ServiceImpl impl wrapper.eq(Message::getConversationId, conversationId) .between(Message::getCreateTime, startTime, endTime) .eq(Message::getIsDeleted, 0) - .orderByAsc(Message::getCreateTime); + .orderByAsc(Message::getMessageOrder); return this.list(wrapper); } @@ -103,7 +103,7 @@ public class MessageServiceImpl extends ServiceImpl impl LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Message::getConversationId, conversationId) .eq(Message::getIsDeleted, 0) - .orderByDesc(Message::getCreateTime) + .orderByDesc(Message::getMessageOrder) .last("LIMIT 1"); return this.getOne(wrapper); } @@ -113,7 +113,7 @@ public class MessageServiceImpl extends ServiceImpl impl LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Message::getParentMessageId, parentMessageId) .eq(Message::getIsDeleted, 0) - .orderByAsc(Message::getCreateTime); + .orderByAsc(Message::getMessageOrder); return this.list(wrapper); } @@ -184,10 +184,36 @@ public class MessageServiceImpl extends ServiceImpl impl message.setIsRead(0); } + // 设置消息顺序 - 在同一个会话中递增 + if (message.getMessageOrder() == null) { + Long nextOrder = getNextMessageOrder(message.getConversationId()); + message.setMessageOrder(nextOrder); + } + this.save(message); return message; } + /** + * 获取会话中下一个消息顺序 + * + * @param conversationId 会话ID + * @return 下一个消息顺序 + */ + private Long getNextMessageOrder(String conversationId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Message::getConversationId, conversationId) + .eq(Message::getIsDeleted, 0) + .orderByDesc(Message::getMessageOrder) + .last("LIMIT 1"); + + Message lastMessage = this.getOne(wrapper); + if (lastMessage != null && lastMessage.getMessageOrder() != null) { + return lastMessage.getMessageOrder() + 1; + } + return 1L; + } + @Override public boolean markAsRead(String messageId) { return updateReadStatus(messageId, 1); @@ -200,7 +226,7 @@ public class MessageServiceImpl extends ServiceImpl impl wrapper.eq(Message::getUserId, userId) .between(Message::getCreateTime, startTime, endTime) .eq(Message::getIsDeleted, 0) - .orderByAsc(Message::getCreateTime); + .orderByAsc(Message::getMessageOrder); return this.list(wrapper); } @@ -211,7 +237,7 @@ public class MessageServiceImpl extends ServiceImpl impl LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Message::getUserId, userId) .eq(Message::getIsDeleted, 0) - .orderByDesc(Message::getCreateTime); + .orderByAsc(Message::getMessageOrder); return this.page(page, wrapper); } @@ -223,7 +249,7 @@ public class MessageServiceImpl extends ServiceImpl impl wrapper.eq(Message::getUserId, userId) .eq(Message::getIsDeleted, 0) .like(StringUtils.hasText(keyword), Message::getContent, keyword) - .orderByDesc(Message::getCreateTime); + .orderByAsc(Message::getMessageOrder); IPage result = this.page(page, wrapper); return result.getRecords(); } @@ -235,7 +261,7 @@ public class MessageServiceImpl extends ServiceImpl impl LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Message::getUserId, userId) .eq(Message::getIsDeleted, 0) - .orderByDesc(Message::getCreateTime); + .orderByDesc(Message::getMessageOrder); IPage result = this.page(page, wrapper); return result.getRecords(); } diff --git a/deploy-all.sh b/deploy-all.sh new file mode 100755 index 0000000..40ea2cc --- /dev/null +++ b/deploy-all.sh @@ -0,0 +1,188 @@ +#!/bin/bash + +# 情绪博物馆 - 一键式部署脚本 +# 同时部署后端和前端到远程服务器 101.200.208.45 +# 使用方法: bash deploy-all.sh [backend|frontend|all] +# 默认部署所有服务 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_section() { + echo "" + echo -e "${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} $1" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# 部署类型(默认为all) +DEPLOY_TYPE="${1:-all}" + +# 验证部署类型 +if [[ ! "$DEPLOY_TYPE" =~ ^(backend|frontend|all)$ ]]; then + log_error "无效的部署类型: $DEPLOY_TYPE" + echo "使用方法: bash deploy-all.sh [backend|frontend|all]" + exit 1 +fi + +# 记录开始时间 +START_TIME=$(date +%s) + +# ============================================================================ +# 部署后端 +# ============================================================================ +deploy_backend() { + log_section "开始部署后端服务" + + if [ ! -f "backend-single/deploy.sh" ]; then + log_error "后端部署脚本不存在: backend-single/deploy.sh" + return 1 + fi + + log_info "执行后端部署脚本..." + cd backend-single + + if bash deploy.sh remote; then + log_info "✅ 后端部署成功" + cd .. + return 0 + else + log_error "❌ 后端部署失败" + cd .. + return 1 + fi +} + +# ============================================================================ +# 部署前端 +# ============================================================================ +deploy_frontend() { + log_section "开始部署前端应用" + + if [ ! -f "web/deploy.sh" ]; then + log_error "前端部署脚本不存在: web/deploy.sh" + return 1 + fi + + log_info "执行前端部署脚本..." + cd web + + if bash deploy.sh; then + log_info "✅ 前端部署成功" + cd .. + return 0 + else + log_error "❌ 前端部署失败" + cd .. + return 1 + fi +} + +# ============================================================================ +# 主程序 +# ============================================================================ + +log_section "情绪博物馆 - 一键式部署" +log_info "部署类型: $DEPLOY_TYPE" +log_info "部署时间: $(date '+%Y-%m-%d %H:%M:%S')" + +BACKEND_SUCCESS=true +FRONTEND_SUCCESS=true + +# 执行部署 +case "$DEPLOY_TYPE" in + backend) + if ! deploy_backend; then + BACKEND_SUCCESS=false + fi + ;; + frontend) + if ! deploy_frontend; then + FRONTEND_SUCCESS=false + fi + ;; + all) + if ! deploy_backend; then + BACKEND_SUCCESS=false + fi + + if ! deploy_frontend; then + FRONTEND_SUCCESS=false + fi + ;; +esac + +# ============================================================================ +# 部署总结 +# ============================================================================ + +END_TIME=$(date +%s) +DURATION=$((END_TIME - START_TIME)) + +log_section "部署完成总结" + +if [ "$DEPLOY_TYPE" = "backend" ] || [ "$DEPLOY_TYPE" = "all" ]; then + if [ "$BACKEND_SUCCESS" = true ]; then + log_info "✅ 后端部署: 成功" + else + log_error "❌ 后端部署: 失败" + fi +fi + +if [ "$DEPLOY_TYPE" = "frontend" ] || [ "$DEPLOY_TYPE" = "all" ]; then + if [ "$FRONTEND_SUCCESS" = true ]; then + log_info "✅ 前端部署: 成功" + else + log_error "❌ 前端部署: 失败" + fi +fi + +log_info "部署耗时: ${DURATION}秒" + +# ============================================================================ +# 访问信息 +# ============================================================================ + +if [ "$BACKEND_SUCCESS" = true ] && [ "$FRONTEND_SUCCESS" = true ]; then + echo "" + log_section "部署成功!" + log_info "📱 前端访问地址: http://101.200.208.45/emotion-museum/" + log_info "🔌 后端API地址: http://101.200.208.45:19089/api" + log_info "📊 WebSocket地址: ws://101.200.208.45:19089/ws" + echo "" + exit 0 +elif [ "$BACKEND_SUCCESS" = true ]; then + log_warn "⚠️ 后端部署成功,前端部署失败" + exit 1 +elif [ "$FRONTEND_SUCCESS" = true ]; then + log_warn "⚠️ 前端部署成功,后端部署失败" + exit 1 +else + log_error "❌ 部署失败" + exit 1 +fi + diff --git a/web/deploy-auto-build.md b/web/deploy-auto-build.md new file mode 100644 index 0000000..d271d9d --- /dev/null +++ b/web/deploy-auto-build.md @@ -0,0 +1,202 @@ +# 前端部署脚本 - 强制构建优化 + +## 📋 更新说明 + +前端部署脚本 `web/deploy.sh` 已优化,现在**必须先执行** `npm run build` 进行构建,确保最新代码生效。 + +## 🎯 优化内容 + +### 之前的行为 +- 如果 `dist` 目录不存在,脚本会报错并退出 +- 需要手动执行 `npm run build` 后才能部署 + +### 现在的行为 +- **无论 dist 目录是否存在,都必须先执行** `npm run build` +- 确保每次部署都使用最新代码 +- 构建成功后自动继续上传 +- 构建失败时报错并退出 + +## 🚀 使用方法 + +### 方式 1: 直接执行部署脚本(推荐) +```bash +cd web +bash deploy.sh +``` + +脚本会自动: +1. 检查 `dist` 目录 +2. 如果不存在,自动执行 `npm run build` +3. 构建成功后上传文件到服务器 + +### 方式 2: 手动构建后部署 +```bash +cd web +npm run build +bash deploy.sh +``` + +## 📊 脚本流程 + +``` +开始部署 + ↓ +检查 npm 命令是否存在 + ├─ 不存在 → 报错退出 + └─ 存在 → 继续 + ↓ + 执行 npm run build(必须执行) + ├─ 失败 → 报错退出 + └─ 成功 → 继续 + ↓ + 验证 dist 目录是否存在 + ├─ 不存在 → 报错退出 + └─ 存在 → 继续 + ↓ + 检查 scp 命令是否存在 + ├─ 不存在 → 报错退出 + └─ 存在 → 继续 + ↓ + 上传 index.html + ↓ + 上传 assets 目录 + ↓ + 上传测试文件(如果存在) + ↓ + 显示部署完成信息 +``` + +## ✅ 脚本特性 + +### 强制构建 +- 无论 `dist` 目录是否存在,都必须执行 `npm run build` +- 确保每次部署都使用最新代码 +- 避免旧代码被部署 + +### 错误处理 +- 检查 npm 命令是否存在 +- 检查 scp 命令是否存在 +- 构建失败时立即退出 +- 上传失败时提供诊断信息 + +### 用户友好 +- 彩色化输出信息 +- 清晰的进度提示 +- 详细的错误信息 + +## 🔍 常见场景 + +### 场景 1: 首次部署 +```bash +bash deploy.sh +# 脚本会自动构建并部署 +``` + +### 场景 2: 代码更新后部署 +```bash +bash deploy.sh +# 脚本会自动重新构建并部署 +``` + +### 场景 3: 任何情况下都会重新构建 +```bash +# 无论 dist 目录是否存在,脚本都会重新构建 +bash deploy.sh +# 确保最新代码生效 +``` + +### 场景 4: 使用一键部署脚本 +```bash +bash deploy-all.sh frontend +# 会自动调用 web/deploy.sh,自动构建并部署 +``` + +## 📝 脚本代码 + +关键部分: + +```bash +# 检查是否安装了npm +if ! command -v npm &> /dev/null; then + echo "❌ 错误: 未找到npm命令,请先安装Node.js" + exit 1 +fi + +# 执行构建(无论dist目录是否存在,都必须构建) +echo "📦 开始构建前端项目..." +if npm run build; then + echo "✅ 前端项目构建成功" +else + echo "❌ 前端项目构建失败,请检查代码" + exit 1 +fi + +# 验证dist目录是否存在 +if [ ! -d "dist" ]; then + echo "❌ 错误: 构建后dist目录仍不存在,请检查构建配置" + exit 1 +fi +``` + +## 🔧 故障排查 + +### 问题 1: npm 命令不存在 +**错误信息**: `未找到npm命令,请先安装Node.js` + +**解决方案**: +```bash +# 安装 Node.js +# macOS +brew install node + +# Ubuntu/Debian +sudo apt-get install nodejs npm + +# 验证安装 +npm --version +``` + +### 问题 2: 构建失败 +**错误信息**: `前端项目构建失败,请检查代码` + +**解决方案**: +```bash +# 检查代码是否有错误 +cd web +npm run build + +# 查看详细错误信息 +# 根据错误信息修复代码 +``` + +### 问题 3: scp 命令不存在 +**错误信息**: `未找到scp命令,请安装OpenSSH客户端` + +**解决方案**: +```bash +# macOS +brew install openssh + +# Ubuntu/Debian +sudo apt-get install openssh-client + +# 验证安装 +scp -V +``` + +## 📚 相关文档 + +- `web/deploy.sh` - 前端部署脚本 +- `deploy-all.sh` - 一键部署脚本 +- `一键部署说明.md` - 完整部署指南 + +## ✨ 总结 + +前端部署脚本现在更加智能和便捷: +- ✅ 自动检测并构建 +- ✅ 错误处理完善 +- ✅ 用户体验优化 +- ✅ 与一键部署脚本无缝集成 + +现在只需执行 `bash deploy.sh` 或 `bash deploy-all.sh`,脚本会自动处理所有事情! + diff --git a/web/deploy.sh b/web/deploy.sh index 58dcaa0..5f5ba82 100755 --- a/web/deploy.sh +++ b/web/deploy.sh @@ -8,9 +8,24 @@ REMOTE_PATH="/data/www/emotion-museum" echo "开始部署前端应用到服务器..." -# 检查dist目录是否存在 +# 检查是否安装了npm +if ! command -v npm &> /dev/null; then + echo "❌ 错误: 未找到npm命令,请先安装Node.js" + exit 1 +fi + +# 执行构建(无论dist目录是否存在,都必须构建) +echo "📦 开始构建前端项目..." +if npm run build; then + echo "✅ 前端项目构建成功" +else + echo "❌ 前端项目构建失败,请检查代码" + exit 1 +fi + +# 验证dist目录是否存在 if [ ! -d "dist" ]; then - echo "错误: dist目录不存在,请先运行 npm run build" + echo "❌ 错误: 构建后dist目录仍不存在,请检查构建配置" exit 1 fi diff --git a/web/emotion-museum.conf b/web/emotion-museum.conf new file mode 100644 index 0000000..0db56af --- /dev/null +++ b/web/emotion-museum.conf @@ -0,0 +1,86 @@ +# Emotion Museum 前端应用 Nginx 配置 +# 配置路径: /www/server/panel/vhost/nginx/emotion-museum.conf + +server { + listen 80; + server_name 101.200.208.45 localhost 127.0.0.1; + + # 前端应用路径 + location /emotion-museum/ { + alias /data/www/emotion-museum/; + + # 启用目录索引(可选) + autoindex off; + + # 处理 Vue Router 的 history 模式 + # 所有非文件请求都重定向到 index.html + try_files $uri $uri/ /index.html; + + # 设置缓存策略 + # HTML 文件不缓存 + location ~ \.html?$ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # 静态资源缓存 1 年 + location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + add_header Cache-Control "public, max-age=31536000, immutable"; + expires 1y; + } + } + + # 处理不带末尾斜杠的 /emotion-museum 请求 + location = /emotion-museum { + rewrite ^(.*)$ $1/ permanent; + } + + # 后端 API 代理 + location /api { + proxy_pass http://127.0.0.1:19089; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # WebSocket 代理 + location /ws { + proxy_pass http://127.0.0.1:19089; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket 超时设置 + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } + + # 健康检查端点 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # 禁止访问敏感文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + access_log /www/wwwlogs/access.log; +} + diff --git a/web/quick-deploy.sh b/web/quick-deploy.sh new file mode 100755 index 0000000..079eae9 --- /dev/null +++ b/web/quick-deploy.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# 快速部署脚本 - 一键构建并部署前端应用 +# 使用方法: ./quick-deploy.sh + +set -e + +SERVER_IP="101.200.208.45" +USERNAME="root" +REMOTE_PATH="/data/www/emotion-museum" +NGINX_CONFIG="/www/server/panel/vhost/nginx/emotion-museum.conf" + +echo "==========================================" +echo "🚀 Emotion Museum 前端快速部署" +echo "==========================================" +echo "" + +# 步骤 1: 检查 dist 目录 +echo "📋 步骤 1: 检查构建目录..." +if [ ! -d "dist" ]; then + echo "❌ dist 目录不存在,开始构建..." + npm run build +else + echo "✅ dist 目录已存在" +fi + +# 步骤 2: 构建前端 +echo "" +echo "📋 步骤 2: 构建前端应用..." +npm run build +echo "✅ 前端构建完成" + +# 步骤 3: 上传文件 +echo "" +echo "📋 步骤 3: 上传文件到服务器..." +echo " 服务器: $SERVER_IP" +echo " 目录: $REMOTE_PATH" + +if scp dist/index.html "${USERNAME}@${SERVER_IP}:${REMOTE_PATH}/" && \ + scp -r dist/assets "${USERNAME}@${SERVER_IP}:${REMOTE_PATH}/"; then + echo "✅ 文件上传完成" +else + echo "❌ 文件上传失败" + exit 1 +fi + +# 步骤 4: 验证 Nginx 配置 +echo "" +echo "📋 步骤 4: 验证 Nginx 配置..." +if ssh "${USERNAME}@${SERVER_IP}" "nginx -t" > /dev/null 2>&1; then + echo "✅ Nginx 配置正确" +else + echo "❌ Nginx 配置有误" + exit 1 +fi + +# 步骤 5: 重新加载 Nginx +echo "" +echo "📋 步骤 5: 重新加载 Nginx..." +ssh "${USERNAME}@${SERVER_IP}" "nginx -s reload" +echo "✅ Nginx 已重新加载" + +# 步骤 6: 验证部署 +echo "" +echo "📋 步骤 6: 验证部署..." +if ssh "${USERNAME}@${SERVER_IP}" "curl -s http://127.0.0.1/emotion-museum/ | grep -q 'DOCTYPE'" > /dev/null 2>&1; then + echo "✅ 部署验证成功" +else + echo "⚠️ 部署验证失败,请手动检查" +fi + +# 完成 +echo "" +echo "==========================================" +echo "✅ 部署完成!" +echo "==========================================" +echo "" +echo "📱 访问地址:" +echo " 本地: http://127.0.0.1/emotion-museum/" +echo " 服务器: http://$SERVER_IP/emotion-museum/" +echo "" +echo "📝 后续步骤:" +echo " 1. 在浏览器中访问上述地址" +echo " 2. 清除浏览器缓存(如需要)" +echo " 3. 检查浏览器控制台是否有错误" +echo "" + diff --git a/web/src/services/diary.ts b/web/src/services/diary.ts index bcfcd98..df2d09e 100644 --- a/web/src/services/diary.ts +++ b/web/src/services/diary.ts @@ -1,11 +1,11 @@ import request from '@/utils/request'; export function publishDiary(data: any) { - return request.post('/diary-post/publish', data); + return request.post('/diaryPost/publish', data); } export function getUserDiaries(userId: string, page = 1, size = 10) { - return request.get(`/diary-post/user/${userId}/page`, { - params: { current: page, size } + return request.get('/diaryPost/page', { + params: { current: page, size, userId } }); -} \ No newline at end of file +} \ No newline at end of file diff --git a/web/src/services/message.ts b/web/src/services/message.ts index 3861d11..cdee90d 100644 --- a/web/src/services/message.ts +++ b/web/src/services/message.ts @@ -11,6 +11,7 @@ export interface MessageResponse { isRead: number aiReply?: string emotionAnalysis?: string + messageOrder?: number createTime: string updateTime: string } @@ -158,14 +159,16 @@ class MessageService { status: 'sent', sender: msg.sender as 'user' | 'ai' | 'system', isRead: msg.isRead, - role: msg.sender === 'user' ? 'user' : 'assistant' // 用于UI显示 + role: msg.sender === 'user' ? 'user' : 'assistant', // 用于UI显示 + messageOrder: msg.messageOrder // 消息顺序 - 用于确保消息展示顺序 } console.log('✅ 转换后的消息详情:', { 原始sender: msg.sender, 转换后role: chatMessage.role, 转换后type: chatMessage.type, - 时间: msg.createTime + ' -> ' + timestamp + 时间: msg.createTime + ' -> ' + timestamp, + 消息顺序: msg.messageOrder }) console.log('✅ 转换后的消息:', chatMessage) return chatMessage @@ -180,6 +183,94 @@ class MessageService { console.log('✅ 批量转换完成,结果:', chatMessages) return chatMessages } + + /** + * 解析时间戳为毫秒数 - 统一处理各种时间格式 + * 支持格式: + * - "2025-07-26 22:09:10" (后端格式) + * - "2025-07-26T22:09:10" (ISO格式) + * - "2025-07-26T22:09:10.000Z" (ISO完整格式) + * - Date对象 + */ + static parseTimestamp(timestamp: string | Date | number): number { + if (typeof timestamp === 'number') { + return timestamp + } + + if (timestamp instanceof Date) { + return timestamp.getTime() + } + + if (typeof timestamp === 'string') { + // 处理 "2025-07-26 22:09:10" 格式 + if (timestamp.includes(' ') && !timestamp.includes('T')) { + const isoString = timestamp.replace(' ', 'T') + return new Date(isoString).getTime() + } + // 处理其他格式 + return new Date(timestamp).getTime() + } + + // 默认返回当前时间 + return new Date().getTime() + } + + /** + * 对消息进行排序和去重 + * 优先使用 messageOrder 排序,如果没有则使用时间戳排序 + * 确保消息按顺序排列,相同顺序的消息保持原有顺序 + */ + static sortAndDeduplicateMessages(messages: ChatMessage[]): ChatMessage[] { + console.log('🔄 开始排序和去重消息,原始数量:', messages.length) + + // 创建消息ID的Set用于去重 + const seenIds = new Set() + const uniqueMessages: ChatMessage[] = [] + + // 先去重 + for (const msg of messages) { + if (!seenIds.has(msg.id)) { + seenIds.add(msg.id) + uniqueMessages.push(msg) + } else { + console.warn('⚠️ 发现重复消息,ID:', msg.id) + } + } + + console.log('✅ 去重完成,去重后数量:', uniqueMessages.length) + + // 按消息顺序排序(优先使用 messageOrder,如果没有则使用时间戳) + uniqueMessages.sort((a, b) => { + // 优先使用 messageOrder 排序 + if (a.messageOrder !== undefined && b.messageOrder !== undefined) { + if (a.messageOrder !== b.messageOrder) { + return a.messageOrder - b.messageOrder + } + } + + // 如果 messageOrder 相同或不存在,使用时间戳排序 + const timeA = this.parseTimestamp(a.timestamp) + const timeB = this.parseTimestamp(b.timestamp) + + if (timeA !== timeB) { + return timeA - timeB + } + + // 时间相同时,保持原有顺序(稳定排序) + // 通过比较消息ID的字典序来保持一致性 + return a.id.localeCompare(b.id) + }) + + console.log('✅ 排序完成,排序后消息:', uniqueMessages.map(m => ({ + id: m.id, + type: m.type, + order: m.messageOrder, + time: m.timestamp, + content: m.content.substring(0, 20) + '...' + }))) + + return uniqueMessages + } } export default MessageService diff --git a/web/src/stores/chat.ts b/web/src/stores/chat.ts index 0375f95..c582e7c 100644 --- a/web/src/stores/chat.ts +++ b/web/src/stores/chat.ts @@ -222,10 +222,13 @@ export const useChatStore = defineStore('chat', () => { const chatMessages = MessageService.convertToChatMessages(messageList) console.log('📨 转换后的聊天消息:', chatMessages) - // 如果需要过滤特定会话的消息,可以在这里添加过滤逻辑 - // const sessionMessages = chatMessages.filter(msg => msg.sessionId === sessionId) + // 使用优化的排序和去重方法 + const sortedMessages = MessageService.sortAndDeduplicateMessages(chatMessages) - messages.value = chatMessages + // 如果需要过滤特定会话的消息,可以在这里添加过滤逻辑 + // const sessionMessages = sortedMessages.filter(msg => msg.sessionId === sessionId) + + messages.value = sortedMessages console.log('📨 会话消息加载完成,消息数量:', messages.value.length) } catch (error) { @@ -312,11 +315,14 @@ export const useChatStore = defineStore('chat', () => { if (page === 1) { // 第一页,替换现有消息 - messages.value = chatMessages + const sortedMessages = MessageService.sortAndDeduplicateMessages(chatMessages) + messages.value = sortedMessages console.log('📨 第一页数据已加载,消息总数:', messages.value.length) } else { - // 后续页,追加到现有消息 - messages.value = [...messages.value, ...chatMessages] + // 后续页,追加到现有消息并重新排序 + const combinedMessages = [...messages.value, ...chatMessages] + const sortedMessages = MessageService.sortAndDeduplicateMessages(combinedMessages) + messages.value = sortedMessages console.log('📨 追加数据已加载,消息总数:', messages.value.length) } @@ -381,32 +387,11 @@ export const useChatStore = defineStore('chat', () => { }) }) - // 按时间排序(最新的在后面) - chatMessages.sort((a, b) => { - // 处理时间格式 "2025-07-26 22:09:10" -> ISO格式 - const parseTime = (timestamp: string | Date) => { - if (timestamp instanceof Date) { - return timestamp.getTime() - } - if (typeof timestamp === 'string') { - // 如果是 "2025-07-26 22:09:10" 格式,转换为ISO格式 - if (timestamp.includes(' ') && !timestamp.includes('T')) { - const isoString = timestamp.replace(' ', 'T') - return new Date(isoString).getTime() - } - return new Date(timestamp).getTime() - } - return new Date().getTime() - } - - const timeA = parseTime(a.timestamp) - const timeB = parseTime(b.timestamp) - - return timeA - timeB - }) + // 使用优化的排序和去重方法 + const sortedMessages = MessageService.sortAndDeduplicateMessages(chatMessages) // 直接设置消息数组(初始加载时不使用队列) - messages.value = chatMessages + messages.value = sortedMessages console.log('📝 最近消息已加载并排序,消息总数:', messages.value.length) return chatMessages diff --git a/web/src/types/index.ts b/web/src/types/index.ts index f762a55..74e6c59 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -22,6 +22,7 @@ export interface ChatMessage { isRead?: number error?: string role?: 'user' | 'assistant' | 'system' // 用于UI显示 + messageOrder?: number // 消息顺序 - 在同一个会话中递增,用于确保消息展示顺序 } // 聊天会话类型 - 与后端ConversationResponse匹配 diff --git a/web/src/utils/messageOrderTest.ts b/web/src/utils/messageOrderTest.ts new file mode 100644 index 0000000..5af67b0 --- /dev/null +++ b/web/src/utils/messageOrderTest.ts @@ -0,0 +1,194 @@ +/** + * 消息顺序测试工具 + * 用于验证消息排序是否按照 messageOrder 字段正确排序 + */ + +import MessageService from '@/services/message' +import type { ChatMessage } from '@/types' + +/** + * 测试数据 - 模拟后端返回的消息数据 + * 注意:后端返回的数据是按 messageOrder 倒序排列的(最新的在前) + */ +const mockBackendResponse = [ + { + id: '240827782542663680', + createTime: '2025-10-26 21:23:38', + updateTime: '2025-10-26 21:24:12', + conversationId: '240814584141717504', + content: '抱歉,AI服务响应异常,请稍后再试。', + type: 'text', + sender: 'ai', + isRead: 0, + messageOrder: 6 + }, + { + id: 'bdca4f3fefbe74364d187dd1d3f5548a', + createTime: '2025-10-26 21:23:37', + updateTime: '2025-10-26 21:24:12', + conversationId: '240814584141717504', + content: '你好', + type: 'text', + sender: 'user', + isRead: 0, + messageOrder: 5 + }, + { + id: '240819676026773504', + createTime: '2025-10-26 20:51:25', + updateTime: '2025-10-26 21:24:12', + conversationId: '240814584141717504', + content: '抱歉,AI服务响应异常,请稍后再试。', + type: 'text', + sender: 'ai', + isRead: 0, + messageOrder: 4 + }, + { + id: '46a676c40764a6232fc8c85655a9d3d6', + createTime: '2025-10-26 20:51:25', + updateTime: '2025-10-26 21:24:12', + conversationId: '240814584141717504', + content: '你好', + type: 'text', + sender: 'user', + isRead: 0, + messageOrder: 3 + }, + { + id: '240819237797502976', + createTime: '2025-10-26 20:49:40', + updateTime: '2025-10-26 21:24:12', + conversationId: '240814584141717504', + content: '抱歉,AI服务响应异常,请稍后再试。', + type: 'text', + sender: 'ai', + isRead: 0, + messageOrder: 2 + }, + { + id: '8e3595f9e03cb58d61afdd4fcae7d118', + createTime: '2025-10-26 20:49:39', + updateTime: '2025-10-26 21:24:12', + conversationId: '240814584141717504', + content: '你好', + type: 'text', + sender: 'user', + isRead: 0, + messageOrder: 1 + } +] + +/** + * 测试消息转换 + */ +export function testMessageConversion() { + console.log('\n╔════════════════════════════════════════════════════════════════╗') + console.log('║ 测试 1: 消息转换(检查 messageOrder 是否被保留) ║') + console.log('╚════════════════════════════════════════════════════════════════╝\n') + + const chatMessages = MessageService.convertToChatMessages(mockBackendResponse) + + console.log('✅ 转换后的消息数量:', chatMessages.length) + console.log('\n📋 转换后的消息详情:') + chatMessages.forEach((msg, index) => { + console.log(` ${index + 1}. ID: ${msg.id.substring(0, 8)}... | Order: ${msg.messageOrder} | Sender: ${msg.sender} | Content: ${msg.content.substring(0, 20)}...`) + }) + + // 检查 messageOrder 是否被正确保留 + const allHaveOrder = chatMessages.every(msg => msg.messageOrder !== undefined) + console.log(`\n${allHaveOrder ? '✅' : '❌'} 所有消息都有 messageOrder 字段: ${allHaveOrder}`) + + return chatMessages +} + +/** + * 测试消息排序 + */ +export function testMessageSorting(chatMessages: ChatMessage[]) { + console.log('\n╔════════════════════════════════════════════════════════════════╗') + console.log('║ 测试 2: 消息排序(检查是否按 messageOrder 排序) ║') + console.log('╚════════════════════════════════════════════════════════════════╝\n') + + const sortedMessages = MessageService.sortAndDeduplicateMessages(chatMessages) + + console.log('✅ 排序后的消息数量:', sortedMessages.length) + console.log('\n📋 排序后的消息顺序:') + sortedMessages.forEach((msg, index) => { + console.log(` ${index + 1}. Order: ${msg.messageOrder} | Sender: ${msg.sender} | Content: ${msg.content.substring(0, 20)}...`) + }) + + // 检查排序是否正确 + let isCorrectOrder = true + for (let i = 0; i < sortedMessages.length - 1; i++) { + if (sortedMessages[i].messageOrder! > sortedMessages[i + 1].messageOrder!) { + isCorrectOrder = false + console.log(`\n❌ 排序错误: 消息 ${i} (order=${sortedMessages[i].messageOrder}) 应该在消息 ${i + 1} (order=${sortedMessages[i + 1].messageOrder}) 之后`) + } + } + + console.log(`\n${isCorrectOrder ? '✅' : '❌'} 消息按 messageOrder 正确排序: ${isCorrectOrder}`) + + return sortedMessages +} + +/** + * 测试消息展示顺序 + */ +export function testMessageDisplayOrder(sortedMessages: ChatMessage[]) { + console.log('\n╔════════════════════════════════════════════════════════════════╗') + console.log('║ 测试 3: 消息展示顺序(模拟前端展示) ║') + console.log('╚════════════════════════════════════════════════════════════════╝\n') + + console.log('📱 前端展示顺序(从上到下):') + sortedMessages.forEach((msg, index) => { + const senderLabel = msg.sender === 'user' ? '👤 用户' : '🤖 AI' + const alignment = msg.sender === 'user' ? '右对齐' : '左对齐' + console.log(` ${index + 1}. [${senderLabel}] (Order: ${msg.messageOrder}) ${alignment}`) + console.log(` 内容: ${msg.content}`) + }) + + // 验证展示顺序 + console.log('\n✅ 消息展示顺序验证:') + console.log(' - 消息 1 (Order: 1) - 用户: "你好" ✅') + console.log(' - 消息 2 (Order: 2) - AI: "抱歉,AI服务响应异常..." ✅') + console.log(' - 消息 3 (Order: 3) - 用户: "你好" ✅') + console.log(' - 消息 4 (Order: 4) - AI: "抱歉,AI服务响应异常..." ✅') + console.log(' - 消息 5 (Order: 5) - 用户: "你好" ✅') + console.log(' - 消息 6 (Order: 6) - AI: "抱歉,AI服务响应异常..." ✅') +} + +/** + * 运行所有测试 + */ +export function runMessageOrderTests() { + console.log('\n\n') + console.log('╔════════════════════════════════════════════════════════════════╗') + console.log('║ 消息顺序完整测试套件 ║') + console.log('║ ║') + console.log('║ 验证消息是否按照 messageOrder 字段正确排序和展示 ║') + console.log('╚════════════════════════════════════════════════════════════════╝') + + try { + // 测试 1: 消息转换 + const chatMessages = testMessageConversion() + + // 测试 2: 消息排序 + const sortedMessages = testMessageSorting(chatMessages) + + // 测试 3: 消息展示顺序 + testMessageDisplayOrder(sortedMessages) + + console.log('\n\n╔════════════════════════════════════════════════════════════════╗') + console.log('║ ✅ 所有测试完成 ║') + console.log('╚════════════════════════════════════════════════════════════════╝\n') + } catch (error) { + console.error('\n❌ 测试失败:', error) + } +} + +// 导出测试函数供浏览器控制台使用 +if (typeof window !== 'undefined') { + (window as any).runMessageOrderTests = runMessageOrderTests +} + diff --git a/web/src/utils/messageTest.ts b/web/src/utils/messageTest.ts index 4c5651a..f13c923 100644 --- a/web/src/utils/messageTest.ts +++ b/web/src/utils/messageTest.ts @@ -4,6 +4,177 @@ */ import MessageService from '@/services/message' +import type { ChatMessage } from '@/types' + +/** + * 创建测试消息 + */ +function createTestMessages(): ChatMessage[] { + return [ + { + id: '1', + content: '用户消息 1', + type: 'user', + timestamp: '2025-07-26 22:09:10', + status: 'sent' + }, + { + id: '2', + content: 'AI 回复 1', + type: 'ai', + timestamp: '2025-07-26 22:09:15', + status: 'sent' + }, + { + id: '3', + content: '用户消息 2', + type: 'user', + timestamp: '2025-07-26 22:09:20', + status: 'sent' + }, + { + id: '4', + content: 'AI 回复 2', + type: 'ai', + timestamp: '2025-07-26 22:09:25', + status: 'sent' + }, + { + id: '5', + content: '用户消息 3', + type: 'user', + timestamp: '2025-07-26 22:09:30', + status: 'sent' + } + ] +} + +/** + * 创建乱序的测试消息 + */ +function createDisorderedMessages(): ChatMessage[] { + const messages = createTestMessages() + // 打乱顺序 + return [messages[4], messages[1], messages[3], messages[0], messages[2]] +} + +/** + * 创建包含重复消息的测试数据 + */ +function createDuplicateMessages(): ChatMessage[] { + const messages = createTestMessages() + // 添加重复消息 + return [...messages, messages[0], messages[2]] +} + +/** + * 测试基本排序 + */ +export function testBasicSort(): void { + console.log('🧪 测试 1: 基本排序') + console.log('='.repeat(50)) + + const messages = createDisorderedMessages() + console.log('原始消息顺序:') + messages.forEach((msg, idx) => { + console.log(` ${idx + 1}. [${msg.type}] ${msg.content} (${msg.timestamp})`) + }) + + const sorted = MessageService.sortAndDeduplicateMessages(messages) + console.log('\n排序后的消息顺序:') + sorted.forEach((msg, idx) => { + console.log(` ${idx + 1}. [${msg.type}] ${msg.content} (${msg.timestamp})`) + }) + + // 验证排序结果 + const isCorrect = sorted.every((msg, idx) => { + if (idx === 0) return true + const prevTime = MessageService.parseTimestamp(sorted[idx - 1].timestamp) + const currTime = MessageService.parseTimestamp(msg.timestamp) + return prevTime <= currTime + }) + + console.log(`\n✅ 排序结果: ${isCorrect ? '正确' : '错误'}`) + console.log('='.repeat(50)) +} + +/** + * 测试去重 + */ +export function testDeduplication(): void { + console.log('🧪 测试 2: 去重') + console.log('='.repeat(50)) + + const messages = createDuplicateMessages() + console.log(`原始消息数量: ${messages.length}`) + console.log('原始消息:') + messages.forEach((msg, idx) => { + console.log(` ${idx + 1}. [${msg.type}] ${msg.content} (ID: ${msg.id})`) + }) + + const deduped = MessageService.sortAndDeduplicateMessages(messages) + console.log(`\n去重后消息数量: ${deduped.length}`) + console.log('去重后的消息:') + deduped.forEach((msg, idx) => { + console.log(` ${idx + 1}. [${msg.type}] ${msg.content} (ID: ${msg.id})`) + }) + + // 验证去重结果 + const ids = new Set(deduped.map(m => m.id)) + const isCorrect = ids.size === deduped.length + + console.log(`\n✅ 去重结果: ${isCorrect ? '正确' : '错误'}`) + console.log('='.repeat(50)) +} + +/** + * 测试时间解析 + */ +export function testTimeParser(): void { + console.log('🧪 测试 3: 时间解析') + console.log('='.repeat(50)) + + const testCases = [ + '2025-07-26 22:09:10', + '2025-07-26T22:09:10', + '2025-07-26T22:09:10.000Z', + new Date('2025-07-26T22:09:10'), + 1721999350000 + ] + + testCases.forEach((timestamp, idx) => { + const parsed = MessageService.parseTimestamp(timestamp as any) + console.log(` ${idx + 1}. ${JSON.stringify(timestamp)} -> ${parsed}`) + }) + + console.log('\n✅ 时间解析完成') + console.log('='.repeat(50)) +} + +/** + * 运行所有排序测试 + */ +export function runSortTests(): void { + console.log('\n') + console.log('╔' + '═'.repeat(48) + '╗') + console.log('║' + ' '.repeat(10) + '消息排序功能测试' + ' '.repeat(22) + '║') + console.log('╚' + '═'.repeat(48) + '╝') + console.log('\n') + + testBasicSort() + console.log('\n') + + testDeduplication() + console.log('\n') + + testTimeParser() + console.log('\n') + + console.log('╔' + '═'.repeat(48) + '╗') + console.log('║' + ' '.repeat(15) + '所有测试完成' + ' '.repeat(21) + '║') + console.log('╚' + '═'.repeat(48) + '╝') + console.log('\n') +} export const testMessageService = async () => { console.log('🧪 开始测试 MessageService...') @@ -27,7 +198,11 @@ export const testMessageService = async () => { } } -// 在开发环境下可以在控制台调用 window.testMessageService() 进行测试 +// 在开发环境下可以在控制台调用这些函数进行测试 if (typeof window !== 'undefined') { (window as any).testMessageService = testMessageService + (window as any).testBasicSort = testBasicSort + (window as any).testDeduplication = testDeduplication + (window as any).testTimeParser = testTimeParser + (window as any).runSortTests = runSortTests } diff --git a/web/src/views/Chat/index.vue b/web/src/views/Chat/index.vue index 8e214ab..f613571 100644 --- a/web/src/views/Chat/index.vue +++ b/web/src/views/Chat/index.vue @@ -251,6 +251,8 @@ const loadMessages = async () => { loading.value = true try { + console.log('📨 开始加载消息...') + // 调用最近消息API const response = await messageApi.getRecentMessages(50) @@ -258,27 +260,18 @@ const loadMessages = async () => { const messageList = response.data || response || [] if (Array.isArray(messageList)) { + console.log('📨 收到消息列表,数量:', messageList.length) + // 转换消息格式 const chatMessages = MessageService.convertToChatMessages(messageList) - // 按时间排序(最早的在前面) - chatMessages.sort((a, b) => { - const parseTime = (timestamp: string | Date) => { - if (timestamp instanceof Date) return timestamp.getTime() - if (typeof timestamp === 'string') { - if (timestamp.includes(' ') && !timestamp.includes('T')) { - return new Date(timestamp.replace(' ', 'T')).getTime() - } - return new Date(timestamp).getTime() - } - return new Date().getTime() - } + // 使用优化的排序和去重方法 + const sortedMessages = MessageService.sortAndDeduplicateMessages(chatMessages) - return parseTime(a.timestamp) - parseTime(b.timestamp) - }) + console.log('✅ 消息排序完成,最终数量:', sortedMessages.length) // 将消息添加到 chatStore - chatStore.messages.splice(0, chatStore.messages.length, ...chatMessages) + chatStore.messages.splice(0, chatStore.messages.length, ...sortedMessages) // 强制滚动到底部 await nextTick() diff --git a/web/部署完成说明.md b/web/部署完成说明.md new file mode 100644 index 0000000..4586c2b --- /dev/null +++ b/web/部署完成说明.md @@ -0,0 +1,191 @@ +# Emotion Museum 前端部署完成说明 + +## ✅ 部署状态 + +前端应用已成功部署到服务器 `101.200.208.45`,可通过以下地址访问: + +- **本地访问**: `http://127.0.0.1/emotion-museum/` +- **服务器访问**: `http://101.200.208.45/emotion-museum/` +- **外网访问**: 需要配置防火墙规则 + +## 📁 部署目录结构 + +``` +/data/www/emotion-museum/ # 前端应用根目录 +├── index.html # 主页面 +└── assets/ # 静态资源目录 + ├── *.js # JavaScript 文件 + ├── *.css # CSS 文件 + └── ... # 其他资源 +``` + +## 🔧 Nginx 配置 + +**配置文件位置**: `/www/server/panel/vhost/nginx/emotion-museum.conf` + +### 主要配置内容 + +1. **前端应用路由** (`/emotion-museum`) + - 使用 `alias` 指向 `/data/www/emotion-museum` + - 支持 Vue Router history 模式 + - 所有非文件请求重定向到 `index.html` + +2. **缓存策略** + - HTML 文件: 不缓存(Cache-Control: no-cache) + - 静态资源(JS/CSS/图片等): 缓存 1 年 + +3. **API 代理** (`/api`) + - 代理到后端服务 `http://127.0.0.1:19089` + - 设置正确的代理头信息 + +4. **WebSocket 代理** (`/ws`) + - 支持 WebSocket 升级 + - 使用 `$connection_upgrade` 变量处理连接 + - 超时时间设置为 7 天 + +## 📦 部署脚本 + +**脚本位置**: `web/deploy.sh` + +### 使用方法 + +```bash +# 1. 构建前端 +cd web +npm run build + +# 2. 部署到服务器 +bash deploy.sh +``` + +### 脚本功能 + +- 检查 `dist` 目录是否存在 +- 上传 `index.html` 到服务器 +- 上传 `assets` 目录到服务器 +- 上传测试文件(如果存在) + +## 🔄 更新流程 + +每次更新前端代码后,执行以下步骤: + +```bash +# 1. 在本地构建 +cd web +npm run build + +# 2. 部署到服务器 +bash deploy.sh + +# 3. 验证部署 +curl http://127.0.0.1/emotion-museum/ +``` + +## 🌐 访问测试 + +### 本地测试(在服务器上) + +```bash +# 测试前端 +curl http://127.0.0.1/emotion-museum/ + +# 测试 API 代理 +curl http://127.0.0.1/api/health + +# 测试 WebSocket +curl http://127.0.0.1/ws +``` + +### 外网访问 + +如果需要从外网访问,需要: + +1. 配置服务器防火墙规则,允许 80 端口访问 +2. 配置域名解析(可选) +3. 配置 HTTPS(推荐) + +## 📝 配置文件说明 + +### emotion-museum.conf + +```nginx +server { + listen 80; + server_name 101.200.208.45 localhost 127.0.0.1; + + # 前端应用 + location /emotion-museum { + alias /data/www/emotion-museum; + try_files $uri $uri/ /index.html; + # ... 缓存配置 + } + + # API 代理 + location /api { + proxy_pass http://127.0.0.1:19089; + # ... 代理配置 + } + + # WebSocket 代理 + location /ws { + proxy_pass http://127.0.0.1:19089; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + # ... WebSocket 配置 + } +} +``` + +## ⚠️ 注意事项 + +1. **服务器名称警告**: 配置中可能出现 "conflicting server name" 警告,这是正常的,不影响功能 + +2. **缓存问题**: 如果更新后看不到最新内容,清除浏览器缓存或使用 Ctrl+Shift+Delete + +3. **WebSocket 连接**: 确保后端服务在 `127.0.0.1:19089` 正常运行 + +4. **静态资源**: 所有静态资源都会被缓存 1 年,更新时需要更改文件名或清除缓存 + +## 🚀 后续优化 + +1. **HTTPS 配置**: 添加 SSL 证书支持 +2. **域名配置**: 配置自定义域名 +3. **性能优化**: 启用 Gzip 压缩、HTTP/2 等 +4. **监控告警**: 配置 Nginx 日志监控 + +## 📞 故障排查 + +### 问题:访问返回 404 + +**解决方案**: +1. 检查文件是否正确上传: `ls -la /data/www/emotion-museum/` +2. 检查 Nginx 配置: `nginx -t` +3. 重新加载 Nginx: `nginx -s reload` + +### 问题:API 请求失败 + +**解决方案**: +1. 检查后端服务是否运行: `curl http://127.0.0.1:19089/api/health` +2. 检查 Nginx 代理配置 +3. 查看 Nginx 错误日志: `tail -f /www/server/nginx/logs/error.log` + +### 问题:WebSocket 连接失败 + +**解决方案**: +1. 检查后端 WebSocket 服务是否运行 +2. 检查浏览器控制台错误信息 +3. 确保 Nginx 配置中正确设置了 `Upgrade` 和 `Connection` 头 + +## 📋 检查清单 + +- [x] 前端代码构建成功 +- [x] 文件上传到服务器 +- [x] Nginx 配置正确 +- [x] 本地访问测试通过 +- [x] API 代理配置完成 +- [x] WebSocket 代理配置完成 +- [ ] 外网访问测试(需要防火墙配置) +- [ ] HTTPS 配置(可选) +- [ ] 域名配置(可选) + diff --git a/一键部署说明.md b/一键部署说明.md new file mode 100644 index 0000000..2950501 --- /dev/null +++ b/一键部署说明.md @@ -0,0 +1,275 @@ +# 一键部署脚本使用说明 + +## 📋 概述 + +`deploy-all.sh` 是一个一键式部署脚本,可以同时部署后端和前端服务到远程服务器 `101.200.208.45`。 + +该脚本会自动执行以下操作: +1. 编译后端项目 +2. 上传后端 JAR 包到服务器 +3. 启动后端服务 +4. 构建前端项目 +5. 上传前端文件到服务器 +6. 验证部署结果 + +## 🚀 快速开始 + +### 基本用法 + +```bash +# 部署所有服务(后端 + 前端) +bash deploy-all.sh + +# 或指定部署类型 +bash deploy-all.sh all # 部署所有服务 +bash deploy-all.sh backend # 仅部署后端 +bash deploy-all.sh frontend # 仅部署前端 +``` + +### 完整部署流程 + +```bash +# 1. 进入项目根目录 +cd /path/to/EmotionMuseum + +# 2. 执行一键部署 +bash deploy-all.sh + +# 3. 等待部署完成(通常需要 2-5 分钟) + +# 4. 访问应用 +# 前端: http://101.200.208.45/emotion-museum/ +# 后端: http://101.200.208.45:19089/api +``` + +## 📝 部署选项 + +### 部署所有服务 +```bash +bash deploy-all.sh all +# 或 +bash deploy-all.sh +``` +同时部署后端和前端,推荐用于完整的应用更新。 + +### 仅部署后端 +```bash +bash deploy-all.sh backend +``` +仅更新后端服务,适用于后端代码更新。 + +### 仅部署前端 +```bash +bash deploy-all.sh frontend +``` +仅更新前端应用,适用于前端代码更新。 + +## 📊 部署流程详解 + +### 后端部署流程 +1. 检查 `backend-single/deploy.sh` 是否存在 +2. 执行后端部署脚本 + - 清理旧的构建文件 + - 编译项目 (`mvn clean package`) + - 上传 JAR 包到服务器 + - 启动后端服务 + - 验证服务是否正常运行 + +### 前端部署流程 +1. 检查 `web/deploy.sh` 是否存在 +2. 执行前端部署脚本 + - 检查 `dist` 目录是否存在 + - 上传 `index.html` 到服务器 + - 上传 `assets` 目录到服务器 + - 验证文件是否上传成功 + +## ✅ 验证部署 + +### 部署完成后的验证 + +```bash +# 1. 检查后端服务是否运行 +curl http://101.200.208.45:19089/api/health + +# 2. 检查前端是否可访问 +curl http://101.200.208.45/emotion-museum/ + +# 3. 在浏览器中访问 +# 前端: http://101.200.208.45/emotion-museum/ +# 登录页面应该正常显示 +``` + +### 查看服务日志 + +```bash +# SSH 连接到服务器 +ssh root@101.200.208.45 + +# 查看后端日志 +tail -f /data/logs/emotion-museum/emotion-single.log + +# 查看前端文件是否存在 +ls -lh /data/www/emotion-museum/ +``` + +## 🔧 故障排查 + +### 后端部署失败 + +**问题**: 后端部署失败,显示 "JAR 文件不存在" + +**解决方案**: +```bash +# 1. 检查是否安装了 Maven +mvn -v + +# 2. 手动编译项目 +cd backend-single +mvn clean package -DskipTests + +# 3. 检查 JAR 文件是否生成 +ls -lh target/backend-single-1.0.0.jar + +# 4. 重新执行部署 +bash deploy-all.sh backend +``` + +**问题**: SSH 连接失败 + +**解决方案**: +```bash +# 1. 检查 SSH 密钥是否配置 +ssh-keygen -t rsa -b 4096 + +# 2. 复制公钥到服务器 +ssh-copy-id -i ~/.ssh/id_rsa.pub root@101.200.208.45 + +# 3. 测试 SSH 连接 +ssh root@101.200.208.45 "echo 'SSH 连接成功'" +``` + +### 前端部署失败 + +**问题**: 前端部署失败,显示 "dist 目录不存在" + +**解决方案**: +```bash +# 1. 进入前端目录 +cd web + +# 2. 安装依赖 +npm install + +# 3. 构建项目 +npm run build + +# 4. 检查 dist 目录 +ls -lh dist/ + +# 5. 重新执行部署 +bash deploy-all.sh frontend +``` + +**问题**: 前端访问 404 + +**解决方案**: +```bash +# 1. 检查 Nginx 配置 +ssh root@101.200.208.45 "nginx -t" + +# 2. 重新加载 Nginx +ssh root@101.200.208.45 "nginx -s reload" + +# 3. 检查前端文件是否存在 +ssh root@101.200.208.45 "ls -lh /data/www/emotion-museum/" +``` + +## 📊 部署日志 + +部署过程中会输出详细的日志信息: + +``` +[INFO] 情绪博物馆 - 一键式部署 +[INFO] 部署类型: all +[INFO] 部署时间: 2025-10-26 21:30:00 + +╔════════════════════════════════════════════════════════════════╗ +║ 开始部署后端服务 +╚════════════════════════════════════════════════════════════════╝ + +[INFO] 执行后端部署脚本... +[INFO] ✅ 后端部署成功 + +╔════════════════════════════════════════════════════════════════╗ +║ 开始部署前端应用 +╚════════════════════════════════════════════════════════════════╝ + +[INFO] 执行前端部署脚本... +[INFO] ✅ 前端部署成功 + +╔════════════════════════════════════════════════════════════════╗ +║ 部署完成总结 +╚════════════════════════════════════════════════════════════════╝ + +[INFO] ✅ 后端部署: 成功 +[INFO] ✅ 前端部署: 成功 +[INFO] 部署耗时: 180秒 + +╔════════════════════════════════════════════════════════════════╗ +║ 部署成功! +╚════════════════════════════════════════════════════════════════╝ + +[INFO] 📱 前端访问地址: http://101.200.208.45/emotion-museum/ +[INFO] 🔌 后端API地址: http://101.200.208.45:19089/api +[INFO] 📊 WebSocket地址: ws://101.200.208.45:19089/ws +``` + +## 🔄 常见场景 + +### 场景 1: 仅更新后端代码 + +```bash +# 1. 修改后端代码 +# 2. 执行部署 +bash deploy-all.sh backend + +# 3. 验证 +curl http://101.200.208.45:19089/api/health +``` + +### 场景 2: 仅更新前端代码 + +```bash +# 1. 修改前端代码 +# 2. 执行部署 +bash deploy-all.sh frontend + +# 3. 在浏览器中刷新页面 +# http://101.200.208.45/emotion-museum/ +``` + +### 场景 3: 同时更新前后端 + +```bash +# 1. 修改前后端代码 +# 2. 执行部署 +bash deploy-all.sh all + +# 3. 等待部署完成 +# 4. 验证应用 +``` + +## 📞 支持 + +如有问题,请参考以下文件: +- `backend-single/部署说明.md` - 后端部署详情 +- `web/部署说明.md` - 前端部署详情 +- `backend-single/deploy.sh` - 后端部署脚本 +- `web/deploy.sh` - 前端部署脚本 + +--- + +**最后更新**: 2025-10-26 +**版本**: 1.0 +**状态**: ✅ 已完成 + diff --git a/快速部署参考.md b/快速部署参考.md new file mode 100644 index 0000000..8500477 --- /dev/null +++ b/快速部署参考.md @@ -0,0 +1,143 @@ +# 快速部署参考卡片 + +## 🚀 一键部署命令 + +```bash +# 部署所有服务(推荐) +bash deploy-all.sh + +# 仅部署后端 +bash deploy-all.sh backend + +# 仅部署前端 +bash deploy-all.sh frontend +``` + +## 📱 访问地址 + +| 服务 | 地址 | 说明 | +|------|------|------| +| 前端 | http://101.200.208.45/emotion-museum/ | 主应用 | +| 后端 API | http://101.200.208.45:19089/api | REST API | +| WebSocket | ws://101.200.208.45:19089/ws | 实时通信 | + +## ⏱️ 部署耗时 + +| 操作 | 耗时 | +|------|------| +| 后端编译 | 1-2 分钟 | +| 后端上传 | 30 秒 | +| 后端启动 | 30 秒 | +| 前端构建 | 1-2 分钟 | +| 前端上传 | 30 秒 | +| **总计** | **3-5 分钟** | + +## ✅ 部署检查清单 + +部署前: +- [ ] 代码已提交 +- [ ] 本地编译通过 +- [ ] 没有未保存的文件 + +部署中: +- [ ] 脚本正常运行 +- [ ] 没有错误提示 + +部署后: +- [ ] 前端可访问 +- [ ] 后端 API 响应正常 +- [ ] WebSocket 连接正常 + +## 🔍 快速验证 + +```bash +# 验证后端 +curl http://101.200.208.45:19089/api/health + +# 验证前端 +curl http://101.200.208.45/emotion-museum/ + +# 查看后端日志 +ssh root@101.200.208.45 "tail -f /data/logs/emotion-museum/emotion-single.log" + +# 查看前端文件 +ssh root@101.200.208.45 "ls -lh /data/www/emotion-museum/" +``` + +## 🆘 常见问题 + +| 问题 | 解决方案 | +|------|---------| +| JAR 文件不存在 | `cd backend-single && mvn clean package -DskipTests` | +| dist 目录不存在 | `cd web && npm install && npm run build` | +| SSH 连接失败 | `ssh-copy-id -i ~/.ssh/id_rsa.pub root@101.200.208.45` | +| 前端 404 | `ssh root@101.200.208.45 "nginx -s reload"` | +| 后端无响应 | `ssh root@101.200.208.45 "ps aux \| grep emotion"` | + +## 📊 部署脚本结构 + +``` +deploy-all.sh +├── 后端部署 +│ ├── 检查脚本存在 +│ ├── 执行 backend-single/deploy.sh +│ └── 验证部署结果 +├── 前端部署 +│ ├── 检查脚本存在 +│ ├── 执行 web/deploy.sh +│ └── 验证部署结果 +└── 输出部署总结 +``` + +## 💡 最佳实践 + +1. **定期备份** + ```bash + ssh root@101.200.208.45 "tar -czf /data/backups/emotion-museum-$(date +%Y%m%d).tar.gz /data/www/emotion-museum /data/programs/emotion-museum" + ``` + +2. **查看部署历史** + ```bash + ssh root@101.200.208.45 "ls -lh /data/logs/emotion-museum/" + ``` + +3. **监控服务状态** + ```bash + ssh root@101.200.208.45 "ps aux | grep emotion" + ``` + +4. **查看磁盘使用** + ```bash + ssh root@101.200.208.45 "df -h /data" + ``` + +## 🔄 部署流程图 + +``` +开始 + ↓ +检查部署类型 + ├─→ backend: 执行后端部署 + ├─→ frontend: 执行前端部署 + └─→ all: 执行后端 + 前端部署 + ↓ +验证部署结果 + ├─→ 成功: 输出访问地址 + └─→ 失败: 输出错误信息 + ↓ +结束 +``` + +## 📞 获取帮助 + +- 详细说明: `一键部署说明.md` +- 后端部署: `backend-single/部署说明.md` +- 前端部署: `web/部署说明.md` +- 后端脚本: `backend-single/deploy.sh` +- 前端脚本: `web/deploy.sh` + +--- + +**快速参考版本**: 1.0 +**最后更新**: 2025-10-26 + diff --git a/消息顺序问题诊断和修复.md b/消息顺序问题诊断和修复.md new file mode 100644 index 0000000..c096fdd --- /dev/null +++ b/消息顺序问题诊断和修复.md @@ -0,0 +1,210 @@ +# 消息顺序问题诊断和修复 + +## 📋 问题描述 + +前端聊天页面展示消息时,消息顺序不正确,AI 回复消息有时会出现在用户消息上方,导致对话流程混乱。 + +**后端返回的数据示例**: +```json +{ + "records": [ + { "id": "...", "sender": "ai", "messageOrder": 6, "content": "抱歉,AI服务响应异常..." }, + { "id": "...", "sender": "user", "messageOrder": 5, "content": "你好" }, + { "id": "...", "sender": "ai", "messageOrder": 4, "content": "抱歉,AI服务响应异常..." }, + { "id": "...", "sender": "user", "messageOrder": 3, "content": "你好" }, + { "id": "...", "sender": "ai", "messageOrder": 2, "content": "抱歉,AI服务响应异常..." }, + { "id": "...", "sender": "user", "messageOrder": 1, "content": "你好" } + ] +} +``` + +**问题现象**: +- 后端返回的数据是按 `messageOrder` 倒序排列的(最新的在前) +- 前端展示时应该按 `messageOrder` 升序排列(最早的在前) +- 但前端展示的顺序混乱,没有按照 `messageOrder` 排序 + +## 🔍 根本原因 + +### 问题 1: messageOrder 字段未被保留 + +在 `web/src/services/message.ts` 的 `convertToChatMessage` 方法中,**没有将 `messageOrder` 字段从后端响应复制到 ChatMessage 对象中**。 + +**修改前的代码**: +```typescript +const chatMessage: ChatMessage = { + id: msg.id, + content: msg.content, + type: msg.sender === 'user' ? 'user' : (msg.sender === 'ai' ? 'ai' : 'system'), + timestamp: timestamp, + conversationId: msg.conversationId, + sessionId: msg.conversationId, + status: 'sent', + sender: msg.sender as 'user' | 'ai' | 'system', + isRead: msg.isRead, + role: msg.sender === 'user' ? 'user' : 'assistant' + // ❌ 缺少 messageOrder 字段 +} +``` + +**结果**: +- 转换后的 ChatMessage 对象中 `messageOrder` 为 `undefined` +- 排序时无法使用 `messageOrder` 进行排序 +- 只能依赖时间戳排序,导致顺序混乱 + +## ✅ 解决方案 + +### 修复 1: 在消息转换时保留 messageOrder 字段 + +**修改后的代码**: +```typescript +const chatMessage: ChatMessage = { + id: msg.id, + content: msg.content, + type: msg.sender === 'user' ? 'user' : (msg.sender === 'ai' ? 'ai' : 'system'), + timestamp: timestamp, + conversationId: msg.conversationId, + sessionId: msg.conversationId, + status: 'sent', + sender: msg.sender as 'user' | 'ai' | 'system', + isRead: msg.isRead, + role: msg.sender === 'user' ? 'user' : 'assistant', + messageOrder: msg.messageOrder // ✅ 添加 messageOrder 字段 +} +``` + +### 排序逻辑验证 + +排序方法 `sortAndDeduplicateMessages` 中的排序逻辑是正确的: + +```typescript +uniqueMessages.sort((a, b) => { + // 优先使用 messageOrder 排序 + if (a.messageOrder !== undefined && b.messageOrder !== undefined) { + if (a.messageOrder !== b.messageOrder) { + return a.messageOrder - b.messageOrder // ✅ 按升序排列 + } + } + + // 如果 messageOrder 相同或不存在,使用时间戳排序 + const timeA = this.parseTimestamp(a.timestamp) + const timeB = this.parseTimestamp(b.timestamp) + + if (timeA !== timeB) { + return timeA - timeB + } + + // 时间相同时,保持原有顺序 + return a.id.localeCompare(b.id) +}) +``` + +## 📊 修复前后对比 + +### 修复前 +``` +后端返回顺序(倒序): + 1. messageOrder: 6 - AI 回复 + 2. messageOrder: 5 - 用户消息 + 3. messageOrder: 4 - AI 回复 + 4. messageOrder: 3 - 用户消息 + 5. messageOrder: 2 - AI 回复 + 6. messageOrder: 1 - 用户消息 + +前端展示顺序(混乱): + ❌ 消息顺序混乱,AI 回复可能出现在用户消息上方 +``` + +### 修复后 +``` +后端返回顺序(倒序): + 1. messageOrder: 6 - AI 回复 + 2. messageOrder: 5 - 用户消息 + 3. messageOrder: 4 - AI 回复 + 4. messageOrder: 3 - 用户消息 + 5. messageOrder: 2 - AI 回复 + 6. messageOrder: 1 - 用户消息 + +前端排序后(升序): + 1. messageOrder: 1 - 用户消息 ✅ + 2. messageOrder: 2 - AI 回复 ✅ + 3. messageOrder: 3 - 用户消息 ✅ + 4. messageOrder: 4 - AI 回复 ✅ + 5. messageOrder: 5 - 用户消息 ✅ + 6. messageOrder: 6 - AI 回复 ✅ + +前端展示顺序(正确): + ✅ 消息按照 messageOrder 正确排序 +``` + +## 🧪 测试验证 + +### 测试脚本位置 +`web/src/utils/messageOrderTest.ts` + +### 运行测试 +在浏览器控制台执行: +```javascript +// 导入测试模块 +import { runMessageOrderTests } from '@/utils/messageOrderTest' + +// 运行所有测试 +runMessageOrderTests() +``` + +### 测试内容 +1. **测试 1**: 消息转换 - 检查 messageOrder 是否被保留 +2. **测试 2**: 消息排序 - 检查是否按 messageOrder 排序 +3. **测试 3**: 消息展示顺序 - 模拟前端展示 + +## 📁 修改文件 + +### 修改的文件 +- `web/src/services/message.ts` - 在 `convertToChatMessage` 方法中添加 `messageOrder` 字段 + +### 新增的文件 +- `web/src/utils/messageOrderTest.ts` - 消息顺序测试工具 + +## 🚀 验证步骤 + +1. **本地测试** + ```bash + cd web + npm run dev + ``` + +2. **打开浏览器控制台** + - 按 F12 打开开发者工具 + - 进入 Console 标签 + +3. **运行测试** + ```javascript + import { runMessageOrderTests } from '@/utils/utils/messageOrderTest' + runMessageOrderTests() + ``` + +4. **查看测试结果** + - 检查所有消息是否都有 `messageOrder` 字段 + - 检查消息是否按 `messageOrder` 升序排列 + - 检查前端展示顺序是否正确 + +5. **实际测试** + - 打开聊天页面 + - 发送几条消息 + - 刷新页面 + - 验证历史消息顺序是否正确 + +## ✨ 关键改进 + +✅ **messageOrder 字段保留** - 消息转换时保留 messageOrder 字段 +✅ **正确的排序逻辑** - 按 messageOrder 升序排列消息 +✅ **完整的测试覆盖** - 提供测试工具验证修复效果 +✅ **详细的日志输出** - 便于调试和问题排查 + +## 📝 总结 + +通过在消息转换时保留 `messageOrder` 字段,前端现在能够正确地按照消息顺序排序和展示消息,确保对话流程的正确性。 + +**修复状态**: ✅ 已完成 +**测试状态**: ✅ 已验证 +**部署状态**: 待部署 +