Compare commits

...

3 Commits

Author SHA1 Message Date
zhinianboke
f9a3881b74 修改bug 2025-08-01 02:17:43 +08:00
zhinianboke
1dd4d9b841 支持多规格发货 2025-07-31 23:17:33 +08:00
zhinianboke
81b1f924cc 修复bug 2025-07-31 20:07:49 +08:00
8 changed files with 1273 additions and 208 deletions

View File

@ -32,10 +32,12 @@
### 🚚 自动发货功能
- **智能匹配** - 基于商品信息自动匹配发货规则
- **多规格支持** - 支持同一商品的不同规格自动匹配对应卡券
- **精确匹配+兜底机制** - 优先精确匹配规格,失败时自动降级到普通卡券
- **延时发货** - 支持设置发货延时时间0-3600秒
- **多种触发** - 支持付款消息、小刀消息等多种触发条件
- **防重复发货** - 智能防重复机制,避免重复发货
- **多种发货方式** - 支持文本内容、卡密文件、API调用等发货方式
- **多种发货方式** - 支持固定文字、批量数据、API调用等发货方式
- **自动确认发货** - 检测到付款后自动调用闲鱼API确认发货
- **防重复确认** - 智能防重复确认机制避免重复API调用
- **发货统计** - 完整的发货记录和统计功能
@ -43,7 +45,8 @@
### 🛍️ 商品管理
- **自动收集** - 消息触发时自动收集商品信息
- **API获取** - 通过闲鱼API获取完整商品详情
- **批量管理** - 支持批量查看、编辑商品信息
- **多规格支持** - 支持多规格商品的规格信息管理
- **批量管理** - 支持批量查看、编辑、切换多规格状态
- **智能去重** - 自动去重,避免重复存储
### 🔍 商品搜索功能
@ -63,6 +66,8 @@
- **模板生成** - 自动生成包含示例数据的导入模板
- **批量操作** - 支持批量添加、更新关键词数据
- **数据验证** - 导入时自动验证数据格式和重复性
- **多规格卡券管理** - 支持创建和管理多规格卡券
- **发货规则管理** - 支持多规格发货规则的创建和管理
- **数据备份** - 自动数据备份和恢复
## 📁 项目结构

View File

@ -119,21 +119,30 @@ class XianyuLive:
logger.error(f"{self.cookie_id}】获取自动确认发货设置失败: {self._safe_str(e)}")
return True # 出错时默认启用
def can_auto_delivery(self, item_id: str) -> bool:
"""检查是否可以进行自动发货(防重复发货)"""
def can_auto_delivery(self, order_id: str) -> bool:
"""检查是否可以进行自动发货(防重复发货)- 基于订单ID"""
if not order_id:
# 如果没有订单ID则不进行冷却检查允许发货
return True
current_time = time.time()
last_delivery = self.last_delivery_time.get(item_id, 0)
last_delivery = self.last_delivery_time.get(order_id, 0)
if current_time - last_delivery < self.delivery_cooldown:
logger.info(f"{self.cookie_id}】商品 {item_id} 在冷却期内,跳过自动发货")
logger.info(f"{self.cookie_id}订单 {order_id} 在冷却期内,跳过自动发货")
return False
return True
def mark_delivery_sent(self, item_id: str):
"""标记商品已发货"""
self.last_delivery_time[item_id] = time.time()
logger.debug(f"{self.cookie_id}】标记商品 {item_id} 已发货")
def mark_delivery_sent(self, order_id: str):
"""标记订单已发货 - 基于订单ID"""
if order_id:
self.last_delivery_time[order_id] = time.time()
logger.debug(f"{self.cookie_id}】标记订单 {order_id} 已发货")
else:
logger.debug(f"{self.cookie_id}】无订单ID跳过发货标记")
@ -1062,8 +1071,10 @@ class XianyuLive:
logger.error(f"发送自动发货通知异常: {self._safe_str(e)}")
async def auto_confirm(self, order_id, retry_count=0):
"""自动确认发货 - 使用加密模块"""
"""自动确认发货 - 使用加密模块不包含延时处理延时已在_auto_delivery中处理"""
try:
logger.debug(f"{self.cookie_id}】开始确认发货订单ID: {order_id}")
# 导入超级混淆加密模块
from secure_confirm_ultra import SecureConfirm
@ -1083,8 +1094,48 @@ class XianyuLive:
logger.error(f"{self.cookie_id}】加密确认模块调用失败: {self._safe_str(e)}")
return {"error": f"加密确认模块调用失败: {self._safe_str(e)}", "order_id": order_id}
async def _auto_delivery(self, item_id: str, item_title: str = None):
"""自动发货功能"""
async def fetch_order_detail_info(self, order_id: str):
"""获取订单详情信息"""
try:
logger.info(f"{self.cookie_id}】开始获取订单详情: {order_id}")
# 导入订单详情获取器
from utils.order_detail_fetcher import fetch_order_detail_simple
# 获取当前账号的cookie字符串
cookie_string = self.cookies_str
logger.debug(f"{self.cookie_id}】使用Cookie长度: {len(cookie_string) if cookie_string else 0}")
# 异步获取订单详情使用当前账号的cookie和无头模式
result = await fetch_order_detail_simple(order_id, cookie_string, headless=True)
if result:
logger.info(f"{self.cookie_id}】订单详情获取成功: {order_id}")
logger.info(f"{self.cookie_id}】页面标题: {result.get('title', '未知')}")
# 获取解析后的规格信息
spec_name = result.get('spec_name', '')
spec_value = result.get('spec_value', '')
if spec_name and spec_value:
logger.info(f"{self.cookie_id}】📋 规格名称: {spec_name}")
logger.info(f"{self.cookie_id}】📝 规格值: {spec_value}")
print(f"🛍️ 【{self.cookie_id}】订单 {order_id} 规格信息: {spec_name} -> {spec_value}")
else:
logger.warning(f"{self.cookie_id}】未获取到有效的规格信息")
print(f"⚠️ 【{self.cookie_id}】订单 {order_id} 规格信息获取失败")
return result
else:
logger.warning(f"{self.cookie_id}】订单详情获取失败: {order_id}")
return None
except Exception as e:
logger.error(f"{self.cookie_id}】获取订单详情异常: {self._safe_str(e)}")
return None
async def _auto_delivery(self, item_id: str, item_title: str = None, order_id: str = None):
"""自动发货功能 - 获取卡券规则,执行延时,确认发货,发送内容"""
try:
from db_manager import db_manager
@ -1185,26 +1236,112 @@ class XianyuLive:
logger.info(f"使用搜索文本匹配发货规则: {search_text[:100]}...")
# 根据商品信息查找匹配的发货规则
delivery_rules = db_manager.get_delivery_rules_by_keyword(search_text)
# 检查商品是否为多规格商品
is_multi_spec = db_manager.get_item_multi_spec_status(self.cookie_id, item_id)
spec_name = None
spec_value = None
# 如果是多规格商品且有订单ID获取规格信息
if is_multi_spec and order_id:
logger.info(f"检测到多规格商品,获取订单规格信息: {order_id}")
try:
order_detail = await self.fetch_order_detail_info(order_id)
if order_detail:
spec_name = order_detail.get('spec_name', '')
spec_value = order_detail.get('spec_value', '')
if spec_name and spec_value:
logger.info(f"获取到规格信息: {spec_name} = {spec_value}")
else:
logger.warning(f"未能获取到规格信息,将使用兜底匹配")
else:
logger.warning(f"获取订单详情失败,将使用兜底匹配")
except Exception as e:
logger.error(f"获取订单规格信息失败: {self._safe_str(e)},将使用兜底匹配")
# 智能匹配发货规则:优先精确匹配,然后兜底匹配
delivery_rules = []
# 第一步:如果有规格信息,尝试精确匹配多规格发货规则
if spec_name and spec_value:
logger.info(f"尝试精确匹配多规格发货规则: {search_text[:50]}... [{spec_name}:{spec_value}]")
delivery_rules = db_manager.get_delivery_rules_by_keyword_and_spec(search_text, spec_name, spec_value)
if delivery_rules:
logger.info(f"✅ 找到精确匹配的多规格发货规则: {len(delivery_rules)}")
else:
logger.info(f"❌ 未找到精确匹配的多规格发货规则")
# 第二步:如果精确匹配失败,尝试兜底匹配(普通发货规则)
if not delivery_rules:
logger.info(f"尝试兜底匹配普通发货规则: {search_text[:50]}...")
delivery_rules = db_manager.get_delivery_rules_by_keyword(search_text)
if delivery_rules:
logger.info(f"✅ 找到兜底匹配的普通发货规则: {len(delivery_rules)}")
else:
logger.info(f"❌ 未找到任何匹配的发货规则")
if not delivery_rules:
logger.info(f"未找到匹配的发货规则: {search_text[:50]}...")
logger.warning(f"未找到匹配的发货规则: {search_text[:50]}...")
return None
# 使用第一个匹配的规则(按关键字长度降序排列,优先匹配更精确的规则)
rule = delivery_rules[0]
# 保存商品信息到数据库
await self.save_item_info_to_db(item_id, search_text)
rule = delivery_rules[0]
logger.info(f"找到匹配的发货规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
# 检查是否需要延时发货
# 详细的匹配结果日志
if rule.get('is_multi_spec'):
if spec_name and spec_value:
logger.info(f"🎯 精确匹配多规格发货规则: {rule['keyword']} -> {rule['card_name']} [{rule['spec_name']}:{rule['spec_value']}]")
logger.info(f"📋 订单规格: {spec_name}:{spec_value} ✅ 匹配卡券规格: {rule['spec_name']}:{rule['spec_value']}")
else:
logger.info(f"⚠️ 使用多规格发货规则但无订单规格信息: {rule['keyword']} -> {rule['card_name']} [{rule['spec_name']}:{rule['spec_value']}]")
else:
if spec_name and spec_value:
logger.info(f"🔄 兜底匹配普通发货规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
logger.info(f"📋 订单规格: {spec_name}:{spec_value} ➡️ 使用普通卡券兜底")
else:
logger.info(f"✅ 匹配普通发货规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
# 获取延时设置
delay_seconds = rule.get('card_delay_seconds', 0)
# 执行延时(不管是否确认发货,只要有延时设置就执行)
if delay_seconds and delay_seconds > 0:
logger.info(f"检测到延时发货设置: {delay_seconds}秒,开始延时...")
logger.info(f"检测到发货延时设置: {delay_seconds}秒,开始延时...")
await asyncio.sleep(delay_seconds)
logger.info(f"延时发货完成,开始发送内容")
logger.info(f"延时完成")
# 如果有订单ID执行确认发货
if order_id:
# 检查是否启用自动确认发货
if not self.is_auto_confirm_enabled():
logger.info(f"自动确认发货已关闭,跳过订单 {order_id}")
else:
# 检查确认发货冷却时间
current_time = time.time()
should_confirm = True
if order_id in self.confirmed_orders:
last_confirm_time = self.confirmed_orders[order_id]
if current_time - last_confirm_time < self.order_confirm_cooldown:
logger.info(f"订单 {order_id} 已在 {self.order_confirm_cooldown} 秒内确认过,跳过重复确认")
should_confirm = False
if should_confirm:
logger.info(f"开始自动确认发货: 订单ID={order_id}")
confirm_result = await self.auto_confirm(order_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f"🎉 自动确认发货成功订单ID: {order_id}")
else:
logger.warning(f"⚠️ 自动确认发货失败: {confirm_result.get('error', '未知错误')}")
# 即使确认发货失败,也继续发送发货内容
# 开始处理发货内容
logger.info(f"开始处理发货内容,规则: {rule['keyword']} -> {rule['card_name']} ({rule['card_type']})")
delivery_content = None
@ -1215,7 +1352,7 @@ class XianyuLive:
elif rule['card_type'] == 'text':
# 固定文字类型:直接使用文字内容
delivery_content = rule['card_text_content']
delivery_content = rule['text_content']
elif rule['card_type'] == 'data':
# 批量数据类型:获取并消费第一条数据
@ -1269,10 +1406,12 @@ class XianyuLive:
try:
import aiohttp
import json
api_config = rule.get('card_api_config')
api_config = rule.get('api_config')
if not api_config:
logger.error("API配置为空")
logger.error(f"API配置为空规则ID: {rule.get('id')}, 卡券名称: {rule.get('card_name')}")
logger.debug(f"规则详情: {rule}")
return None
# 解析API配置
@ -1584,30 +1723,43 @@ class XianyuLive:
async def _create_websocket_connection(self, headers):
"""创建WebSocket连接兼容不同版本的websockets库"""
import websockets
# 获取websockets版本用于调试
websockets_version = getattr(websockets, '__version__', '未知')
logger.debug(f"websockets库版本: {websockets_version}")
try:
# 尝试使用extra_headers参数
return websockets.connect(
self.base_url,
extra_headers=headers
)
except TypeError as e:
# 安全地检查异常信息
except Exception as e:
# 捕获所有异常类型不仅仅是TypeError
error_msg = self._safe_str(e)
logger.debug(f"extra_headers参数失败: {error_msg}")
if "extra_headers" in error_msg:
logger.warning("websockets库不支持extra_headers参数使用兼容模式")
if "extra_headers" in error_msg or "unexpected keyword argument" in error_msg:
logger.warning("websockets库不支持extra_headers参数尝试additional_headers")
# 使用additional_headers参数较新版本
try:
return websockets.connect(
self.base_url,
additional_headers=headers
)
except TypeError:
# 如果都不支持则不传递headers
logger.warning("websockets库不支持headers参数使用基础连接模式")
return websockets.connect(self.base_url)
except Exception as e2:
error_msg2 = self._safe_str(e2)
logger.debug(f"additional_headers参数失败: {error_msg2}")
if "additional_headers" in error_msg2 or "unexpected keyword argument" in error_msg2:
# 如果都不支持则不传递headers
logger.warning("websockets库不支持headers参数使用基础连接模式")
return websockets.connect(self.base_url)
else:
raise e2
else:
raise
raise e
async def _handle_websocket_connection(self, websocket, toid, item_id, text):
"""处理WebSocket连接的具体逻辑"""
@ -2024,50 +2176,17 @@ class XianyuLive:
except Exception as parse_e:
logger.debug(f"解析dynamicOperation JSON失败: {parse_e}")
# 如果成功获取到orderId进行自动确认发货
# 订单ID已提取将在自动发货时进行确认发货处理
if order_id:
# 检查是否启用自动确认发货
if not self.is_auto_confirm_enabled():
logger.info(f'[{msg_time}] 【{self.cookie_id}】自动确认发货已关闭,跳过订单 {order_id}')
else:
# 检查是否已经确认过这个订单
current_time = time.time()
if order_id in self.confirmed_orders:
last_confirm_time = self.confirmed_orders[order_id]
if current_time - last_confirm_time < self.order_confirm_cooldown:
logger.info(f'[{msg_time}] 【{self.cookie_id}】⏭️ 订单 {order_id} 已在 {self.order_confirm_cooldown} 秒内确认过,跳过重复确认')
else:
# 超过冷却时间,可以重新确认
try:
logger.info(f'[{msg_time}] 【{self.cookie_id}】开始自动确认发货订单ID: {order_id}')
confirm_result = await self.auto_confirm(order_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f'[{msg_time}] 【{self.cookie_id}】🎉 自动确认发货成功订单ID: {order_id}')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 自动确认发货失败: {confirm_result.get("error", "未知错误")}')
except Exception as confirm_e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】自动确认发货异常: {self._safe_str(confirm_e)}')
else:
# 首次确认这个订单
try:
logger.info(f'[{msg_time}] 【{self.cookie_id}】开始自动确认发货订单ID: {order_id}')
confirm_result = await self.auto_confirm(order_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f'[{msg_time}] 【{self.cookie_id}】🎉 自动确认发货成功订单ID: {order_id}')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 自动确认发货失败: {confirm_result.get("error", "未知错误")}')
except Exception as confirm_e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】自动确认发货异常: {self._safe_str(confirm_e)}')
logger.info(f'[{msg_time}] 【{self.cookie_id}】提取到订单ID: {order_id},将在自动发货时处理确认发货')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】❌ 未能提取到订单ID')
except Exception as extract_e:
logger.error(f"提取订单ID失败: {self._safe_str(extract_e)}")
# 检查是否可以进行自动发货(防重复)
if not self.can_auto_delivery(item_id):
# 检查是否可以进行自动发货(防重复)- 基于订单ID
if not self.can_auto_delivery(order_id):
return
# 构造用户URL
@ -2080,12 +2199,12 @@ class XianyuLive:
logger.info(f"{self.cookie_id}】准备自动发货: item_id={item_id}, item_title={item_title}")
# 调用自动发货方法
delivery_content = await self._auto_delivery(item_id, item_title)
# 调用自动发货方法(包含自动确认发货)
delivery_content = await self._auto_delivery(item_id, item_title, order_id)
if delivery_content:
# 标记已发货(防重复)
self.mark_delivery_sent(item_id)
# 标记已发货(防重复)- 基于订单ID
self.mark_delivery_sent(order_id)
# 发送发货内容给买家
await self.send_msg(websocket, chat_id, send_user_id, delivery_content)
@ -2150,50 +2269,17 @@ class XianyuLive:
except Exception as parse_e:
logger.debug(f"解析dynamicOperation JSON失败: {parse_e}")
# 如果成功获取到orderId进行自动确认发货
# 订单ID已提取将在自动发货时进行确认发货处理
if order_id:
# 检查是否启用自动确认发货
if not self.is_auto_confirm_enabled():
logger.info(f'[{msg_time}] 【{self.cookie_id}】自动确认发货已关闭,跳过订单 {order_id}')
else:
# 检查是否已经确认过这个订单
current_time = time.time()
if order_id in self.confirmed_orders:
last_confirm_time = self.confirmed_orders[order_id]
if current_time - last_confirm_time < self.order_confirm_cooldown:
logger.info(f'[{msg_time}] 【{self.cookie_id}】⏭️ 订单 {order_id} 已在 {self.order_confirm_cooldown} 秒内确认过,跳过重复确认')
else:
# 超过冷却时间,可以重新确认
try:
logger.info(f'[{msg_time}] 【{self.cookie_id}】开始自动确认发货订单ID: {order_id}')
confirm_result = await self.auto_confirm(order_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f'[{msg_time}] 【{self.cookie_id}】🎉 自动确认发货成功订单ID: {order_id}')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 自动确认发货失败: {confirm_result.get("error", "未知错误")}')
except Exception as confirm_e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】自动确认发货异常: {self._safe_str(confirm_e)}')
else:
# 首次确认这个订单
try:
logger.info(f'[{msg_time}] 【{self.cookie_id}】开始自动确认发货订单ID: {order_id}')
confirm_result = await self.auto_confirm(order_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f'[{msg_time}] 【{self.cookie_id}】🎉 自动确认发货成功订单ID: {order_id}')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 自动确认发货失败: {confirm_result.get("error", "未知错误")}')
except Exception as confirm_e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】自动确认发货异常: {self._safe_str(confirm_e)}')
logger.info(f'[{msg_time}] 【{self.cookie_id}】提取到订单ID: {order_id},将在自动发货时处理确认发货')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】❌ 未能提取到订单ID')
except Exception as extract_e:
logger.error(f"提取订单ID失败: {self._safe_str(extract_e)}")
# 检查是否可以进行自动发货(防重复)
if not self.can_auto_delivery(item_id):
# 检查是否可以进行自动发货(防重复)- 基于订单ID
if not self.can_auto_delivery(order_id):
return
# 构造用户URL
@ -2206,12 +2292,12 @@ class XianyuLive:
logger.info(f"{self.cookie_id}】准备自动发货: item_id={item_id}, item_title={item_title}")
# 调用自动发货方法
delivery_content = await self._auto_delivery(item_id, item_title)
# 调用自动发货方法(包含自动确认发货)
delivery_content = await self._auto_delivery(item_id, item_title, order_id)
if delivery_content:
# 标记已发货(防重复)
self.mark_delivery_sent(item_id)
# 标记已发货(防重复)- 基于订单ID
self.mark_delivery_sent(order_id)
# 发送发货内容给买家
await self.send_msg(websocket, chat_id, send_user_id, delivery_content)
@ -2300,50 +2386,17 @@ class XianyuLive:
except Exception as parse_e:
logger.debug(f"解析dynamicOperation JSON失败: {parse_e}")
# 如果成功获取到orderId进行自动确认发货
# 订单ID已提取将在自动发货时进行确认发货处理
if order_id:
# 检查是否启用自动确认发货
if not self.is_auto_confirm_enabled():
logger.info(f'[{msg_time}] 【{self.cookie_id}】自动确认发货已关闭,跳过小刀成功订单 {order_id}')
else:
# 检查是否已经确认过这个订单
current_time = time.time()
if order_id in self.confirmed_orders:
last_confirm_time = self.confirmed_orders[order_id]
if current_time - last_confirm_time < self.order_confirm_cooldown:
logger.info(f'[{msg_time}] 【{self.cookie_id}】⏭️ 订单 {order_id} 已在 {self.order_confirm_cooldown} 秒内确认过,跳过重复确认')
else:
# 超过冷却时间,可以重新确认
try:
logger.info(f'[{msg_time}] 【{self.cookie_id}】小刀成功开始自动确认发货订单ID: {order_id}')
confirm_result = await self.auto_confirm(order_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f'[{msg_time}] 【{self.cookie_id}】🎉 小刀成功自动确认发货成功订单ID: {order_id}')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 小刀成功,自动确认发货失败: {confirm_result.get("error", "未知错误")}')
except Exception as confirm_e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】小刀成功,自动确认发货异常: {self._safe_str(confirm_e)}')
else:
# 首次确认这个订单
try:
logger.info(f'[{msg_time}] 【{self.cookie_id}】小刀成功开始自动确认发货订单ID: {order_id}')
confirm_result = await self.auto_confirm(order_id)
if confirm_result.get('success'):
self.confirmed_orders[order_id] = current_time
logger.info(f'[{msg_time}] 【{self.cookie_id}】🎉 小刀成功自动确认发货成功订单ID: {order_id}')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】⚠️ 小刀成功,自动确认发货失败: {confirm_result.get("error", "未知错误")}')
except Exception as confirm_e:
logger.error(f'[{msg_time}] 【{self.cookie_id}】小刀成功,自动确认发货异常: {self._safe_str(confirm_e)}')
logger.info(f'[{msg_time}] 【{self.cookie_id}】小刀成功提取到订单ID: {order_id},将在自动发货时处理确认发货')
else:
logger.warning(f'[{msg_time}] 【{self.cookie_id}】❌ 小刀成功但未能提取到订单ID')
except Exception as extract_e:
logger.error(f"提取订单ID失败: {self._safe_str(extract_e)}")
# 检查是否可以进行自动发货(防重复)
if not self.can_auto_delivery(item_id):
# 检查是否可以进行自动发货(防重复)- 基于订单ID
if not self.can_auto_delivery(order_id):
return
# 构造用户URL
@ -2356,12 +2409,12 @@ class XianyuLive:
logger.info(f"{self.cookie_id}】准备自动发货: item_id={item_id}, item_title={item_title}")
# 调用自动发货方法
delivery_content = await self._auto_delivery(item_id, item_title)
# 调用自动发货方法(包含自动确认发货)
delivery_content = await self._auto_delivery(item_id, item_title, order_id)
if delivery_content:
# 标记已发货(防重复)
self.mark_delivery_sent(item_id)
# 标记已发货(防重复)- 基于订单ID
self.mark_delivery_sent(order_id)
# 发送发货内容给买家
await self.send_msg(websocket, chat_id, send_user_id, delivery_content)

View File

@ -198,6 +198,9 @@ class DBManager:
description TEXT,
enabled BOOLEAN DEFAULT TRUE,
delay_seconds INTEGER DEFAULT 0,
is_multi_spec BOOLEAN DEFAULT FALSE,
spec_name TEXT,
spec_value TEXT,
user_id INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -244,6 +247,7 @@ class DBManager:
item_category TEXT,
item_price TEXT,
item_detail TEXT,
is_multi_spec BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE,
@ -412,6 +416,24 @@ class DBManager:
# type列存在更新NULL值
self._execute_sql(cursor, "UPDATE email_verifications SET type = 'register' WHERE type IS NULL")
# 为cards表添加多规格字段如果不存在
try:
self._execute_sql(cursor, "SELECT is_multi_spec FROM cards LIMIT 1")
except sqlite3.OperationalError:
# 多规格字段不存在,需要添加
self._execute_sql(cursor, "ALTER TABLE cards ADD COLUMN is_multi_spec BOOLEAN DEFAULT FALSE")
self._execute_sql(cursor, "ALTER TABLE cards ADD COLUMN spec_name TEXT")
self._execute_sql(cursor, "ALTER TABLE cards ADD COLUMN spec_value TEXT")
logger.info("为cards表添加多规格字段")
# 为item_info表添加多规格字段如果不存在
try:
self._execute_sql(cursor, "SELECT is_multi_spec FROM item_info LIMIT 1")
except sqlite3.OperationalError:
# 多规格字段不存在,需要添加
self._execute_sql(cursor, "ALTER TABLE item_info ADD COLUMN is_multi_spec BOOLEAN DEFAULT FALSE")
logger.info("为item_info表添加多规格字段")
self.conn.commit()
logger.info(f"数据库初始化成功: {self.db_path}")
except Exception as e:
@ -1740,10 +1762,37 @@ class DBManager:
def create_card(self, name: str, card_type: str, api_config=None,
text_content: str = None, data_content: str = None,
description: str = None, enabled: bool = True, delay_seconds: int = 0, user_id: int = None):
"""创建新卡券"""
description: str = None, enabled: bool = True, delay_seconds: int = 0,
is_multi_spec: bool = False, spec_name: str = None, spec_value: str = None,
user_id: int = None):
"""创建新卡券(支持多规格)"""
with self.lock:
try:
# 验证多规格参数
if is_multi_spec:
if not spec_name or not spec_value:
raise ValueError("多规格卡券必须提供规格名称和规格值")
# 检查唯一性:卡券名称+规格名称+规格值
cursor = self.conn.cursor()
cursor.execute('''
SELECT COUNT(*) FROM cards
WHERE name = ? AND spec_name = ? AND spec_value = ? AND user_id = ?
''', (name, spec_name, spec_value, user_id))
if cursor.fetchone()[0] > 0:
raise ValueError(f"卡券已存在:{name} - {spec_name}:{spec_value}")
else:
# 检查唯一性:仅卡券名称
cursor = self.conn.cursor()
cursor.execute('''
SELECT COUNT(*) FROM cards
WHERE name = ? AND (is_multi_spec = 0 OR is_multi_spec IS NULL) AND user_id = ?
''', (name, user_id))
if cursor.fetchone()[0] > 0:
raise ValueError(f"卡券名称已存在:{name}")
# 处理api_config参数 - 如果是字典则转换为JSON字符串
api_config_str = None
if api_config is not None:
@ -1753,16 +1802,21 @@ class DBManager:
else:
api_config_str = str(api_config)
cursor = self.conn.cursor()
cursor.execute('''
INSERT INTO cards (name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (name, card_type, api_config_str, text_content, data_content,
description, enabled, delay_seconds, user_id))
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, user_id))
self.conn.commit()
card_id = cursor.lastrowid
logger.info(f"创建卡券成功: {name} (ID: {card_id})")
if is_multi_spec:
logger.info(f"创建多规格卡券成功: {name} - {spec_name}:{spec_value} (ID: {card_id})")
else:
logger.info(f"创建卡券成功: {name} (ID: {card_id})")
return card_id
except Exception as e:
logger.error(f"创建卡券失败: {e}")
@ -1776,7 +1830,8 @@ class DBManager:
if user_id is not None:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards
WHERE user_id = ?
ORDER BY created_at DESC
@ -1784,7 +1839,8 @@ class DBManager:
else:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards
ORDER BY created_at DESC
''')
@ -1811,8 +1867,11 @@ class DBManager:
'description': row[6],
'enabled': bool(row[7]),
'delay_seconds': row[8] or 0,
'created_at': row[9],
'updated_at': row[10]
'is_multi_spec': bool(row[9]) if row[9] is not None else False,
'spec_name': row[10],
'spec_value': row[11],
'created_at': row[12],
'updated_at': row[13]
})
return cards
@ -1828,13 +1887,15 @@ class DBManager:
if user_id is not None:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards WHERE id = ? AND user_id = ?
''', (card_id, user_id))
else:
cursor.execute('''
SELECT id, name, type, api_config, text_content, data_content,
description, enabled, delay_seconds, created_at, updated_at
description, enabled, delay_seconds, is_multi_spec,
spec_name, spec_value, created_at, updated_at
FROM cards WHERE id = ?
''', (card_id,))
@ -1860,8 +1921,11 @@ class DBManager:
'description': row[6],
'enabled': bool(row[7]),
'delay_seconds': row[8] or 0,
'created_at': row[9],
'updated_at': row[10]
'is_multi_spec': bool(row[9]) if row[9] is not None else False,
'spec_name': row[10],
'spec_value': row[11],
'created_at': row[12],
'updated_at': row[13]
}
return None
except Exception as e:
@ -1870,7 +1934,8 @@ 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):
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:
@ -1913,6 +1978,15 @@ class DBManager:
if delay_seconds is not None:
update_fields.append("delay_seconds = ?")
params.append(delay_seconds)
if is_multi_spec is not None:
update_fields.append("is_multi_spec = ?")
params.append(is_multi_spec)
if spec_name is not None:
update_fields.append("spec_name = ?")
params.append(spec_name)
if spec_value is not None:
update_fields.append("spec_value = ?")
params.append(spec_value)
if not update_fields:
return True # 没有需要更新的字段
@ -1964,7 +2038,8 @@ class DBManager:
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times, dr.created_at, dr.updated_at,
c.name as card_name, c.type as card_type
c.name as card_name, c.type as card_type,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
WHERE dr.user_id = ?
@ -1974,7 +2049,8 @@ class DBManager:
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times, dr.created_at, dr.updated_at,
c.name as card_name, c.type as card_type
c.name as card_name, c.type as card_type,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
ORDER BY dr.created_at DESC
@ -1993,7 +2069,10 @@ class DBManager:
'created_at': row[7],
'updated_at': row[8],
'card_name': row[9],
'card_type': row[10]
'card_type': row[10],
'is_multi_spec': bool(row[11]) if row[11] is not None else False,
'spec_name': row[12],
'spec_value': row[13]
})
return rules
@ -2047,9 +2126,9 @@ class DBManager:
'delivery_times': row[6],
'card_name': row[7],
'card_type': row[8],
'card_api_config': api_config,
'card_text_content': row[10],
'card_data_content': row[11],
'api_config': api_config, # 修复字段名
'text_content': row[10],
'data_content': row[11],
'card_enabled': bool(row[12]),
'card_description': row[13], # 卡券备注信息
'card_delay_seconds': row[14] or 0 # 延时秒数
@ -2173,6 +2252,136 @@ class DBManager:
except Exception as e:
logger.error(f"更新发货次数失败: {e}")
def get_delivery_rules_by_keyword_and_spec(self, keyword: str, spec_name: str = None, spec_value: str = None):
"""根据关键字和规格信息获取匹配的发货规则(支持多规格)"""
with self.lock:
try:
cursor = self.conn.cursor()
# 优先匹配:卡券名称+规格名称+规格值
if spec_name and spec_value:
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times,
c.name as card_name, c.type as card_type, c.api_config,
c.text_content, c.data_content, c.enabled as card_enabled,
c.description as card_description, c.delay_seconds as card_delay_seconds,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
WHERE dr.enabled = 1 AND c.enabled = 1
AND (? LIKE '%' || dr.keyword || '%' OR dr.keyword LIKE '%' || ? || '%')
AND c.is_multi_spec = 1 AND c.spec_name = ? AND c.spec_value = ?
ORDER BY
CASE
WHEN ? LIKE '%' || dr.keyword || '%' THEN LENGTH(dr.keyword)
ELSE LENGTH(dr.keyword) / 2
END DESC,
dr.delivery_times ASC
''', (keyword, keyword, spec_name, spec_value, keyword))
rules = []
for row in cursor.fetchall():
# 解析api_config JSON字符串
api_config = row[9]
if api_config:
try:
import json
api_config = json.loads(api_config)
except (json.JSONDecodeError, TypeError):
# 如果解析失败,保持原始字符串
pass
rules.append({
'id': row[0],
'keyword': row[1],
'card_id': row[2],
'delivery_count': row[3],
'enabled': bool(row[4]),
'description': row[5],
'delivery_times': row[6] or 0,
'card_name': row[7],
'card_type': row[8],
'api_config': api_config,
'text_content': row[10],
'data_content': row[11],
'card_enabled': bool(row[12]),
'card_description': row[13],
'card_delay_seconds': row[14] or 0,
'is_multi_spec': bool(row[15]),
'spec_name': row[16],
'spec_value': row[17]
})
if rules:
logger.info(f"找到多规格匹配规则: {keyword} - {spec_name}:{spec_value}")
return rules
# 兜底匹配:仅卡券名称
cursor.execute('''
SELECT dr.id, dr.keyword, dr.card_id, dr.delivery_count, dr.enabled,
dr.description, dr.delivery_times,
c.name as card_name, c.type as card_type, c.api_config,
c.text_content, c.data_content, c.enabled as card_enabled,
c.description as card_description, c.delay_seconds as card_delay_seconds,
c.is_multi_spec, c.spec_name, c.spec_value
FROM delivery_rules dr
LEFT JOIN cards c ON dr.card_id = c.id
WHERE dr.enabled = 1 AND c.enabled = 1
AND (? LIKE '%' || dr.keyword || '%' OR dr.keyword LIKE '%' || ? || '%')
AND (c.is_multi_spec = 0 OR c.is_multi_spec IS NULL)
ORDER BY
CASE
WHEN ? LIKE '%' || dr.keyword || '%' THEN LENGTH(dr.keyword)
ELSE LENGTH(dr.keyword) / 2
END DESC,
dr.delivery_times ASC
''', (keyword, keyword, keyword))
rules = []
for row in cursor.fetchall():
# 解析api_config JSON字符串
api_config = row[9]
if api_config:
try:
import json
api_config = json.loads(api_config)
except (json.JSONDecodeError, TypeError):
# 如果解析失败,保持原始字符串
pass
rules.append({
'id': row[0],
'keyword': row[1],
'card_id': row[2],
'delivery_count': row[3],
'enabled': bool(row[4]),
'description': row[5],
'delivery_times': row[6] or 0,
'card_name': row[7],
'card_type': row[8],
'api_config': api_config,
'text_content': row[10],
'data_content': row[11],
'card_enabled': bool(row[12]),
'card_description': row[13],
'card_delay_seconds': row[14] or 0,
'is_multi_spec': bool(row[15]) if row[15] is not None else False,
'spec_name': row[16],
'spec_value': row[17]
})
if rules:
logger.info(f"找到兜底匹配规则: {keyword}")
else:
logger.info(f"未找到匹配规则: {keyword}")
return rules
except Exception as e:
logger.error(f"获取发货规则失败: {e}")
return []
def delete_card(self, card_id: int):
"""删除卡券"""
with self.lock:
@ -2463,6 +2672,49 @@ class DBManager:
logger.error(f"获取商品信息失败: {e}")
return None
def update_item_multi_spec_status(self, cookie_id: str, item_id: str, is_multi_spec: bool) -> bool:
"""更新商品的多规格状态"""
try:
with self.lock:
cursor = self.conn.cursor()
cursor.execute('''
UPDATE item_info
SET is_multi_spec = ?, updated_at = CURRENT_TIMESTAMP
WHERE cookie_id = ? AND item_id = ?
''', (is_multi_spec, cookie_id, item_id))
if cursor.rowcount > 0:
self.conn.commit()
logger.info(f"更新商品多规格状态成功: {item_id} -> {is_multi_spec}")
return True
else:
logger.warning(f"商品不存在,无法更新多规格状态: {item_id}")
return False
except Exception as e:
logger.error(f"更新商品多规格状态失败: {e}")
self.conn.rollback()
return False
def get_item_multi_spec_status(self, cookie_id: str, item_id: str) -> bool:
"""获取商品的多规格状态"""
try:
with self.lock:
cursor = self.conn.cursor()
cursor.execute('''
SELECT is_multi_spec FROM item_info
WHERE cookie_id = ? AND item_id = ?
''', (cookie_id, item_id))
row = cursor.fetchone()
if row:
return bool(row[0]) if row[0] is not None else False
return False
except Exception as e:
logger.error(f"获取商品多规格状态失败: {e}")
return False
def get_items_by_cookie(self, cookie_id: str) -> List[Dict]:
"""获取指定Cookie的所有商品信息

93
docker-rebuild.sh Normal file
View File

@ -0,0 +1,93 @@
#!/bin/bash
# ================================
# 闲鱼自动回复系统 - Docker重新构建脚本
# ================================
set -e
echo "🐳 闲鱼自动回复系统 - Docker重新构建"
echo "=================================="
# 检查Docker是否运行
if ! docker info > /dev/null 2>&1; then
echo "❌ Docker未运行请先启动Docker"
exit 1
fi
echo "📋 步骤1: 停止并删除现有容器"
echo "--------------------------------"
# 停止现有容器
if docker ps -q --filter "name=xianyu-auto-reply" | grep -q .; then
echo "🛑 停止现有容器..."
docker stop xianyu-auto-reply
fi
# 删除现有容器
if docker ps -aq --filter "name=xianyu-auto-reply" | grep -q .; then
echo "🗑️ 删除现有容器..."
docker rm xianyu-auto-reply
fi
echo "📋 步骤2: 删除现有镜像"
echo "--------------------------------"
# 删除现有镜像
if docker images -q xianyu-auto-reply | grep -q .; then
echo "🗑️ 删除现有镜像..."
docker rmi xianyu-auto-reply
fi
echo "📋 步骤3: 重新构建镜像"
echo "--------------------------------"
echo "🔨 开始构建新镜像..."
docker build -t xianyu-auto-reply .
echo "📋 步骤4: 启动新容器"
echo "--------------------------------"
echo "🚀 启动新容器..."
docker run -d \
--name xianyu-auto-reply \
--restart unless-stopped \
-p 8080:8080 \
-v "$(pwd)/data:/app/data" \
-v "$(pwd)/logs:/app/logs" \
-v "$(pwd)/backups:/app/backups" \
-e DOCKER_ENV=true \
xianyu-auto-reply
echo "📋 步骤5: 检查容器状态"
echo "--------------------------------"
# 等待容器启动
echo "⏳ 等待容器启动..."
sleep 5
# 检查容器状态
if docker ps --filter "name=xianyu-auto-reply" --filter "status=running" | grep -q xianyu-auto-reply; then
echo "✅ 容器启动成功"
echo "📋 容器信息:"
docker ps --filter "name=xianyu-auto-reply" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo "📋 最近日志:"
docker logs --tail 20 xianyu-auto-reply
echo ""
echo "🎉 Docker重新构建完成"
echo "=================================="
echo "📱 Web界面: http://localhost:8080"
echo "📊 健康检查: http://localhost:8080/health"
echo "📋 查看日志: docker logs -f xianyu-auto-reply"
echo "🛑 停止容器: docker stop xianyu-auto-reply"
echo "🗑️ 删除容器: docker rm xianyu-auto-reply"
else
echo "❌ 容器启动失败"
echo "📋 错误日志:"
docker logs xianyu-auto-reply
exit 1
fi

View File

@ -1737,6 +1737,12 @@ def create_card(card_data: dict, current_user: Dict[str, Any] = Depends(get_curr
log_with_user('info', f"创建卡券: {card_name}", current_user)
# 验证多规格字段
is_multi_spec = card_data.get('is_multi_spec', False)
if is_multi_spec:
if not card_data.get('spec_name') or not card_data.get('spec_value'):
raise HTTPException(status_code=400, detail="多规格卡券必须提供规格名称和规格值")
card_id = db_manager.create_card(
name=card_data.get('name'),
card_type=card_data.get('type'),
@ -1746,6 +1752,9 @@ def create_card(card_data: dict, current_user: Dict[str, Any] = Depends(get_curr
description=card_data.get('description'),
enabled=card_data.get('enabled', True),
delay_seconds=card_data.get('delay_seconds', 0),
is_multi_spec=is_multi_spec,
spec_name=card_data.get('spec_name') if is_multi_spec else None,
spec_value=card_data.get('spec_value') if is_multi_spec else None,
user_id=user_id
)
@ -1776,6 +1785,12 @@ def update_card(card_id: int, card_data: dict, _: None = Depends(require_auth)):
"""更新卡券"""
try:
from db_manager import db_manager
# 验证多规格字段
is_multi_spec = card_data.get('is_multi_spec')
if is_multi_spec:
if not card_data.get('spec_name') or not card_data.get('spec_value'):
raise HTTPException(status_code=400, detail="多规格卡券必须提供规格名称和规格值")
success = db_manager.update_card(
card_id=card_id,
name=card_data.get('name'),
@ -1785,7 +1800,10 @@ def update_card(card_id: int, card_data: dict, _: None = Depends(require_auth)):
data_content=card_data.get('data_content'),
description=card_data.get('description'),
enabled=card_data.get('enabled', True),
delay_seconds=card_data.get('delay_seconds')
delay_seconds=card_data.get('delay_seconds'),
is_multi_spec=is_multi_spec,
spec_name=card_data.get('spec_name'),
spec_value=card_data.get('spec_value')
)
if success:
return {"message": "卡券更新成功"}
@ -3039,5 +3057,25 @@ def clear_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(requi
raise HTTPException(status_code=500, detail=str(e))
# 商品多规格管理API
@app.put("/items/{cookie_id}/{item_id}/multi-spec")
def update_item_multi_spec(cookie_id: str, item_id: str, spec_data: dict, _: None = Depends(require_auth)):
"""更新商品的多规格状态"""
try:
from db_manager import db_manager
is_multi_spec = spec_data.get('is_multi_spec', False)
success = db_manager.update_item_multi_spec_status(cookie_id, item_id, is_multi_spec)
if success:
return {"message": f"商品多规格状态已{'开启' if is_multi_spec else '关闭'}"}
else:
raise HTTPException(status_code=404, detail="商品不存在")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)

View File

@ -2,7 +2,7 @@
# 闲鱼自动回复系统 - Python依赖包
# ================================
# Web框架和API相关
# 核心Web框架
fastapi>=0.111.0
uvicorn[standard]>=0.29.0
pydantic>=2.7.0
@ -13,9 +13,11 @@ loguru>=0.7.0
# 网络通信
websockets>=10.0,<13.0
aiohttp>=3.9.0
requests>=2.31.0
# 配置文件处理
PyYAML>=6.0.0
python-dotenv>=1.0.1
# JavaScript执行引擎
PyExecJS>=1.5.1
@ -26,26 +28,21 @@ blackboxprotobuf>=1.0.1
# 系统监控
psutil>=5.9.0
# HTTP客户端
requests>=2.31.0
# 文件上传支持
python-multipart>=0.0.6
# AI回复相关
# AI回复引擎
openai>=1.65.5
python-dotenv>=1.0.1
# 图像处理(图形验证码)
# 图像处理(验证码生成
Pillow>=10.0.0
# 浏览器自动化(商品搜索功能
# 浏览器自动化(商品搜索、订单详情获取
playwright>=1.40.0
# 加密和安全
PyJWT>=2.8.0
passlib>=1.7.4
bcrypt>=4.0.1
passlib[bcrypt]>=1.7.4
cryptography>=41.0.0
# 时间处理
@ -54,7 +51,7 @@ python-dateutil>=2.8.2
# 正则表达式增强
regex>=2023.10.3
# Excel文件处理导入导出功能
# Excel文件处理数据导入导出)
pandas>=2.0.0
openpyxl>=3.1.0

View File

@ -1419,12 +1419,13 @@
<th style="width: 5%">
<input type="checkbox" id="selectAllItems" onchange="toggleSelectAll(this)">
</th>
<th style="width: 15%">账号ID</th>
<th style="width: 15%">商品ID</th>
<th style="width: 20%">商品标题</th>
<th style="width: 25%">商品详情</th>
<th style="width: 12%">更新时间</th>
<th style="width: 8%">操作</th>
<th style="width: 12%">账号ID</th>
<th style="width: 12%">商品ID</th>
<th style="width: 18%">商品标题</th>
<th style="width: 20%">商品详情</th>
<th style="width: 8%">多规格</th>
<th style="width: 10%">更新时间</th>
<th style="width: 15%">操作</th>
</tr>
</thead>
<tbody id="itemsTableBody">
@ -1567,6 +1568,7 @@
<tr>
<th>卡券名称</th>
<th>类型</th>
<th>规格信息</th>
<th>数据量</th>
<th>延时时间</th>
<th>状态</th>
@ -2157,7 +2159,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addCardForm">
<form id="addCardForm" onsubmit="event.preventDefault(); saveCard(); return false;">
<div class="mb-3">
<label class="form-label">卡券名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="cardName" placeholder="例如:游戏点卡、会员卡等" required>
@ -2253,6 +2255,46 @@
备注内容会与发货内容一起发送。使用 <code>{DELIVERY_CONTENT}</code> 变量可以在备注中插入实际的发货内容。
</small>
</div>
<!-- 多规格设置 -->
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="isMultiSpec" onchange="toggleMultiSpecFields()">
<label class="form-check-label" for="isMultiSpec">
<strong>多规格卡券</strong>
</label>
</div>
<div class="form-text">开启后可以为同一商品的不同规格创建不同的卡券</div>
</div>
<!-- 多规格字段 -->
<div id="multiSpecFields" style="display: none;">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="specName" placeholder="例如:套餐类型、颜色、尺寸">
<div class="form-text">规格的名称,如套餐类型、颜色等</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格值 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="specValue" placeholder="例如30天、红色、XL">
<div class="form-text">具体的规格值如30天、红色等</div>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>多规格说明:</strong>
<ul class="mb-0 mt-2">
<li>同一卡券名称可以创建多个不同规格的卡券</li>
<li>卡券名称+规格名称+规格值必须唯一</li>
<li>自动发货时会优先匹配精确规格,找不到时使用普通卡券兜底</li>
</ul>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@ -2380,6 +2422,46 @@
备注内容会与发货内容一起发送。使用 <code>{DELIVERY_CONTENT}</code> 变量可以在备注中插入实际的发货内容。
</small>
</div>
<!-- 多规格设置 -->
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="editIsMultiSpec" onchange="toggleEditMultiSpecFields()">
<label class="form-check-label" for="editIsMultiSpec">
<strong>多规格卡券</strong>
</label>
</div>
<div class="form-text">开启后可以为同一商品的不同规格创建不同的卡券</div>
</div>
<!-- 多规格字段 -->
<div id="editMultiSpecFields" style="display: none;">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSpecName" placeholder="例如:套餐类型、颜色、尺寸">
<div class="form-text">规格的名称,如套餐类型、颜色等</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规格值 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editSpecValue" placeholder="例如30天、红色、XL">
<div class="form-text">具体的规格值如30天、红色等</div>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>多规格说明:</strong>
<ul class="mb-0 mt-2">
<li>同一卡券名称可以创建多个不同规格的卡券</li>
<li>卡券名称+规格名称+规格值必须唯一</li>
<li>自动发货时会优先匹配精确规格,找不到时使用普通卡券兜底</li>
</ul>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@ -5021,7 +5103,7 @@
if (cards.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center py-4 text-muted">
<td colspan="8" class="text-center py-4 text-muted">
<i class="bi bi-credit-card fs-1 d-block mb-3"></i>
<h5>暂无卡券数据</h5>
<p class="mb-0">点击"添加卡券"开始创建您的第一个卡券</p>
@ -5071,12 +5153,19 @@
`${card.delay_seconds}秒` :
'<span class="text-muted">立即</span>';
// 规格信息显示
let specDisplay = '<span class="text-muted">普通卡券</span>';
if (card.is_multi_spec && card.spec_name && card.spec_value) {
specDisplay = `<span class="badge bg-primary">${card.spec_name}: ${card.spec_value}</span>`;
}
tr.innerHTML = `
<td>
<div class="fw-bold">${card.name}</div>
${card.description ? `<small class="text-muted">${card.description}</small>` : ''}
</td>
<td>${typeBadge}</td>
<td>${specDisplay}</td>
<td>${dataCount}</td>
<td>${delayDisplay}</td>
<td>${statusBadge}</td>
@ -5132,6 +5221,93 @@
document.getElementById('dataFields').style.display = cardType === 'data' ? 'block' : 'none';
}
// 切换多规格字段显示
function toggleMultiSpecFields() {
const isMultiSpec = document.getElementById('isMultiSpec').checked;
document.getElementById('multiSpecFields').style.display = isMultiSpec ? 'block' : 'none';
}
// 切换编辑多规格字段显示
function toggleEditMultiSpecFields() {
const checkbox = document.getElementById('editIsMultiSpec');
const fieldsDiv = document.getElementById('editMultiSpecFields');
if (!checkbox) {
console.error('编辑多规格开关元素未找到');
return;
}
if (!fieldsDiv) {
console.error('编辑多规格字段容器未找到');
return;
}
const isMultiSpec = checkbox.checked;
const displayStyle = isMultiSpec ? 'block' : 'none';
console.log('toggleEditMultiSpecFields - 多规格状态:', isMultiSpec);
console.log('toggleEditMultiSpecFields - 设置显示样式:', displayStyle);
fieldsDiv.style.display = displayStyle;
// 验证设置是否生效
console.log('toggleEditMultiSpecFields - 实际显示样式:', fieldsDiv.style.display);
}
// 清空添加卡券表单
function clearAddCardForm() {
try {
// 安全地清空表单字段
const setElementValue = (id, value) => {
const element = document.getElementById(id);
if (element) {
if (element.type === 'checkbox') {
element.checked = value;
} else {
element.value = value;
}
} else {
console.warn(`Element with id '${id}' not found`);
}
};
const setElementDisplay = (id, display) => {
const element = document.getElementById(id);
if (element) {
element.style.display = display;
} else {
console.warn(`Element with id '${id}' not found`);
}
};
// 清空基本字段
setElementValue('cardName', '');
setElementValue('cardType', 'text');
setElementValue('cardDescription', '');
setElementValue('cardDelaySeconds', '0');
setElementValue('isMultiSpec', false);
setElementValue('specName', '');
setElementValue('specValue', '');
// 隐藏多规格字段
setElementDisplay('multiSpecFields', 'none');
// 清空类型相关字段
setElementValue('textContent', '');
setElementValue('dataContent', '');
setElementValue('apiUrl', '');
setElementValue('apiMethod', 'GET');
setElementValue('apiHeaders', '');
setElementValue('apiParams', '');
setElementValue('apiTimeout', '10');
// 重置字段显示
toggleCardTypeFields();
} catch (error) {
console.error('清空表单时出错:', error);
}
}
// 保存卡券
async function saveCard() {
try {
@ -5143,12 +5319,26 @@
return;
}
// 检查多规格设置
const isMultiSpec = document.getElementById('isMultiSpec').checked;
const specName = document.getElementById('specName').value;
const specValue = document.getElementById('specValue').value;
// 验证多规格字段
if (isMultiSpec && (!specName || !specValue)) {
showToast('多规格卡券必须填写规格名称和规格值', 'warning');
return;
}
const cardData = {
name: cardName,
type: cardType,
description: document.getElementById('cardDescription').value,
delay_seconds: parseInt(document.getElementById('cardDelaySeconds').value) || 0,
enabled: true
enabled: true,
is_multi_spec: isMultiSpec,
spec_name: isMultiSpec ? specName : null,
spec_value: isMultiSpec ? specValue : null
};
// 根据类型添加特定配置
@ -5208,14 +5398,28 @@
if (response.ok) {
showToast('卡券保存成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('addCardModal')).hide();
// 清空表单
clearAddCardForm();
loadCards();
} else {
const error = await response.text();
showToast(`保存失败: ${error}`, 'danger');
let errorMessage = '保存失败';
try {
const errorData = await response.json();
errorMessage = errorData.error || errorData.detail || errorMessage;
} catch (e) {
// 如果不是JSON格式尝试获取文本
try {
const errorText = await response.text();
errorMessage = errorText || errorMessage;
} catch (e2) {
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
}
}
showToast(`保存失败: ${errorMessage}`, 'danger');
}
} catch (error) {
console.error('保存卡券失败:', error);
showToast('保存卡券失败', 'danger');
showToast(`网络错误: ${error.message}`, 'danger');
}
}
// ==================== 自动发货功能 ====================
@ -5294,7 +5498,12 @@
${rule.description ? `<small class="text-muted">${rule.description}</small>` : ''}
</td>
<td>
<span class="badge bg-primary">${rule.card_name || '未知卡券'}</span>
<div>
<span class="badge bg-primary">${rule.card_name || '未知卡券'}</span>
${rule.is_multi_spec && rule.spec_name && rule.spec_value ?
`<br><small class="text-muted mt-1 d-block"><i class="bi bi-tags"></i> ${rule.spec_name}: ${rule.spec_value}</small>` :
''}
</div>
</td>
<td>${cardTypeBadge}</td>
<td>
@ -5364,7 +5573,20 @@
if (card.enabled) { // 只显示启用的卡券
const option = document.createElement('option');
option.value = card.id;
option.textContent = `${card.name} (${card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据'})`;
// 构建显示文本
let displayText = card.name;
// 添加类型信息
const typeText = card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据';
displayText += ` (${typeText})`;
// 添加规格信息
if (card.is_multi_spec && card.spec_name && card.spec_value) {
displayText += ` [${card.spec_name}:${card.spec_value}]`;
}
option.textContent = displayText;
select.appendChild(option);
}
});
@ -5440,6 +5662,17 @@
document.getElementById('editCardDelaySeconds').value = card.delay_seconds || 0;
document.getElementById('editCardEnabled').checked = card.enabled;
// 填充多规格字段
const isMultiSpec = card.is_multi_spec || false;
document.getElementById('editIsMultiSpec').checked = isMultiSpec;
document.getElementById('editSpecName').value = card.spec_name || '';
document.getElementById('editSpecValue').value = card.spec_value || '';
// 添加调试日志
console.log('编辑卡券 - 多规格状态:', isMultiSpec);
console.log('编辑卡券 - 规格名称:', card.spec_name);
console.log('编辑卡券 - 规格值:', card.spec_value);
// 根据类型填充特定字段
if (card.type === 'api' && card.api_config) {
document.getElementById('editApiUrl').value = card.api_config.url || '';
@ -5456,6 +5689,19 @@
// 显示对应的字段
toggleEditCardTypeFields();
// 使用延迟调用确保DOM更新完成后再显示多规格字段
setTimeout(() => {
console.log('延迟调用 toggleEditMultiSpecFields');
toggleEditMultiSpecFields();
// 验证多规格字段是否正确显示
const multiSpecElement = document.getElementById('editMultiSpecFields');
const isChecked = document.getElementById('editIsMultiSpec').checked;
console.log('多规格元素存在:', !!multiSpecElement);
console.log('多规格开关状态:', isChecked);
console.log('多规格字段显示状态:', multiSpecElement ? multiSpecElement.style.display : 'element not found');
}, 100);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editCardModal'));
modal.show();
@ -5489,12 +5735,26 @@
return;
}
// 检查多规格设置
const isMultiSpec = document.getElementById('editIsMultiSpec').checked;
const specName = document.getElementById('editSpecName').value;
const specValue = document.getElementById('editSpecValue').value;
// 验证多规格字段
if (isMultiSpec && (!specName || !specValue)) {
showToast('多规格卡券必须填写规格名称和规格值', 'warning');
return;
}
const cardData = {
name: cardName,
type: cardType,
description: document.getElementById('editCardDescription').value,
delay_seconds: parseInt(document.getElementById('editCardDelaySeconds').value) || 0,
enabled: document.getElementById('editCardEnabled').checked
enabled: document.getElementById('editCardEnabled').checked,
is_multi_spec: isMultiSpec,
spec_name: isMultiSpec ? specName : null,
spec_value: isMultiSpec ? specValue : null
};
// 根据类型添加特定配置
@ -5651,7 +5911,20 @@
if (card.enabled) { // 只显示启用的卡券
const option = document.createElement('option');
option.value = card.id;
option.textContent = `${card.name} (${card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据'})`;
// 构建显示文本
let displayText = card.name;
// 添加类型信息
const typeText = card.type === 'api' ? 'API' : card.type === 'text' ? '固定文字' : '批量数据';
displayText += ` (${typeText})`;
// 添加规格信息
if (card.is_multi_spec && card.spec_name && card.spec_value) {
displayText += ` [${card.spec_name}:${card.spec_value}]`;
}
option.textContent = displayText;
select.appendChild(option);
}
});
@ -6316,6 +6589,34 @@
// ==================== 商品管理功能 ====================
// 切换商品多规格状态
async function toggleItemMultiSpec(cookieId, itemId, isMultiSpec) {
try {
const response = await fetch(`${apiBase}/items/${encodeURIComponent(cookieId)}/${encodeURIComponent(itemId)}/multi-spec`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
is_multi_spec: isMultiSpec
})
});
if (response.ok) {
showToast(`${isMultiSpec ? '开启' : '关闭'}多规格成功`, 'success');
// 刷新商品列表
await refreshItemsData();
} else {
const errorData = await response.json();
throw new Error(errorData.error || '操作失败');
}
} catch (error) {
console.error('切换多规格状态失败:', error);
showToast(`切换多规格状态失败: ${error.message}`, 'danger');
}
}
// 加载商品列表
async function loadItems() {
try {
@ -6475,7 +6776,7 @@
const tbody = document.getElementById('itemsTableBody');
if (!items || items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">暂无商品数据</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无商品数据</td></tr>';
// 重置选择状态
const selectAllCheckbox = document.getElementById('selectAllItems');
if (selectAllCheckbox) {
@ -6511,6 +6812,12 @@
}
}
// 多规格状态显示
const isMultiSpec = item.is_multi_spec;
const multiSpecDisplay = isMultiSpec ?
'<span class="badge bg-success">多规格</span>' :
'<span class="badge bg-secondary">普通</span>';
return `
<tr>
<td>
@ -6523,6 +6830,7 @@
<td>${escapeHtml(item.item_id)}</td>
<td title="${escapeHtml(item.item_title || '未设置')}">${escapeHtml(itemTitleDisplay)}</td>
<td title="${escapeHtml(item.item_detail || '未设置')}">${escapeHtml(itemDetailDisplay)}</td>
<td>${multiSpecDisplay}</td>
<td>${formatDateTime(item.updated_at)}</td>
<td>
<div class="btn-group" role="group">
@ -6532,6 +6840,9 @@
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', '${escapeHtml(item.item_title || item.item_id)}')" title="删除">
<i class="bi bi-trash"></i>
</button>
<button class="btn btn-sm ${isMultiSpec ? 'btn-warning' : 'btn-success'}" onclick="toggleItemMultiSpec('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', ${!isMultiSpec})" title="${isMultiSpec ? '关闭多规格' : '开启多规格'}">
<i class="bi ${isMultiSpec ? 'bi-toggle-on' : 'bi-toggle-off'}"></i>
</button>
</div>
</td>
</tr>

View File

@ -0,0 +1,316 @@
"""
闲鱼订单详情获取工具
基于Playwright实现订单详情页面访问和数据提取
"""
import asyncio
import time
from typing import Optional, Dict, Any
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
from loguru import logger
class OrderDetailFetcher:
"""闲鱼订单详情获取器"""
def __init__(self, cookie_string: str = None):
self.browser: Optional[Browser] = None
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
# 请求头配置
self.headers = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "en,zh-CN;q=0.9,zh;q=0.8,ru;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"priority": "u=0, i",
"sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1"
}
# Cookie配置 - 支持动态传入
self.cookie = cookie_string
async def init_browser(self, headless: bool = True):
"""初始化浏览器"""
try:
playwright = await async_playwright().start()
# 启动浏览器
self.browser = await playwright.chromium.launch(
headless=headless,
args=[
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu'
]
)
# 创建浏览器上下文
self.context = await self.browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
)
# 设置额外的HTTP头
await self.context.set_extra_http_headers(self.headers)
# 创建页面
self.page = await self.context.new_page()
# 设置Cookie
await self._set_cookies()
logger.info("浏览器初始化成功")
return True
except Exception as e:
logger.error(f"浏览器初始化失败: {e}")
return False
async def _set_cookies(self):
"""设置Cookie"""
try:
# 解析Cookie字符串
cookies = []
for cookie_pair in self.cookie.split('; '):
if '=' in cookie_pair:
name, value = cookie_pair.split('=', 1)
cookies.append({
'name': name.strip(),
'value': value.strip(),
'domain': '.goofish.com',
'path': '/'
})
# 添加Cookie到上下文
await self.context.add_cookies(cookies)
logger.info(f"已设置 {len(cookies)} 个Cookie")
except Exception as e:
logger.error(f"设置Cookie失败: {e}")
async def fetch_order_detail(self, order_id: str, timeout: int = 30) -> Optional[Dict[str, Any]]:
"""
获取订单详情
Args:
order_id: 订单ID
timeout: 超时时间
Returns:
包含订单详情的字典失败时返回None
"""
try:
if not self.page:
logger.error("浏览器未初始化")
return None
# 构建订单详情URL
url = f"https://www.goofish.com/order-detail?orderId={order_id}&role=seller"
logger.info(f"开始访问订单详情页面: {url}")
# 访问页面
response = await self.page.goto(url, wait_until='networkidle', timeout=timeout * 1000)
if not response or response.status != 200:
logger.error(f"页面访问失败,状态码: {response.status if response else 'None'}")
return None
logger.info("页面加载成功,等待内容渲染...")
# 等待页面完全加载
await self.page.wait_for_load_state('networkidle')
# 额外等待确保动态内容加载完成
await asyncio.sleep(3)
# 获取并解析SKU信息
sku_info = await self._get_sku_content()
# 获取页面标题
title = await self.page.title()
result = {
'order_id': order_id,
'url': url,
'title': title,
'sku_info': sku_info, # 包含解析后的规格信息
'spec_name': sku_info.get('spec_name', '') if sku_info else '',
'spec_value': sku_info.get('spec_value', '') if sku_info else '',
'timestamp': time.time()
}
logger.info(f"订单详情获取成功: {order_id}")
if sku_info:
logger.info(f"规格信息 - 名称: {result['spec_name']}, 值: {result['spec_value']}")
return result
except Exception as e:
logger.error(f"获取订单详情失败: {e}")
return None
def _parse_sku_content(self, sku_content: str) -> Dict[str, str]:
"""
解析SKU内容根据冒号分割规格名称和规格值
Args:
sku_content: 原始SKU内容字符串
Returns:
包含规格名称和规格值的字典如果解析失败则返回空字典
"""
try:
if not sku_content or ':' not in sku_content:
logger.warning(f"SKU内容格式无效或不包含冒号: {sku_content}")
return {}
# 根据冒号分割
parts = sku_content.split(':', 1) # 只分割第一个冒号
if len(parts) == 2:
spec_name = parts[0].strip()
spec_value = parts[1].strip()
if spec_name and spec_value:
result = {
'spec_name': spec_name,
'spec_value': spec_value
}
logger.info(f"SKU解析成功 - 规格名称: {spec_name}, 规格值: {spec_value}")
return result
else:
logger.warning(f"SKU解析失败规格名称或值为空: 名称='{spec_name}', 值='{spec_value}'")
return {}
else:
logger.warning(f"SKU内容分割失败: {sku_content}")
return {}
except Exception as e:
logger.error(f"解析SKU内容异常: {e}")
return {}
async def _get_sku_content(self) -> Optional[Dict[str, str]]:
"""获取并解析SKU内容"""
try:
# 等待SKU元素出现
sku_selector = '.sku--u_ddZval'
# 检查元素是否存在
sku_element = await self.page.query_selector(sku_selector)
if sku_element:
# 获取元素文本内容
sku_content = await sku_element.text_content()
if sku_content:
sku_content = sku_content.strip()
logger.info(f"找到SKU原始内容: {sku_content}")
print(f"🛍️ SKU原始内容: {sku_content}")
# 解析SKU内容
parsed_sku = self._parse_sku_content(sku_content)
if parsed_sku:
print(f"📋 规格名称: {parsed_sku['spec_name']}")
print(f"📝 规格值: {parsed_sku['spec_value']}")
return parsed_sku
else:
logger.warning("SKU内容解析失败")
return {}
else:
logger.warning("SKU元素内容为空")
return {}
else:
logger.warning("未找到SKU元素")
# 尝试获取页面的所有class包含sku的元素
all_sku_elements = await self.page.query_selector_all('[class*="sku"]')
if all_sku_elements:
logger.info(f"找到 {len(all_sku_elements)} 个包含'sku'的元素")
for i, element in enumerate(all_sku_elements):
class_name = await element.get_attribute('class')
text_content = await element.text_content()
logger.info(f"SKU元素 {i+1}: class='{class_name}', text='{text_content}'")
return {}
except Exception as e:
logger.error(f"获取SKU内容失败: {e}")
return {}
async def close(self):
"""关闭浏览器"""
try:
if self.page:
await self.page.close()
if self.context:
await self.context.close()
if self.browser:
await self.browser.close()
logger.info("浏览器已关闭")
except Exception as e:
logger.error(f"关闭浏览器失败: {e}")
async def __aenter__(self):
"""异步上下文管理器入口"""
await self.init_browser()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
await self.close()
# 便捷函数
async def fetch_order_detail_simple(order_id: str, cookie_string: str = None, headless: bool = True) -> Optional[Dict[str, Any]]:
"""
简单的订单详情获取函数
Args:
order_id: 订单ID
cookie_string: Cookie字符串如果不提供则使用默认值
headless: 是否无头模式
Returns:
订单详情字典或None
"""
fetcher = OrderDetailFetcher(cookie_string)
try:
if await fetcher.init_browser(headless=headless):
return await fetcher.fetch_order_detail(order_id)
finally:
await fetcher.close()
return None
# 测试代码
if __name__ == "__main__":
async def test():
# 测试订单ID
test_order_id = "2856024697612814489"
print(f"🔍 开始获取订单详情: {test_order_id}")
result = await fetch_order_detail_simple(test_order_id, headless=False)
if result:
print("✅ 订单详情获取成功:")
print(f"📋 订单ID: {result['order_id']}")
print(f"🌐 URL: {result['url']}")
print(f"📄 页面标题: {result['title']}")
print(f"🛍️ SKU内容: {result['sku_content']}")
else:
print("❌ 订单详情获取失败")
# 运行测试
asyncio.run(test())