main.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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, abort
  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('/js/<path:filename>')
  90. @login_required
  91. def serve_templates_js(filename):
  92. """提供 templates/js 下的脚本(如 im-sdk.js)"""
  93. safe = os.path.basename(filename)
  94. if safe != 'im-sdk.js':
  95. abort(404)
  96. return send_from_directory(os.path.join(current_app.root_path, 'templates', 'js'), safe)
  97. @main_bp.route('/api/attachment_test/status', methods=['GET'])
  98. @login_required
  99. def attachment_test_status():
  100. """当前会话中的附件信息"""
  101. fn = session.get(ATTACHMENT_TEST_SESSION_KEY)
  102. if not fn:
  103. return jsonify({'success': True, 'has_file': False})
  104. folder = current_app.config['UPLOAD_FOLDER']
  105. path = os.path.join(folder, fn)
  106. if not os.path.isfile(path):
  107. session.pop(ATTACHMENT_TEST_SESSION_KEY, None)
  108. return jsonify({'success': True, 'has_file': False})
  109. return jsonify({
  110. 'success': True,
  111. 'has_file': True,
  112. 'filename': fn,
  113. 'url': _attachment_test_public_url(fn),
  114. })
  115. @main_bp.route('/api/attachment_test/upload', methods=['POST'])
  116. @login_required
  117. def attachment_test_upload():
  118. """上传单个附件,覆盖会话中已有文件"""
  119. if 'file' not in request.files:
  120. return jsonify({'success': False, 'message': '缺少文件字段 file'}), 400
  121. file = request.files['file']
  122. if not file or file.filename == '':
  123. return jsonify({'success': False, 'message': '未选择文件'}), 400
  124. original = file.filename
  125. base = secure_filename(original)
  126. ext = None
  127. if base and '.' in base:
  128. ext = base.rsplit('.', 1)[1].lower()
  129. if not ext or ext not in ATTACHMENT_TEST_EXTENSIONS:
  130. ext = _guess_ext_from_content_type(file.content_type or '')
  131. if not ext or ext not in ATTACHMENT_TEST_EXTENSIONS:
  132. return jsonify({
  133. 'success': False,
  134. 'message': f'不支持的文件类型,允许: {", ".join(sorted(ATTACHMENT_TEST_EXTENSIONS))}',
  135. }), 400
  136. _attachment_test_remove_stored()
  137. new_name = f"att_{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}.{ext}"
  138. folder = current_app.config['UPLOAD_FOLDER']
  139. os.makedirs(folder, exist_ok=True)
  140. filepath = os.path.join(folder, new_name)
  141. file.save(filepath)
  142. session[ATTACHMENT_TEST_SESSION_KEY] = new_name
  143. return jsonify({
  144. 'success': True,
  145. 'message': '上传成功',
  146. 'filename': new_name,
  147. 'url': _attachment_test_public_url(new_name),
  148. })
  149. @main_bp.route('/api/attachment_test', methods=['DELETE'])
  150. @login_required
  151. def attachment_test_delete():
  152. """删除当前会话中的附件"""
  153. if not session.get(ATTACHMENT_TEST_SESSION_KEY):
  154. return jsonify({'success': True, 'message': '没有可删除的附件'})
  155. _attachment_test_remove_stored()
  156. return jsonify({'success': True, 'message': '已删除'})
  157. @main_bp.route('/api/send_report', methods=['POST'])
  158. @login_required
  159. def send_report_api():
  160. """手动发送自检报告"""
  161. try:
  162. data = request.get_json()
  163. target_email = data.get('email')
  164. # 执行全量自检
  165. results = run_all_checks()
  166. # 生成 HTML 报告
  167. html_report = scheduler_service.format_report_html(results)
  168. # 发送邮件
  169. subject = "手动触发设备自检报告"
  170. scheduler_service.send_email(subject, html_report, receivers=target_email)
  171. # 向统一登录平台运维人员推送通知
  172. summary = scheduler_service._format_summary_text(results)
  173. send_self_check_notification(summary)
  174. return jsonify({
  175. "success": True,
  176. "message": f"自检报告已发送至 {target_email if target_email else '默认邮箱'},并已通知运维人员"
  177. })
  178. except Exception as e:
  179. logger.error(f"发送报告失败: {e}")
  180. return jsonify({
  181. "success": False,
  182. "message": f"发送报告失败: {str(e)}"
  183. }), 500
  184. # 提供上传文件的访问接口
  185. @main_bp.route('/uploads/<filename>')
  186. def uploaded_file(filename):
  187. """提供上传文件的访问接口"""
  188. return send_from_directory(current_app.config['UPLOAD_FOLDER'], filename)