@image_bp.route('/add-article-cover-images/', methods=['POST']) @require_auth def add_article_cover_images(article_id): """添加文章封面图片 支持两种上传方式: 1. action=image_content (默认): 从request.files上传图片文件 2. action=image_url: 从image_url参数下载图片 """ try: # 获取action参数,默认为image_content action = request.form.get('action', 'image_content') logger.info(f"[添加文章封面] 上传方式: {action}") # 根据action参数处理图片源 if action == 'image_url': # 方式1: 从URL下载图片 image_url = request.form.get('image_url', '').strip() if not image_url: return jsonify({ 'code': 400, 'message': '缺少image_url参数', 'data': None }), 400 logger.info(f"[添加文章封面] 从URL下载图片: {image_url}") try: import requests from io import BytesIO # 下载图片(禁用代理) session = requests.Session() session.trust_env = False response = session.get(image_url, timeout=30) response.raise_for_status() # 检查Content-Type content_type = response.headers.get('Content-Type', '') if 'image' not in content_type: return jsonify({ 'code': 400, 'message': f'URL不是有效的图片资源,Content-Type: {content_type}', 'data': None }), 400 # 获取文件扩展名 file_ext = 'jpg' # 默认扩展名 if 'png' in content_type: file_ext = 'png' elif 'jpeg' in content_type or 'jpg' in content_type: file_ext = 'jpg' elif 'gif' in content_type: file_ext = 'gif' elif 'webp' in content_type: file_ext = 'webp' # 创建一个模拟的文件对象 image_data = response.content file = BytesIO(image_data) file.filename = f"downloaded_image.{file_ext}" file.seek(0) logger.info(f"[添加文章封面] 图片下载成功,大小: {len(image_data)} bytes, 类型: {file_ext}") except requests.RequestException as e: logger.error(f"[添加文章封面] 下载图片失败: {str(e)}") return jsonify({ 'code': 400, 'message': f'下载图片失败: {str(e)}', 'data': None }), 400 except Exception as e: logger.error(f"[添加文章封面] 处理URL图片失败: {str(e)}") return jsonify({ 'code': 400, 'message': f'处理URL图片失败: {str(e)}', 'data': None }), 400 else: # 方式2: 从request.files上传图片(默认方式) if 'image' not in request.files: return jsonify({ 'code': 400, 'message': '没有上传图片文件', 'data': None }), 400 file = request.files['image'] if file.filename == '': return jsonify({ 'code': 400, 'message': '没有选择文件', 'data': None }), 400 # 验证文件类型 allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'} file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else '' if file_ext not in allowed_extensions: return jsonify({ 'code': 400, 'message': '不支持的文件格式,仅支持: png, jpg, jpeg, gif, webp', 'data': None }), 400 logger.info(f"[添加文章封面] 从文件上传,文件名: {file.filename}, 类型: {file_ext}") db_manager = get_db_manager() current_user = AuthUtils.get_current_user() # 检查文章是否存在 check_article_sql = "SELECT id, title, topic, created_user_id FROM ai_articles WHERE id = %s AND status = %s" logger.info(f"[添加文章封面] 执行SQL查询文章: {check_article_sql} - 参数: {article_id}") article_result = db_manager.execute_query(check_article_sql, (article_id, 'pending_review', )) logger.info(f"[添加文章封面] 查询文章结果: {article_result}") if not article_result: return jsonify({ 'code': 404, 'message': '文章不存在', 'data': None }), 404 article = article_result[0] topic = article['topic'] # 检查权限(只有创建者或管理员可以编辑) if article['created_user_id'] != current_user['user_id'] and current_user['role'] != 'admin': return jsonify({ 'code': 403, 'message': '没有权限编辑此文章', 'data': None }), 403 # 生成新的文件名 current_time = datetime.now() date_str = current_time.strftime('%Y%m%d') timestamp = int(time.time() * 1000) # 毫秒时间戳 random_num = random.randint(100, 999) # 3位随机数 base_filename = f"{timestamp}{random_num}" new_filename = f"{base_filename}.png" #original_filename = f"{base_filename}_original.png" thumb_filename = f"{base_filename}_thumb.png" # 创建日期目录 date_dir = os.path.join(IMAGE_UPLOAD_DIR, date_str) os.makedirs(date_dir, exist_ok=True) # 处理图片:优化压缩和生成缩略图 try: # 读取上传的图片 image_data = file.read() original_image = Image.open(io.BytesIO(image_data)) # 转换为RGB模式(确保兼容性) if original_image.mode in ('RGBA', 'LA', 'P'): # 创建白色背景 background = Image.new('RGB', original_image.size, (255, 255, 255)) if original_image.mode == 'P': original_image = original_image.convert('RGBA') background.paste(original_image, mask=original_image.split()[-1] if original_image.mode == 'RGBA' else None) original_image = background elif original_image.mode != 'RGB': original_image = original_image.convert('RGB') # 1. 保存优化后的原图 file_path = os.path.join(date_dir, new_filename) # 1. 保存优化后的原图 #original_file_path = os.path.join(date_dir, original_filename) # 大图优化:在清晰条件下缩小质量 # 如果图片过大,先进行适当缩放 max_size = (1920, 1080) # 最大尺寸限制 if original_image.size[0] > max_size[0] or original_image.size[1] > max_size[1]: original_image.thumbnail(max_size, Image.Resampling.LANCZOS) logger.info(f"[添加文章封面] 图片尺寸优化: 缩放到 {original_image.size}") # 保存优化后的原图(高质量压缩) #original_image.save(original_file_path, 'PNG', optimize=True, compress_level=6) #logger.info(f"[添加文章封面] 优化原图保存成功: {original_file_path}") # 保存优化后的原图(高质量压缩) original_image.save(file_path, 'PNG', optimize=True, compress_level=6) logger.info(f"[添加文章封面] 优化原图保存成功: {file_path}") # ⭐ 关键修改:图片和文字融合 try: logger.info(f"[添加文章封面] 开始图片文字融合,文章标题: {topic}") # 生成带文字的文件名 text_filename = f"{base_filename}_text.png" text_file_path = os.path.join(date_dir, text_filename) # 调用图片文字融合功能(文字居中显示,深褐色文字+白色描边) fusion_success = add_text_to_image( image_path=file_path, text=topic, output_path=text_file_path, position='center', # ⭐ 文字居中 font_size=120, # 字体大小(基础值,会自适应调整) font_color=(180, 60, 50) # ⭐ 深褐色文字(与参考图保持一致) ) if fusion_success: logger.info(f"[添加文章封面] 图片文字融合成功: {text_file_path}") # 用融合后的图片替换原图 os.replace(text_file_path, file_path) logger.info(f"[添加文章封面] 已用融合图片替换原图: {file_path}") else: logger.warning(f"[添加文章封面] 图片文字融合失败,继续使用原图") except Exception as fusion_error: logger.error(f"[添加文章封面] 图片文字融合异常: {str(fusion_error)}", exc_info=True) logger.info(f"[添加文章封面] 融合失败,继续使用原图") # 2. 生成缩略图 (120x160) thumb_path = os.path.join(date_dir, thumb_filename) thumb_image = original_image.copy() # 使用高质量重采样算法生成缩略图 thumb_size = (120, 160) # 计算缩放比例,保持宽高比 img_ratio = thumb_image.size[0] / thumb_image.size[1] thumb_ratio = thumb_size[0] / thumb_size[1] if img_ratio > thumb_ratio: # 图片更宽,以高度为准 new_height = thumb_size[1] new_width = int(new_height * img_ratio) thumb_image = thumb_image.resize((new_width, new_height), Image.Resampling.LANCZOS) # 裁剪中心部分 left = (new_width - thumb_size[0]) // 2 thumb_image = thumb_image.crop((left, 0, left + thumb_size[0], thumb_size[1])) else: # 图片更高,以宽度为准 new_width = thumb_size[0] new_height = int(new_width / img_ratio) thumb_image = thumb_image.resize((new_width, new_height), Image.Resampling.LANCZOS) # 裁剪中心部分 top = (new_height - thumb_size[1]) // 2 thumb_image = thumb_image.crop((0, top, thumb_size[0], top + thumb_size[1])) # 保存缩略图 thumb_image.save(thumb_path, 'PNG', optimize=True, compress_level=9) logger.info(f"[添加文章封面] 缩略图生成成功: {thumb_path} (尺寸: {thumb_image.size})") except Exception as img_error: logger.error(f"[添加文章封面] 图片处理失败: {str(img_error)}", exc_info=True) # 如果图片处理失败,回退到原始保存方式 file.seek(0) # 重置文件指针 file_path = os.path.join(date_dir, new_filename) file.save(file_path) logger.info(f"[添加文章封面] 回退保存成功: {file_path}") # 生成相对路径用于数据库存储 relative_path = f"{date_str}/{new_filename}" thumb_relative_path = f"{date_str}/{thumb_filename}" #fusion_before_relative_path = f"{date_str}/{original_filename}" # 图片上传成功后,调用 TransformerImage 方法处理原图、缩图 try: logger.info(f"[添加文章封面] 开始调用 TransformerImage 处理图片") # 创建 SyncImageToOSS 实例 sync_oss = SyncImageToOSS() # 调用 TransformerImage 方法处理原图 #local_result = sync_oss.TransformerImage(original_file_path) #logger.info(f"[添加文章封面] 原图上传结果: {local_result}") # 调用 TransformerImage 方法处理原图 original_result = sync_oss.TransformerImage(file_path) logger.info(f"[添加文章封面] 原图上传结果: {original_result}") # 调用 TransformerImage 方法处理缩图 thumb_result = sync_oss.TransformerImage(thumb_path) logger.info(f"[添加文章封面] 缩图上传结果: {thumb_result}") # 检查所有上传是否成功 if not (original_result['success'] and thumb_result['success']): logger.warning(f"[添加文章封面] OSS上传部分失败 - 原图: {original_result['success']}, 缩图: {thumb_result['success']}") except Exception as e: logger.error(f"[添加文章封面] TransformerImage 调用失败: {str(e)}") # 在 ai_images 表中创建新记录 image_insert_sql = """INSERT INTO ai_images (image_name, image_url, image_thumb_url, upload_user_id, status) VALUES (%s, %s, %s, %s, %s)""" logger.info(f"[添加文章封面] 执行SQL插入图片记录") image_id = db_manager.execute_insert(image_insert_sql, ( new_filename, relative_path, thumb_relative_path, current_user['user_id'], 'active' )) logger.info(f"[添加文章封面] 图片记录创建成功: 图片ID {image_id}") # ⭐ 关键修改:先判断是否存在 sort_order=1 的封面图 check_cover_sql = """SELECT id, image_id FROM ai_article_images WHERE article_id = %s AND sort_order = 1""" logger.info(f"[添加文章封面] 检查是否已存在封面图: article_id={article_id}, sort_order=1") existing_cover = db_manager.execute_query(check_cover_sql, (article_id,)) logger.info(f"[添加文章封面] 查询结果: {existing_cover}") if existing_cover: # 已存在封面图,走更新流程 old_relation_id = existing_cover[0]['id'] old_image_id = existing_cover[0]['image_id'] logger.info(f"[添加文章封面] 检测到已存在封面图,执行更新操作") logger.info(f"[添加文章封面] 旧关联ID: {old_relation_id}, 旧图片ID: {old_image_id}") # 更新 ai_article_images 表中的关联记录 relation_update_sql = """UPDATE ai_article_images SET image_id = %s, image_url = %s, image_thumb_url = %s, updated_at = NOW() WHERE id = %s""" logger.info(f"[添加文章封面] 执行SQL更新文章图片关联") db_manager.execute_update(relation_update_sql, ( image_id, relative_path, thumb_relative_path, old_relation_id )) logger.info(f"[添加文章封面] 文章图片关联更新成功: 关联ID {old_relation_id}, 新图片ID {image_id}") # 记录操作日志 log_sql = """ INSERT INTO ai_logs (user_id, action, target_type, target_id, description, ip_address, user_agent, status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """ client_ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', '未知')) user_agent = request.headers.get('User-Agent', '未知') action_desc = f'更新文章封面: 文章ID {article_id}, 新图片ID {image_id}, 旧图片ID {old_image_id}, 路径 {relative_path}' log_params = ( current_user['user_id'], 'update_article_cover_images', 'article_cover', article_id, action_desc, client_ip, user_agent, 'success' ) logger.info(f"[添加文章封面] 执行SQL插入日志: {log_sql} - 参数: {log_params}") db_manager.execute_insert(log_sql, log_params) logger.info(f"[添加文章封面] 操作日志记录成功") logger.info(f"更新文章封面成功: {action_desc}") return jsonify({ 'code': 200, 'message': '封面图片更新成功', 'data': { 'article_id': article_id, 'image_id': image_id, 'old_image_id': old_image_id, 'image_url': IMAGE_BASE_URL + relative_path, 'relative_path': relative_path, 'image_thumb_url': IMAGE_BASE_URL + thumb_relative_path, 'thumb_relative_path': thumb_relative_path, 'operation': 'update', 'optimization_info': { 'original_size': f"{original_image.size[0]}x{original_image.size[1]}" if 'original_image' in locals() else 'unknown', 'thumb_size': '120x160', 'compression': 'PNG优化压缩', 'features': ['大图优化', '缩略图生成', '智能裁剪', '高质量重采样', '文字融合'] } }, 'timestamp': int(datetime.now().timestamp() * 1000) }) else: # 不存在封面图,走新增流程 logger.info(f"[添加文章封面] 未检测到封面图,执行新增操作") # 在 ai_article_images 表中创建关联记录 relation_insert_sql = """INSERT INTO ai_article_images (article_id, image_id, image_url, image_thumb_url, image_source, sort_order) VALUES (%s, %s, %s, %s, %s, %s)""" logger.info(f"[添加文章封面] 执行SQL创建文章图片关联") db_manager.execute_insert(relation_insert_sql, ( article_id, image_id, relative_path, thumb_relative_path, 4, 1 # image_source=4表示封面图片,sort_order=1表示第一张 )) logger.info(f"[添加文章封面] 文章图片关联创建成功: 文章ID {article_id}, 图片ID {image_id}") # 记录操作日志 log_sql = """ INSERT INTO ai_logs (user_id, action, target_type, target_id, description, ip_address, user_agent, status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """ client_ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', '未知')) user_agent = request.headers.get('User-Agent', '未知') action_desc = f'添加文章封面: 文章ID {article_id}, 图片ID {image_id}, 路径 {relative_path}' log_params = ( current_user['user_id'], 'add_article_cover_images', 'article_cover', article_id, action_desc, client_ip, user_agent, 'success' ) logger.info(f"[添加文章封面] 执行SQL插入日志: {log_sql} - 参数: {log_params}") db_manager.execute_insert(log_sql, log_params) logger.info(f"[添加文章封面] 操作日志记录成功") logger.info(f"添加文章封面成功: {action_desc}") return jsonify({ 'code': 200, 'message': '封面图片添加成功', 'data': { 'article_id': article_id, 'image_id': image_id, 'image_url': IMAGE_BASE_URL + relative_path, 'relative_path': relative_path, 'image_thumb_url': IMAGE_BASE_URL + thumb_relative_path, 'thumb_relative_path': thumb_relative_path, 'operation': 'insert', 'optimization_info': { 'original_size': f"{original_image.size[0]}x{original_image.size[1]}" if 'original_image' in locals() else 'unknown', 'thumb_size': '120x160', 'compression': 'PNG优化压缩', 'features': ['大图优化', '缩略图生成', '智能裁剪', '高质量重采样', '文字融合'] } }, 'timestamp': int(datetime.now().timestamp() * 1000) }) except Exception as e: logger.error(f"[添加文章封面] 处理请求时发生错误: {str(e)}", exc_info=True) return jsonify({ 'code': 500, 'message': '服务器内部错误', 'data': None }), 500