diff --git a/2_3banben/app.py b/2_3banben/app.py index 9d3a766..e82606d 100644 --- a/2_3banben/app.py +++ b/2_3banben/app.py @@ -164,7 +164,7 @@ def create_app(): func=auto_monitor_job, args=[app], trigger='cron', - hour=10, + hour=12, minute=0 ) if not scheduler.running: diff --git a/2_3banben/init_db.py b/2_3banben/init_db.py new file mode 100644 index 0000000..fc982f7 --- /dev/null +++ b/2_3banben/init_db.py @@ -0,0 +1,62 @@ +import os +from app import create_app +from extensions import db +from models import User, Device, UserDevicePermission + +# 创建应用实例 +app = create_app() + + +def init_db(): + with app.app_context(): + # ========================================== + # ⚠️ 警告:这会清空现有的数据库表结构并重建 + # 如果只想更新 User 表,可以注释掉 db.drop_all(), + # 但因为增加了字段,直接重建是最稳妥的。 + # ========================================== + print("正在清理旧数据库...") + db.drop_all() + + print("正在创建新表结构...") + db.create_all() + + print("✅ 数据库表结构创建完成 (devices.db 和 users.db)") + + # ========================================== + # 🟢 1. 创建超级管理员 (Root) + # 即使代码里有后门,数据库里有一个对应的实体也是最好的 + # ========================================== + admin = User(username='admin', role='admin') + admin.set_password('licahk') # 设置密码 + db.session.add(admin) + print(f"👤 用户创建: [admin] (角色: 超级管理员)") + + # ========================================== + # 🟡 2. 创建一个测试工程师 (可选) + # ========================================== + engineer = User(username='engineer01', role='engineer') + engineer.set_password('123456') + db.session.add(engineer) + print(f"👤 用户创建: [engineer01] (角色: 工程师)") + + # ========================================== + # ⚪ 3. 创建一个测试普通客户 (可选) + # ========================================== + client = User(username='client01', role='client') + client.set_password('123456') + db.session.add(client) + print(f"👤 用户创建: [client01] (角色: 客户)") + + # 提交更改 + db.session.commit() + print("\n🚀 初始化完成!请运行 run.py 启动服务器。") + + +if __name__ == '__main__': + # 再次确认防止误删 + print("此操作会删除现有的 'users.db' 和 'devices.db' 中的数据并重建。") + confirm = input("确认继续吗? (y/n): ") + if confirm.lower() == 'y': + init_db() + else: + print("操作已取消。") \ No newline at end of file diff --git a/2_3banben/models.py b/2_3banben/models.py index 6f0a501..b35bc32 100644 --- a/2_3banben/models.py +++ b/2_3banben/models.py @@ -1,9 +1,12 @@ from datetime import datetime +# 引入 UserMixin 是 Flask 标准做法,虽然你主要用 JWT,但保留它可以方便未来扩展或使用某些 Flask 插件 +from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from extensions import db + # ======================= -# 数据库 1: 业务数据 (devices.db) +# 数据库 1: 业务数据 (默认数据库 / devices.db) # ======================= class Device(db.Model): @@ -14,29 +17,33 @@ class Device(db.Model): name = db.Column(db.String(100), unique=True, index=True) source = db.Column(db.String(50)) - # 快照字段(爬虫更新) + # 快照字段(爬虫自动更新) status = db.Column(db.String(50)) current_value = db.Column(db.String(200)) latest_time = db.Column(db.String(50)) - json_data = db.Column(db.Text) + json_data = db.Column(db.Text) # 存储完整原始JSON check_time = db.Column(db.String(50)) reason = db.Column(db.String(255)) - offset = db.Column(db.String(50)) + offset = db.Column(db.String(50)) # 时间偏移量说明 - # 手动录入字段 + # 手动录入字段 (管理员/工程师可改) install_site = db.Column(db.String(100), default="") is_maintaining = db.Column(db.Boolean, default=False) is_hidden = db.Column(db.Boolean, default=False) def to_dict(self): + """ + 转换为前端友好的字典格式 + """ + # 简单处理状态:只要不是明确的离线/异常,就视为 online api_status = 'offline' if self.status in ['离线', '异常', '已离线'] else 'online' return { 'id': self.id, 'name': self.name, 'source': self.source, 'latest_time': self.latest_time, - 'status': api_status, - 'status_text': self.status, + 'status': api_status, # 给前端图标用的状态 (online/offline) + 'status_text': self.status, # 显示在界面的原始状态文字 'value': self.current_value, 'reason': self.reason, 'install_site': self.install_site or '', @@ -45,21 +52,26 @@ class Device(db.Model): 'offset': self.offset } + class DeviceHistory(db.Model): __tablename__ = 'device_history' id = db.Column(db.Integer, primary_key=True) + # 这里虽有 ForeignKey,但在 SQLite 中如果不开启外键约束检查通常没事 + # 但建议逻辑上视为级联删除 device_id = db.Column(db.Integer, db.ForeignKey('devices.id')) data_time = db.Column(db.String(50)) status = db.Column(db.String(50)) result_data = db.Column(db.String(200), default="") json_data = db.Column(db.Text) - file_path = db.Column(db.String(255)) + file_path = db.Column(db.String(255)) # 如果有文件下载功能 recorded_at = db.Column(db.DateTime, default=datetime.now) + class MaintenanceLog(db.Model): __tablename__ = 'maintenance_logs' + id = db.Column(db.Integer, primary_key=True) device_name = db.Column(db.String(100), nullable=False) engineer = db.Column(db.String(50)) @@ -77,18 +89,23 @@ class MaintenanceLog(db.Model): 'timestamp': self.timestamp.strftime('%Y-%m-%d %H:%M:%S') } + # ======================= # 数据库 2: 用户管理 (users.db) # ======================= -class User(db.Model): +class User(UserMixin, db.Model): __bind_key__ = 'users_db' # 关键:指定存储在 users.db __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128)) - role = db.Column(db.String(20), default='client') # 'admin' or 'client' + + # 🟢 核心字段:角色权限 + # 可选值: 'admin' (超管), 'engineer' (工程师), 'client' (客户) + role = db.Column(db.String(20), default='client') + created_at = db.Column(db.DateTime, default=datetime.now) def set_password(self, password): @@ -97,6 +114,7 @@ class User(db.Model): def check_password(self, password): return check_password_hash(self.password_hash, password) + class UserDevicePermission(db.Model): """ 关联表:存储用户ID和设备ID的对应关系。 @@ -107,5 +125,7 @@ class UserDevicePermission(db.Model): __tablename__ = 'user_device_permissions' id = db.Column(db.Integer, primary_key=True) + # 关联 User 表 (同库) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) - device_id = db.Column(db.Integer, nullable=False) # 对应 devices.db 中的 Device.id \ No newline at end of file + # 关联 Device 表 (异库,只存 ID) + device_id = db.Column(db.Integer, nullable=False) \ No newline at end of file diff --git a/2_3banben/routes/api.py b/2_3banben/routes/api.py index 9d4741a..de38abc 100644 --- a/2_3banben/routes/api.py +++ b/2_3banben/routes/api.py @@ -3,13 +3,13 @@ import re from datetime import datetime from flask import Blueprint, jsonify, request from sqlalchemy import desc, or_ -# 引入 get_jwt_identity 用来获取当前用户ID +# 引入 jwt 相关函数 from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity from extensions import db from models import Device, DeviceHistory, MaintenanceLog, User, UserDevicePermission -# 尝试导入爬虫模块 +# 尝试导入爬虫模块 (如果没有则跳过,防止报错) try: from services.core import execute_monitor_task except ImportError: @@ -19,28 +19,42 @@ api_bp = Blueprint('api', __name__, url_prefix='/api') # ======================= -# 🔧 辅助函数 +# 🔧 辅助函数 (权限核心) # ======================= def is_admin(user_id): """ - 判断用户权限 - 兼容 Token 中存储的 String 类型 ID + 判断是否为超级管理员 (Root权限) + 逻辑: + 1. ID 为 '0' (硬编码后门) -> 通过 + 2. 数据库中角色为 'admin' -> 通过 """ - # 1. 既然 Token 里是字符串,这里必须转成字符串比较,或者转成 int 比较 - if str(user_id) == '0': return True - - if not user_id: return False - + if str(user_id) == '0': + return True + if not user_id: + return False try: - # 2. 查库时需要 int - uid = int(user_id) - u = User.query.get(uid) + u = User.query.get(int(user_id)) return u and u.role == 'admin' except: return False +def is_manager(user_id): + """ + 判断是否为管理者 (Admin OR Engineer) + 用于:修改地点、切换维修模式、写日志 + """ + if is_admin(user_id): + return True + try: + u = User.query.get(int(user_id)) + return u and u.role == 'engineer' + except: + return False + + def calculate_offset(latest_time_str): + """计算时间滞后天数""" if not latest_time_str or latest_time_str == "N/A": return "从未同步" try: clean = str(latest_time_str).split()[0].replace('_', '-') @@ -60,30 +74,21 @@ def login(): username = data.get('username') password = data.get('password') - # 1. 查库登录 - user = User.query.filter_by(username=username).first() - if user and user.check_password(password): - # 🟢 修复核心:必须用 str() 将 ID 转为字符串 - # 否则 flask-jwt-extended 会报错 "Subject must be a string" - token = create_access_token( - identity=str(user.id), - additional_claims={'role': user.role} - ) + # 1. 后门判定 + if username == 'admin' and password == 'licahk': + token = create_access_token(identity='0', additional_claims={'role': 'admin'}) return jsonify({ - 'code': 200, 'message': '登录成功', - 'token': token, 'role': user.role, 'user_id': user.id + 'code': 200, 'message': 'Root后门登录', + 'token': token, 'role': 'admin', 'user_id': 0, 'username': 'admin' }) - # 2. 硬编码后门 (修正:生成真实 Token) - if username == 'admin' and password == 'licahk': - # 🟢 修复核心:'0' 必须是字符串 - token = create_access_token( - identity='0', - additional_claims={'role': 'admin'} - ) + # 2. 正常查库登录 + user = User.query.filter_by(username=username).first() + if user and user.check_password(password): + token = create_access_token(identity=str(user.id), additional_claims={'role': user.role}) return jsonify({ - 'code': 200, 'message': 'Root登录', - 'token': token, 'role': 'admin', 'user_id': 0 + 'code': 200, 'message': '登录成功', + 'token': token, 'role': user.role, 'user_id': user.id, 'username': user.username }) return jsonify({'code': 401, 'message': '用户名或密码错误'}), 401 @@ -96,29 +101,16 @@ def login(): @jwt_required() def devices_overview(): try: - # 获取到的 user_id 现在是字符串类型 (例如 "1" 或 "0") user_id = get_jwt_identity() - - target_devices = [] if is_admin(user_id): target_devices = Device.query.all() else: - # 普通用户权限查询 (users_db) - # 必须转回 int 才能去数据库查 ID - try: - uid_int = int(user_id) - user = User.query.get(uid_int) - if user: - perms = UserDevicePermission.query.filter_by(user_id=user.id).all() - allowed_ids = [p.device_id for p in perms] - if allowed_ids: - target_devices = Device.query.filter(Device.id.in_(allowed_ids)).all() - except ValueError: - return jsonify({'code': 401, 'message': '无效的用户ID格式'}), 401 + perms = UserDevicePermission.query.filter_by(user_id=int(user_id)).all() + allowed_ids = [p.device_id for p in perms] + target_devices = Device.query.filter(Device.id.in_(allowed_ids)).all() if allowed_ids else [] return jsonify({'code': 200, 'data': [d.to_dict() for d in target_devices]}) except Exception as e: - print(f"Error in devices_overview: {e}") return jsonify({'code': 500, 'message': str(e)}) @@ -127,108 +119,121 @@ def devices_overview(): def device_data_by_date(): name = request.args.get('name') date_str = request.args.get('date') - - if not name or not date_str: - return jsonify({'code': 400, 'message': 'Missing params'}), 400 + if not name or not date_str: return jsonify({'code': 400}), 400 device = Device.query.filter_by(name=name).first() - if not device: return jsonify({'code': 404, 'message': 'Device not found'}), 404 + if not device: return jsonify({'code': 404}), 404 - content = None - # 优先查历史 - hist = DeviceHistory.query.filter( - DeviceHistory.device_id == device.id, - DeviceHistory.data_time.like(f"{date_str}%") - ).order_by(desc(DeviceHistory.id)).first() - - if hist: - content = hist.json_data - elif device.latest_time and str(device.latest_time).startswith(date_str): - content = device.json_data + hist = DeviceHistory.query.filter(DeviceHistory.device_id == device.id, DeviceHistory.data_time.like(f"{date_str}%")).order_by(desc(DeviceHistory.id)).first() + content = hist.json_data if hist else (device.json_data if device.latest_time and str(device.latest_time).startswith(date_str) else None) if content: - # 尝试转JSON对象返回 - try: - if isinstance(content, str): content = json.loads(content) - except: - pass + if isinstance(content, str): + try: content = json.loads(content) + except: pass return jsonify({'code': 200, 'name': device.name, 'source': device.source, 'content': content}) - return jsonify({'code': 404, 'message': '无数据'}), 404 # ======================= -# 2. 用户管理 (Admin) +# 2. 用户管理 (Admin Only) # ======================= @api_bp.route('/admin/users', methods=['GET']) @jwt_required() def admin_get_users(): if not is_admin(get_jwt_identity()): return jsonify({'code': 403}), 403 - - clients = User.query.filter_by(role='client').all() + users = User.query.order_by(desc(User.created_at)).all() result = [] - for c in clients: - perms = UserDevicePermission.query.filter_by(user_id=c.id).all() + for u in users: + if str(u.id) == str(get_jwt_identity()): continue + perms = UserDevicePermission.query.filter_by(user_id=u.id).all() result.append({ - "id": c.id, "username": c.username, "created_at": c.created_at, - "allowed_device_ids": [p.device_id for p in perms] + "id": u.id, "username": u.username, "role": u.role, + "created_at": u.created_at, "allowed_device_ids": [p.device_id for p in perms] }) - return jsonify(result) + return jsonify({'code': 200, 'data': result}) -@api_bp.route('/admin/create_client', methods=['POST']) +@api_bp.route('/admin/create_user', methods=['POST']) @jwt_required() -def admin_create_client(): +def admin_create_user(): if not is_admin(get_jwt_identity()): return jsonify({'code': 403}), 403 data = request.get_json() - if User.query.filter_by(username=data['username']).first(): - return jsonify({'code': 400, 'msg': '用户已存在'}), 400 - - u = User(username=data['username'], role='client') - u.set_password(data['password']) + if User.query.filter_by(username=data.get('username')).first(): + return jsonify({'code': 400, 'msg': '用户名已存在'}), 400 + u = User(username=data.get('username'), role=data.get('role', 'client')) + u.set_password(data.get('password')) db.session.add(u) db.session.commit() return jsonify({'code': 200, 'msg': '创建成功'}) +@api_bp.route('/admin/delete_user', methods=['POST']) +@jwt_required() +def admin_delete_user(): + curr = get_jwt_identity() + if not is_admin(curr): return jsonify({'code': 403}), 403 + uid = request.get_json().get('user_id') + if str(uid) == str(curr): return jsonify({'code': 400, 'msg': '无法删除自己'}), 400 + user = User.query.get(uid) + if user: + UserDevicePermission.query.filter_by(user_id=user.id).delete() + db.session.delete(user) + db.session.commit() + return jsonify({'code': 200, 'msg': '删除成功'}) + + @api_bp.route('/admin/assign_devices', methods=['POST']) @jwt_required() def admin_assign_devices(): if not is_admin(get_jwt_identity()): return jsonify({'code': 403}), 403 data = request.get_json() - uid = data.get('user_id') - d_ids = data.get('device_ids', []) - - UserDevicePermission.query.filter_by(user_id=uid).delete() - for did in d_ids: - db.session.add(UserDevicePermission(user_id=uid, device_id=did)) + UserDevicePermission.query.filter_by(user_id=data.get('user_id')).delete() + for did in data.get('device_ids', []): + db.session.add(UserDevicePermission(user_id=data.get('user_id'), device_id=did)) db.session.commit() return jsonify({'code': 200, 'msg': '权限已保存'}) # ======================= -# 3. 日志与工具 +# 3. 日志与工具 (权限隔离) # ======================= @api_bp.route('/logs/list', methods=['GET']) +@jwt_required() def get_logs(): + """ + 获取日志列表 + 权限逻辑更新: + - Admin: 可以看所有 + - Engineer/Client: 只能看自己名下设备的日志 (严格过滤) + """ + user_id = get_jwt_identity() keyword = request.args.get('keyword', '') start_date = request.args.get('start_date') end_date = request.args.get('end_date') query = MaintenanceLog.query + + # 🛡️ 权限隔离 + if not is_admin(user_id): + perms = UserDevicePermission.query.filter_by(user_id=int(user_id)).all() + if not perms: return jsonify({'code': 200, 'data': []}) + allowed_names = [d.name for d in Device.query.filter(Device.id.in_([p.device_id for p in perms])).all()] + query = query.filter(MaintenanceLog.device_name.in_(allowed_names)) + if keyword: kw = f"%{keyword}%" query = query.filter(or_( MaintenanceLog.device_name.like(kw), MaintenanceLog.engineer.like(kw), MaintenanceLog.location.like(kw), MaintenanceLog.content.like(kw) )) + if start_date and end_date: try: s = datetime.strptime(start_date, '%Y-%m-%d') e = datetime.strptime(end_date, '%Y-%m-%d').replace(hour=23, minute=59, second=59) query = query.filter(MaintenanceLog.timestamp.between(s, e)) - except: - pass + except: pass logs = query.order_by(desc(MaintenanceLog.timestamp)).all() return jsonify({'code': 200, 'data': [l.to_dict() for l in logs]}) @@ -237,7 +242,9 @@ def get_logs(): @api_bp.route('/logs/add', methods=['POST']) @jwt_required() def add_log(): + if not is_manager(get_jwt_identity()): return jsonify({'code': 403}), 403 data = request.get_json() + # 后端安全校验:如果是工程师,建议再次校验 engineer 字段是否匹配其 username db.session.add(MaintenanceLog( device_name=data.get('device_name'), engineer=data.get('engineer'), @@ -248,6 +255,21 @@ def add_log(): return jsonify({'code': 200}) +@api_bp.route('/logs/update', methods=['POST']) +@jwt_required() +def update_log(): + if not is_manager(get_jwt_identity()): return jsonify({'code': 403}), 403 + data = request.get_json() + log = MaintenanceLog.query.get(data.get('id')) + if not log: return jsonify({'code': 404}), 404 + + log.engineer = data.get('engineer') + log.location = data.get('location') + log.content = data.get('content') + db.session.commit() + return jsonify({'code': 200, 'msg': '更新成功'}) + + @api_bp.route('/logs/delete', methods=['POST']) @jwt_required() def delete_log(): @@ -260,16 +282,18 @@ def delete_log(): return jsonify({'code': 404}) +# ======================= +# 4. 系统检测与控制 (原有功能完整保留) +# ======================= @api_bp.route('/run_monitor', methods=['POST']) @jwt_required() def run_monitor(): if not is_admin(get_jwt_identity()): return jsonify({'code': 403}), 403 - if not execute_monitor_task: - return jsonify({'code': 500, 'msg': '爬虫模块未加载'}) + if not execute_monitor_task: return jsonify({'code': 500, 'msg': '爬虫模块未加载'}) try: task_result = execute_monitor_task() - if not task_result: return jsonify({'code': 200, 'msg': '跳过'}) + if not task_result: return jsonify({'code': 200, 'msg': '无任务'}) scraped_list = task_result.get('device_list', []) now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -279,25 +303,23 @@ def run_monitor(): d_name = item.get('name') if not d_name: continue - # --- 原始保留的逻辑:处理特殊路径 --- d_raw = item.get('raw_json', {}) target_time = item.get('target_time') source = item.get('source', '') + # 针对 106 源码进行特殊路径解析 if '106' in str(source): try: path_str = d_raw.get('path', '') match = re.search(r'/Data/(\d{4}_\d{2}_\d{2})/\w+_(\d{2}_\d{2}_\d{2})\.csv', path_str) if match: target_time = f"{match.group(1).replace('_', '-')} {match.group(2).replace('_', ':')}" - except: - pass + except: pass json_str = json.dumps(d_raw, ensure_ascii=False) - device = Device.query.filter_by(name=d_name).first() if not device: - device = Device(name=d_name, source=source, install_site="") + device = Device(name=d_name, source=source) db.session.add(device) db.session.flush() @@ -316,7 +338,7 @@ def run_monitor(): count += 1 db.session.commit() - return jsonify({'code': 200, 'message': f'成功更新 {count} 台设备'}) + return jsonify({'code': 200, 'message': f'更新 {count} 台设备'}) except Exception as e: db.session.rollback() return jsonify({'code': 500, 'message': str(e)}) @@ -325,7 +347,7 @@ def run_monitor(): @api_bp.route('/update_site', methods=['POST']) @jwt_required() def update_site(): - if not is_admin(get_jwt_identity()): return jsonify({'code': 403}), 403 + if not is_manager(get_jwt_identity()): return jsonify({'code': 403}), 403 d = Device.query.filter_by(name=request.get_json().get('name')).first() if d: d.install_site = request.get_json().get('site') @@ -337,7 +359,7 @@ def update_site(): @api_bp.route('/toggle_maintenance', methods=['POST']) @jwt_required() def toggle_maintenance(): - if not is_admin(get_jwt_identity()): return jsonify({'code': 403}), 403 + if not is_manager(get_jwt_identity()): return jsonify({'code': 403}), 403 d = Device.query.filter_by(name=request.get_json().get('name')).first() if d: d.is_maintaining = request.get_json().get('is_maintaining') diff --git a/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue b/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue index ee2b52c..27b121f 100644 --- a/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue +++ b/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue @@ -9,12 +9,15 @@ 更新: {{ lastCheckTime || '...' }} + + {{ roleDisplayName }} +
- 日志 + 日志中心 - - 检测 + + + 系统检测 +
@@ -55,7 +67,7 @@ 异常({{ summary.errorCount + summary.warningCount }}) - + 回收({{ summary.hiddenCount }}) @@ -111,7 +123,7 @@ @@ -146,19 +158,23 @@