feat: 添加封面图压字花功能和启动脚本
- 新增封面图本地化压字花处理(深褐色文字+白色描边,居中显示) - 支持Linux/Windows跨平台字体加载 - 新增启动脚本 start_article_auto_image_matching.sh - 优化图片生成策略(0张图/1张图/多张图不同处理) - 绕过网络接口IncompleteRead问题,本地化处理更稳定 - 更新README文档,完善使用说明
This commit is contained in:
436
abc.py
Normal file
436
abc.py
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user