Files
shengyudong 97dcff8c8b feat: 添加封面图压字花功能和启动脚本
- 新增封面图本地化压字花处理(深褐色文字+白色描边,居中显示)
- 支持Linux/Windows跨平台字体加载
- 新增启动脚本 start_article_auto_image_matching.sh
- 优化图片生成策略(0张图/1张图/多张图不同处理)
- 绕过网络接口IncompleteRead问题,本地化处理更稳定
- 更新README文档,完善使用说明
2026-02-05 20:25:23 +08:00

437 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@image_bp.route('/add-article-cover-images/<int:article_id>', 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