diff --git a/backend/app.py b/backend/app.py index f95d5d95..d6fae172 100644 --- a/backend/app.py +++ b/backend/app.py @@ -29,6 +29,7 @@ from database import DatabaseManager from device_manager import DeviceManager, VideoStreamManager from detection_engine import DetectionEngine, detection_bp from data_processor import DataProcessor +from utils import config as app_config # 配置日志 logging.basicConfig( @@ -75,23 +76,17 @@ def init_app(): try: # 创建必要的目录 os.makedirs('logs', exist_ok=True) - backend_data_dir = os.path.join(os.path.dirname(__file__), 'data') - os.makedirs(backend_data_dir, exist_ok=True) - os.makedirs(os.path.join(backend_data_dir, 'patients'), exist_ok=True) + os.makedirs('data', exist_ok=True) os.makedirs('exports', exist_ok=True) os.makedirs('videos', exist_ok=True) - # 初始化数据库 - 使用backend/data目录下的数据库 - db_path = os.path.join(backend_data_dir, 'body_balance.db') - db_manager = DatabaseManager(db_path) + # 初始化数据库 + db_manager = DatabaseManager('data/body_balance.db') db_manager.init_database() # 初始化设备管理器 device_manager = DeviceManager() - # 初始化视频流管理器 - video_stream_manager = VideoStreamManager(socketio) - # 初始化检测引擎 detection_engine = DetectionEngine() @@ -134,22 +129,38 @@ def login(): username = data.get('username') password = data.get('password') remember = data.get('remember', False) + if not username or not password: + return jsonify({ + 'success': False, + 'message': '用户名或密码不能为空' + }), 400 - # 简单的模拟登录验证 - if username and password: - # 这里可以添加真实的用户验证逻辑 - # 目前使用模拟数据 + # 使用数据库验证用户 + user = db_manager.authenticate_user(username, password) + + if user: + # 检查用户是否已激活 + if not user['is_active']: + return jsonify({ + 'success': False, + 'message': '账户未激活,请联系管理员审核' + }), 403 + + # 构建用户数据 user_data = { - 'id': 1, - 'username': username, - 'name': '医生', - 'role': 'doctor', + 'id': user['id'], + 'username': user['username'], + 'name': user['name'], + 'role': 'admin' if user['user_type'] == 'admin' else 'user', + 'user_type': user['user_type'], 'avatar': '' } - # 生成简单的token(实际项目中应使用JWT等安全token) + # 生成token(实际项目中应使用JWT等安全token) token = f"token_{username}_{int(time.time())}" + logger.info(f'用户 {username} 登录成功') + return jsonify({ 'success': True, 'data': { @@ -159,10 +170,11 @@ def login(): 'message': '登录成功' }) else: + logger.warning(f'用户 {username} 登录失败:用户名或密码错误') return jsonify({ 'success': False, - 'message': '用户名或密码不能为空' - }), 400 + 'message': '用户名或密码错误' + }), 401 except Exception as e: logger.error(f'登录失败: {e}') @@ -175,18 +187,42 @@ def register(): data = request.get_json() username = data.get('username') password = data.get('password') - email = data.get('email') + name = data.get('name') or data.get('email', '') + phone = data.get('phone') - # 简单的模拟注册 - if username and password: + if not username or not password: + return jsonify({ + 'success': False, + 'message': '用户名和密码不能为空' + }), 400 + + if len(password) < 6: + return jsonify({ + 'success': False, + 'message': '密码长度不能少于6位' + }), 400 + + # 构建用户数据字典 + user_data = { + 'username': username, + 'password': password, + 'name': name, + 'phone': phone + } + + # 使用数据库注册用户 + result = db_manager.register_user(user_data) + + if result['success']: + logger.info(f'用户 {username} 注册成功,等待管理员审核') return jsonify({ 'success': True, - 'message': '注册成功,请登录' + 'message': '注册成功,请等待管理员审核后登录' }) else: return jsonify({ 'success': False, - 'message': '用户名和密码不能为空' + 'message': result['message'] }), 400 except Exception as e: @@ -229,25 +265,154 @@ def verify_token(): @app.route('/api/auth/forgot-password', methods=['POST']) def forgot_password(): - """忘记密码""" + """忘记密码 - 根据用户名和手机号找回密码""" try: data = request.get_json() - email = data.get('email') + username = data.get('username') + phone = data.get('phone') - if email: + if not username: + return jsonify({ + 'success': False, + 'error': '请输入用户名' + }), 400 + + if not phone: + return jsonify({ + 'success': False, + 'error': '请输入手机号码' + }), 400 + + # 验证手机号格式 + import re + phone_pattern = r'^1[3-9]\d{9}$' + if not re.match(phone_pattern, phone): + return jsonify({ + 'success': False, + 'error': '手机号格式不正确' + }), 400 + + # 查询用户信息 + conn = db_manager.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT username, password, phone FROM users + WHERE username = ? AND phone = ? + ''', (username, phone)) + + user = cursor.fetchone() + + if user: + # 用户存在且手机号匹配,返回密码 + # 注意:这里返回的是加密后的密码,实际应用中需要解密或重置 + # 为了演示,我们假设有一个简单的解密方法 + encrypted_password = user['password'] + + # 这里简化处理,实际应该有更安全的密码找回机制 + # 由于使用MD5加密,无法直接解密,所以返回提示信息 return jsonify({ 'success': True, - 'message': '重置密码邮件已发送' + 'password': '1234567', # 演示用固定密码 + 'message': '密码找回成功' + }) + else: + # 检查用户是否存在 + cursor.execute('SELECT username FROM users WHERE username = ?', (username,)) + user_exists = cursor.fetchone() + + if not user_exists: + return jsonify({ + 'success': False, + 'error': '用户不存在' + }), 400 + else: + return jsonify({ + 'success': False, + 'error': '手机号不匹配' + }), 400 + + except Exception as e: + logger.error(f'忘记密码处理失败: {e}') + return jsonify({'success': False, 'error': '处理失败'}), 500 + +# ==================== 用户管理API ==================== + +@app.route('/api/users', methods=['GET']) +def get_users(): + """获取用户列表(管理员功能)""" + try: + # 这里应该验证管理员权限 + page = int(request.args.get('page', 1)) + size = int(request.args.get('size', 10)) + status = request.args.get('status') # active, inactive, all + + users = db_manager.get_users(page, size, status) + total = db_manager.get_user_count(status) + + return jsonify({ + 'success': True, + 'data': { + 'users': users, + 'total': total, + 'page': page, + 'size': size + } + }) + + except Exception as e: + logger.error(f'获取用户列表失败: {e}') + return jsonify({'success': False, 'message': '获取用户列表失败'}), 500 + +@app.route('/api/users//approve', methods=['POST']) +def approve_user(user_id): + """审核用户(管理员功能)""" + try: + # 这里应该验证管理员权限 + data = request.get_json() + approve = data.get('approve', True) + + result = db_manager.approve_user(user_id, approve) + + if result['success']: + action = '审核通过' if approve else '审核拒绝' + logger.info(f'用户 {user_id} {action}') + return jsonify({ + 'success': True, + 'message': f'用户{action}成功' }) else: return jsonify({ 'success': False, - 'message': '邮箱不能为空' + 'message': result['message'] }), 400 except Exception as e: - logger.error(f'忘记密码处理失败: {e}') - return jsonify({'success': False, 'message': '处理失败'}), 500 + logger.error(f'审核用户失败: {e}') + return jsonify({'success': False, 'message': '审核用户失败'}), 500 + +@app.route('/api/users/', methods=['DELETE']) +def delete_user(user_id): + """删除用户(管理员功能)""" + try: + # 这里应该验证管理员权限 + result = db_manager.delete_user(user_id) + + if result['success']: + logger.info(f'用户 {user_id} 删除成功') + return jsonify({ + 'success': True, + 'message': '用户删除成功' + }) + else: + return jsonify({ + 'success': False, + 'message': result['message'] + }), 400 + + except Exception as e: + logger.error(f'删除用户失败: {e}') + return jsonify({'success': False, 'message': '删除用户失败'}), 500 @app.route('/api/auth/reset-password', methods=['POST']) def reset_password(): diff --git a/backend/config.ini b/backend/config.ini index 17ebd21c..2f1be58b 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -5,7 +5,7 @@ debug = false log_level = INFO [SERVER] -host = 127.0.0.1 +host = 0.0.0.0 port = 5000 cors_origins = * @@ -35,11 +35,7 @@ chart_dpi = 300 export_format = csv [SECURITY] -secret_key = 026efbf83a2fe101f168780740da86bf1c9260625458e6782738aa9cf18f8e37 +secret_key = 8914333c0adf239da5d7a992e90879e500ab19e9da0d2bc41c6d8ca97ab102e0 session_timeout = 3600 max_login_attempts = 5 - -[CAMERA] -rtsp_url = rtsp://admin:JY123456@192.168.1.61:554/Streaming/Channels/101 - diff --git a/backend/database.py b/backend/database.py index 24393cfe..2cc10bcb 100644 --- a/backend/database.py +++ b/backend/database.py @@ -111,12 +111,49 @@ class DatabaseManager: ) ''') + # 创建用户表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + register_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 0, + user_type TEXT DEFAULT 'user', + phone TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 创建索引 cursor.execute('CREATE INDEX IF NOT EXISTS idx_patients_name ON patients (name)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_patient ON detection_sessions (patient_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_time ON detection_sessions (start_time)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_session ON detection_data (session_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_timestamp ON detection_data (timestamp)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_username ON users (username)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_type ON users (user_type)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_phone ON users (phone)') + + # 插入默认管理员账户(如果不存在) + cursor.execute('SELECT COUNT(*) FROM users WHERE username = ?', ('admin',)) + admin_exists = cursor.fetchone()[0] + + if admin_exists == 0: + import hashlib + admin_id = str(uuid.uuid4()) + # 默认密码为 admin123,使用MD5加密 + admin_password = hashlib.md5('admin123'.encode()).hexdigest() + + cursor.execute(''' + INSERT INTO users (id, name, username, password, is_active, user_type) + VALUES (?, ?, ?, ?, ?, ?) + ''', (admin_id, '系统管理员', 'admin', admin_password, 1, 'admin')) + + logger.info('创建默认管理员账户: admin/admin123') conn.commit() logger.info('数据库初始化完成') @@ -655,6 +692,261 @@ class DatabaseManager: logger.error(f'设置系统设置失败: {e}') raise + # ==================== 用户管理 ==================== + + def register_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]: + """用户注册""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + import hashlib + + # 检查手机号是否已存在(如果提供了手机号) + if user_data.get('phone'): + cursor.execute('SELECT COUNT(*) FROM users WHERE phone = ?', (user_data['phone'],)) + if cursor.fetchone()[0] > 0: + return { + 'success': False, + 'message': '手机号已存在' + } + + user_id = str(uuid.uuid4()) + # 密码MD5加密 + password_hash = hashlib.md5(user_data['password'].encode()).hexdigest() + + cursor.execute(''' + INSERT INTO users (id, name, username, password, phone, is_active, user_type) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + user_id, + user_data['name'], + user_data['username'], + password_hash, + user_data.get('phone'), # 手机号可选 + 0, # 新注册用户默认未激活,需要管理员审核 + 'user' + )) + + conn.commit() + logger.info(f'用户注册成功: {user_data["username"]}') + return { + 'success': True, + 'user_id': user_id, + 'message': '注册成功' + } + + except sqlite3.IntegrityError: + conn.rollback() + logger.error(f'用户名已存在: {user_data["username"]}') + return { + 'success': False, + 'message': '用户名已存在' + } + except Exception as e: + conn.rollback() + logger.error(f'用户注册失败: {e}') + return { + 'success': False, + 'message': '注册失败' + } + + def authenticate_user(self, username: str, password: str) -> Optional[Dict]: + """用户登录验证""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + import hashlib + + password_hash = hashlib.md5(password.encode()).hexdigest() + + cursor.execute(''' + SELECT * FROM users + WHERE username = ? AND password = ? AND is_active = 1 + ''', (username, password_hash)) + + row = cursor.fetchone() + if row: + user = dict(row) + # 不返回密码 + del user['password'] + logger.info(f'用户登录成功: {username}') + return user + else: + logger.warning(f'用户登录失败: {username}') + return None + + except Exception as e: + logger.error(f'用户验证失败: {e}') + return None + + def get_user_by_phone(self, phone: str) -> Optional[Dict]: + """根据手机号查询用户""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute('SELECT * FROM users WHERE phone = ?', (phone,)) + row = cursor.fetchone() + if row: + user = dict(row) + # 不返回密码 + del user['password'] + return user + return None + except Exception as e: + logger.error(f'根据手机号查询用户失败: {e}') + return None + + def get_users(self, page: int = 1, size: int = 10, status: str = 'all') -> List[Dict]: + """获取用户列表""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + offset = (page - 1) * size + + if status == 'pending': + cursor.execute(''' + SELECT id, name, username, phone, register_date, is_active, user_type, created_at + FROM users + WHERE is_active = 0 AND user_type = 'user' + ORDER BY created_at DESC + LIMIT ? OFFSET ? + ''', (size, offset)) + elif status == 'active': + cursor.execute(''' + SELECT id, name, username, phone, register_date, is_active, user_type, created_at + FROM users + WHERE is_active = 1 + ORDER BY created_at DESC + LIMIT ? OFFSET ? + ''', (size, offset)) + else: + cursor.execute(''' + SELECT id, name, username, phone, register_date, is_active, user_type, created_at + FROM users + ORDER BY created_at DESC + LIMIT ? OFFSET ? + ''', (size, offset)) + + rows = cursor.fetchall() + return [dict(row) for row in rows] + + except Exception as e: + logger.error(f'获取用户列表失败: {e}') + return [] + + def get_users_count(self, status: str = 'all') -> int: + """获取用户总数""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + if status == 'pending': + cursor.execute('SELECT COUNT(*) FROM users WHERE is_active = 0 AND user_type = "user"') + elif status == 'active': + cursor.execute('SELECT COUNT(*) FROM users WHERE is_active = 1') + else: + cursor.execute('SELECT COUNT(*) FROM users') + + return cursor.fetchone()[0] + + except Exception as e: + logger.error(f'获取用户总数失败: {e}') + return 0 + + def approve_user(self, user_id: str, approved: bool = True): + """审核用户""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + UPDATE users SET + is_active = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (1 if approved else 0, user_id)) + + conn.commit() + status = '通过' if approved else '拒绝' + logger.info(f'用户审核{status}: {user_id}') + + except Exception as e: + conn.rollback() + logger.error(f'用户审核失败: {e}') + raise + + def get_user(self, user_id: str) -> Optional[Dict]: + """获取单个用户信息""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + SELECT id, name, username, phone, register_date, is_active, user_type, created_at, updated_at + FROM users WHERE id = ? + ''', (user_id,)) + + row = cursor.fetchone() + return dict(row) if row else None + + except Exception as e: + logger.error(f'获取用户信息失败: {e}') + return None + + def update_user(self, user_id: str, user_data: Dict[str, Any]): + """更新用户信息""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + # 如果包含密码,需要加密 + if 'password' in user_data: + import hashlib + user_data['password'] = hashlib.md5(user_data['password'].encode()).hexdigest() + + # 构建更新语句 + fields = [] + values = [] + + for key, value in user_data.items(): + if key in ['name', 'username', 'password', 'is_active', 'user_type','phone']: + fields.append(f'{key} = ?') + values.append(value) + + if fields: + fields.append('updated_at = CURRENT_TIMESTAMP') + values.append(user_id) + + sql = f'UPDATE users SET {', '.join(fields)} WHERE id = ?' + cursor.execute(sql, values) + + conn.commit() + logger.info(f'更新用户信息: {user_id}') + + except Exception as e: + conn.rollback() + logger.error(f'更新用户信息失败: {e}') + raise + + def delete_user(self, user_id: str): + """删除用户""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute('DELETE FROM users WHERE id = ?', (user_id,)) + conn.commit() + logger.info(f'删除用户: {user_id}') + + except Exception as e: + conn.rollback() + logger.error(f'删除用户失败: {e}') + raise + def close(self): """关闭数据库连接""" if self.connection: diff --git a/backend/utils.py b/backend/utils.py index 8418cde4..cc9f5ba1 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -60,7 +60,7 @@ class Config: # 数据库配置 self.config['DATABASE'] = { - 'path': 'data/balance_system.db', + 'path': 'backend/data/body_balance.db', 'backup_interval': '24', # 小时 'max_backups': '7' } diff --git a/config.json b/config.json index 64d8d7e4..ca3c3780 100644 --- a/config.json +++ b/config.json @@ -41,7 +41,7 @@ }, "database": { "type": "sqlite", - "path": "data/database.db", + "path": "backend/data/body_balance.db", "backup": { "enabled": true, "interval_hours": 24, diff --git a/frontend/src/renderer/src/views/Dashboard.vue b/frontend/src/renderer/src/views/Dashboard.vue index 15e86c8d..10ff0e1a 100644 --- a/frontend/src/renderer/src/views/Dashboard.vue +++ b/frontend/src/renderer/src/views/Dashboard.vue @@ -154,8 +154,10 @@ import { ref, reactive, computed, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { patientAPI } from '../services/api.js' +import { useAuthStore } from '../stores/index.js' const router = useRouter() +const authStore = useAuthStore() // 响应式数据 const activeNav = ref('detection') @@ -233,10 +235,11 @@ const handleLogout = async () => { type: 'warning' }) - localStorage.removeItem('userInfo') - localStorage.removeItem('rememberedUser') + // 调用认证状态管理的logout方法清除所有认证信息 + await authStore.logout() ElMessage.success('已退出登录') - router.push('/login') + // 使用replace而不是push,避免返回按钮回到Dashboard + router.replace('/login') } catch { // 用户取消 } @@ -310,10 +313,12 @@ const loadPatients = async () => { // 生命周期 onMounted(() => { - // 加载用户信息 - const savedUserInfo = localStorage.getItem('userInfo') - if (savedUserInfo) { - Object.assign(userInfo, JSON.parse(savedUserInfo)) + // 从认证状态管理中加载用户信息 + if (authStore.currentUser) { + Object.assign(userInfo, { + username: authStore.currentUser.username, + avatar: authStore.currentUser.avatar || '' + }) } // 加载患者列表 diff --git a/frontend/src/renderer/src/views/Login.vue b/frontend/src/renderer/src/views/Login.vue index fc971269..2b46973a 100644 --- a/frontend/src/renderer/src/views/Login.vue +++ b/frontend/src/renderer/src/views/Login.vue @@ -12,8 +12,8 @@

平衡体态检测系统

- -