from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime, timedelta, timezone from operator import or_ import os import random import string import traceback from flask import Blueprint, json, jsonify, current_app, request from sqlalchemy.exc import SQLAlchemyError from app.models import AccidList, DictItem, DictType, FileUpload from app import db file_uploads_bp = Blueprint("file_uploads_bp", __name__) ALLOWED_EXTENSIONS={'png', 'jpg', 'jpeg', 'gif'} def allowed_file(filename): """验证图片格式是否合法""" return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def generate_file_alias(filename): """生成唯一存储别名(避免重名):时间戳+6位随机字符串+扩展名""" ext = filename.rsplit('.', 1)[1].lower() random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=6)) timestamp = int(datetime.now().timestamp()) return f"{timestamp}_{random_str}.{ext}" # 接口1:图片上传(基础接口,返回文件ID) @file_uploads_bp.route('/upload-image', methods=['POST']) def upload_image(): """ 图片上传接口 - 请求方式:POST - Content-Type: multipart/form-data - 请求参数: file: 图片文件(必传) create_by: 上传人(可选,如admin) - 返回:文件ID、访问URL等信息 """ try: # 1. 校验文件是否存在 if 'file' not in request.files: return jsonify({'code': 400, 'msg': '请选择要上传的图片', 'data': None}), 400 file = request.files['file'] if file.filename == '': return jsonify({'code': 400, 'msg': '文件名不能为空', 'data': None}), 400 # 2. 校验文件格式 if not allowed_file(file.filename): return jsonify({ 'code': 400, 'msg': f'仅支持{"/".join(ALLOWED_EXTENSIONS)}格式的图片', 'data': None }), 400 # 3. 创建上传目录(不存在则自动创建) os.makedirs(current_app.config['UPLOAD_FOLDER'], exist_ok=True) # 4. 生成唯一别名并保存文件 file_alias = generate_file_alias(file.filename) file_save_path = os.path.join(current_app.config['UPLOAD_FOLDER'], file_alias) file.save(file_save_path) # 5. 写入FileUpload表 file_size = os.path.getsize(file_save_path) # 获取文件大小(字节) file_type = file.content_type # 获取MIME类型(如image/jpeg) create_by = request.form.get('create_by', None) new_file = FileUpload( file_name=file.filename, file_alias=file_alias, file_path=f"/uploads/{file_alias}", # 相对路径,前端拼接域名访问 file_size=file_size, file_type=file_type, create_by=create_by, business_type='case', # 固定关联事故工单业务 business_id=None # 暂不关联具体工单,后续绑定 ) db.session.add(new_file) db.session.commit() # 6. 返回成功响应 return jsonify({ 'code': 200, 'msg': '图片上传成功', 'data': new_file.to_dict() }), 200 except Exception as e: db.session.rollback() # 异常回滚 return jsonify({ 'code': 500, 'msg': f'上传失败:{str(e)}', 'data': None }), 500 # 接口2:绑定图片到事故工单 @file_uploads_bp.route('/bind-image', methods=['POST']) def bind_image_to_case(): """ 绑定图片到事故工单(更新image_ids和business_id) - 请求方式:POST - Content-Type: application/json - 请求参数: case_id: 事故工单ID(必传) file_ids: 图片ID列表(如[1,2,3],必传) """ try: data = request.get_json() case_id = data.get('case_id') file_ids = data.get('file_ids', []) # 1. 校验参数 if not case_id or not isinstance(file_ids, list) or len(file_ids) == 0: return jsonify({'code': 400, 'msg': '工单ID和图片ID列表不能为空', 'data': None}), 400 # 2. 检查工单是否存在 case = AccidList.query.get(case_id) if not case: return jsonify({'code': 404, 'msg': '事故工单不存在', 'data': None}), 404 # 3. 拼接图片ID串(去重) existing_ids = case.image_ids.split(',') if case.image_ids.strip() else [] new_ids = list(set(existing_ids + [str(fid) for fid in file_ids])) # 去重 case.image_ids = ','.join(new_ids) case.update_at = db.func.current_timestamp() # 更新工单修改时间 # 4. 更新FileUpload的business_id(关联具体工单) FileUpload.query.filter( FileUpload.id.in_(file_ids), FileUpload.business_type == 'case' ).update({'business_id': case_id}, synchronize_session=False) db.session.commit() # 5. 返回结果 return jsonify({ 'code': 200, 'msg': '图片绑定成功', 'data': { 'case_id': case_id, 'image_ids': case.image_ids, 'image_count': len(new_ids) } }), 200 except Exception as e: db.session.rollback() return jsonify({ 'code': 500, 'msg': f'绑定失败:{str(e)}', 'data': None }), 500 # 接口3:查询事故工单关联的图片 @file_uploads_bp.route('/get-images/', methods=['GET']) def get_case_images(case_id): """ 查询工单关联的所有有效图片 - 请求方式:GET - 路径参数:case_id - 事故工单ID """ try: # 1. 检查工单是否存在 case = AccidList.query.get(case_id) if not case: return jsonify({'code': 404, 'msg': '事故工单不存在', 'data': None}), 404 # 2. 拆分图片ID并查询 image_ids = case.image_ids.split(',') if case.image_ids.strip() else [] if not image_ids: return jsonify({'code': 200, 'msg': '暂无关联图片', 'data': []}), 200 # 3. 查询有效图片(status=1) images = FileUpload.query.filter( FileUpload.id.in_(image_ids), FileUpload.status == 1 ).all() # ========== 核心修改:拼接完整图片URL ========== image_base_url = current_app.config['IMAGE_BASE_URL'] # 获取配置的基础URL image_data = [] for img in images: img_dict = img.to_dict() # 原有模型的to_dict方法 # 把相对路径拼接成完整URL(覆盖原有file_path) img_dict['file_path'] = f"{image_base_url}{img_dict['file_path']}" image_data.append(img_dict) # 4. 返回结果(改用拼接后的image_data) return jsonify({ 'code': 200, 'msg': '查询成功', 'data': image_data # 现在data里的file_path是完整URL }), 200 except Exception as e: return jsonify({ 'code': 500, 'msg': f'查询失败:{str(e)}', 'data': None }), 500 # 接口4:移除工单关联的图片(逻辑删除) @file_uploads_bp.route('/remove-image', methods=['POST']) def remove_case_image(): """ 移除工单关联的图片(逻辑删除) - 请求方式:POST - Content-Type: application/json - 请求参数: case_id: 工单ID(必传) file_id: 图片ID(必传) """ try: data = request.get_json() case_id = data.get('case_id') file_id = data.get('file_id') # 1. 校验参数 if not case_id or not file_id: return jsonify({'code': 400, 'msg': '工单ID和图片ID不能为空', 'data': None}), 400 # 2. 检查工单和图片是否存在 case = AccidList.query.get(case_id) file = FileUpload.query.get(file_id) if not case: return jsonify({'code': 404, 'msg': '事故工单不存在', 'data': None}), 404 if not file: return jsonify({'code': 404, 'msg': '图片不存在', 'data': None}), 404 # 3. 移除工单的image_ids中的该ID existing_ids = case.image_ids.split(',') if case.image_ids.strip() else [] if str(file_id) in existing_ids: existing_ids.remove(str(file_id)) case.image_ids = ','.join(existing_ids) case.update_at = db.func.current_timestamp() # 4. 逻辑删除图片(status=0) file.status = 0 db.session.commit() return jsonify({ 'code': 200, 'msg': '图片移除成功', 'data': {'case_id': case_id, 'file_id': file_id} }), 200 except Exception as e: db.session.rollback() return jsonify({ 'code': 500, 'msg': f'移除失败:{str(e)}', 'data': None }), 500