diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py index 45d7bd1..a02e232 100644 --- a/XianyuAutoAsync.py +++ b/XianyuAutoAsync.py @@ -261,11 +261,23 @@ class XianyuLive: # 检查是否是图片发送标记 if delivery_content.startswith("__IMAGE_SEND__"): - # 提取图片URL - image_url = delivery_content.replace("__IMAGE_SEND__", "") + # 提取卡券ID和图片URL + image_data = delivery_content.replace("__IMAGE_SEND__", "") + if "|" in image_data: + card_id_str, image_url = image_data.split("|", 1) + try: + card_id = int(card_id_str) + except ValueError: + logger.error(f"无效的卡券ID: {card_id_str}") + card_id = None + else: + # 兼容旧格式(没有卡券ID) + card_id = None + image_url = image_data + # 发送图片消息 try: - await self.send_image_msg(websocket, chat_id, send_user_id, image_url) + await self.send_image_msg(websocket, chat_id, send_user_id, image_url, card_id=card_id) logger.info(f'[{msg_time}] 【自动发货图片】已向 {user_url} 发送图片: {image_url}') await self.send_delivery_failure_notification(send_user_name, send_user_id, item_id, "发货成功") except Exception as e: @@ -1081,6 +1093,18 @@ class XianyuLive: except Exception as e: logger.error(f"更新关键词图片URL失败: {e}") + async def _update_card_image_url(self, card_id: int, new_image_url: str): + """更新卡券的图片URL""" + try: + from db_manager import db_manager + success = db_manager.update_card_image_url(card_id, new_image_url) + if success: + logger.info(f"卡券图片URL已更新: 卡券ID={card_id} -> {new_image_url}") + else: + logger.warning(f"卡券图片URL更新失败: 卡券ID={card_id}") + except Exception as e: + logger.error(f"更新卡券图片URL失败: {e}") + async def get_ai_reply(self, send_user_name: str, send_user_id: str, send_message: str, item_id: str, chat_id: str): """获取AI回复""" try: @@ -1935,11 +1959,11 @@ class XianyuLive: delivery_content = db_manager.consume_batch_data(rule['card_id']) elif rule['card_type'] == 'image': - # 图片类型:返回图片发送标记 + # 图片类型:返回图片发送标记,包含卡券ID image_url = rule.get('image_url') if image_url: - delivery_content = f"__IMAGE_SEND__{image_url}" - logger.info(f"准备发送图片: {image_url}") + delivery_content = f"__IMAGE_SEND__{rule['card_id']}|{image_url}" + logger.info(f"准备发送图片: {image_url} (卡券ID: {rule['card_id']})") else: logger.error(f"图片卡券缺少图片URL: 卡券ID={rule['card_id']}") delivery_content = None @@ -2788,7 +2812,7 @@ class XianyuLive: if reply: # 检查是否是图片发送标记 if reply.startswith("__IMAGE_SEND__"): - # 提取图片URL + # 提取图片URL(关键词回复不包含卡券ID) image_url = reply.replace("__IMAGE_SEND__", "") # 发送图片消息 try: @@ -3124,7 +3148,7 @@ class XianyuLive: "items": all_items } - async def send_image_msg(self, ws, cid, toid, image_url, width=800, height=600): + async def send_image_msg(self, ws, cid, toid, image_url, width=800, height=600, card_id=None): """发送图片消息""" try: # 检查图片URL是否需要上传到CDN @@ -3149,6 +3173,10 @@ class XianyuLive: logger.info(f"【{self.cookie_id}】图片上传成功,CDN URL: {cdn_url}") image_url = cdn_url + # 如果是卡券图片,更新数据库中的图片URL + if card_id is not None: + await self._update_card_image_url(card_id, cdn_url) + # 获取实际图片尺寸 from utils.image_utils import image_manager try: diff --git a/db_manager.py b/db_manager.py index dee8dfa..d3e04b9 100644 --- a/db_manager.py +++ b/db_manager.py @@ -2545,8 +2545,9 @@ class DBManager: def update_card(self, card_id: int, name: str = None, card_type: str = None, api_config=None, text_content: str = None, data_content: str = None, - description: str = None, enabled: bool = None, delay_seconds: int = None, - is_multi_spec: bool = None, spec_name: str = None, spec_value: str = None): + image_url: str = None, description: str = None, enabled: bool = None, + delay_seconds: int = None, is_multi_spec: bool = None, spec_name: str = None, + spec_value: str = None): """更新卡券""" with self.lock: try: @@ -2580,6 +2581,9 @@ class DBManager: if data_content is not None: update_fields.append("data_content = ?") params.append(data_content) + if image_url is not None: + update_fields.append("image_url = ?") + params.append(image_url) if description is not None: update_fields.append("description = ?") params.append(description) @@ -2620,6 +2624,32 @@ class DBManager: self.conn.rollback() raise + def update_card_image_url(self, card_id: int, new_image_url: str) -> bool: + """更新卡券的图片URL""" + with self.lock: + try: + cursor = self.conn.cursor() + + # 更新图片URL + self._execute_sql(cursor, + "UPDATE cards SET image_url = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND type = 'image'", + (new_image_url, card_id)) + + self.conn.commit() + + # 检查是否有行被更新 + if cursor.rowcount > 0: + logger.info(f"卡券图片URL更新成功: 卡券ID: {card_id}, 新URL: {new_image_url}") + return True + else: + logger.warning(f"未找到匹配的图片卡券: 卡券ID: {card_id}") + return False + + except Exception as e: + logger.error(f"更新卡券图片URL失败: {e}") + self.conn.rollback() + return False + # ==================== 自动发货规则方法 ==================== def create_delivery_rule(self, keyword: str, card_id: int, delivery_count: int = 1, diff --git a/reply_server.py b/reply_server.py index 7f5a205..bf7a406 100644 --- a/reply_server.py +++ b/reply_server.py @@ -2148,6 +2148,7 @@ def update_card(card_id: int, card_data: dict, _: None = Depends(require_auth)): api_config=card_data.get('api_config'), text_content=card_data.get('text_content'), data_content=card_data.get('data_content'), + image_url=card_data.get('image_url'), description=card_data.get('description'), enabled=card_data.get('enabled', True), delay_seconds=card_data.get('delay_seconds'), @@ -2163,6 +2164,76 @@ def update_card(card_id: int, card_data: dict, _: None = Depends(require_auth)): raise HTTPException(status_code=500, detail=str(e)) +@app.put("/cards/{card_id}/image") +async def update_card_with_image( + card_id: int, + image: UploadFile = File(...), + name: str = Form(...), + type: str = Form(...), + description: str = Form(default=""), + delay_seconds: int = Form(default=0), + enabled: bool = Form(default=True), + is_multi_spec: bool = Form(default=False), + spec_name: str = Form(default=""), + spec_value: str = Form(default=""), + current_user: Dict[str, Any] = Depends(get_current_user) +): + """更新带图片的卡券""" + try: + logger.info(f"接收到带图片的卡券更新请求: card_id={card_id}, name={name}, type={type}") + + # 验证图片文件 + if not image.content_type or not image.content_type.startswith('image/'): + logger.warning(f"无效的图片文件类型: {image.content_type}") + raise HTTPException(status_code=400, detail="请上传图片文件") + + # 验证多规格字段 + if is_multi_spec: + if not spec_name or not spec_value: + raise HTTPException(status_code=400, detail="多规格卡券必须提供规格名称和规格值") + + # 读取图片数据 + image_data = await image.read() + logger.info(f"读取图片数据成功,大小: {len(image_data)} bytes") + + # 保存图片 + image_url = image_manager.save_image(image_data, image.filename) + if not image_url: + logger.error("图片保存失败") + raise HTTPException(status_code=400, detail="图片保存失败") + + logger.info(f"图片保存成功: {image_url}") + + # 更新卡券 + from db_manager import db_manager + success = db_manager.update_card( + card_id=card_id, + name=name, + card_type=type, + image_url=image_url, + description=description, + enabled=enabled, + delay_seconds=delay_seconds, + is_multi_spec=is_multi_spec, + spec_name=spec_name if is_multi_spec else None, + spec_value=spec_value if is_multi_spec else None + ) + + if success: + logger.info(f"卡券更新成功: {name} (ID: {card_id})") + return {"message": "卡券更新成功", "image_url": image_url} + else: + # 如果数据库更新失败,删除已保存的图片 + image_manager.delete_image(image_url) + raise HTTPException(status_code=404, detail="卡券不存在") + + except HTTPException: + raise + except Exception as e: + logger.error(f"更新带图片的卡券失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + # 自动发货规则API @app.get("/delivery-rules") def get_delivery_rules(current_user: Dict[str, Any] = Depends(get_current_user)): diff --git a/static/index.html b/static/index.html index 0641f7f..2f2b7a3 100644 --- a/static/index.html +++ b/static/index.html @@ -1528,6 +1528,7 @@ + @@ -1592,6 +1593,45 @@ + + +
diff --git a/static/js/app.js b/static/js/app.js index 91c7a45..b5e9dda 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1682,6 +1682,9 @@ document.addEventListener('DOMContentLoaded', async () => { // 初始化卡券图片文件选择器 initCardImageFileSelector(); + // 初始化编辑卡券图片文件选择器 + initEditCardImageFileSelector(); + // 点击侧边栏外部关闭移动端菜单 document.addEventListener('click', function(e) { const sidebar = document.getElementById('sidebar'); @@ -3183,6 +3186,101 @@ function hideCardImagePreview() { } } +// 初始化编辑卡券图片文件选择器 +function initEditCardImageFileSelector() { + const fileInput = document.getElementById('editCardImageFile'); + if (fileInput) { + fileInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + // 验证文件类型 + if (!file.type.startsWith('image/')) { + showToast('❌ 请选择图片文件,当前文件类型:' + file.type, 'warning'); + e.target.value = ''; + hideEditCardImagePreview(); + return; + } + + // 验证文件大小(5MB) + if (file.size > 5 * 1024 * 1024) { + showToast('❌ 图片文件大小不能超过 5MB,当前文件大小:' + (file.size / 1024 / 1024).toFixed(1) + 'MB', 'warning'); + e.target.value = ''; + hideEditCardImagePreview(); + return; + } + + // 验证图片尺寸 + validateEditCardImageDimensions(file, e.target); + } else { + hideEditCardImagePreview(); + } + }); + } +} + +// 验证编辑卡券图片尺寸 +function validateEditCardImageDimensions(file, inputElement) { + const img = new Image(); + const url = URL.createObjectURL(file); + + img.onload = function() { + const width = this.naturalWidth; + const height = this.naturalHeight; + + URL.revokeObjectURL(url); + + // 检查尺寸限制 + if (width > 4096 || height > 4096) { + showToast(`❌ 图片尺寸过大(${width}x${height}),最大支持 4096x4096 像素`, 'warning'); + inputElement.value = ''; + hideEditCardImagePreview(); + return; + } + + // 显示图片预览 + showEditCardImagePreview(file); + + // 如果图片较大,提示会被压缩 + if (width > 2048 || height > 2048) { + showToast(`ℹ️ 图片尺寸较大(${width}x${height}),上传时将自动压缩以优化性能`, 'info'); + } else { + showToast(`✅ 图片尺寸合适(${width}x${height}),可以上传`, 'success'); + } + }; + + img.onerror = function() { + URL.revokeObjectURL(url); + showToast('❌ 无法读取图片文件,请选择有效的图片', 'warning'); + inputElement.value = ''; + hideEditCardImagePreview(); + }; + + img.src = url; +} + +// 显示编辑卡券图片预览 +function showEditCardImagePreview(file) { + const reader = new FileReader(); + reader.onload = function(e) { + const previewImg = document.getElementById('editCardPreviewImg'); + const previewContainer = document.getElementById('editCardImagePreview'); + + if (previewImg && previewContainer) { + previewImg.src = e.target.result; + previewContainer.style.display = 'block'; + } + }; + reader.readAsDataURL(file); +} + +// 隐藏编辑卡券图片预览 +function hideEditCardImagePreview() { + const previewContainer = document.getElementById('editCardImagePreview'); + if (previewContainer) { + previewContainer.style.display = 'none'; + } +} + // 切换编辑多规格字段显示 function toggleEditMultiSpecFields() { const checkbox = document.getElementById('editIsMultiSpec'); @@ -3688,6 +3786,26 @@ async function editCard(cardId) { document.getElementById('editTextContent').value = card.text_content || ''; } else if (card.type === 'data') { document.getElementById('editDataContent').value = card.data_content || ''; + } else if (card.type === 'image') { + // 处理图片类型 + const currentImagePreview = document.getElementById('editCurrentImagePreview'); + const currentImg = document.getElementById('editCurrentImg'); + const noImageText = document.getElementById('editNoImageText'); + + if (card.image_url) { + // 显示当前图片 + currentImg.src = card.image_url; + currentImagePreview.style.display = 'block'; + noImageText.style.display = 'none'; + } else { + // 没有图片 + currentImagePreview.style.display = 'none'; + noImageText.style.display = 'block'; + } + + // 清空文件选择器和预览 + document.getElementById('editCardImageFile').value = ''; + document.getElementById('editCardImagePreview').style.display = 'none'; } // 显示对应的字段 @@ -3725,6 +3843,7 @@ function toggleEditCardTypeFields() { document.getElementById('editApiFields').style.display = cardType === 'api' ? 'block' : 'none'; document.getElementById('editTextFields').style.display = cardType === 'text' ? 'block' : 'none'; document.getElementById('editDataFields').style.display = cardType === 'data' ? 'block' : 'none'; + document.getElementById('editImageFields').style.display = cardType === 'image' ? 'block' : 'none'; } // 更新卡券 @@ -3804,6 +3923,16 @@ async function updateCard() { case 'data': cardData.data_content = document.getElementById('editDataContent').value; break; + case 'image': + // 处理图片类型 - 如果有新图片则上传,否则保持原有图片 + const imageFile = document.getElementById('editCardImageFile').files[0]; + if (imageFile) { + // 有新图片,需要上传 + await updateCardWithImage(cardId, cardData, imageFile); + return; // 提前返回,因为上传图片是异步的 + } + // 没有新图片,保持原有配置,继续正常更新流程 + break; } const response = await fetch(`${apiBase}/cards/${cardId}`, { @@ -3829,6 +3958,51 @@ async function updateCard() { } } +// 更新带图片的卡券 +async function updateCardWithImage(cardId, cardData, imageFile) { + try { + // 创建FormData对象 + const formData = new FormData(); + + // 添加图片文件 + formData.append('image', imageFile); + + // 添加卡券数据 + Object.keys(cardData).forEach(key => { + if (cardData[key] !== null && cardData[key] !== undefined) { + if (typeof cardData[key] === 'object') { + formData.append(key, JSON.stringify(cardData[key])); + } else { + formData.append(key, cardData[key]); + } + } + }); + + const response = await fetch(`${apiBase}/cards/${cardId}/image`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${authToken}` + // 不设置Content-Type,让浏览器自动设置multipart/form-data + }, + body: formData + }); + + if (response.ok) { + showToast('卡券更新成功', 'success'); + bootstrap.Modal.getInstance(document.getElementById('editCardModal')).hide(); + loadCards(); + } else { + const error = await response.text(); + showToast(`更新失败: ${error}`, 'danger'); + } + } catch (error) { + console.error('更新带图片的卡券失败:', error); + showToast('更新卡券失败', 'danger'); + } +} + + + // 测试卡券(占位函数) function testCard(cardId) { showToast('测试功能开发中...', 'info');