main.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import os
  2. import time
  3. import uuid
  4. from flask import Blueprint, render_template, send_from_directory, current_app, redirect, url_for, request, jsonify, session
  5. from werkzeug.utils import secure_filename
  6. from api.utils import login_required, load_led_config, get_server_ip
  7. from application.scheduler_service import scheduler_service
  8. from application.self_check_service import run_all_checks
  9. from application.uap_message_service import send_self_check_notification
  10. from utils.logger_config import logger
  11. main_bp = Blueprint('main', __name__)
  12. ATTACHMENT_TEST_SESSION_KEY = 'attachment_test_filename'
  13. # 附件测试页允许的类型(单附件,新上传会替换)
  14. ATTACHMENT_TEST_EXTENSIONS = {
  15. 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'heic', 'heif',
  16. 'mp4', 'mov', 'webm', 'mkv', 'avi',
  17. 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
  18. 'txt', 'csv', 'zip', 'rar', '7z',
  19. 'mp3', 'wav', 'm4a',
  20. }
  21. def _attachment_test_public_url(filename):
  22. if not filename:
  23. return None
  24. host = request.host
  25. if 'localhost' in host or '127.0.0.1' in host:
  26. server_ip = get_server_ip()
  27. port = request.environ.get('SERVER_PORT', '5000')
  28. host = f"{server_ip}:{port}"
  29. scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
  30. return f"{scheme}://{host}/uploads/{filename}"
  31. def _attachment_test_remove_stored():
  32. """删除 session 中记录的文件(若存在)。"""
  33. name = session.pop(ATTACHMENT_TEST_SESSION_KEY, None)
  34. if not name:
  35. return
  36. path = os.path.join(current_app.config['UPLOAD_FOLDER'], name)
  37. try:
  38. if os.path.isfile(path):
  39. os.remove(path)
  40. except OSError as e:
  41. logger.warning(f"删除附件测试文件失败 {path}: {e}")
  42. def _guess_ext_from_content_type(content_type):
  43. if not content_type:
  44. return None
  45. ct = content_type.lower()
  46. mapping = {
  47. 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif',
  48. 'image/webp': 'webp', 'image/bmp': 'bmp', 'image/heic': 'heic', 'image/heif': 'heif',
  49. 'video/mp4': 'mp4', 'video/quicktime': 'mov', 'video/webm': 'webm',
  50. 'application/pdf': 'pdf',
  51. }
  52. return mapping.get(ct.split(';')[0].strip())
  53. @main_bp.route('/')
  54. @login_required
  55. def index():
  56. """重定向到 Kodi 控制页面"""
  57. return redirect(url_for('main.kodi_page'))
  58. @main_bp.route('/kodi')
  59. @login_required
  60. def kodi_page():
  61. """Kodi 控制页面"""
  62. return render_template('kodi/index.html', active_page='kodi')
  63. @main_bp.route('/door')
  64. @login_required
  65. def door_page():
  66. """门禁控制页面"""
  67. return render_template('door/index.html', active_page='door')
  68. @main_bp.route('/led')
  69. @login_required
  70. def led_page():
  71. """LED 控制页面"""
  72. led_segments = load_led_config()
  73. return render_template('led/index.html', active_page='led', led_segments=led_segments)
  74. @main_bp.route('/ha')
  75. @login_required
  76. def ha_page():
  77. """HA 灯光控制页面"""
  78. return render_template('ha/index.html', active_page='ha')
  79. @main_bp.route('/self_check')
  80. @login_required
  81. def self_check_page():
  82. """设备自检页面"""
  83. return render_template('self_check.html', active_page='self_check')
  84. @main_bp.route('/attachment_upload_test')
  85. @login_required
  86. def attachment_test_page():
  87. """附件上传测试页面(单附件,会话内替换)"""
  88. return render_template('attachment_test/index.html', active_page='attachment_test')
  89. @main_bp.route('/api/attachment_test/status', methods=['GET'])
  90. @login_required
  91. def attachment_test_status():
  92. """当前会话中的附件信息"""
  93. fn = session.get(ATTACHMENT_TEST_SESSION_KEY)
  94. if not fn:
  95. return jsonify({'success': True, 'has_file': False})
  96. folder = current_app.config['UPLOAD_FOLDER']
  97. path = os.path.join(folder, fn)
  98. if not os.path.isfile(path):
  99. session.pop(ATTACHMENT_TEST_SESSION_KEY, None)
  100. return jsonify({'success': True, 'has_file': False})
  101. return jsonify({
  102. 'success': True,
  103. 'has_file': True,
  104. 'filename': fn,
  105. 'url': _attachment_test_public_url(fn),
  106. })
  107. @main_bp.route('/api/attachment_test/upload', methods=['POST'])
  108. @login_required
  109. def attachment_test_upload():
  110. """上传单个附件,覆盖会话中已有文件"""
  111. if 'file' not in request.files:
  112. return jsonify({'success': False, 'message': '缺少文件字段 file'}), 400
  113. file = request.files['file']
  114. if not file or file.filename == '':
  115. return jsonify({'success': False, 'message': '未选择文件'}), 400
  116. original = file.filename
  117. base = secure_filename(original)
  118. ext = None
  119. if base and '.' in base:
  120. ext = base.rsplit('.', 1)[1].lower()
  121. if not ext or ext not in ATTACHMENT_TEST_EXTENSIONS:
  122. ext = _guess_ext_from_content_type(file.content_type or '')
  123. if not ext or ext not in ATTACHMENT_TEST_EXTENSIONS:
  124. return jsonify({
  125. 'success': False,
  126. 'message': f'不支持的文件类型,允许: {", ".join(sorted(ATTACHMENT_TEST_EXTENSIONS))}',
  127. }), 400
  128. _attachment_test_remove_stored()
  129. new_name = f"att_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}.{ext}"
  130. folder = current_app.config['UPLOAD_FOLDER']
  131. os.makedirs(folder, exist_ok=True)
  132. filepath = os.path.join(folder, new_name)
  133. file.save(filepath)
  134. session[ATTACHMENT_TEST_SESSION_KEY] = new_name
  135. return jsonify({
  136. 'success': True,
  137. 'message': '上传成功',
  138. 'filename': new_name,
  139. 'url': _attachment_test_public_url(new_name),
  140. })
  141. @main_bp.route('/api/attachment_test', methods=['DELETE'])
  142. @login_required
  143. def attachment_test_delete():
  144. """删除当前会话中的附件"""
  145. if not session.get(ATTACHMENT_TEST_SESSION_KEY):
  146. return jsonify({'success': True, 'message': '没有可删除的附件'})
  147. _attachment_test_remove_stored()
  148. return jsonify({'success': True, 'message': '已删除'})
  149. @main_bp.route('/api/send_report', methods=['POST'])
  150. @login_required
  151. def send_report_api():
  152. """手动发送自检报告"""
  153. try:
  154. data = request.get_json()
  155. target_email = data.get('email')
  156. # 执行全量自检
  157. results = run_all_checks()
  158. # 生成 HTML 报告
  159. html_report = scheduler_service.format_report_html(results)
  160. # 发送邮件
  161. subject = "手动触发设备自检报告"
  162. scheduler_service.send_email(subject, html_report, receivers=target_email)
  163. # 向统一登录平台运维人员推送通知
  164. summary = scheduler_service._format_summary_text(results)
  165. send_self_check_notification(summary)
  166. return jsonify({
  167. "success": True,
  168. "message": f"自检报告已发送至 {target_email if target_email else '默认邮箱'},并已通知运维人员"
  169. })
  170. except Exception as e:
  171. logger.error(f"发送报告失败: {e}")
  172. return jsonify({
  173. "success": False,
  174. "message": f"发送报告失败: {str(e)}"
  175. }), 500
  176. # 提供上传文件的访问接口
  177. @main_bp.route('/uploads/<filename>')
  178. def uploaded_file(filename):
  179. """提供上传文件的访问接口"""
  180. return send_from_directory(current_app.config['UPLOAD_FOLDER'], filename)