feat: 添加封面图压字花功能和启动脚本

- 新增封面图本地化压字花处理(深褐色文字+白色描边,居中显示)
- 支持Linux/Windows跨平台字体加载
- 新增启动脚本 start_article_auto_image_matching.sh
- 优化图片生成策略(0张图/1张图/多张图不同处理)
- 绕过网络接口IncompleteRead问题,本地化处理更稳定
- 更新README文档,完善使用说明
This commit is contained in:
2026-02-05 20:25:23 +08:00
parent 1436129845
commit 97dcff8c8b
9 changed files with 3292 additions and 1093 deletions

436
abc.py Normal file
View File

@@ -0,0 +1,436 @@
@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