+
+
+
{t('settings.title')}
+
+
+
+
+ {currentUser && (
+
+
+ {currentUser.username?.charAt(0) || '?'}
+
+
{currentUser.username}
+
{currentUser.email || 'user@example.com'}
+ {currentUser.uid && (
+
+ UID:
+ {currentUser.uid}
+
+ )}
+
+ )}
+
+
{t('settings.languageSelection')}
+
+
+
-
消息通知
+
{t('settings.notification')}
+
+
+
+
+
+
+
+
+
-
+
-
+
{
{settings.messageSound === 'custom' && (
-
+
{
-
侧边栏样式
+ {t('settings.sidebarStyle')}
-
聊天列表
+ {t('settings.chatList')}
-
主题
+ {t('settings.theme')}
-
联系人铃声方案
+
{t('settings.soundScheme')}
-
+
-
+
-
+
+
);
diff --git a/client/src/i18n/config.js b/client/src/i18n/config.js
new file mode 100644
index 0000000..1d21225
--- /dev/null
+++ b/client/src/i18n/config.js
@@ -0,0 +1,71 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+
+// 导入翻译资源
+import zhCN from './locales/zh-CN.json';
+import zhTW from './locales/zh-TW.json';
+import enUS from './locales/en-US.json';
+import ruRU from './locales/ru-RU.json';
+
+// 获取浏览器语言
+const getBrowserLanguage = () => {
+ const browserLang = navigator.language || navigator.languages[0];
+
+ // 映射浏览器语言到我们支持的语言
+ if (browserLang.startsWith('zh')) {
+ if (browserLang.includes('TW') || browserLang.includes('HK')) {
+ return 'zh-TW';
+ }
+ return 'zh-CN';
+ }
+ if (browserLang.startsWith('en')) {
+ return 'en-US';
+ }
+ if (browserLang.startsWith('ru')) {
+ return 'ru-RU';
+ }
+
+ // 默认返回简体中文
+ return 'zh-CN';
+};
+
+// 从本地存储获取用户设置的语言,如果没有则使用浏览器语言
+const getInitialLanguage = () => {
+ const savedLanguage = localStorage.getItem('halloChat_language');
+ return savedLanguage || getBrowserLanguage();
+};
+
+// 配置 i18next
+i18n
+ .use(initReactI18next) // 将 i18next 传递给 react-i18next
+ .init({
+ resources: {
+ 'zh-CN': {
+ translation: zhCN
+ },
+ 'zh-TW': {
+ translation: zhTW
+ },
+ 'en-US': {
+ translation: enUS
+ },
+ 'ru-RU': {
+ translation: ruRU
+ }
+ },
+ lng: getInitialLanguage(), // 默认语言
+ fallbackLng: 'zh-CN', // 后备语言
+ interpolation: {
+ escapeValue: false // React 已经默认转义了
+ },
+ react: {
+ useSuspense: false // 禁用 Suspense,避免加载问题
+ }
+ });
+
+// 监听语言变化,保存到本地存储
+i18n.on('languageChanged', (lng) => {
+ localStorage.setItem('halloChat_language', lng);
+});
+
+export default i18n;
diff --git a/client/src/i18n/locales/en-US.json b/client/src/i18n/locales/en-US.json
new file mode 100644
index 0000000..15e5139
--- /dev/null
+++ b/client/src/i18n/locales/en-US.json
@@ -0,0 +1,101 @@
+{
+ "login": {
+ "title": "Welcome to HalloChat",
+ "subtitle": "Secure, Fast, and Reliable Instant Messaging Platform",
+ "username": "Username",
+ "password": "Password",
+ "email": "Email",
+ "confirmPassword": "Confirm Password",
+ "rememberMe": "Remember Me",
+ "loginButton": "Login",
+ "registerButton": "Register",
+ "hasAccount": "Already have an account?",
+ "noAccount": "Don't have an account?",
+ "goLogin": "Login Now",
+ "goRegister": "Register Now",
+ "selectServer": "Select Server",
+ "changeServer": "Change Server",
+ "serverConfig": "Server Configuration",
+ "serverName": "Server Name",
+ "serverAddress": "Server Address",
+ "noServerSelected": "No server selected, please select a server first",
+ "pleaseSelectServer": "Please select a server first",
+ "pleaseEnterUsername": "Please enter username",
+ "pleaseEnterPassword": "Please enter password",
+ "pleaseEnterEmail": "Please enter a valid email address",
+ "pleaseConfirmPassword": "Please confirm password",
+ "passwordNotMatch": "Passwords do not match",
+ "usernameFormat": "Username can only contain Chinese characters, letters, numbers, and underscores, 3-20 characters long",
+ "passwordFormat": "Password must contain at least three of: uppercase letters, lowercase letters, numbers, special characters, 6-20 characters long",
+ "errorTitle": "Error",
+ "adminMode": "Admin Mode",
+ "adminModeSuccess": "Admin mode login successful",
+ "adminCodeError": "Admin code is incorrect",
+ "selectBgColor": "Select Background Color",
+ "bgColorChanged": "Background color changed",
+ "languageSetting": "Language Setting"
+ },
+ "settings": {
+ "title": "Settings",
+ "currentUser": "Current User",
+ "language": "Language",
+ "languageSelection": "Language Selection",
+ "theme": "Theme",
+ "light": "Light",
+ "dark": "Dark",
+ "sidebarStyle": "Sidebar Style",
+ "default": "Default",
+ "compact": "Compact",
+ "qq9Style": "QQ9 Style",
+ "chatList": "Chat List",
+ "showStarredContacts": "Show Starred Contacts",
+ "showPinnedChats": "Show Pinned Chats",
+ "notification": "Message Notification",
+ "messageSound": "Message Sound",
+ "soundVolume": "Volume",
+ "customSound": "Custom Ringtone",
+ "selectLocalSound": "Select Local Sound (.wav/.mp3)",
+ "soundScheme": "Contact Sound Scheme",
+ "starredContactSound": "Starred Contact Sound",
+ "normalContactSound": "Normal Contact Sound",
+ "soundDefault": "Default",
+ "soundDing": "Ding",
+ "soundBell": "Bell",
+ "soundChime": "Chime",
+ "soundCustom": "Custom",
+ "logout": "Logout"
+ },
+ "main": {
+ "contacts": "Contacts",
+ "chats": "Chats",
+ "settings": "Settings",
+ "search": "Search",
+ "addFriend": "Add Friend",
+ "createGroup": "Create Group",
+ "welcomeBack": "Welcome Back",
+ "pleaseLogin": "Please Login First",
+ "redirectingToLogin": "Redirecting to login page...",
+ "selectContactToChat": "Please select a contact from the left to start chatting"
+ },
+ "chat": {
+ "typeMessage": "Type a message...",
+ "send": "Send",
+ "sendFile": "Send File",
+ "sendImage": "Send Image",
+ "voiceCall": "Voice Call",
+ "videoCall": "Video Call",
+ "moreOptions": "More Options"
+ },
+ "common": {
+ "confirm": "Confirm",
+ "cancel": "Cancel",
+ "save": "Save",
+ "delete": "Delete",
+ "edit": "Edit",
+ "close": "Close",
+ "loading": "Loading...",
+ "success": "Success",
+ "error": "Error",
+ "warning": "Warning"
+ }
+}
diff --git a/client/src/i18n/locales/ru-RU.json b/client/src/i18n/locales/ru-RU.json
new file mode 100644
index 0000000..0930fcc
--- /dev/null
+++ b/client/src/i18n/locales/ru-RU.json
@@ -0,0 +1,101 @@
+{
+ "login": {
+ "title": "Добро пожаловать в HalloChat",
+ "subtitle": "Безопасная, быстрая и надежная платформа для обмена сообщениями",
+ "username": "Имя пользователя",
+ "password": "Пароль",
+ "email": "Электронная почта",
+ "confirmPassword": "Подтвердите пароль",
+ "rememberMe": "Запомнить меня",
+ "loginButton": "Войти",
+ "registerButton": "Зарегистрироваться",
+ "hasAccount": "Уже есть аккаунт?",
+ "noAccount": "Нет аккаунта?",
+ "goLogin": "Войти сейчас",
+ "goRegister": "Зарегистрироваться сейчас",
+ "selectServer": "Выбрать сервер",
+ "changeServer": "Сменить сервер",
+ "serverConfig": "Конфигурация сервера",
+ "serverName": "Имя сервера",
+ "serverAddress": "Адрес сервера",
+ "noServerSelected": "Сервер не выбран, пожалуйста, выберите сервер",
+ "pleaseSelectServer": "Пожалуйста, сначала выберите сервер",
+ "pleaseEnterUsername": "Пожалуйста, введите имя пользователя",
+ "pleaseEnterPassword": "Пожалуйста, введите пароль",
+ "pleaseEnterEmail": "Пожалуйста, введите действительный адрес электронной почты",
+ "pleaseConfirmPassword": "Пожалуйста, подтвердите пароль",
+ "passwordNotMatch": "Пароли не совпадают",
+ "usernameFormat": "Имя пользователя может содержать только китайские символы, буквы, цифры и подчеркивания, длиной 3-20 символов",
+ "passwordFormat": "Пароль должен содержать как минимум три типа символов: заглавные буквы, строчные буквы, цифры, специальные символы, длиной 6-20 символов",
+ "errorTitle": "Ошибка",
+ "adminMode": "Режим администратора",
+ "adminModeSuccess": "Вход в режиме администратора выполнен успешно",
+ "adminCodeError": "Неверный код администратора",
+ "selectBgColor": "Выберите цвет фона",
+ "bgColorChanged": "Цвет фона изменен",
+ "languageSetting": "Настройка языка"
+ },
+ "settings": {
+ "title": "Настройки",
+ "currentUser": "Текущий пользователь",
+ "language": "Язык",
+ "languageSelection": "Выбор языка",
+ "theme": "Тема",
+ "light": "Светлая",
+ "dark": "Тёмная",
+ "sidebarStyle": "Стиль боковой панели",
+ "default": "По умолчанию",
+ "compact": "Компактный",
+ "qq9Style": "Стиль QQ9",
+ "chatList": "Список чатов",
+ "showStarredContacts": "Показать избранные контакты",
+ "showPinnedChats": "Показать закрепленные чаты",
+ "notification": "Уведомление о сообщении",
+ "messageSound": "Звук сообщения",
+ "soundVolume": "Громкость",
+ "customSound": "Пользовательский рингтон",
+ "selectLocalSound": "Выбрать локальный звук (.wav/.mp3)",
+ "soundScheme": "Звуковая схема контактов",
+ "starredContactSound": "Звук избранного контакта",
+ "normalContactSound": "Звук обычного контакта",
+ "soundDefault": "По умолчанию",
+ "soundDing": "Динь",
+ "soundBell": "Колокольчик",
+ "soundChime": "Колокол",
+ "soundCustom": "Пользовательский",
+ "logout": "Выйти"
+ },
+ "main": {
+ "contacts": "Контакты",
+ "chats": "Чаты",
+ "settings": "Настройки",
+ "search": "Поиск",
+ "addFriend": "Добавить друга",
+ "createGroup": "Создать группу",
+ "welcomeBack": "Добро пожаловать обратно",
+ "pleaseLogin": "Пожалуйста, войдите в систему",
+ "redirectingToLogin": "Перенаправление на страницу входа...",
+ "selectContactToChat": "Пожалуйста, выберите контакт слева, чтобы начать общение"
+ },
+ "chat": {
+ "typeMessage": "Введите сообщение...",
+ "send": "Отправить",
+ "sendFile": "Отправить файл",
+ "sendImage": "Отправить изображение",
+ "voiceCall": "Голосовой вызов",
+ "videoCall": "Видеозвонок",
+ "moreOptions": "Дополнительные опции"
+ },
+ "common": {
+ "confirm": "Подтвердить",
+ "cancel": "Отмена",
+ "save": "Сохранить",
+ "delete": "Удалить",
+ "edit": "Редактировать",
+ "close": "Закрыть",
+ "loading": "Загрузка...",
+ "success": "Успешно",
+ "error": "Ошибка",
+ "warning": "Предупреждение"
+ }
+}
diff --git a/client/src/i18n/locales/zh-CN.json b/client/src/i18n/locales/zh-CN.json
new file mode 100644
index 0000000..436e1ca
--- /dev/null
+++ b/client/src/i18n/locales/zh-CN.json
@@ -0,0 +1,101 @@
+{
+ "login": {
+ "title": "欢迎使用 HalloChat",
+ "subtitle": "安全、快速、可靠的即时通讯平台",
+ "username": "用户名",
+ "password": "密码",
+ "email": "邮箱",
+ "confirmPassword": "确认密码",
+ "rememberMe": "记住我",
+ "loginButton": "立即登录",
+ "registerButton": "立即注册",
+ "hasAccount": "已有账号?",
+ "noAccount": "没有账号?",
+ "goLogin": "立即登录",
+ "goRegister": "去注册",
+ "selectServer": "选择服务器",
+ "changeServer": "更换服务器",
+ "serverConfig": "服务器配置",
+ "serverName": "服务器名称",
+ "serverAddress": "服务器地址",
+ "noServerSelected": "未选择服务器,请先选择服务器",
+ "pleaseSelectServer": "请先选择服务器",
+ "pleaseEnterUsername": "请输入用户名",
+ "pleaseEnterPassword": "请输入密码",
+ "pleaseEnterEmail": "请输入有效的邮箱地址",
+ "pleaseConfirmPassword": "请确认密码",
+ "passwordNotMatch": "两次输入的密码不一致",
+ "usernameFormat": "用户名只能包含中文、字母、数字、下划线,长度3-20位",
+ "passwordFormat": "密码必须包含大写字母、小写字母、数字、特殊符号中的至少三种,长度为6-20个字符",
+ "errorTitle": "错误提示",
+ "adminMode": "管理员模式",
+ "adminModeSuccess": "管理员模式登录成功",
+ "adminCodeError": "管理员代码错误",
+ "selectBgColor": "选择背景颜色",
+ "bgColorChanged": "背景颜色已更改",
+ "languageSetting": "语言设置"
+ },
+ "settings": {
+ "title": "设置",
+ "currentUser": "当前用户",
+ "language": "语言",
+ "languageSelection": "语言选择",
+ "theme": "主题",
+ "light": "浅色",
+ "dark": "深色",
+ "sidebarStyle": "侧边栏样式",
+ "default": "默认",
+ "compact": "紧凑",
+ "qq9Style": "QQ9风格",
+ "chatList": "聊天列表",
+ "showStarredContacts": "显示星标联系人",
+ "showPinnedChats": "显示置顶聊天",
+ "notification": "消息通知",
+ "messageSound": "消息提示音",
+ "soundVolume": "音量",
+ "customSound": "自定义铃声",
+ "selectLocalSound": "选择本地铃声(.wav/.mp3)",
+ "soundScheme": "联系人铃声方案",
+ "starredContactSound": "星标联系人铃声",
+ "normalContactSound": "普通联系人铃声",
+ "soundDefault": "默认",
+ "soundDing": "叮咚",
+ "soundBell": "铃声",
+ "soundChime": "钟声",
+ "soundCustom": "自定义铃声",
+ "logout": "登出"
+ },
+ "main": {
+ "contacts": "联系人",
+ "chats": "聊天",
+ "settings": "设置",
+ "search": "搜索",
+ "addFriend": "添加好友",
+ "createGroup": "创建群组",
+ "welcomeBack": "欢迎回来",
+ "pleaseLogin": "请先登录",
+ "redirectingToLogin": "正在重定向到登录界面...",
+ "selectContactToChat": "请从左侧选择联系人开始聊天"
+ },
+ "chat": {
+ "typeMessage": "输入消息...",
+ "send": "发送",
+ "sendFile": "发送文件",
+ "sendImage": "发送图片",
+ "voiceCall": "语音通话",
+ "videoCall": "视频通话",
+ "moreOptions": "更多选项"
+ },
+ "common": {
+ "confirm": "确认",
+ "cancel": "取消",
+ "save": "保存",
+ "delete": "删除",
+ "edit": "编辑",
+ "close": "关闭",
+ "loading": "加载中...",
+ "success": "成功",
+ "error": "错误",
+ "warning": "警告"
+ }
+}
diff --git a/client/src/i18n/locales/zh-TW.json b/client/src/i18n/locales/zh-TW.json
new file mode 100644
index 0000000..72a6719
--- /dev/null
+++ b/client/src/i18n/locales/zh-TW.json
@@ -0,0 +1,101 @@
+{
+ "login": {
+ "title": "歡迎使用 HalloChat",
+ "subtitle": "安全、快速、可靠的即時通訊平台",
+ "username": "用戶名",
+ "password": "密碼",
+ "email": "郵箱",
+ "confirmPassword": "確認密碼",
+ "rememberMe": "記住我",
+ "loginButton": "立即登錄",
+ "registerButton": "立即註冊",
+ "hasAccount": "已有賬號?",
+ "noAccount": "沒有賬號?",
+ "goLogin": "立即登錄",
+ "goRegister": "去註冊",
+ "selectServer": "選擇服務器",
+ "changeServer": "更換服務器",
+ "serverConfig": "服務器配置",
+ "serverName": "服務器名稱",
+ "serverAddress": "服務器地址",
+ "noServerSelected": "未選擇服務器,請先選擇服務器",
+ "pleaseSelectServer": "請先選擇服務器",
+ "pleaseEnterUsername": "請輸入用戶名",
+ "pleaseEnterPassword": "請輸入密碼",
+ "pleaseEnterEmail": "請輸入有效的郵箱地址",
+ "pleaseConfirmPassword": "請確認密碼",
+ "passwordNotMatch": "兩次輸入的密碼不一致",
+ "usernameFormat": "用戶名只能包含中文、字母、數字、下劃線,長度3-20位",
+ "passwordFormat": "密碼必須包含大寫字母、小寫字母、數字、特殊符號中的至少三種,長度為6-20個字符",
+ "errorTitle": "錯誤提示",
+ "adminMode": "管理員模式",
+ "adminModeSuccess": "管理員模式登錄成功",
+ "adminCodeError": "管理員代碼錯誤",
+ "selectBgColor": "選擇背景顏色",
+ "bgColorChanged": "背景顏色已更改",
+ "languageSetting": "語言設置"
+ },
+ "settings": {
+ "title": "設置",
+ "currentUser": "當前用戶",
+ "language": "語言",
+ "languageSelection": "語言選擇",
+ "theme": "主題",
+ "light": "淺色",
+ "dark": "深色",
+ "sidebarStyle": "側邊欄樣式",
+ "default": "默認",
+ "compact": "緊湊",
+ "qq9Style": "QQ9風格",
+ "chatList": "聊天列表",
+ "showStarredContacts": "顯示星標聯繫人",
+ "showPinnedChats": "顯示置頂聊天",
+ "notification": "消息通知",
+ "messageSound": "消息提示音",
+ "soundVolume": "音量",
+ "customSound": "自定義鈴聲",
+ "selectLocalSound": "選擇本地鈴聲(.wav/.mp3)",
+ "soundScheme": "聯繫人鈴聲方案",
+ "starredContactSound": "星標聯繫人鈴聲",
+ "normalContactSound": "普通聯繫人鈴聲",
+ "soundDefault": "默認",
+ "soundDing": "叮咚",
+ "soundBell": "鈴聲",
+ "soundChime": "鐘聲",
+ "soundCustom": "自定義鈴聲",
+ "logout": "登出"
+ },
+ "main": {
+ "contacts": "聯繫人",
+ "chats": "聊天",
+ "settings": "設置",
+ "search": "搜索",
+ "addFriend": "添加好友",
+ "createGroup": "創建群組",
+ "welcomeBack": "歡迎回來",
+ "pleaseLogin": "請先登錄",
+ "redirectingToLogin": "正在重定向到登錄界面...",
+ "selectContactToChat": "請從左側選擇聯繫人開始聊天"
+ },
+ "chat": {
+ "typeMessage": "輸入消息...",
+ "send": "發送",
+ "sendFile": "發送文件",
+ "sendImage": "發送圖片",
+ "voiceCall": "語音通話",
+ "videoCall": "視頻通話",
+ "moreOptions": "更多選項"
+ },
+ "common": {
+ "confirm": "確認",
+ "cancel": "取消",
+ "save": "保存",
+ "delete": "刪除",
+ "edit": "編輯",
+ "close": "關閉",
+ "loading": "加載中...",
+ "success": "成功",
+ "error": "錯誤",
+ "warning": "警告"
+ }
+}
diff --git a/client/src/index.js b/client/src/index.js
index e3c8900..28fc95c 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import { AuthProvider } from './contexts/AuthContext';
import './index.css';
+import './i18n/config'; // 导入 i18n 配置
// 客户端应用入口文件
console.log('HalloChat 客户端启动中...');
diff --git a/client/src/services/chatService.js b/client/src/services/chatService.js
index 06f5a2f..86264ed 100644
--- a/client/src/services/chatService.js
+++ b/client/src/services/chatService.js
@@ -16,6 +16,86 @@ class ChatService {
this.syncHandlers = []; // 用于同步状态
}
+ // 保存消息到本地存储
+ saveMessageToLocal(message) {
+ try {
+ const key = `messages_${this.currentUser.id}_${message.type === 'group' ? message.groupId : message.receiverId || message.senderId}`;
+ const messages = this.getMessagesFromLocal(key);
+ const existingMessageIndex = messages.findIndex(m => m.id === message.id);
+
+ if (existingMessageIndex >= 0) {
+ // 更新现有消息
+ messages[existingMessageIndex] = message;
+ } else {
+ // 添加新消息
+ messages.push(message);
+ }
+
+ localStorage.setItem(key, JSON.stringify(messages));
+ } catch (error) {
+ console.error('保存消息到本地存储失败:', error);
+ }
+ }
+
+ // 从本地存储获取消息
+ getMessagesFromLocal(key) {
+ try {
+ const messages = localStorage.getItem(key);
+ return messages ? JSON.parse(messages) : [];
+ } catch (error) {
+ console.error('从本地存储获取消息失败:', error);
+ return [];
+ }
+ }
+
+ // 获取与特定联系人/群组的聊天记录
+ getChatHistory(contactId, isGroup = false) {
+ const key = `messages_${this.currentUser.id}_${isGroup ? contactId : contactId}`;
+ return this.getMessagesFromLocal(key);
+ }
+
+ // 同步未发送成功的消息
+ syncPendingMessages() {
+ try {
+ // 遍历所有本地存储的消息,查找状态为pending的消息
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key.startsWith(`messages_${this.currentUser.id}_`)) {
+ const messages = this.getMessagesFromLocal(key);
+ const pendingMessages = messages.filter(m => m.syncStatus === 'pending' || m.status === 'sending');
+
+ pendingMessages.forEach(message => {
+ console.log(`重新发送消息: ${message.id}`);
+ // 更新消息状态为sending
+ message.status = 'sending';
+ this.saveMessageToLocal(message);
+
+ // 重新发送消息到服务器
+ if (message.type === 'group' || message.groupId) {
+ this.socket.emit('message', {
+ content: message.content,
+ type: message.type,
+ receiverId: message.receiverId,
+ groupId: message.groupId,
+ channelId: message.channelId
+ });
+ } else {
+ this.socket.emit('message', {
+ content: message.content,
+ type: message.type,
+ receiverId: message.receiverId,
+ groupId: message.groupId,
+ channelId: message.channelId
+ });
+ }
+ });
+ }
+ }
+ } catch (error) {
+ console.error('同步未发送消息失败:', error);
+ }
+ }
+
// 设置服务器地址
setServerAddress(address) {
this.serverAddress = address;
@@ -25,12 +105,20 @@ class ChatService {
connect(user) {
this.currentUser = user;
this.socket = io(this.serverAddress, {
- query: { userId: user.id }
+ query: { userId: user.id },
+ reconnection: true, // 启用自动重连
+ reconnectionAttempts: Infinity, // 无限次重连尝试
+ reconnectionDelay: 1000, // 重连延迟(毫秒)
+ reconnectionDelayMax: 5000, // 最大重连延迟(毫秒)
+ timeout: 20000, // 连接超时时间(毫秒)
+ transports: ['websocket'] // 使用websocket传输
});
// 监听消息
this.socket.on('message', (messageData) => {
const message = new Message(messageData);
+ // 保存接收的消息到本地存储
+ this.saveMessageToLocal(message);
this.messageHandlers.forEach(handler => handler(message));
});
@@ -64,15 +152,49 @@ class ChatService {
this.syncHandlers.forEach(handler => handler(messageId, status));
});
+ // 监听连接成功
+ this.socket.on('connect', () => {
+ console.log('WebSocket连接成功');
+ // 重新同步未发送成功的消息
+ this.syncPendingMessages();
+ });
+
+ // 监听连接断开
+ this.socket.on('disconnect', (reason) => {
+ console.log('WebSocket连接断开:', reason);
+ });
+
+ // 监听重连尝试
+ this.socket.on('reconnect_attempt', (attemptNumber) => {
+ console.log(`WebSocket重连尝试 #${attemptNumber}`);
+ });
+
+ // 监听重连成功
+ this.socket.on('reconnect', (attemptNumber) => {
+ console.log(`WebSocket重连成功,尝试次数: ${attemptNumber}`);
+ // 重新同步未发送成功的消息
+ this.syncPendingMessages();
+ });
+
+ // 监听重连失败
+ this.socket.on('reconnect_failed', () => {
+ console.error('WebSocket重连失败');
+ });
+
+ // 监听连接超时
+ this.socket.on('connect_timeout', (timeout) => {
+ console.error(`WebSocket连接超时: ${timeout}ms`);
+ });
+
// 监听错误
this.socket.on('error', (error) => {
- console.error('WebSocket error:', error);
+ console.error('WebSocket错误:', error);
});
}
// 发送消息
sendMessage(receiverId, content, type = 'text', groupId = null, channelId = null) {
- const message = new Message({
+ const message = new Message({
id: `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, // 临时ID
senderId: this.currentUser.id,
receiverId,
@@ -83,6 +205,9 @@ class ChatService {
syncStatus: 'pending'
});
+ // 保存消息到本地存储
+ this.saveMessageToLocal(message);
+
// 发送消息到服务器
this.socket.emit('message', {
content,
diff --git a/client/src/services/contactService.js b/client/src/services/contactService.js
new file mode 100644
index 0000000..1168f52
--- /dev/null
+++ b/client/src/services/contactService.js
@@ -0,0 +1,93 @@
+import axios from 'axios';
+import authService from './authService';
+
+class ContactService {
+ constructor() {
+ this.apiUrl = authService.apiUrl;
+ }
+
+ getToken() {
+ return localStorage.getItem('halloChat_token');
+ }
+
+ getAuthHeaders() {
+ const token = this.getToken();
+ return {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ };
+ }
+
+ // 获取好友列表
+ async getFriends() {
+ try {
+ const response = await axios.get(`${this.apiUrl}/friends`, {
+ headers: this.getAuthHeaders()
+ });
+ return response.data?.data || [];
+ } catch (error) {
+ console.error('获取好友列表失败:', error);
+ throw error;
+ }
+ }
+
+ // 添加好友
+ async addFriend(friendUsername) {
+ try {
+ const response = await axios.post(
+ `${this.apiUrl}/friends/add`,
+ { friendUsername },
+ { headers: this.getAuthHeaders() }
+ );
+ return response.data?.data;
+ } catch (error) {
+ console.error('添加好友失败:', error);
+ throw error;
+ }
+ }
+
+ // 删除好友
+ async removeFriend(friendId) {
+ try {
+ const response = await axios.delete(
+ `${this.apiUrl}/friends/${friendId}`,
+ { headers: this.getAuthHeaders() }
+ );
+ return response.data;
+ } catch (error) {
+ console.error('删除好友失败:', error);
+ throw error;
+ }
+ }
+
+ // 获取群组列表
+ async getGroups() {
+ try {
+ const response = await axios.get(`${this.apiUrl}/groups`, {
+ headers: this.getAuthHeaders()
+ });
+ return response.data?.data || [];
+ } catch (error) {
+ console.error('获取群组列表失败:', error);
+ throw error;
+ }
+ }
+
+ // 创建群组
+ async createGroup(groupData) {
+ try {
+ const response = await axios.post(
+ `${this.apiUrl}/groups`,
+ groupData,
+ { headers: this.getAuthHeaders() }
+ );
+ return response.data?.data;
+ } catch (error) {
+ console.error('创建群组失败:', error);
+ throw error;
+ }
+ }
+}
+
+const contactService = new ContactService();
+export default contactService;
diff --git a/client/src/services/messageService.js b/client/src/services/messageService.js
new file mode 100644
index 0000000..88f23d1
--- /dev/null
+++ b/client/src/services/messageService.js
@@ -0,0 +1,102 @@
+import axios from 'axios';
+import authService from './authService';
+
+class MessageService {
+ constructor() {
+ this.apiUrl = authService.apiUrl;
+ }
+
+ getToken() {
+ return localStorage.getItem('halloChat_token');
+ }
+
+ getAuthHeaders() {
+ const token = this.getToken();
+ return {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ };
+ }
+
+ // 获取与某个用户的聊天历史
+ async getChatHistory(contactId, limit = 50, offset = 0) {
+ try {
+ const response = await axios.get(
+ `${this.apiUrl}/messages/history/${contactId}`,
+ {
+ headers: this.getAuthHeaders(),
+ params: { limit, offset }
+ }
+ );
+ return response.data?.data || [];
+ } catch (error) {
+ console.error('获取聊天历史失败:', error);
+ throw error;
+ }
+ }
+
+ // 发送消息
+ async sendMessage(receiverId, content, type = 'text') {
+ try {
+ const response = await axios.post(
+ `${this.apiUrl}/messages/send`,
+ { receiverId, content, type },
+ { headers: this.getAuthHeaders() }
+ );
+ return response.data?.data;
+ } catch (error) {
+ console.error('发送消息失败:', error);
+ throw error;
+ }
+ }
+
+ // 标记消息为已读
+ async markAsRead(messageId) {
+ try {
+ const response = await axios.put(
+ `${this.apiUrl}/messages/${messageId}/read`,
+ {},
+ { headers: this.getAuthHeaders() }
+ );
+ return response.data;
+ } catch (error) {
+ console.error('标记消息已读失败:', error);
+ throw error;
+ }
+ }
+
+ // 获取群组聊天历史
+ async getGroupChatHistory(groupId, limit = 50, offset = 0) {
+ try {
+ const response = await axios.get(
+ `${this.apiUrl}/messages/group/${groupId}`,
+ {
+ headers: this.getAuthHeaders(),
+ params: { limit, offset }
+ }
+ );
+ return response.data?.data || [];
+ } catch (error) {
+ console.error('获取群组聊天历史失败:', error);
+ throw error;
+ }
+ }
+
+ // 发送群组消息
+ async sendGroupMessage(groupId, content, type = 'text') {
+ try {
+ const response = await axios.post(
+ `${this.apiUrl}/messages/group/${groupId}`,
+ { content, type },
+ { headers: this.getAuthHeaders() }
+ );
+ return response.data?.data;
+ } catch (error) {
+ console.error('发送群组消息失败:', error);
+ throw error;
+ }
+ }
+}
+
+const messageService = new MessageService();
+export default messageService;
diff --git a/docs/LOGIN_UI_UPGRADE.md b/docs/LOGIN_UI_UPGRADE.md
new file mode 100644
index 0000000..3140583
--- /dev/null
+++ b/docs/LOGIN_UI_UPGRADE.md
@@ -0,0 +1,103 @@
+# 登录页面 UI 优化说明
+
+## 🎨 设计升级概览
+
+本次优化将 HalloChat 登录页面从传统设计升级为现代化、高端的用户界面,提升用户体验和视觉吸引力。
+
+## ✨ 主要改进
+
+### 1. **视觉设计**
+- ✅ **渐变背景**: 采用紫色系渐变(#667eea → #764ba2),营造专业科技感
+- ✅ **毛玻璃效果**: 登录卡片使用半透明背景和模糊效果(backdrop-filter)
+- ✅ **动态装饰**: 添加浮动的背景装饰元素,增加页面动感
+- ✅ **现代阴影**: 使用多层阴影营造立体感和深度
+
+### 2. **品牌展示**
+- ✅ **Logo 区域**: 添加渐变色 Logo 图标,带有浮动动画
+- ✅ **标题优化**: 使用渐变文字效果,更具视觉冲击力
+- ✅ **副标题**: 添加产品 Slogan,强化品牌认知
+
+### 3. **交互体验**
+- ✅ **输入框图标**: 为每个输入框添加语义化图标
+- ✅ **大尺寸输入**: 增大输入框尺寸,提升移动端体验
+- ✅ **圆角设计**: 统一使用圆角设计,更加柔和友好
+- ✅ **悬停效果**: 所有交互元素都有平滑的悬停动画
+- ✅ **渐进动画**: 表单元素依次淡入,提升视觉流畅度
+
+### 4. **服务器选择优化**
+- ✅ **卡片式展示**: 服务器信息以精美卡片形式展示
+- ✅ **图标标识**: 使用云服务器和确认图标,增强可识别性
+- ✅ **悬停动效**: 卡片悬停时上浮并显示阴影
+- ✅ **渐变背景**: 服务器卡片采用淡蓝色渐变
+
+### 5. **按钮设计**
+- ✅ **渐变按钮**: 主按钮使用紫色渐变,更加醒目
+- ✅ **立体阴影**: 按钮带有颜色匹配的阴影效果
+- ✅ **按压反馈**: 点击时有下沉动画,提供触感反馈
+- ✅ **加载状态**: 优化加载动画的视觉效果
+
+### 6. **响应式设计**
+- ✅ **移动端适配**: 针对小屏幕优化布局和尺寸
+- ✅ **弹性布局**: 使用 Flexbox 确保各种屏幕都能完美展示
+
+
+## 🎯 设计亮点
+### 配色方案
+```css
+主色调: #667eea (紫蓝)
+辅助色: #764ba2 (紫色)
+强调色: #4a5568 (深灰)
+背景色: #f7fafc (浅灰)
+文字色: #2d3748 (炭黑)
+```
+
+### 动画效果
+1. **入场动画**: 卡片从下方滑入并淡入
+2. **Logo 浮动**: Logo 持续上下浮动
+3. **背景装饰**: 大型圆形光晕缓慢移动
+4. **表单渐入**: 表单项依次淡入,有延迟效果
+5. **按钮反馈**: 悬停上浮,点击下沉
+
+### 用户体验优化
+- **清晰的视觉层次**: 通过颜色、大小、阴影建立信息层级
+- **即时反馈**: 所有交互都有视觉和动画反馈
+- **引导性设计**: 使用图标和颜色引导用户操作
+- **无障碍考虑**: 保持足够的对比度和可读性
+
+## 📱 兼容性
+
+- ✅ 现代浏览器(Chrome, Firefox, Safari, Edge)
+- ✅ 移动端浏览器
+- ✅ Electron 桌面应用
+- ⚠️ backdrop-filter 在旧版浏览器可能不支持(会降级为纯色背景)
+
+## 🚀 使用的技术
+
+- **CSS3 动画**: 使用 @keyframes 创建流畅动画
+- **渐变**: linear-gradient 和 radial-gradient
+- **模糊效果**: backdrop-filter: blur()
+- **Flexbox**: 响应式布局
+- **Ant Design 图标**: 现代化图标库
+
+## 📸 视觉效果
+
+新的登录页面具有以下特征:
+- 🌈 紫色渐变背景,专业科技感
+- 💎 半透明毛玻璃卡片,时尚现代
+- ✨ 流畅的动画效果,提升体验
+- 🎨 统一的设计语言,品牌感强
+- 📱 完美的响应式适配
+
+## 🔄 后续优化建议
+
+1. 可以根据品牌需求调整配色方案
+2. 可以添加暗色模式支持
+3. 可以集成更多动画效果(如粒子背景)
+4. 可以添加多语言切换功能
+5. 可以增加社交登录按钮(如微信、QQ等)
+
+---
+
+**优化完成时间**: 2026-01-29
+**设计师**: SanYou(LUCA.NEX)
+**技术栈**: React + Ant Design + CSS3
diff --git a/server/manager/HalloChat.ico b/server/manager/HalloChat.ico
new file mode 100644
index 0000000..62fedc7
Binary files /dev/null and b/server/manager/HalloChat.ico differ
diff --git a/server/manager/requirements.txt b/server/manager/requirements.txt
index a94b3ae..ec3ed43 100644
--- a/server/manager/requirements.txt
+++ b/server/manager/requirements.txt
@@ -1,5 +1,6 @@
# HalloChat server management console required Python libraries
-# Note: This project only uses Python standard libraries, no additional third-party libraries needed
+# Third-party dependencies:
+requests>=2.25.0
# Used standard libraries:
# - os: Operating system interface
diff --git a/server/manager/server_manager.log b/server/manager/server_manager.log
new file mode 100644
index 0000000..e765188
--- /dev/null
+++ b/server/manager/server_manager.log
@@ -0,0 +1,178 @@
+[2025-11-12 00:01:55] HalloChat服务器管理器启动
+[2025-11-12 00:05:39] HalloChat服务器管理器启动
+[2025-11-12 00:05:42] 开始下载MongoDB...
+[2025-11-12 00:05:42] 下载MongoDB时出错: No module named 'requests'
+[2025-11-12 00:07:06] HalloChat服务器管理器启动
+[2025-11-12 00:07:08] 开始下载MongoDB...
+[2025-11-12 00:07:09] 开始下载MongoDB安装程序: https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.0-signed.msi
+[2025-11-12 00:08:07] MongoDB安装程序下载完成: C:\Users\haoya\AppData\Local\Temp\tmp9i3ufb3_\mongodb-installer.msi
+[2025-11-12 00:08:07] 提示用户以管理员权限运行安装向导
+[2025-11-12 00:12:55] MongoDB下载安装成功
+[2025-11-12 00:13:00] 开始安装依赖...
+[2025-11-12 00:13:54] [npm] npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+[2025-11-12 00:13:55] [npm] npm warn deprecated @humanwhocodes/config-array@0.13.0: Use @eslint/config-array instead
+[2025-11-12 00:13:55] [npm] npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
+[2025-11-12 00:13:55] [npm] npm warn deprecated supertest@6.3.4: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net
+[2025-11-12 00:13:55] [npm] npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
+[2025-11-12 00:13:55] [npm] npm warn deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
+[2025-11-12 00:13:55] [npm] npm warn deprecated multer@1.4.5-lts.2: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.
+[2025-11-12 00:13:56] [npm] npm warn deprecated superagent@8.1.2: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net
+[2025-11-12 00:13:57] [npm] npm warn deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.
+[2025-11-12 00:22:14] HalloChat服务器管理器启动
+[2025-11-12 00:22:22] 开始安装依赖...
+[2025-11-12 00:22:25] [npm] up to date, audited 777 packages in 3s
+[2025-11-12 00:22:25] [npm] 161 packages are looking for funding
+[2025-11-12 00:22:25] [npm] run `npm fund` for details
+[2025-11-12 00:22:25] [npm] 2 high severity vulnerabilities
+[2025-11-12 00:22:25] [npm] To address all issues (including breaking changes), run:
+[2025-11-12 00:22:25] [npm] npm audit fix --force
+[2025-11-12 00:22:25] [npm] Run `npm audit` for details.
+[2025-11-12 00:22:25] 依赖安装成功
+[2025-11-12 00:22:25] 依赖安装过程完成(无论成功或失败)
+[2025-11-12 00:22:29] 检测到依赖未安装,正在尝试安装...
+[2025-11-12 00:22:31] 正在启动服务器...
+[2025-11-12 00:22:31] 服务器启动命令已发送
+[2025-11-12 00:22:31]
+[2025-11-12 00:22:31] > hallo-chat-server@2.1.1-alpha start
+[2025-11-12 00:22:31] > node src/index.js
+[2025-11-12 00:22:31]
+[2025-11-12 00:22:32] 读取日志时出错: 'gbk' codec can't decode byte 0xa8 in position 20: illegal multibyte sequence
+[2025-11-12 00:22:39] 正在停止服务器...
+[2025-11-12 00:22:40] 服务器已停止
+[2025-11-12 00:22:59] 正在停止MongoDB服务...
+[2025-11-12 00:23:00] Windows服务停止MongoDB失败: 发生系统错误 5。
+
+拒绝访问。
+
+
+[2025-11-12 00:23:00] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5:
+
+拒绝访问。
+
+
+[2025-11-12 00:23:00] 已终止所有MongoDB进程
+[2025-11-12 00:23:02] MongoDB服务已停止
+[2025-11-12 00:23:04] 正在停止MongoDB服务...
+[2025-11-12 00:23:05] Windows服务停止MongoDB失败: 发生系统错误 5。
+
+拒绝访问。
+
+
+[2025-11-12 00:23:05] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5:
+
+拒绝访问。
+
+
+[2025-11-12 00:23:06] 已终止所有MongoDB进程
+[2025-11-12 00:23:08] MongoDB服务已停止
+[2025-11-12 00:23:08] 正在停止MongoDB服务...
+[2025-11-12 00:23:08] Windows服务停止MongoDB失败: 发生系统错误 5。
+
+拒绝访问。
+
+
+[2025-11-12 00:23:09] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5:
+
+拒绝访问。
+
+
+[2025-11-12 00:23:09] 已终止所有MongoDB进程
+[2025-11-12 00:23:11] MongoDB服务已停止
+[2025-11-12 00:26:55] HalloChat服务器管理器启动
+[2025-11-12 00:27:20] 检测到依赖未安装,正在尝试安装...
+[2025-11-12 00:27:22] 正在启动服务器...
+[2025-11-12 00:27:22] 服务器启动命令已发送
+[2025-11-12 00:27:23]
+[2025-11-12 00:27:23] > hallo-chat-server@2.1.1-alpha start
+[2025-11-12 00:27:23] > node src/index.js
+[2025-11-12 00:27:23]
+[2025-11-12 00:27:23] info: 服务已启动,端口:7932 {"timestamp":"2025-11-12 00:27:23"}
+[2025-11-12 00:27:23] 服务已启动,端口:7932
+[2025-11-12 00:27:23] info: MongoDB数据库连接成功 {"timestamp":"2025-11-12 00:27:23"}
+[2025-11-12 00:27:23] MongoDB数据库连接成功
+[2025-11-12 00:32:47] 正在停止服务器...
+[2025-11-12 00:32:47] 服务器已停止
+[2025-11-12 00:32:48] 正在停止MongoDB服务...
+[2025-11-12 00:32:48] Windows服务停止MongoDB失败: 发生系统错误 5。
+
+拒绝访问。
+
+
+[2025-11-12 00:32:49] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5:
+
+拒绝访问。
+
+
+[2025-11-12 00:32:49] 已终止所有MongoDB进程
+[2025-11-12 00:32:51] MongoDB服务已停止
+[2025-11-12 00:36:41] HalloChat服务器管理器启动
+[2025-11-12 00:36:45] 正在停止MongoDB服务...
+[2025-11-12 00:36:45] 尝试以管理员权限停止MongoDB服务...
+[2025-11-12 00:36:45] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框
+[2025-11-12 00:36:46] 已终止所有MongoDB进程
+[2025-11-12 00:36:48] MongoDB服务已停止
+[2025-11-12 00:36:54] 正在停止MongoDB服务...
+[2025-11-12 00:36:54] 尝试以管理员权限停止MongoDB服务...
+[2025-11-12 00:36:54] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框
+[2025-11-12 00:36:55] 已终止所有MongoDB进程
+[2025-11-12 00:36:57] MongoDB服务已停止
+[2025-11-12 00:36:57] 正在停止MongoDB服务...
+[2025-11-12 00:36:57] 尝试以管理员权限停止MongoDB服务...
+[2025-11-12 00:36:58] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框
+[2025-11-12 00:36:58] 已终止所有MongoDB进程
+[2025-11-12 00:37:00] MongoDB服务已停止
+[2025-11-12 00:37:08] 正在停止MongoDB服务...
+[2025-11-12 00:37:08] 尝试以管理员权限停止MongoDB服务...
+[2025-11-12 00:37:08] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框
+[2025-11-12 00:37:09] 已终止所有MongoDB进程
+[2025-11-12 00:37:11] MongoDB服务已停止
+[2025-11-12 00:41:21] HalloChat服务器管理器启动
+[2025-11-12 00:41:24] 正在启动MongoDB服务...
+[2025-11-12 00:41:24] Windows服务启动MongoDB失败: 发生系统错误 5。
+
+拒绝访问。
+
+
+[2025-11-12 00:41:24] 使用sc命令启动MongoDB服务失败: [SC] StartService: OpenService 失败 5:
+
+拒绝访问。
+
+
+[2025-11-12 00:41:24] 尝试直接启动MongoDB进程...
+[2025-11-12 00:41:24] 找到MongoDB安装: C:\Program Files\MongoDB\Server\7.0\bin\mongod.exe
+[2025-11-12 00:41:24] 创建MongoDB数据目录: C:\ProgramData\MongoDB\data\db
+[2025-11-12 00:41:25] 直接启动MongoDB成功
+[2025-11-12 00:41:33] 正在停止MongoDB服务...
+[2025-11-12 00:41:33] 尝试以管理员权限停止MongoDB服务...
+[2025-11-12 00:41:33] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框
+[2025-11-12 00:41:34] 终止MongoDB进程PID: 59468
+[2025-11-12 00:41:34] 终止直接启动的MongoDB进程
+[2025-11-12 00:41:34] 第1次尝试终止所有MongoDB进程
+[2025-11-12 00:41:34] 已尝试终止所有MongoDB进程
+[2025-11-12 00:41:38] 第2次尝试终止所有MongoDB进程
+[2025-11-12 00:41:38] 已尝试终止所有MongoDB进程
+[2025-11-12 00:41:42] 第3次尝试终止所有MongoDB进程
+[2025-11-12 00:41:42] 已尝试终止所有MongoDB进程
+[2025-11-12 00:41:44] 等待MongoDB进程完全终止...
+[2025-11-12 00:41:48] MongoDB似乎仍在运行,等待并再次检查...(1/5)
+[2025-11-12 00:41:50] MongoDB似乎仍在运行,等待并再次检查...(2/5)
+[2025-11-12 00:41:53] MongoDB似乎仍在运行,等待并再次检查...(3/5)
+[2025-11-12 00:41:55] MongoDB似乎仍在运行,等待并再次检查...(4/5)
+[2025-11-12 00:41:57] MongoDB似乎仍在运行,等待并再次检查...(5/5)
+[2025-11-12 00:41:59] MongoDB似乎仍在运行,尝试最后一次强力终止...
+[2025-11-12 00:42:02] 警告:无法确认MongoDB是否已完全停止,请手动检查
+[2025-11-12 00:42:10] 正在停止MongoDB服务...
+[2025-11-12 00:42:10] 尝试以管理员权限停止MongoDB服务...
+[2025-11-12 00:42:10] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框
+[2025-11-12 00:42:10] 第1次尝试终止所有MongoDB进程
+[2025-11-12 00:42:11] 已尝试终止所有MongoDB进程
+[2025-11-12 00:42:14] 第2次尝试终止所有MongoDB进程
+[2025-11-12 00:42:15] 已尝试终止所有MongoDB进程
+[2025-11-12 00:42:18] 第3次尝试终止所有MongoDB进程
+[2025-11-12 00:42:19] 已尝试终止所有MongoDB进程
+[2025-11-12 00:42:21] 等待MongoDB进程完全终止...
+[2025-11-12 00:42:24] MongoDB似乎仍在运行,等待并再次检查...(1/5)
+[2025-11-12 00:42:27] MongoDB似乎仍在运行,等待并再次检查...(2/5)
+[2025-11-12 00:42:29] MongoDB似乎仍在运行,等待并再次检查...(3/5)
+[2025-11-12 00:42:32] MongoDB似乎仍在运行,等待并再次检查...(4/5)
+[2025-11-12 00:42:35] MongoDB服务已成功停止
diff --git a/server/manager/server_manager.py b/server/manager/server_manager.py
index c8a18ed..b60c327 100644
--- a/server/manager/server_manager.py
+++ b/server/manager/server_manager.py
@@ -18,6 +18,7 @@
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
from threading import Thread, Lock
+import re
class ServerManager:
@@ -28,6 +29,13 @@ def __init__(self, root):
self.root.resizable(True, True)
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
+ # 设置软件图标为服务端控制台根目录的HalloChat.ico
+ try:
+ icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'HalloChat.ico')
+ self.root.iconbitmap(icon_path)
+ except Exception as e:
+ print(f"设置图标失败: {str(e)}") # 静默失败,不影响主要功能
+
# 设置中文字体支持
self.configure_styles()
@@ -73,8 +81,8 @@ def show_startup_notice(self):
# 添加提示文本
notice_text = (
"使用须知:\n"\
- "1. MongoDB 全程使用Homebrew管理MongoDB社区版\n"\
- "2. 代码中部分注释由AI辅助生成,以提高可读性"
+ "本软件为测试版本 有些许bug\n"\
+ "代码中部分注释由AI辅助生成,以提高可读性"
)
notice_label = ttk.Label(content_frame, text=notice_text, style="Notice.TLabel", justify=tk.LEFT)
@@ -199,6 +207,30 @@ def create_widgets(self):
control_frame = ttk.Frame(main_frame, padding="10")
control_frame.pack(fill=tk.X, pady=(0, 20))
+ # 进度条区域 - 用于显示依赖安装进度
+ self.progress_frame = ttk.LabelFrame(main_frame, text="安装进度", padding="10")
+ self.progress_frame.pack(fill=tk.X, pady=(0, 20))
+ self.progress_frame.pack_forget() # 初始隐藏
+
+ # 进度条
+ self.progress_var = tk.DoubleVar()
+ self.progress_bar = ttk.Progressbar(
+ self.progress_frame,
+ variable=self.progress_var,
+ maximum=100,
+ length=0, # 自适应长度
+ mode='determinate'
+ )
+ self.progress_bar.pack(fill=tk.X, pady=(0, 10))
+
+ # 进度文本
+ self.progress_text_var = tk.StringVar(value="准备安装依赖...")
+ self.progress_text = ttk.Label(
+ self.progress_frame,
+ textvariable=self.progress_text_var
+ )
+ self.progress_text.pack(anchor=tk.W)
+
# 安装依赖按钮
self.install_deps_button = ttk.Button(
control_frame,
@@ -343,6 +375,7 @@ def start_server(self):
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
+ encoding='utf-8', # 明确指定UTF-8编码
cwd=current_dir,
shell=False if sys.platform != 'win32' else True
)
@@ -362,10 +395,31 @@ def start_server(self):
messagebox.showerror("错误", f"启动服务器失败: {str(e)}")
def install_dependencies(self):
- """安装Node.js依赖"""
+ """安装Node.js依赖,带进度条显示"""
+ import re
+
+ def update_progress_bar(progress, text):
+ """更新进度条和文本"""
+ # 在主线程中更新UI
+ def update():
+ self.progress_var.set(progress)
+ self.progress_text_var.set(text)
+ self.root.update_idletasks() # 立即刷新UI
+
+ if self.root.winfo_exists():
+ self.root.after(0, update)
+
try:
self.log("开始安装依赖...")
+ # 显示进度条区域
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: self.progress_frame.pack(fill=tk.X, pady=(0, 20)))
+ self.root.update_idletasks()
+
+ # 初始化进度
+ update_progress_bar(0, "准备安装...")
+
# 禁用按钮防止重复点击
self.install_deps_button.config(state=tk.DISABLED)
@@ -386,36 +440,227 @@ def install_dependencies(self):
except:
pass # 如果找不到,就使用默认的npm
- install_process = subprocess.Popen(
- [npm_path, "install"],
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- cwd=current_dir,
- shell=False if sys.platform != 'win32' else True
- )
-
- # 读取安装进度
- for line in iter(install_process.stdout.readline, ''):
- if line:
- self.log(f"[npm] {line.strip()}")
-
- # 等待安装完成
- install_process.wait()
+ # 使用--progress参数启用进度输出
+ try:
+ install_process = subprocess.Popen(
+ [npm_path, "install", "--progress=true"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding='utf-8', # 明确指定UTF-8编码
+ cwd=current_dir,
+ shell=False if sys.platform != 'win32' else True
+ )
+
+ # 用于跟踪已安装的包数量
+ installed_count = 0
+
+ # 解析npm输出的正则表达式
+ # 匹配npm进度条输出、包安装信息等
+ progress_regex = re.compile(r'progress: ([\d.]+)%')
+ package_regex = re.compile(r'(added|updated)\s+(\d+)\s+packages?')
+ fetching_regex = re.compile(r'fetching\s+from\s+(.+)')
+ installing_regex = re.compile(r'installing\s+(.+)')
+ error_regex = re.compile(r'(error|fail|warning|warn)', re.IGNORECASE)
+
+ # 读取安装进度
+ start_time = time.time()
+ last_progress_update = time.time()
+
+ for line in iter(install_process.stdout.readline, ''):
+ if not line or not self.root.winfo_exists():
+ # 如果窗口已关闭或无输出,中断安装
+ if install_process.poll() is None:
+ self.log("检测到窗口关闭或中断,终止安装进程...")
+ try:
+ # 尝试优雅地终止进程
+ if sys.platform == 'win32':
+ install_process.terminate()
+ # 给进程一点时间终止
+ time.sleep(1)
+ if install_process.poll() is None:
+ install_process.kill() # 强制终止
+ else:
+ install_process.kill()
+ except Exception as kill_error:
+ self.log(f"终止安装进程时出错: {str(kill_error)}")
+ break
+
+ line = line.strip()
+ if line:
+ # 记录所有输出
+ self.log(f"[npm] {line}")
+
+ # 检查是否包含错误信息
+ if error_regex.search(line):
+ # 对于错误或警告信息,特别处理
+ error_level = "警告" if any(w in line.lower() for w in ['warning', 'warn']) else "错误"
+ update_progress_bar(self.progress_var.get(), f"{error_level}: {line[:100]}...")
+
+ # 尝试匹配进度百分比
+ progress_match = progress_regex.search(line)
+ if progress_match:
+ progress = float(progress_match.group(1))
+ update_progress_bar(progress, f"安装进度: {progress:.1f}%")
+ last_progress_update = time.time()
+
+ # 尝试匹配包安装信息
+ package_match = package_regex.search(line)
+ if package_match:
+ installed_count = int(package_match.group(2))
+ # 估算进度,基于假设的包总数(实际中可能需要动态计算)
+ estimated_progress = min(50 + (installed_count * 0.5), 90)
+ update_progress_bar(estimated_progress, f"已安装 {installed_count} 个包...")
+ last_progress_update = time.time()
+
+ # 尝试匹配正在获取的包
+ fetching_match = fetching_regex.search(line.lower())
+ if fetching_match:
+ package_name = fetching_match.group(1)
+ update_progress_bar(self.progress_var.get(), f"正在获取: {package_name}")
+
+ # 尝试匹配正在安装的包
+ installing_match = installing_regex.search(line.lower())
+ if installing_match:
+ package_name = installing_match.group(1)
+ update_progress_bar(self.progress_var.get(), f"正在安装: {package_name}")
+
+ # 检查是否超时(超过30分钟)
+ if time.time() - start_time > 30 * 60:
+ self.log("安装依赖超时(超过30分钟),终止安装...")
+ update_progress_bar(self.progress_var.get(), "安装超时,正在终止...")
+ if install_process.poll() is None:
+ try:
+ if sys.platform == 'win32':
+ install_process.terminate()
+ time.sleep(1)
+ if install_process.poll() is None:
+ install_process.kill()
+ else:
+ install_process.kill()
+ except Exception as kill_error:
+ self.log(f"终止超时进程时出错: {str(kill_error)}")
+ raise TimeoutError("依赖安装超时,请检查网络连接或尝试手动安装")
+
+ # 如果长时间没有进度更新,显示活动状态
+ if time.time() - last_progress_update > 10: # 10秒无更新
+ current_progress = self.progress_var.get()
+ # 小幅度增加进度以显示活动状态
+ if current_progress < 95:
+ new_progress = min(current_progress + 0.5, 95)
+ update_progress_bar(new_progress, f"安装进行中...")
+ last_progress_update = time.time()
+
+ # 等待安装完成并更新最终进度
+ update_progress_bar(95, "安装完成,正在清理...")
+
+ # 设置等待超时,避免无限等待
+ try:
+ # 使用wait但设置超时
+ return_code = install_process.wait(timeout=30) # 30秒超时
+ except subprocess.TimeoutExpired:
+ self.log("等待安装完成超时,强制终止进程...")
+ if install_process.poll() is None:
+ try:
+ if sys.platform == 'win32':
+ install_process.terminate()
+ time.sleep(1)
+ if install_process.poll() is None:
+ install_process.kill()
+ else:
+ install_process.kill()
+ except Exception as kill_error:
+ self.log(f"强制终止进程时出错: {str(kill_error)}")
+ raise TimeoutError("等待安装完成超时")
+
+ except (KeyboardInterrupt, SystemExit):
+ # 处理用户中断或系统退出
+ self.log("检测到用户中断或系统退出,终止安装...")
+ update_progress_bar(self.progress_var.get(), "安装已中断")
+ if 'install_process' in locals() and install_process.poll() is None:
+ try:
+ if sys.platform == 'win32':
+ install_process.terminate()
+ time.sleep(1)
+ if install_process.poll() is None:
+ install_process.kill()
+ else:
+ install_process.kill()
+ except Exception as kill_error:
+ self.log(f"终止进程时出错: {str(kill_error)}")
+ raise
+ except Exception as process_error:
+ # 捕获进程相关的错误
+ self.log(f"安装进程执行出错: {str(process_error)}")
+ update_progress_bar(self.progress_var.get(), f"执行错误: {str(process_error)[:50]}...")
+ if 'install_process' in locals() and install_process.poll() is None:
+ try:
+ if sys.platform == 'win32':
+ install_process.terminate()
+ time.sleep(1)
+ if install_process.poll() is None:
+ install_process.kill()
+ else:
+ install_process.kill()
+ except Exception as kill_error:
+ self.log(f"终止错误进程时出错: {str(kill_error)}")
+ raise
if install_process.returncode == 0:
+ update_progress_bar(100, "依赖安装成功!")
self.log("依赖安装成功")
- messagebox.showinfo("成功", "依赖安装完成")
+ # 延迟一下再显示成功消息,让用户看到100%的进度
+ if self.root.winfo_exists():
+ self.root.after(500, lambda: messagebox.showinfo("成功", "依赖安装完成"))
else:
self.log(f"依赖安装失败,退出码: {install_process.returncode}")
- messagebox.showerror("错误", "依赖安装失败,请查看日志获取详细信息")
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: messagebox.showerror("错误", "依赖安装失败,请查看日志获取详细信息"))
+ except TimeoutError as te:
+ # 处理超时错误
+ error_msg = f"安装超时: {str(te)}"
+ self.log(error_msg)
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: messagebox.showerror("安装超时", error_msg))
+ except KeyboardInterrupt:
+ # 处理用户中断
+ self.log("安装已被用户中断")
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: messagebox.showinfo("已中断", "依赖安装已被中断"))
+ except subprocess.SubprocessError as se:
+ # 处理子进程相关错误
+ error_msg = f"安装进程错误: {str(se)}"
+ self.log(error_msg)
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: messagebox.showerror("进程错误", error_msg))
+ except IOError as ioe:
+ # 处理IO错误
+ error_msg = f"输入/输出错误: {str(ioe)}"
+ self.log(error_msg)
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: messagebox.showerror("IO错误", f"文件读写错误,可能是权限问题: {str(ioe)}"))
+ except OSError as ose:
+ # 处理操作系统错误
+ error_msg = f"系统错误: {str(ose)}"
+ self.log(error_msg)
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: messagebox.showerror("系统错误", f"操作系统错误: {str(ose)}\n可能需要管理员权限或检查磁盘空间"))
except Exception as e:
- self.log(f"安装依赖时出错: {str(e)}")
- messagebox.showerror("错误", f"安装依赖时出错: {str(e)}")
+ # 处理其他所有错误
+ error_msg = f"安装依赖时出错: {str(e)}"
+ self.log(error_msg)
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: messagebox.showerror("错误", error_msg))
finally:
+ # 隐藏进度条区域
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: self.progress_frame.pack_forget())
# 重新启用按钮
- self.install_deps_button.config(state=tk.NORMAL)
+ if self.root.winfo_exists():
+ self.root.after(0, lambda: self.install_deps_button.config(state=tk.NORMAL))
+ # 确保资源释放
+ self.log("依赖安装过程完成(无论成功或失败)")
def _install_dependencies_silent(self):
"""静默安装依赖(用于自动检查时)"""
@@ -508,10 +753,19 @@ def read_server_logs(self):
self.log(f"读取日志时出错: {str(e)}")
def log(self, message):
- """添加日志到日志窗口"""
+ """添加日志到日志窗口并保存到文件"""
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
log_message = f"[{timestamp}] {message}"
+ # 尝试将日志写入文件
+ try:
+ log_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "server_manager.log")
+ with open(log_file_path, "a", encoding="utf-8") as log_file:
+ log_file.write(log_message + "\n")
+ except Exception as e:
+ # 如果写入日志文件失败,不影响主要功能,但在控制台打印错误
+ print(f"写入日志文件失败: {str(e)}")
+
# 在主线程中更新UI
self.root.after(0, lambda: self._append_log(log_message))
@@ -579,26 +833,13 @@ def is_mongodb_running(self):
return self.is_port_in_use(self.mongodb_port)
def download_mongodb(self):
- """使用Homebrew下载MongoDB"""
+ """下载MongoDB,支持Windows、macOS和Ubuntu Linux平台"""
try:
self.log("开始下载MongoDB...")
- # 检查是否是macOS系统
- if sys.platform != 'darwin':
- messagebox.showerror("错误", "此功能仅支持macOS系统")
- return
-
# 禁用下载按钮
self.download_mongodb_button.config(state=tk.DISABLED)
- # 执行Homebrew命令
- commands = [
- ["brew", "update"],
- ["bash", "-c", "export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles && brew update"],
- ["brew", "tap", "mongodb/brew"],
- ["brew", "install", "mongodb-community@7.0"]
- ]
-
# 创建进度条窗口
progress_window = tk.Toplevel(self.root)
progress_window.title("下载MongoDB")
@@ -608,7 +849,7 @@ def download_mongodb(self):
progress_window.grab_set()
# 创建进度条
- progress_label = ttk.Label(progress_window, text="正在执行命令...", padding=20)
+ progress_label = ttk.Label(progress_window, text="正在准备下载...", padding=20)
progress_label.pack(fill=tk.X)
progress = ttk.Progressbar(progress_window, length=480, mode='determinate')
@@ -616,44 +857,341 @@ def download_mongodb(self):
progress['value'] = 0
progress_window.update()
- # 执行命令序列
- success = True
- for i, cmd in enumerate(commands):
- cmd_str = ' '.join(cmd)
- progress_label.config(text=f"正在执行: {cmd_str}")
- progress['value'] = (i + 1) * 25
+ success = False
+
+ # 根据操作系统选择不同的下载方式
+ if sys.platform == 'darwin': # macOS
+ # 执行Homebrew命令
+ commands = [
+ ["brew", "update"],
+ ["bash", "-c", "export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles && brew update"],
+ ["brew", "tap", "mongodb/brew"],
+ ["brew", "install", "mongodb-community@7.0"]
+ ]
+
+ # 执行命令序列
+ success = True
+ for i, cmd in enumerate(commands):
+ cmd_str = ' '.join(cmd)
+ progress_label.config(text=f"正在执行: {cmd_str}")
+ progress['value'] = (i + 1) * 25
+ progress_window.update()
+
+ try:
+ self.log(f"执行命令: {cmd_str}")
+
+ # 对于包含环境变量的命令,使用shell=True
+ shell = len(cmd) > 1 and cmd[0] == "bash" and cmd[1] == "-c"
+
+ result = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ shell=shell,
+ timeout=600 # 设置10分钟超时
+ )
+
+ self.log(f"命令输出: {result.stdout}")
+
+ if result.returncode != 0:
+ self.log(f"命令执行失败,返回码: {result.returncode}")
+ success = False
+ break
+
+ except subprocess.TimeoutExpired:
+ self.log(f"命令执行超时: {cmd_str}")
+ success = False
+ break
+ except Exception as e:
+ self.log(f"执行命令时出错: {str(e)}")
+ success = False
+ break
+
+ elif sys.platform == 'win32': # Windows
+ import requests
+ import tempfile
+ import shutil
+ import time
+ import ctypes
+
+ # MongoDB Windows安装程序下载链接 (MongoDB 7.0 Community Edition)
+ download_url = "https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.0-signed.msi"
+
+ # 显示下载进度
+ progress_label.config(text="正在下载MongoDB安装程序...")
+ progress['value'] = 10
progress_window.update()
try:
- self.log(f"执行命令: {cmd_str}")
+ # 创建临时目录
+ temp_dir = tempfile.mkdtemp()
+ msi_path = os.path.join(temp_dir, "mongodb-installer.msi")
+
+ self.log(f"开始下载MongoDB安装程序: {download_url}")
+
+ # 下载MongoDB安装程序
+ response = requests.get(download_url, stream=True)
+ total_size = int(response.headers.get('content-length', 0))
+ downloaded_size = 0
+
+ with open(msi_path, 'wb') as file:
+ for data in response.iter_content(chunk_size=8192):
+ file.write(data)
+ downloaded_size += len(data)
+ if total_size > 0:
+ percent = (downloaded_size / total_size) * 80 + 10 # 10-90%
+ progress['value'] = percent
+ progress_window.update()
- # 对于包含环境变量的命令,使用shell=True
- shell = len(cmd) > 1 and cmd[0] == "bash" and cmd[1] == "-c"
+ self.log(f"MongoDB安装程序下载完成: {msi_path}")
+ progress_label.config(text="正在启动安装向导...")
+ progress['value'] = 90
+ progress_window.update()
+
+ # 检查是否以管理员权限运行
+ def is_admin():
+ try:
+ return ctypes.windll.shell32.IsUserAnAdmin()
+ except:
+ return False
+
+ # 启动MongoDB安装向导
+ if is_admin():
+ self.log("以管理员权限启动MongoDB安装向导")
+ subprocess.run(["msiexec", "/i", msi_path])
+ else:
+ self.log("提示用户以管理员权限运行安装向导")
+ messagebox.showinfo("提示", "请以管理员权限运行MongoDB安装向导")
+ subprocess.run(["msiexec", "/i", msi_path])
- result = subprocess.run(
- cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- shell=shell,
- timeout=600 # 设置10分钟超时
- )
+ # 等待安装完成(用户手动关闭安装向导)
+ progress_label.config(text="等待安装完成...")
+ progress['value'] = 95
+ progress_window.update()
- self.log(f"命令输出: {result.stdout}")
+ # 让用户确认安装是否完成
+ if messagebox.askyesno("确认", "MongoDB安装完成了吗?"):
+ success = True
- if result.returncode != 0:
- self.log(f"命令执行失败,返回码: {result.returncode}")
+ except Exception as e:
+ self.log(f"Windows下载安装MongoDB出错: {str(e)}")
+ messagebox.showerror("错误", f"下载或安装MongoDB时出错: {str(e)}")
+ success = False
+ finally:
+ # 清理临时文件
+ if 'temp_dir' in locals():
+ try:
+ shutil.rmtree(temp_dir)
+ except:
+ pass
+
+ elif sys.platform.startswith('linux'): # Linux (Ubuntu)
+ import re
+
+ # 检查是否为Ubuntu系统
+ try:
+ with open('/etc/os-release', 'r') as f:
+ os_release = f.read()
+ if 'Ubuntu' not in os_release:
+ self.log("检测到非Ubuntu Linux系统,此功能专为Ubuntu设计")
+ messagebox.showerror("错误", "此MongoDB安装功能专为Ubuntu设计")
+ success = False
+ return
+
+ # 提取Ubuntu版本
+ version_match = re.search(r'VERSION_ID="(\d+\.\d+)"', os_release)
+ if version_match:
+ ubuntu_version = version_match.group(1)
+ self.log(f"检测到Ubuntu {ubuntu_version}")
+ else:
+ self.log("无法确定Ubuntu版本")
+ except Exception as e:
+ self.log(f"检测Ubuntu系统信息时出错: {str(e)}")
+ # 继续尝试安装
+
+ # Ubuntu安装步骤
+ ubuntu_commands = [
+ # 更新包列表
+ ["sudo", "apt-get", "update"],
+ # 安装必要的依赖
+ ["sudo", "apt-get", "install", "-y", "gnupg"],
+ # 添加MongoDB GPG密钥
+ ["sudo", "wget", "-qO-", "https://www.mongodb.org/static/pgp/server-7.0.asc", "|", "sudo", "apt-key", "add", "-"],
+ # 创建MongoDB源列表文件
+ ["sudo", "echo", "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu $(lsb_release -cs)/mongodb-org/7.0 multiverse", "|", "sudo", "tee", "/etc/apt/sources.list.d/mongodb-org-7.0.list"],
+ # 更新包列表
+ ["sudo", "apt-get", "update"],
+ # 安装MongoDB
+ ["sudo", "apt-get", "install", "-y", "mongodb-org"]
+ ]
+
+ # 执行Ubuntu安装命令
+ success = True
+ for i, cmd in enumerate(ubuntu_commands):
+ cmd_str = ' '.join(cmd)
+ progress_label.config(text=f"正在执行: {cmd_str}")
+ progress['value'] = (i + 1) * (100 / len(ubuntu_commands))
+ progress_window.update()
+
+ try:
+ self.log(f"执行命令: {cmd_str}")
+
+ # 对于需要管道的命令,使用shell=True
+ shell = '|' in cmd_str
+
+ # 对于包含echo和tee的命令,使用不同的方式处理
+ if "echo" in cmd_str and "tee" in cmd_str:
+ # 提取要写入的内容和目标文件
+ content_match = re.search(r'echo "([^"]*)"', cmd_str)
+ file_match = re.search(r'tee (\S+)', cmd_str)
+ if content_match and file_match:
+ content = content_match.group(1)
+ target_file = file_match.group(1)
+ # 使用echo命令通过shell写入文件
+ shell_cmd = f"echo '{content}' | sudo tee {target_file}"
+ self.log(f"使用shell命令: {shell_cmd}")
+ result = subprocess.run(
+ shell_cmd,
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ timeout=300
+ )
+ else:
+ raise Exception("无法解析echo和tee命令")
+ else:
+ # 对于常规命令,分解命令参数
+ if shell:
+ # 对于包含管道的命令,使用shell=True
+ result = subprocess.run(
+ cmd_str,
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ timeout=300
+ )
+ else:
+ # 对于不包含管道的命令,正常执行
+ result = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ timeout=300
+ )
+
+ self.log(f"命令输出: {result.stdout}")
+
+ if result.returncode != 0:
+ self.log(f"命令执行失败,返回码: {result.returncode}")
+ # 对于某些非关键错误,尝试继续执行
+ if i not in [0, 3, 4]: # 不跳过更新和源列表配置
+ self.log("继续尝试后续命令...")
+ else:
+ success = False
+ break
+
+ except subprocess.TimeoutExpired:
+ self.log(f"命令执行超时: {cmd_str}")
success = False
break
+ except Exception as e:
+ self.log(f"执行命令时出错: {str(e)}")
+ success = False
+ break
+
+ # 启动MongoDB服务
+ if success:
+ progress_label.config(text="正在启动MongoDB服务...")
+ progress['value'] = 90
+ progress_window.update()
+
+ try:
+ # 启动服务
+ subprocess.run(
+ ["sudo", "systemctl", "start", "mongod"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True
+ )
- except subprocess.TimeoutExpired:
- self.log(f"命令执行超时: {cmd_str}")
- success = False
- break
- except Exception as e:
- self.log(f"执行命令时出错: {str(e)}")
- success = False
- break
+ # 设置开机自启
+ subprocess.run(
+ ["sudo", "systemctl", "enable", "mongod"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True
+ )
+
+ # 检查MongoDB是否成功启动
+ time.sleep(3)
+ result = subprocess.run(
+ ["sudo", "systemctl", "is-active", "mongod"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+
+ if "active" in result.stdout:
+ self.log("MongoDB服务已成功启动")
+ success = True
+ else:
+ self.log("MongoDB服务启动失败")
+ # 尝试直接启动mongod
+ self.log("尝试直接启动mongod...")
+ try:
+ subprocess.run(
+ ["mongod", "--dbpath", "/var/lib/mongodb", "--logpath", "/var/log/mongodb/mongod.log", "--fork"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True
+ )
+ # 检查是否成功
+ time.sleep(2)
+ if subprocess.run(["pgrep", "mongod"]).returncode == 0:
+ self.log("直接启动mongod成功")
+ success = True
+ else:
+ self.log("直接启动mongod失败")
+ success = False
+ except Exception as e:
+ self.log(f"直接启动mongod时出错: {str(e)}")
+ success = False
+
+ except Exception as e:
+ self.log(f"启动MongoDB服务时出错: {str(e)}")
+ success = False
+
+ # 检查MongoDB是否安装成功
+ if success:
+ try:
+ result = subprocess.run(
+ ["mongosh", "--version"], # MongoDB 6.0+ 使用mongosh而不是mongo
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+ if result.returncode == 0:
+ self.log(f"MongoDB Shell版本: {result.stdout.splitlines()[0]}")
+ else:
+ # 尝试旧版命令
+ result = subprocess.run(
+ ["mongo", "--version"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+ if result.returncode == 0:
+ self.log(f"MongoDB版本: {result.stdout.splitlines()[0]}")
+ except Exception as e:
+ self.log(f"检查MongoDB版本时出错: {str(e)}")
+
+ else:
+ messagebox.showerror("错误", f"不支持的操作系统: {sys.platform}")
+ success = False
# 关闭进度窗口
progress_window.destroy()
@@ -675,6 +1213,19 @@ def download_mongodb(self):
# 重新启用下载按钮
self.download_mongodb_button.config(state=tk.NORMAL)
+ def _is_command_available(self, command):
+ """检查命令是否可用"""
+ try:
+ if sys.platform == 'win32':
+ # Windows系统使用where命令检查
+ subprocess.run(['where', command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, check=True)
+ else:
+ # Unix系统使用which命令检查
+ subprocess.run(['which', command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
+ return True
+ except (subprocess.SubprocessError, FileNotFoundError):
+ return False
+
def start_mongodb(self):
"""启动MongoDB服务"""
with self.mongodb_lock:
@@ -709,6 +1260,7 @@ def start_mongodb(self):
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
+ encoding='utf-8', # 明确指定UTF-8编码
shell=False
)
# 等待MongoDB启动
@@ -726,95 +1278,241 @@ def start_mongodb(self):
return False
elif sys.platform == 'win32': # Windows
+ # 方法1: 尝试使用net start命令启动MongoDB服务
try:
- # 尝试使用net start命令启动MongoDB服务
- subprocess.run(
- ["net", "start", "MongoDB"],
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- check=True,
- shell=True
- )
- self.log("Windows服务启动MongoDB成功")
- self.mongodb_running = True
- self.update_ui_state()
- return True
- except subprocess.SubprocessError:
- self.log("Windows服务启动MongoDB失败,尝试直接启动")
- # 尝试直接启动mongod
- try:
- # 查找MongoDB安装路径
- mongo_path = "mongod" # 默认在PATH中
- # 检查常见的安装路径
- for path in [
- "C:\\Program Files\\MongoDB\\Server\\5.0\\bin\\mongod.exe",
- "C:\\Program Files\\MongoDB\\Server\\4.4\\bin\\mongod.exe",
- "C:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongod.exe"
- ]:
- if os.path.exists(path):
- mongo_path = path
- break
-
- self.mongodb_process = subprocess.Popen(
- [mongo_path],
+ if self._is_command_available('net'):
+ result = subprocess.run(
+ ["net", "start", "MongoDB"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
shell=True
)
- # 等待MongoDB启动
- time.sleep(3)
- if self.is_mongodb_running():
- self.log("直接启动MongoDB成功")
+ if result.returncode == 0:
+ self.log("Windows服务启动MongoDB成功")
self.mongodb_running = True
self.update_ui_state()
return True
else:
- self.log("直接启动MongoDB失败")
- return False
- except Exception as e:
- self.log(f"直接启动MongoDB时出错: {str(e)}")
- return False
-
- elif sys.platform.startswith('linux'): # Linux
+ self.log(f"Windows服务启动MongoDB失败: {result.stdout}")
+ else:
+ self.log("net命令不可用,跳过服务启动尝试")
+ except Exception as e:
+ self.log(f"使用net命令启动MongoDB服务时出错: {str(e)}")
+
+ # 方法2: 尝试使用sc命令启动MongoDB服务
try:
- # 尝试使用systemctl启动MongoDB
- subprocess.run(
- ["sudo", "systemctl", "start", "mongodb"],
+ if self._is_command_available('sc'):
+ result = subprocess.run(
+ ["sc", "start", "MongoDB"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ shell=True
+ )
+ if result.returncode == 0 or "SUCCESS" in result.stdout:
+ self.log("使用sc命令启动MongoDB服务成功")
+ self.mongodb_running = True
+ self.update_ui_state()
+ return True
+ else:
+ self.log(f"使用sc命令启动MongoDB服务失败: {result.stdout}")
+ else:
+ self.log("sc命令不可用,跳过服务启动尝试")
+ except Exception as e:
+ self.log(f"使用sc命令启动MongoDB服务时出错: {str(e)}")
+
+ # 方法3: 尝试直接启动mongod
+ try:
+ self.log("尝试直接启动MongoDB进程...")
+ # 查找MongoDB安装路径,支持最新版本
+ mongo_path = "mongod" # 默认在PATH中
+ # 检查常见的安装路径,包括最新版本
+ for path in [
+ "C:\\Program Files\\MongoDB\\Server\\7.0\\bin\\mongod.exe",
+ "C:\\Program Files\\MongoDB\\Server\\6.0\\bin\\mongod.exe",
+ "C:\\Program Files\\MongoDB\\Server\\5.0\\bin\\mongod.exe",
+ "C:\\Program Files\\MongoDB\\Server\\4.4\\bin\\mongod.exe",
+ "C:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongod.exe"
+ ]:
+ if os.path.exists(path):
+ mongo_path = path
+ self.log(f"找到MongoDB安装: {path}")
+ break
+
+ # 确保数据目录存在
+ data_dir = os.path.join(os.environ.get('ProgramData', 'C:\\ProgramData'), 'MongoDB', 'data', 'db')
+ if not os.path.exists(data_dir):
+ try:
+ os.makedirs(data_dir, exist_ok=True)
+ self.log(f"创建MongoDB数据目录: {data_dir}")
+ except Exception as e:
+ self.log(f"创建数据目录失败: {str(e)}")
+
+ # 启动MongoDB进程
+ startup_info = subprocess.STARTUPINFO()
+ startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+
+ self.mongodb_process = subprocess.Popen(
+ [mongo_path, f"--dbpath={data_dir}"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
- check=True
+ encoding='utf-8', # 明确指定UTF-8编码
+ shell=True,
+ startupinfo=startup_info
)
- self.log("使用systemctl启动MongoDB成功")
- self.mongodb_running = True
- self.update_ui_state()
- return True
- except subprocess.SubprocessError:
- self.log("使用systemctl启动MongoDB失败,尝试直接启动")
- # 尝试直接启动mongod
+
+ # 添加超时处理,最多等待10秒
+ max_wait_time = 10
+ wait_time = 0
+ check_interval = 1
+
+ while wait_time < max_wait_time:
+ time.sleep(check_interval)
+ wait_time += check_interval
+ if self.is_mongodb_running():
+ self.log("直接启动MongoDB成功")
+ self.mongodb_running = True
+ self.update_ui_state()
+ return True
+
+ # 检查进程是否已经退出
+ if self.mongodb_process.poll() is not None:
+ self.log("MongoDB进程已退出,启动失败")
+ # 尝试读取错误输出
+ if self.mongodb_process.stdout:
+ error_output = self.mongodb_process.stdout.read()
+ if error_output:
+ self.log(f"MongoDB错误输出: {error_output}")
+ break
+
+ self.log("直接启动MongoDB超时失败")
+ return False
+
+ except Exception as e:
+ self.log(f"直接启动MongoDB时出错: {str(e)}")
+ return False
+
+ elif sys.platform.startswith('linux'): # Linux (Ubuntu)
+ try:
+ # 检查Ubuntu系统中的MongoDB服务名称
+ # Ubuntu中MongoDB服务可能是mongodb或mongod
+ service_name = "mongodb"
try:
- self.mongodb_process = subprocess.Popen(
- ["mongod"],
+ # 检查mongod服务是否存在
+ result = subprocess.run(
+ ["sudo", "systemctl", "list-units", "--full", "-all", "|", "grep", "mongod"],
+ shell=True,
stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- shell=False
+ stderr=subprocess.PIPE
)
- # 等待MongoDB启动
- time.sleep(3)
+ if result.returncode == 0:
+ service_name = "mongod"
+ self.log(f"检测到MongoDB服务名称: {service_name}")
+ except:
+ self.log("使用默认MongoDB服务名称: mongodb")
+
+ # 尝试使用systemctl启动MongoDB
+ self.log(f"尝试使用systemctl启动MongoDB服务 ({service_name})...")
+ result = subprocess.run(
+ ["sudo", "systemctl", "start", service_name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True
+ )
+
+ if result.returncode == 0:
+ self.log(f"使用systemctl启动{service_name}成功")
+ self.mongodb_running = True
+ self.update_ui_state()
+ return True
+ else:
+ self.log(f"使用systemctl启动{service_name}失败: {result.stdout}")
+
+ # 检查MongoDB是否已经运行
if self.is_mongodb_running():
- self.log("直接启动MongoDB成功")
+ self.log("MongoDB已经在运行")
self.mongodb_running = True
self.update_ui_state()
return True
- else:
- self.log("直接启动MongoDB失败")
+
+ # 尝试直接启动mongod
+ self.log("尝试直接启动mongod...")
+ try:
+ # 确保数据目录存在
+ data_dir = "/var/lib/mongodb"
+ if not os.path.exists(data_dir):
+ try:
+ os.makedirs(data_dir, exist_ok=True)
+ # 设置权限
+ subprocess.run(["sudo", "chown", "mongodb:mongodb", data_dir], check=False)
+ self.log(f"创建MongoDB数据目录: {data_dir}")
+ except Exception as e:
+ self.log(f"创建数据目录失败: {str(e)}")
+ # 使用临时目录作为备选
+ data_dir = "/tmp/mongodb_data"
+ os.makedirs(data_dir, exist_ok=True)
+ self.log(f"使用临时数据目录: {data_dir}")
+
+ # 确保日志目录存在
+ log_dir = "/var/log/mongodb"
+ log_file = os.path.join(log_dir, "mongod.log")
+ if not os.path.exists(log_dir):
+ try:
+ os.makedirs(log_dir, exist_ok=True)
+ # 设置权限
+ subprocess.run(["sudo", "chown", "mongodb:mongodb", log_dir], check=False)
+ open(log_file, 'a').close()
+ subprocess.run(["sudo", "chown", "mongodb:mongodb", log_file], check=False)
+ except Exception as e:
+ self.log(f"创建日志目录失败: {str(e)}")
+ log_file = os.path.join("/tmp", "mongod.log")
+ self.log(f"使用临时日志文件: {log_file}")
+
+ # 直接启动mongod进程
+ self.mongodb_process = subprocess.Popen(
+ ["mongod", "--dbpath", data_dir, "--logpath", log_file],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding='utf-8' # 明确指定UTF-8编码
+ )
+
+ # 等待MongoDB启动
+ max_wait_time = 10
+ wait_time = 0
+ check_interval = 1
+
+ while wait_time < max_wait_time:
+ time.sleep(check_interval)
+ wait_time += check_interval
+ if self.is_mongodb_running():
+ self.log("直接启动MongoDB成功")
+ self.mongodb_running = True
+ self.update_ui_state()
+ return True
+
+ # 检查进程是否已经退出
+ if self.mongodb_process.poll() is not None:
+ self.log("MongoDB进程已退出,启动失败")
+ # 尝试读取错误输出
+ if self.mongodb_process.stdout:
+ error_output = self.mongodb_process.stdout.read()
+ if error_output:
+ self.log(f"MongoDB错误输出: {error_output}")
+ break
+
+ self.log("直接启动MongoDB超时失败")
return False
- except Exception as e:
- self.log(f"直接启动MongoDB时出错: {str(e)}")
- return False
+
+ except Exception as e:
+ self.log(f"直接启动MongoDB时出错: {str(e)}")
+ return False
+
+ except Exception as e:
+ self.log(f"启动MongoDB时出错: {str(e)}")
+ return False
# 如果没有找到对应的操作系统处理方法
messagebox.showerror("错误", "不支持的操作系统,请手动启动MongoDB")
@@ -851,38 +1549,108 @@ def stop_mongodb(self):
self.log("使用brew服务停止MongoDB失败,检查是否有直接启动的进程")
elif sys.platform == 'win32': # Windows
+ # 尝试以管理员权限停止MongoDB服务
+ self.log("尝试以管理员权限停止MongoDB服务...")
+
+ # 方法1: 使用PowerShell以管理员权限停止MongoDB服务
try:
- # 尝试使用net stop命令停止MongoDB服务
- subprocess.run(
- ["net", "stop", "MongoDB"],
+ # 创建PowerShell命令来停止服务
+ powershell_command = """
+ # 尝试停止MongoDB服务
+ try {
+ $service = Get-Service -Name MongoDB -ErrorAction SilentlyContinue
+ if ($service) {
+ Stop-Service -Name MongoDB -Force
+ Write-Output "MongoDB服务停止成功"
+ } else {
+ Write-Output "MongoDB服务不存在"
+ }
+ } catch {
+ Write-Output "停止MongoDB服务时出错: $_"
+ }
+ """
+
+ # 使用PowerShell以管理员权限运行
+ result = subprocess.run(
+ ["powershell", "-Command",
+ "Start-Process", "powershell",
+ "-ArgumentList", f"-NoProfile -ExecutionPolicy Bypass -Command \"{powershell_command}\"",
+ "-Verb", "RunAs",
+ "-Wait",
+ "-PassThru"],
stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- check=True,
- shell=True
+ stderr=subprocess.PIPE,
+ text=True
)
- self.log("Windows服务停止MongoDB成功")
- except subprocess.SubprocessError:
- self.log("Windows服务停止MongoDB失败,检查是否有直接启动的进程")
- elif sys.platform.startswith('linux'): # Linux
+ self.log("已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框")
+ except Exception as e:
+ self.log(f"以管理员权限停止MongoDB服务时出错: {str(e)}")
+
+ # 方法2: 尝试使用net stop命令停止MongoDB服务(备用方案)
+ try:
+ if self._is_command_available('net'):
+ result = subprocess.run(
+ ["net", "stop", "MongoDB"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ shell=True
+ )
+ if result.returncode == 0:
+ self.log("Windows服务停止MongoDB成功")
+ else:
+ self.log(f"Windows服务停止MongoDB失败: {result.stdout}")
+ except Exception as e:
+ self.log(f"使用net命令停止MongoDB服务时出错: {str(e)}")
+
+ # 方法3: 尝试使用sc命令停止MongoDB服务(备用方案)
+ try:
+ if self._is_command_available('sc'):
+ result = subprocess.run(
+ ["sc", "stop", "MongoDB"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ shell=True
+ )
+ if result.returncode == 0 or "SUCCESS" in result.stdout:
+ self.log("使用sc命令停止MongoDB服务成功")
+ else:
+ self.log(f"使用sc命令停止MongoDB服务失败: {result.stdout}")
+ except Exception as e:
+ self.log(f"使用sc命令停止MongoDB服务时出错: {str(e)}")
+
+ elif sys.platform.startswith('linux'): # Linux (Ubuntu)
try:
- # 尝试使用systemctl停止MongoDB
- subprocess.run(
- ["sudo", "systemctl", "stop", "mongodb"],
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- check=True
- )
- self.log("使用systemctl停止MongoDB成功")
- except subprocess.SubprocessError:
- self.log("使用systemctl停止MongoDB失败,检查是否有直接启动的进程")
+ # 检查Ubuntu系统中的MongoDB服务名称
+ service_names = ["mongod", "mongodb"] # 尝试常见的服务名称
+ stopped = False
+
+ for service_name in service_names:
+ self.log(f"尝试使用systemctl停止{service_name}服务...")
+ result = subprocess.run(
+ ["sudo", "systemctl", "stop", service_name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True
+ )
+
+ if result.returncode == 0:
+ self.log(f"使用systemctl停止{service_name}成功")
+ stopped = True
+ break
+
+ if not stopped:
+ self.log("使用systemctl停止MongoDB服务失败,检查是否有直接启动的进程")
+ except Exception as e:
+ self.log(f"使用systemctl停止MongoDB服务时出错: {str(e)}")
# 检查并终止直接启动的MongoDB进程
if self.mongodb_process:
if sys.platform == 'win32':
subprocess.call(['taskkill', '/F', '/T', '/PID', str(self.mongodb_process.pid)])
+ self.log(f"终止MongoDB进程PID: {self.mongodb_process.pid}")
else:
self.mongodb_process.terminate()
try:
@@ -892,18 +1660,132 @@ def stop_mongodb(self):
self.mongodb_process = None
self.log("终止直接启动的MongoDB进程")
- # 等待MongoDB停止
- time.sleep(2)
+ # Windows系统: 尝试终止所有MongoDB进程
+ if sys.platform == 'win32':
+ # 重试机制:最多重试3次
+ retry_count = 0
+ max_retries = 3
+
+ while retry_count < max_retries:
+ try:
+ # 查找所有mongod.exe进程
+ result = subprocess.run(
+ ['tasklist', '/FI', 'IMAGENAME eq mongod.exe', '/NH'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ shell=True
+ )
+
+ if 'mongod.exe' in result.stdout:
+ self.log(f"第{retry_count+1}次尝试终止所有MongoDB进程")
+ # 终止所有MongoDB进程,使用更强力的参数
+ subprocess.run(
+ ['taskkill', '/F', '/IM', 'mongod.exe', '/T'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=True
+ )
+ self.log("已尝试终止所有MongoDB进程")
+
+ # 等待进程终止
+ time.sleep(2)
+ else:
+ self.log("没有检测到MongoDB进程")
+ break
+ except Exception as e:
+ self.log(f"终止MongoDB进程时出错: {str(e)}")
+
+ retry_count += 1
+ if retry_count < max_retries:
+ time.sleep(1) # 重试间隔
+
+ # Linux/macOS系统: 尝试终止所有MongoDB进程
+ else:
+ try:
+ # 查找并终止所有mongod进程
+ subprocess.run(['pkill', '-f', 'mongod'], check=False)
+ self.log("已尝试终止所有MongoDB进程")
+ except Exception as e:
+ self.log(f"终止MongoDB进程时出错: {str(e)}")
- # 更新状态
- self.mongodb_running = False
- self.update_ui_state()
+ # 增加等待时间,确保进程完全终止
+ self.log("等待MongoDB进程完全终止...")
+ time.sleep(3)
- self.log("MongoDB服务已停止")
+ # 验证MongoDB是否真正停止:检查端口和进程
+ is_really_stopped = False
+ max_checks = 5
+ check_count = 0
+
+ while check_count < max_checks:
+ # 检查端口是否被占用
+ port_in_use = self.is_port_in_use(self.mongodb_port)
+
+ # 检查进程是否存在
+ process_exists = False
+ if sys.platform == 'win32':
+ result = subprocess.run(
+ ['tasklist', '/FI', 'IMAGENAME eq mongod.exe', '/NH'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ shell=True
+ )
+ process_exists = 'mongod.exe' in result.stdout
+ else:
+ try:
+ result = subprocess.run(
+ ['pgrep', '-f', 'mongod'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+ process_exists = result.returncode == 0
+ except:
+ pass
+
+ if not port_in_use and not process_exists:
+ is_really_stopped = True
+ break
+
+ self.log(f"MongoDB似乎仍在运行,等待并再次检查...({check_count+1}/{max_checks})")
+ time.sleep(2)
+ check_count += 1
+
+ # 更新状态
+ if is_really_stopped:
+ self.mongodb_running = False
+ self.update_ui_state()
+ self.log("MongoDB服务已成功停止")
+ else:
+ # 如果仍然检测到MongoDB运行,最后尝试一次强力终止
+ if sys.platform == 'win32':
+ self.log("MongoDB似乎仍在运行,尝试最后一次强力终止...")
+ subprocess.run(['taskkill', '/F', '/IM', 'mongod.exe', '/T'], shell=True)
+ else:
+ subprocess.run(['pkill', '-9', '-f', 'mongod'], check=False)
+
+ time.sleep(2)
+ # 最后再次检查
+ port_in_use = self.is_port_in_use(self.mongodb_port)
+ if not port_in_use:
+ self.mongodb_running = False
+ self.update_ui_state()
+ self.log("MongoDB服务已成功停止")
+ else:
+ self.log("警告:无法确认MongoDB是否已完全停止,请手动检查")
+ messagebox.showwarning("警告", "无法确认MongoDB是否已完全停止,请手动检查端口和进程")
except Exception as e:
self.log(f"停止MongoDB失败: {str(e)}")
messagebox.showerror("错误", f"停止MongoDB失败: {str(e)}")
+ # 即使出现异常,也尝试更新状态
+ try:
+ if not self.is_port_in_use(self.mongodb_port):
+ self.mongodb_running = False
+ self.update_ui_state()
+ except:
+ pass
def update_ui_state(self):
"""更新UI按钮状态"""
@@ -1017,11 +1899,31 @@ def main():
主函数入口
注意:此控制台使用Homebrew管理MongoDB社区版,部分代码注释由AI辅助生成
"""
+ # 在控制台根目录写入启动日志
+ try:
+ log_dir = os.path.dirname(os.path.abspath(__file__))
+ log_file = os.path.join(log_dir, "server_manager.log")
+ current_time = time.strftime("%Y-%m-%d %H:%M:%S")
+ with open(log_file, "a", encoding="utf-8") as f:
+ f.write(f"[{current_time}] HalloChat服务器管理器启动\n")
+ except Exception as e:
+ print(f"写入日志失败: {str(e)}")
+
# 检查Node.js是否安装
try:
subprocess.run(["node", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
except (subprocess.SubprocessError, FileNotFoundError):
- print("错误: 未找到Node.js,请先安装Node.js")
+ error_message = "错误: 未找到Node.js,请先安装Node.js"
+ print(error_message)
+ # 将错误写入日志文件
+ try:
+ log_dir = os.path.dirname(os.path.abspath(__file__))
+ log_file = os.path.join(log_dir, "server_manager.log")
+ current_time = time.strftime("%Y-%m-%d %H:%M:%S")
+ with open(log_file, "a", encoding="utf-8") as f:
+ f.write(f"[{current_time}] {error_message}\n")
+ except Exception as e:
+ print(f"写入错误日志失败: {str(e)}")
sys.exit(1)
# 检查npm是否安装
@@ -1042,7 +1944,17 @@ def main():
subprocess.run([npm_path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
except (subprocess.SubprocessError, FileNotFoundError):
- print("错误: 未找到npm,请先安装Node.js和npm")
+ error_message = "错误: 未找到npm,请先安装npm"
+ print(error_message)
+ # 将错误写入日志文件
+ try:
+ log_dir = os.path.dirname(os.path.abspath(__file__))
+ log_file = os.path.join(log_dir, "server_manager.log")
+ current_time = time.strftime("%Y-%m-%d %H:%M:%S")
+ with open(log_file, "a", encoding="utf-8") as f:
+ f.write(f"[{current_time}] {error_message}\n")
+ except Exception as e:
+ print(f"写入错误日志失败: {str(e)}")
sys.exit(1)
# 创建并运行GUI
diff --git a/server/scripts/migrate-add-uid.js b/server/scripts/migrate-add-uid.js
new file mode 100644
index 0000000..1c24b82
--- /dev/null
+++ b/server/scripts/migrate-add-uid.js
@@ -0,0 +1,91 @@
+/**
+ * 数据库迁移脚本:为现有用户添加 UID 字段
+ *
+ * 运行方式:
+ * node scripts/migrate-add-uid.js
+ */
+
+const mongoose = require('mongoose');
+const User = require('../src/models/user.model');
+
+// 从环境变量或配置文件读取数据库连接
+const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/hallochat';
+
+async function migrateUsers() {
+ try {
+ console.log('[迁移] 连接数据库...');
+ await mongoose.connect(MONGODB_URI, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true
+ });
+ console.log('[迁移] 数据库连接成功!');
+
+ // 获取所有没有 UID 的用户
+ const usersWithoutUID = await User.find({ uid: { $exists: false } });
+ console.log(`[迁移] 找到 ${usersWithoutUID.length} 个需要迁移的用户`);
+
+ if (usersWithoutUID.length === 0) {
+ console.log('[迁移] 所有用户已经有 UID,无需迁移');
+ return;
+ }
+
+ // 检查是否存在管理员用户(通过 role 或 username 判断)
+ const adminUser = usersWithoutUID.find(
+ user => user.role === 'admin' || user.username.toLowerCase() === 'admin'
+ );
+
+ let successCount = 0;
+ let failCount = 0;
+
+ // 为管理员用户设置特殊 UID
+ if (adminUser) {
+ try {
+ adminUser.uid = 'ADMIN';
+ await adminUser.save();
+ console.log(`[迁移] ✅ 管理员用户 ${adminUser.username} 已设置 UID: ADMIN`);
+ successCount++;
+ } catch (error) {
+ console.error(`[迁移] ❌ 管理员用户 ${adminUser.username} UID 设置失败:`, error.message);
+ failCount++;
+ }
+ }
+
+ // 为其他用户生成 UID
+ for (const user of usersWithoutUID) {
+ if (user.uid) continue; // 跳过已经设置过的(如管理员)
+
+ try {
+ const uid = await User.generateUniqueUID();
+ user.uid = uid;
+ await user.save();
+ console.log(`[迁移] ✅ 用户 ${user.username} 已生成 UID: ${uid}`);
+ successCount++;
+ } catch (error) {
+ console.error(`[迁移] ❌ 用户 ${user.username} UID 生成失败:`, error.message);
+ failCount++;
+ }
+ }
+
+ console.log('\n[迁移] ==================== 迁移完成 ====================');
+ console.log(`[迁移] 成功: ${successCount} 个用户`);
+ console.log(`[迁移] 失败: ${failCount} 个用户`);
+ console.log('[迁移] ===================================================\n');
+
+ } catch (error) {
+ console.error('[迁移] 发生错误:', error);
+ } finally {
+ await mongoose.disconnect();
+ console.log('[迁移] 数据库连接已关闭');
+ }
+}
+
+// 运行迁移
+migrateUsers()
+ .then(() => {
+ console.log('[迁移] 脚本执行完成');
+ process.exit(0);
+ })
+ .catch(error => {
+ console.error('[迁移] 脚本执行失败:', error);
+ process.exit(1);
+ });
diff --git a/server/src/models/user.model.js b/server/src/models/user.model.js
index c6c0632..05462ef 100644
--- a/server/src/models/user.model.js
+++ b/server/src/models/user.model.js
@@ -37,6 +37,16 @@ const userSchema = new Schema({
default: () => uuidv1()
},
+ // 用户唯一标识符(以服务器为单位,用于识别和验证)
+ uid: {
+ type: String,
+ required: true,
+ unique: true,
+ uppercase: true,
+ trim: true,
+ index: true
+ },
+
random_id: {
type: String,
required: true,
@@ -132,6 +142,45 @@ userSchema.index({ email: 1 });
userSchema.index({ status: 1 });
userSchema.index({ lastLogin: 1 });
userSchema.index({ random_id: 1 });
+userSchema.index({ uid: 1 }, { unique: true });
+
+// UID 生成器(以服务器为单位)
+function generateUID() {
+ const timestamp = Date.now().toString(36).toUpperCase(); // 时间戳
+ const random = Math.random().toString(36).substr(2, 6).toUpperCase(); // 随机字符
+ return `${timestamp}${random}`;
+}
+
+// 静态方法:生成唯一 UID
+userSchema.statics.generateUniqueUID = async function() {
+ let uid;
+ let exists = true;
+ let attempts = 0;
+ const maxAttempts = 10;
+
+ while (exists && attempts < maxAttempts) {
+ uid = generateUID();
+ exists = await this.findOne({ uid });
+ attempts++;
+ }
+
+ if (exists) {
+ throw new Error('无法生成唯一 UID,请稍后重试');
+ }
+
+ return uid;
+};
+
+// 静态方法:检查 UID 是否存在
+userSchema.statics.isUIDExists = async function(uid) {
+ const user = await this.findOne({ uid });
+ return !!user;
+};
+
+// 静态方法:通过 UID 查找用户
+userSchema.statics.findByUID = async function(uid) {
+ return await this.findOne({ uid }).select('-password');
+};
// 密码加密中间件
userSchema.pre('save', async function(next) {
@@ -161,6 +210,7 @@ userSchema.methods.getPublicProfile = function() {
id: this._id,
username: this.username,
email: this.email,
+ uid: this.uid,
avatar: this.avatar,
status: this.status,
role: this.role,
@@ -219,16 +269,22 @@ userSchema.statics.registerUser = async function(userData) {
throw error;
}
+ // 生成唯一 UID
+ const uid = await this.generateUniqueUID();
+
// 创建用户 - 密码会通过pre('save')中间件自动哈希
const user = new this({
username,
email,
- password // 明文密码,由pre('save')中间件处理哈希
+ password, // 明文密码,由pre('save')中间件处理哈希
+ uid
});
// 保存用户
await user.save();
+ console.log(`[用户注册] 成功创建用户: ${username}, UID: ${uid}`);
+
// 返回用户的公开信息
return user.getPublicProfile();
};
@@ -253,10 +309,14 @@ userSchema.statics.getFriends = async function(userId) {
};
// 用户登录验证
-async function loginUser(usernameOrEmail, plainPassword) {
- // 支持通过用户名或邮箱登录
+async function loginUser(usernameOrEmailOrUID, plainPassword) {
+ // 支持通过用户名、邮箱或 UID 登录
const user = await User.findOne({
- $or: [{ username: usernameOrEmail }, { email: usernameOrEmail }]
+ $or: [
+ { username: usernameOrEmailOrUID },
+ { email: usernameOrEmailOrUID },
+ { uid: usernameOrEmailOrUID.toUpperCase() }
+ ]
});
if (!user) throw new Error('用户不存在');
@@ -267,10 +327,13 @@ async function loginUser(usernameOrEmail, plainPassword) {
user.lastLogin = new Date();
await user.save();
+ console.log(`[用户登录] ${user.username} (UID: ${user.uid}) 登录成功`);
+
return {
id: user._id,
username: user.username,
email: user.email,
+ uid: user.uid,
userUuid: user.user_uuid,
randomId: user.random_id,
avatar: user.avatar,