diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py
index 1950121..796b10c 100644
--- a/XianyuAutoAsync.py
+++ b/XianyuAutoAsync.py
@@ -118,6 +118,10 @@ class XianyuLive:
_order_detail_locks = defaultdict(lambda: asyncio.Lock())
# 记录订单详情锁的使用时间
_order_detail_lock_times = {}
+
+ # 商品详情缓存(24小时有效)
+ _item_detail_cache = {} # {item_id: {'detail': str, 'timestamp': float}}
+ _item_detail_cache_lock = asyncio.Lock()
def _safe_str(self, e):
"""安全地将异常转换为字符串"""
@@ -864,7 +868,7 @@ class XianyuLive:
return False
async def fetch_item_detail_from_api(self, item_id: str) -> str:
- """从外部API获取商品详情
+ """获取商品详情(优先使用浏览器,备用外部API,支持24小时缓存)
Args:
item_id: 商品ID
@@ -881,6 +885,181 @@ class XianyuLive:
logger.debug(f"自动获取商品详情功能已禁用: {item_id}")
return ""
+ # 1. 首先检查缓存(24小时有效)
+ async with self._item_detail_cache_lock:
+ if item_id in self._item_detail_cache:
+ cache_data = self._item_detail_cache[item_id]
+ cache_time = cache_data['timestamp']
+ current_time = time.time()
+
+ # 检查缓存是否在24小时内
+ if current_time - cache_time < 24 * 60 * 60: # 24小时
+ logger.info(f"从缓存获取商品详情: {item_id}")
+ return cache_data['detail']
+ else:
+ # 缓存过期,删除
+ del self._item_detail_cache[item_id]
+ logger.debug(f"缓存已过期,删除: {item_id}")
+
+ # 2. 尝试使用浏览器获取商品详情
+ detail_from_browser = await self._fetch_item_detail_from_browser(item_id)
+ if detail_from_browser:
+ # 保存到缓存
+ async with self._item_detail_cache_lock:
+ self._item_detail_cache[item_id] = {
+ 'detail': detail_from_browser,
+ 'timestamp': time.time()
+ }
+ logger.info(f"成功通过浏览器获取商品详情: {item_id}, 长度: {len(detail_from_browser)}")
+ return detail_from_browser
+
+ # 3. 浏览器获取失败,使用外部API作为备用
+ logger.warning(f"浏览器获取商品详情失败,尝试外部API: {item_id}")
+ detail_from_api = await self._fetch_item_detail_from_external_api(item_id)
+ if detail_from_api:
+ # 保存到缓存
+ async with self._item_detail_cache_lock:
+ self._item_detail_cache[item_id] = {
+ 'detail': detail_from_api,
+ 'timestamp': time.time()
+ }
+ logger.info(f"成功通过外部API获取商品详情: {item_id}, 长度: {len(detail_from_api)}")
+ return detail_from_api
+
+ logger.warning(f"所有方式都无法获取商品详情: {item_id}")
+ return ""
+
+ except Exception as e:
+ logger.error(f"获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
+ return ""
+
+ async def _fetch_item_detail_from_browser(self, item_id: str) -> str:
+ """使用浏览器获取商品详情"""
+ try:
+ from playwright.async_api import async_playwright
+
+ logger.info(f"开始使用浏览器获取商品详情: {item_id}")
+
+ playwright = await async_playwright().start()
+
+ # 启动浏览器(参照order_detail_fetcher的配置)
+ browser_args = [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-accelerated-2d-canvas',
+ '--no-first-run',
+ '--no-zygote',
+ '--disable-gpu',
+ '--disable-background-timer-throttling',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-renderer-backgrounding',
+ '--disable-features=TranslateUI',
+ '--disable-ipc-flooding-protection',
+ '--disable-extensions',
+ '--disable-default-apps',
+ '--disable-sync',
+ '--disable-translate',
+ '--hide-scrollbars',
+ '--mute-audio',
+ '--no-default-browser-check',
+ '--no-pings'
+ ]
+
+ # 在Docker环境中添加额外参数
+ if os.getenv('DOCKER_ENV'):
+ browser_args.extend([
+ '--single-process',
+ '--disable-background-networking',
+ '--disable-client-side-phishing-detection',
+ '--disable-hang-monitor',
+ '--disable-popup-blocking',
+ '--disable-prompt-on-repost',
+ '--disable-web-resources',
+ '--metrics-recording-only',
+ '--safebrowsing-disable-auto-update',
+ '--enable-automation',
+ '--password-store=basic',
+ '--use-mock-keychain'
+ ])
+
+ browser = await playwright.chromium.launch(
+ headless=True,
+ args=browser_args
+ )
+
+ # 创建浏览器上下文
+ context = await 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'
+ )
+
+ # 设置Cookie
+ cookies = []
+ for cookie_pair in self.cookies_str.split('; '):
+ if '=' in cookie_pair:
+ name, value = cookie_pair.split('=', 1)
+ cookies.append({
+ 'name': name.strip(),
+ 'value': value.strip(),
+ 'domain': '.goofish.com',
+ 'path': '/'
+ })
+
+ await context.add_cookies(cookies)
+ logger.debug(f"已设置 {len(cookies)} 个Cookie")
+
+ # 创建页面
+ page = await context.new_page()
+
+ # 构造商品详情页面URL
+ item_url = f"https://www.goofish.com/item?id={item_id}"
+ logger.info(f"访问商品页面: {item_url}")
+
+ # 访问页面
+ await page.goto(item_url, wait_until='networkidle', timeout=30000)
+
+ # 等待页面完全加载
+ await asyncio.sleep(3)
+
+ # 获取商品详情内容
+ try:
+ # 等待目标元素出现
+ await page.wait_for_selector('.desc--GaIUKUQY', timeout=10000)
+
+ # 获取商品详情文本
+ detail_element = await page.query_selector('.desc--GaIUKUQY')
+ if detail_element:
+ detail_text = await detail_element.inner_text()
+ logger.info(f"成功获取商品详情: {item_id}, 长度: {len(detail_text)}")
+
+ # 清理资源
+ await browser.close()
+ await playwright.stop()
+
+ return detail_text.strip()
+ else:
+ logger.warning(f"未找到商品详情元素: {item_id}")
+
+ except Exception as e:
+ logger.warning(f"获取商品详情元素失败: {item_id}, 错误: {self._safe_str(e)}")
+
+ # 清理资源
+ await browser.close()
+ await playwright.stop()
+
+ return ""
+
+ except Exception as e:
+ logger.error(f"浏览器获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
+ return ""
+
+ async def _fetch_item_detail_from_external_api(self, item_id: str) -> str:
+ """从外部API获取商品详情(备用方案)"""
+ try:
+ from config import config
+ auto_fetch_config = config.get('ITEM_DETAIL', {}).get('auto_fetch', {})
+
# 从配置获取API地址和超时时间
api_base_url = auto_fetch_config.get('api_url', 'https://selfapi.zhinianboke.com/api/getItemDetail')
timeout_seconds = auto_fetch_config.get('timeout', 10)
@@ -891,7 +1070,6 @@ class XianyuLive:
# 使用aiohttp发送异步请求
import aiohttp
- import asyncio
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
@@ -903,21 +1081,20 @@ class XianyuLive:
# 检查返回状态
if result.get('status') == '200' and result.get('data'):
item_detail = result['data']
- logger.info(f"成功获取商品详情: {item_id}, 长度: {len(item_detail)}")
- logger.debug(f"商品详情内容: {item_detail[:200]}...")
+ logger.info(f"外部API成功获取商品详情: {item_id}, 长度: {len(item_detail)}")
return item_detail
else:
- logger.warning(f"API返回状态异常: {result.get('status')}, message: {result.get('message')}")
+ logger.warning(f"外部API返回状态异常: {result.get('status')}, message: {result.get('message')}")
return ""
else:
- logger.warning(f"API请求失败: HTTP {response.status}")
+ logger.warning(f"外部API请求失败: HTTP {response.status}")
return ""
except asyncio.TimeoutError:
- logger.warning(f"获取商品详情超时: {item_id}")
+ logger.warning(f"外部API获取商品详情超时: {item_id}")
return ""
except Exception as e:
- logger.error(f"获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
+ logger.error(f"外部API获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
return ""
async def save_items_list_to_db(self, items_list):
diff --git a/reply_server.py b/reply_server.py
index 5ebc527..1c9df63 100644
--- a/reply_server.py
+++ b/reply_server.py
@@ -444,51 +444,18 @@ async def admin_page():
return HTMLResponse(f.read())
-# 用户管理页面路由
-@app.get('/user_management.html', response_class=HTMLResponse)
-async def user_management_page():
- page_path = os.path.join(static_dir, 'user_management.html')
- if os.path.exists(page_path):
- with open(page_path, 'r', encoding='utf-8') as f:
- return HTMLResponse(f.read())
- else:
- return HTMLResponse('
User management page not found
')
-
-
-# 日志管理页面路由
-@app.get('/log_management.html', response_class=HTMLResponse)
-async def log_management_page():
- page_path = os.path.join(static_dir, 'log_management.html')
- if os.path.exists(page_path):
- with open(page_path, 'r', encoding='utf-8') as f:
- return HTMLResponse(f.read())
- else:
- return HTMLResponse('Log management page not found
')
-
-
-# 数据管理页面路由
-@app.get('/data_management.html', response_class=HTMLResponse)
-async def data_management_page():
- page_path = os.path.join(static_dir, 'data_management.html')
- if os.path.exists(page_path):
- with open(page_path, 'r', encoding='utf-8') as f:
- return HTMLResponse(f.read())
- else:
- return HTMLResponse('Data management page not found
')
-# 商品搜索页面路由
-@app.get('/item_search.html', response_class=HTMLResponse)
-async def item_search_page():
- page_path = os.path.join(static_dir, 'item_search.html')
- if os.path.exists(page_path):
- with open(page_path, 'r', encoding='utf-8') as f:
- return HTMLResponse(f.read())
- else:
- return HTMLResponse('Item search page not found
')
+
+
+
+
+
+
+
# 登录接口
@@ -2884,32 +2851,6 @@ async def search_multiple_pages(
raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {error_msg}")
-@app.get("/items/detail/{item_id}")
-async def get_public_item_detail(
- item_id: str,
- current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional)
-):
- """获取公开商品详情(通过外部API)"""
- try:
- from utils.item_search import get_item_detail_from_api
-
- # 从外部API获取商品详情
- detail = await get_item_detail_from_api(item_id)
-
- if detail:
- return {
- "success": True,
- "data": detail
- }
- else:
- raise HTTPException(status_code=404, detail="商品详情获取失败")
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"获取商品详情失败: {str(e)}")
- raise HTTPException(status_code=500, detail=f"获取商品详情失败: {str(e)}")
-
@app.get("/items/cookie/{cookie_id}")
def get_items_by_cookie(cookie_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
diff --git a/static/data_management.html b/static/data_management.html
deleted file mode 100644
index a931dcf..0000000
--- a/static/data_management.html
+++ /dev/null
@@ -1,556 +0,0 @@
-
-
-
-
-
- 数据管理 - 闲鱼自动回复系统
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 表信息
-
-
-
- 表名: -
-
-
- 中文名: -
-
-
- 记录数: -
-
-
-
-
-
-
-
-
-
-
- 加载中...
-
-
正在加载数据...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
您确定要删除这条记录吗?
-
-
- 警告:此操作不可恢复!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
您确定要清空整个表的所有数据吗?
-
-
- 危险操作:这将删除表中的所有记录,此操作不可恢复!
-
-
表名: -
-
记录数: - 条
-
-
-
-
-
-
-
-
-
-
diff --git a/static/item_search.html b/static/item_search.html
deleted file mode 100644
index d137867..0000000
--- a/static/item_search.html
+++ /dev/null
@@ -1,954 +0,0 @@
-
-
-
-
-
- 商品搜索 - 闲鱼管理系统
-
-
-
-
-
-
-
-
-
-
-
-
- 验证登录状态...
-
-
正在验证登录状态...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 搜索中...
-
-
正在搜索商品,请稍候...
-
-
-
-
-
-
-
-
-
-
-
没有找到相关商品
-
请尝试使用其他关键词搜索
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/static/log_management.html b/static/log_management.html
deleted file mode 100644
index 7060b15..0000000
--- a/static/log_management.html
+++ /dev/null
@@ -1,457 +0,0 @@
-
-
-
-
-
- 日志管理 - 闲鱼自动回复系统
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 全部
-
-
- INFO
-
-
- WARNING
-
-
- ERROR
-
-
- DEBUG
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 日志文件:
- -
-
-
- 显示行数:
- -
-
-
- 当前过滤:
- 全部
-
-
-
-
-
-
-
-
-
-
-
- 加载中...
-
-
正在加载日志...
-
-
-
-
-
-
-
-
-
-
-
diff --git a/static/user_management.html b/static/user_management.html
deleted file mode 100644
index 4be17a8..0000000
--- a/static/user_management.html
+++ /dev/null
@@ -1,400 +0,0 @@
-
-
-
-
-
- 用户管理 - 闲鱼自动回复系统
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 加载中...
-
-
正在加载用户信息...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
您确定要删除用户 吗?
-
-
-
警告:此操作将删除该用户的所有数据,包括:
-
- - 所有Cookie账号
- - 所有卡券
- - 所有关键字和回复设置
- - 所有个人设置
-
-
此操作不可恢复!
-
-
-
-
-
-
-
-
-
-
-
diff --git a/utils/item_search.py b/utils/item_search.py
index d9cb54c..dc1e622 100644
--- a/utils/item_search.py
+++ b/utils/item_search.py
@@ -57,23 +57,6 @@ class XianyuSearcher:
return default
return data
- async def test_browser_launch(self):
- """测试浏览器是否能正常启动"""
- try:
- if not PLAYWRIGHT_AVAILABLE:
- return False, "Playwright 未安装"
-
- playwright = await async_playwright().start()
- browser = await playwright.chromium.launch(headless=True)
- context = await browser.new_context()
- page = await context.new_page()
- await page.goto("https://www.baidu.com")
- await asyncio.sleep(2)
- await browser.close()
- return True, "浏览器测试成功"
- except Exception as e:
- return False, f"浏览器测试失败: {str(e)}"
-
async def get_first_valid_cookie(self):
"""获取第一个有效的cookie"""
try:
@@ -889,123 +872,6 @@ class XianyuSearcher:
}
-
-
- async def _parse_item_old(self, item_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
- """
- 解析单个商品数据
-
- Args:
- item_data: 商品原始数据
-
- Returns:
- 解析后的商品信息
- """
- try:
- # 基本信息 - 适配多种可能的字段名
- item_id = (item_data.get('id') or
- item_data.get('itemId') or
- item_data.get('item_id') or
- item_data.get('spuId') or '')
-
- title = (item_data.get('title') or
- item_data.get('name') or
- item_data.get('itemTitle') or
- item_data.get('subject') or '')
-
- # 价格处理
- price_raw = (item_data.get('price') or
- item_data.get('priceText') or
- item_data.get('currentPrice') or
- item_data.get('realPrice') or '')
-
- # 清理价格格式
- if isinstance(price_raw, (int, float)):
- price = str(price_raw)
- elif isinstance(price_raw, str):
- price = price_raw.replace('¥', '').replace('元', '').strip()
- else:
- price = '价格面议'
-
- # 卖家信息
- seller_info = (item_data.get('seller') or
- item_data.get('user') or
- item_data.get('owner') or {})
-
- seller_name = ''
- if seller_info:
- seller_name = (seller_info.get('nick') or
- seller_info.get('nickname') or
- seller_info.get('name') or
- seller_info.get('userName') or '匿名用户')
-
- # 商品链接
- if item_id:
- item_url = f"https://www.goofish.com/item?id={item_id}"
- else:
- item_url = item_data.get('url', item_data.get('link', ''))
-
- # 发布时间
- publish_time = (item_data.get('publishTime') or
- item_data.get('createTime') or
- item_data.get('gmtCreate') or
- item_data.get('time') or '')
-
- # 格式化时间
- if publish_time and isinstance(publish_time, (int, float)):
- import datetime
- publish_time = datetime.datetime.fromtimestamp(publish_time / 1000).strftime('%Y-%m-%d')
-
- # 商品标签
- tags = item_data.get('tags', item_data.get('labels', []))
- tag_names = []
- if isinstance(tags, list):
- for tag in tags:
- if isinstance(tag, dict):
- tag_name = (tag.get('name') or
- tag.get('text') or
- tag.get('label') or '')
- if tag_name:
- tag_names.append(tag_name)
- elif isinstance(tag, str):
- tag_names.append(tag)
-
- # 图片
- images = (item_data.get('images') or
- item_data.get('pics') or
- item_data.get('pictures') or
- item_data.get('imgList') or [])
-
- main_image = ''
- if images and len(images) > 0:
- if isinstance(images[0], str):
- main_image = images[0]
- elif isinstance(images[0], dict):
- main_image = (images[0].get('url') or
- images[0].get('src') or
- images[0].get('image') or '')
-
- # 如果没有图片,使用默认占位图
- if not main_image:
- main_image = f'https://via.placeholder.com/200x200?text={title[:10] if title else "商品"}...'
-
- return {
- 'item_id': item_id,
- 'title': title,
- 'price': price,
- 'seller_name': seller_name,
- 'item_url': item_url,
- 'publish_time': publish_time,
- 'tags': tag_names,
- 'main_image': main_image,
- 'raw_data': item_data # 保留原始数据以备后用
- }
-
- except Exception as e:
- logger.warning(f"解析商品数据失败: {str(e)}")
- return None
-
-
# 搜索器工具函数
async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
@@ -1130,50 +996,3 @@ async def search_multiple_pages_xianyu(keyword: str, total_pages: int = 1) -> Di
-
-async def get_item_detail_from_api(item_id: str) -> Optional[str]:
- """
- 从外部API获取商品详情
-
- Args:
- item_id: 商品ID
-
- Returns:
- 商品详情文本,获取失败返回None
- """
- try:
- # 使用默认的API配置
- api_base_url = 'https://selfapi.zhinianboke.com/api/getItemDetail'
- timeout_seconds = 10
-
- api_url = f"{api_base_url}/{item_id}"
-
- logger.info(f"正在从外部API获取商品详情: {item_id}")
-
- # 使用简单的HTTP请求
- import aiohttp
- timeout = aiohttp.ClientTimeout(total=timeout_seconds)
-
- async with aiohttp.ClientSession(timeout=timeout) as session:
- async with session.get(api_url) as response:
- if response.status == 200:
- result = await response.json()
-
- # 检查返回状态
- if result.get('status') == '200' and result.get('data'):
- item_detail = result['data']
- logger.info(f"成功获取商品详情: {item_id}, 长度: {len(item_detail)}")
- return item_detail
- else:
- logger.warning(f"API返回状态异常: {result.get('status')}, message: {result.get('message')}")
- return None
- else:
- logger.warning(f"API请求失败: HTTP {response.status}")
- return None
-
- except asyncio.TimeoutError:
- logger.warning(f"获取商品详情超时: {item_id}")
- return None
- except Exception as e:
- logger.error(f"获取商品详情异常: {item_id}, 错误: {str(e)}")
- return None