| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  | from flask import Flask, send_from_directory, abort, request | 
					
						
							|  |  |  |  | import os | 
					
						
							|  |  |  |  | import logging | 
					
						
							|  |  |  |  | from functools import wraps | 
					
						
							|  |  |  |  | from pathlib import Path | 
					
						
							|  |  |  |  | from flask_cors import CORS | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # 配置日志(保持原有格式) | 
					
						
							|  |  |  |  | logging.basicConfig( | 
					
						
							|  |  |  |  |     level=logging.INFO, | 
					
						
							|  |  |  |  |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | 
					
						
							|  |  |  |  | ) | 
					
						
							|  |  |  |  | logger = logging.getLogger(__name__) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # 初始化 Flask 应用(供 main.py 导入) | 
					
						
							|  |  |  |  | app = Flask(__name__) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | # 核心修改:与 FastAPI 对齐的跨域配置 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  | # 1. 允许的前端域名(根据实际环境修改,生产环境删除 "*") | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  | ALLOWED_ORIGINS = [ | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |     # "http://localhost:8080",  # 本地前端开发地址 | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |     # "http://127.0.0.1:8080", | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |     # "http://服务器IP:8080",    # 部署后前端地址 | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |     "*" | 
					
						
							|  |  |  |  | ] | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # 2. 配置 CORS(与 FastAPI 规则完全对齐) | 
					
						
							|  |  |  |  | CORS( | 
					
						
							|  |  |  |  |     app, | 
					
						
							|  |  |  |  |     resources={ | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |         r"/*": { | 
					
						
							|  |  |  |  |             "origins": ALLOWED_ORIGINS, | 
					
						
							|  |  |  |  |             "allow_credentials": True, | 
					
						
							|  |  |  |  |             "methods": ["*"], | 
					
						
							|  |  |  |  |             "allow_headers": ["*"], | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |         } | 
					
						
							|  |  |  |  |     }, | 
					
						
							|  |  |  |  | ) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  | # 核心路径配置(关键修改:修正 PROJECT_ROOT 计算) | 
					
						
							|  |  |  |  | # 原问题:file_service.py 在 service 文件夹内,需向上跳一级到项目根目录 | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  | # ------------------------------ | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  | CURRENT_FILE_PATH = Path(__file__).resolve()  # 当前文件路径:service/file_service.py | 
					
						
							|  |  |  |  | PROJECT_ROOT = CURRENT_FILE_PATH.parent.parent  # 项目根目录(service 文件夹的父目录) | 
					
						
							|  |  |  |  | # 资源目录(现在正确指向项目根目录下的文件夹) | 
					
						
							|  |  |  |  | BASE_IMAGE_DIR_DECT = str((PROJECT_ROOT / "resource" / "dect").resolve())       # 根目录/resource/dect | 
					
						
							|  |  |  |  | BASE_IMAGE_DIR_UP_IMAGES = str((PROJECT_ROOT / "up_images").resolve())          # 根目录/up_images | 
					
						
							|  |  |  |  | BASE_MODEL_DIR = str((PROJECT_ROOT / "resource" / "models").resolve())          # 根目录/resource/models | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  | # 安全检查装饰器(不变) | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | def safe_path_check(root_dir: str): | 
					
						
							|  |  |  |  |     def decorator(func): | 
					
						
							|  |  |  |  |         @wraps(func) | 
					
						
							|  |  |  |  |         def wrapper(*args, **kwargs): | 
					
						
							|  |  |  |  |             resource_path = kwargs.get('resource_path', '').strip() | 
					
						
							|  |  |  |  |             # 统一路径分隔符(兼容 Windows \ 和 Linux /) | 
					
						
							|  |  |  |  |             resource_path = resource_path.replace("/", os.sep).replace("\\", os.sep) | 
					
						
							|  |  |  |  |             # 拼接完整路径(防止路径遍历) | 
					
						
							|  |  |  |  |             full_file_path = os.path.abspath(os.path.join(root_dir, resource_path)) | 
					
						
							|  |  |  |  |             logger.debug( | 
					
						
							|  |  |  |  |                 f"[Flask 安全检查] 请求路径:{resource_path} | 完整路径:{full_file_path} | 根目录:{root_dir}" | 
					
						
							|  |  |  |  |             ) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             # 1. 禁止路径遍历(确保请求文件在根目录内) | 
					
						
							|  |  |  |  |             if not full_file_path.startswith(root_dir): | 
					
						
							|  |  |  |  |                 logger.warning( | 
					
						
							|  |  |  |  |                     f"[Flask 安全拦截] 非法路径遍历!IP:{request.remote_addr} | 请求路径:{resource_path}" | 
					
						
							|  |  |  |  |                 ) | 
					
						
							|  |  |  |  |                 abort(403) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             # 2. 检查文件存在且为有效文件(非目录) | 
					
						
							|  |  |  |  |             if not os.path.exists(full_file_path) or not os.path.isfile(full_file_path): | 
					
						
							|  |  |  |  |                 logger.warning( | 
					
						
							|  |  |  |  |                     f"[Flask 资源错误] 文件不存在/非文件!IP:{request.remote_addr} | 路径:{full_file_path}" | 
					
						
							|  |  |  |  |                 ) | 
					
						
							|  |  |  |  |                 abort(404) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |             # 3. 限制文件大小(模型200MB,图片10MB) | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |             max_size = 200 * 1024 * 1024 if "models" in root_dir else 10 * 1024 * 1024 | 
					
						
							|  |  |  |  |             if os.path.getsize(full_file_path) > max_size: | 
					
						
							|  |  |  |  |                 logger.warning( | 
					
						
							|  |  |  |  |                     f"[Flask 大小超限] 文件超过{max_size//1024//1024}MB!IP:{request.remote_addr} | 路径:{full_file_path}" | 
					
						
							|  |  |  |  |                 ) | 
					
						
							|  |  |  |  |                 abort(413) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             # 安全检查通过,传递根目录给视图函数 | 
					
						
							|  |  |  |  |             return func(*args, **kwargs, root_dir=root_dir) | 
					
						
							|  |  |  |  |         return wrapper | 
					
						
							|  |  |  |  |     return decorator | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | # 1. 模型下载接口(/model/download/*) | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | @app.route('/model/download/<path:resource_path>') | 
					
						
							|  |  |  |  | @safe_path_check(root_dir=BASE_MODEL_DIR) | 
					
						
							|  |  |  |  | def download_model(resource_path, root_dir): | 
					
						
							|  |  |  |  |     try: | 
					
						
							|  |  |  |  |         resource_path = resource_path.replace("/", os.sep).replace("\\", os.sep) | 
					
						
							|  |  |  |  |         dir_path, file_name = os.path.split(resource_path) | 
					
						
							|  |  |  |  |         full_dir = os.path.abspath(os.path.join(root_dir, dir_path)) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         # 仅允许 .pt 格式(YOLO 模型) | 
					
						
							|  |  |  |  |         if not file_name.lower().endswith('.pt'): | 
					
						
							|  |  |  |  |             logger.warning( | 
					
						
							|  |  |  |  |                 f"[Flask 格式错误] 非 .pt 模型文件!IP:{request.remote_addr} | 文件名:{file_name}" | 
					
						
							|  |  |  |  |             ) | 
					
						
							|  |  |  |  |             abort(415) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         logger.info( | 
					
						
							|  |  |  |  |             f"[Flask 模型下载] 成功请求!IP:{request.remote_addr} | 文件:{file_name} | 目录:{full_dir}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |         # 强制浏览器下载(而非预览) | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |         return send_from_directory( | 
					
						
							|  |  |  |  |             full_dir, | 
					
						
							|  |  |  |  |             file_name, | 
					
						
							|  |  |  |  |             as_attachment=True, | 
					
						
							|  |  |  |  |             mimetype="application/octet-stream" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     except Exception as e: | 
					
						
							|  |  |  |  |         logger.error( | 
					
						
							|  |  |  |  |             f"[Flask 模型下载异常] IP:{request.remote_addr} | 错误:{str(e)}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  |         abort(500) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | # 2. 人脸图片访问接口(/up_images/*) | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | @app.route('/up_images/<path:resource_path>') | 
					
						
							|  |  |  |  | @safe_path_check(root_dir=BASE_IMAGE_DIR_UP_IMAGES) | 
					
						
							|  |  |  |  | def get_face_image(resource_path, root_dir): | 
					
						
							|  |  |  |  |     try: | 
					
						
							|  |  |  |  |         resource_path = resource_path.replace("/", os.sep).replace("\\", os.sep) | 
					
						
							|  |  |  |  |         dir_path, file_name = os.path.split(resource_path) | 
					
						
							|  |  |  |  |         full_dir = os.path.abspath(os.path.join(root_dir, dir_path)) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         # 仅允许常见图片格式 | 
					
						
							|  |  |  |  |         allowed_ext = ('.png', '.jpg', '.jpeg', '.gif', '.bmp') | 
					
						
							|  |  |  |  |         if not file_name.lower().endswith(allowed_ext): | 
					
						
							|  |  |  |  |             logger.warning( | 
					
						
							|  |  |  |  |                 f"[Flask 格式错误] 非图片文件!IP:{request.remote_addr} | 文件名:{file_name}" | 
					
						
							|  |  |  |  |             ) | 
					
						
							|  |  |  |  |             abort(415) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         logger.info( | 
					
						
							|  |  |  |  |             f"[Flask 人脸图片] 成功请求!IP:{request.remote_addr} | 文件:{file_name} | 目录:{full_dir}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |         # 允许浏览器预览图片 | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |         return send_from_directory(full_dir, file_name, as_attachment=False) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     except Exception as e: | 
					
						
							|  |  |  |  |         logger.error( | 
					
						
							|  |  |  |  |             f"[Flask 人脸图片异常] IP:{request.remote_addr} | 错误:{str(e)}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  |         abort(500) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | # 3. 检测图片访问接口(/resource/dect/*) | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | @app.route('/resource/dect/<path:resource_path>') | 
					
						
							|  |  |  |  | @safe_path_check(root_dir=BASE_IMAGE_DIR_DECT) | 
					
						
							|  |  |  |  | def get_dect_image(resource_path, root_dir): | 
					
						
							|  |  |  |  |     try: | 
					
						
							|  |  |  |  |         resource_path = resource_path.replace("/", os.sep).replace("\\", os.sep) | 
					
						
							|  |  |  |  |         dir_path, file_name = os.path.split(resource_path) | 
					
						
							|  |  |  |  |         full_dir = os.path.abspath(os.path.join(root_dir, dir_path)) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         # 仅允许常见图片格式 | 
					
						
							|  |  |  |  |         allowed_ext = ('.png', '.jpg', '.jpeg', '.gif', '.bmp') | 
					
						
							|  |  |  |  |         if not file_name.lower().endswith(allowed_ext): | 
					
						
							|  |  |  |  |             logger.warning( | 
					
						
							|  |  |  |  |                 f"[Flask 格式错误] 非图片文件!IP:{request.remote_addr} | 文件名:{file_name}" | 
					
						
							|  |  |  |  |             ) | 
					
						
							|  |  |  |  |             abort(415) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         logger.info( | 
					
						
							|  |  |  |  |             f"[Flask 检测图片] 成功请求!IP:{request.remote_addr} | 文件:{file_name} | 目录:{full_dir}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         return send_from_directory(full_dir, file_name, as_attachment=False) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     except Exception as e: | 
					
						
							|  |  |  |  |         logger.error( | 
					
						
							|  |  |  |  |             f"[Flask 检测图片异常] IP:{request.remote_addr} | 错误:{str(e)}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  |         abort(500) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | # 4. 兼容旧图片接口(/images/* → 映射到 /resource/dect/*) | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | @app.route('/images/<path:resource_path>') | 
					
						
							|  |  |  |  | @safe_path_check(root_dir=BASE_IMAGE_DIR_DECT) | 
					
						
							|  |  |  |  | def get_compatible_image(resource_path, root_dir): | 
					
						
							|  |  |  |  |     try: | 
					
						
							|  |  |  |  |         resource_path = resource_path.replace("/", os.sep).replace("\\", os.sep) | 
					
						
							|  |  |  |  |         dir_path, file_name = os.path.split(resource_path) | 
					
						
							|  |  |  |  |         full_dir = os.path.abspath(os.path.join(root_dir, dir_path)) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         allowed_ext = ('.png', '.jpg', '.jpeg', '.gif', '.bmp') | 
					
						
							|  |  |  |  |         if not file_name.lower().endswith(allowed_ext): | 
					
						
							|  |  |  |  |             logger.warning( | 
					
						
							|  |  |  |  |                 f"[Flask 格式错误] 非图片文件!IP:{request.remote_addr} | 文件名:{file_name}" | 
					
						
							|  |  |  |  |             ) | 
					
						
							|  |  |  |  |             abort(415) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         logger.info( | 
					
						
							|  |  |  |  |             f"[Flask 兼容图片] 成功请求!IP:{request.remote_addr} | 文件:{file_name} | 目录:{full_dir}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |         return send_from_directory(full_dir, file_name, as_attachment=False) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     except Exception as e: | 
					
						
							|  |  |  |  |         logger.error( | 
					
						
							|  |  |  |  |             f"[Flask 兼容图片异常] IP:{request.remote_addr} | 错误:{str(e)}" | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  |         abort(500) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  | # 全局错误处理器(不变) | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | @app.errorhandler(403) | 
					
						
							|  |  |  |  | def forbidden_error(error): | 
					
						
							|  |  |  |  |     return "❌ 禁止访问:路径非法(可能存在路径遍历)或无权限", 403 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | @app.errorhandler(404) | 
					
						
							|  |  |  |  | def not_found_error(error): | 
					
						
							|  |  |  |  |     return "❌ 资源不存在:请检查URL路径(IP、目录、文件名)是否正确", 404 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | @app.errorhandler(413) | 
					
						
							|  |  |  |  | def too_large_error(error): | 
					
						
							|  |  |  |  |     return "❌ 文件过大:图片最大10MB,模型最大200MB", 413 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | @app.errorhandler(415) | 
					
						
							|  |  |  |  | def unsupported_type_error(error): | 
					
						
							|  |  |  |  |     return "❌ 不支持的文件类型:图片支持png/jpg/jpeg/gif/bmp,模型仅支持pt", 415 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | @app.errorhandler(500) | 
					
						
							|  |  |  |  | def server_error(error): | 
					
						
							|  |  |  |  |     return "❌ 服务器内部错误:请联系管理员查看后台日志", 500 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | # Flask 独立启动入口(供测试,实际由 main.py 子线程启动) | 
					
						
							|  |  |  |  | # ------------------------------ | 
					
						
							|  |  |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |     # 确保所有资源目录存在 | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |     required_dirs = [ | 
					
						
							|  |  |  |  |         (BASE_IMAGE_DIR_DECT, "检测图片目录"), | 
					
						
							|  |  |  |  |         (BASE_IMAGE_DIR_UP_IMAGES, "人脸图片目录"), | 
					
						
							|  |  |  |  |         (BASE_MODEL_DIR, "模型文件目录") | 
					
						
							|  |  |  |  |     ] | 
					
						
							|  |  |  |  |     for dir_path, dir_desc in required_dirs: | 
					
						
							|  |  |  |  |         if not os.path.exists(dir_path): | 
					
						
							|  |  |  |  |             logger.info(f"[Flask 初始化] {dir_desc}不存在,创建:{dir_path}") | 
					
						
							|  |  |  |  |             os.makedirs(dir_path, exist_ok=True) | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |     # 启动提示 | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |     logger.info("\n[Flask 服务启动成功!] 支持的接口:") | 
					
						
							|  |  |  |  |     logger.info(f"1. 模型下载 → http://服务器IP:5000/model/download/resource/models/xxx.pt") | 
					
						
							|  |  |  |  |     logger.info(f"2. 人脸图片 → http://服务器IP:5000/up_images/xxx.jpg") | 
					
						
							|  |  |  |  |     logger.info(f"3. 检测图片 → http://服务器IP:5000/resource/dect/xxx.jpg 或 http://服务器IP:5000/images/xxx.jpg\n") | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |     # 启动服务(禁用 debug 和自动重载) | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |     app.run( | 
					
						
							| 
									
										
										
										
											2025-09-15 18:35:43 +08:00
										 |  |  |  |         host="0.0.0.0", | 
					
						
							|  |  |  |  |         port=5000, | 
					
						
							| 
									
										
										
										
											2025-09-12 14:05:09 +08:00
										 |  |  |  |         debug=False, | 
					
						
							|  |  |  |  |         use_reloader=False | 
					
						
							|  |  |  |  |     ) |