Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions reply_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9391,7 +9391,7 @@ async def _run_order_history_sync_job(job_id: str) -> None:
user_info = dict(job.get('user_info') or {})
current_user_id = user_info.get('user_id')

from utils.order_history_sync import OrderHistoryPageFetcher
from utils.order_history_sync import OrderHistoryPageFetcher, OrderHistorySyncError

try:
utc_start = local_date_to_utc_start(request_data.get('start_date'))
Expand Down Expand Up @@ -9457,11 +9457,25 @@ async def _run_order_history_sync_job(job_id: str) -> None:
live_instance = cookie_manager.manager.get_xianyu_instance(cookie_id) if cookie_manager.manager else None

try:
fetch_result = await history_fetcher.fetch_recent_orders(
max_orders=remaining_limit,
utc_start=utc_start,
utc_end_exclusive=utc_end_exclusive,
)
try:
fetch_result = await history_fetcher.fetch_recent_orders(
max_orders=remaining_limit,
utc_start=utc_start,
utc_end_exclusive=utc_end_exclusive,
)
except OrderHistorySyncError as history_exc:
logger.warning(
f"历史订单列表同步跳过账号: cookie_id={cookie_id}, "
f"kind={history_exc.kind}, error={history_exc}"
)
warning_message = str(history_exc)
if history_exc.guidance:
warning_message = f'{warning_message};处理建议:{history_exc.guidance}'
_append_order_history_sync_warning(job, warning_message)
job['orders_failed'] += 1
job['accounts_completed'] = account_index
continue

candidates = list(fetch_result.get('orders') or [])
scanned_count = int(fetch_result.get('scanned_count') or 0)
matched_count = int(fetch_result.get('matched_count') or 0)
Expand Down
43 changes: 43 additions & 0 deletions tests/test_order_history_sync_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import unittest

from utils.order_history_sync import (
OrderHistorySyncError,
classify_order_history_api_error,
)


class OrderHistorySyncErrorTests(unittest.TestCase):
def test_classifies_session_expired_as_actionable_auth_error(self):
error = classify_order_history_api_error(
["FAIL_SYS_SESSION_EXPIRED::Session过期"],
cookie_id="acct-1",
)

self.assertIsInstance(error, OrderHistorySyncError)
self.assertEqual(error.kind, "session_expired")
self.assertIn("acct-1", str(error))
self.assertIn("Cookie/Session 已过期", str(error))
self.assertIn("手动刷新 Cookie", error.guidance)

def test_classifies_permission_denied_as_account_permission_error(self):
error = classify_order_history_api_error(
["PERMISSION_EXCEPTION::无权限访问"],
cookie_id="acct-2",
)

self.assertEqual(error.kind, "permission_denied")
self.assertIn("账号暂无订单列表访问权限", str(error))
self.assertIn("卖家中心", error.guidance)

def test_unknown_api_error_keeps_original_ret_value(self):
error = classify_order_history_api_error(
["FAIL_SYS_UNKNOWN::unknown"],
cookie_id="acct-3",
)

self.assertEqual(error.kind, "api_error")
self.assertIn("FAIL_SYS_UNKNOWN::unknown", str(error))


if __name__ == "__main__":
unittest.main()
101 changes: 80 additions & 21 deletions utils/order_history_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,84 @@
}


class OrderHistorySyncError(RuntimeError):
def __init__(
self,
message: str,
*,
kind: str = 'api_error',
ret_value: Any = None,
guidance: str = '',
):
super().__init__(message)
self.kind = kind
self.ret_value = ret_value
self.guidance = guidance


def _ret_value_to_text(ret_value: Any) -> str:
if isinstance(ret_value, str):
return ret_value
if isinstance(ret_value, (list, tuple)):
return ' | '.join(str(item) for item in ret_value)
if isinstance(ret_value, dict):
try:
return json.dumps(ret_value, ensure_ascii=False)
except Exception:
return str(ret_value)
return str(ret_value or '')


def classify_order_history_api_error(ret_value: Any, cookie_id: str = 'unknown') -> OrderHistorySyncError:
ret_text = _ret_value_to_text(ret_value)
ret_text_lower = ret_text.lower()
account_label = cookie_id or 'unknown'

session_keywords = (
'令牌过期',
'session过期',
'session expired',
'FAIL_SYS_USER_VALIDATE',
'FAIL_SYS_TOKEN_EXPIRED',
'FAIL_SYS_TOKEN_EXOIRED',
'FAIL_SYS_SESSION_EXPIRED',
'passport.goofish.com',
'mini_login',
'login',
)
permission_keywords = (
'PERMISSION_EXCEPTION',
'无权限访问',
'permission denied',
'permission',
)

if any(keyword.lower() in ret_text_lower for keyword in session_keywords):
guidance = '请先对该账号执行手动刷新 Cookie 或重新扫码登录;若仍失败,导入浏览器中人工登录后的完整 Cookie 后再同步。'
return OrderHistorySyncError(
f'【{account_label}】历史订单列表 Cookie/Session 已过期,无法同步历史订单。平台返回: {ret_text}',
kind='session_expired',
ret_value=ret_value,
guidance=guidance,
)

if any(keyword.lower() in ret_text_lower for keyword in permission_keywords):
guidance = '请确认该账号可以在闲鱼/鱼店长卖家中心打开订单管理页;如果网页端也无权限,该接口无法由程序侧修复。'
return OrderHistorySyncError(
f'【{account_label}】账号暂无订单列表访问权限,无法同步历史订单。平台返回: {ret_text}',
kind='permission_denied',
ret_value=ret_value,
guidance=guidance,
)

return OrderHistorySyncError(
f'【{account_label}】历史订单列表 API 调用失败: {ret_text}',
kind='api_error',
ret_value=ret_value,
guidance='请稍后重试;如果持续失败,请查看日志中的平台返回值和账号 Cookie 状态。',
)


def normalize_order_history_status(value: Any) -> Optional[str]:
text = str(value or '').strip()
if not text:
Expand Down Expand Up @@ -145,26 +223,7 @@ def __init__(self, cookie_string: str, cookie_id_for_log: str = 'unknown', headl
self.session: Optional[aiohttp.ClientSession] = None

def _is_auth_failure_ret(self, ret_value: Any) -> bool:
if isinstance(ret_value, str):
ret_text = ret_value
elif isinstance(ret_value, (list, tuple)):
ret_text = ' | '.join([str(item) for item in ret_value])
else:
ret_text = str(ret_value or '')

auth_keywords = (
'令牌过期',
'session过期',
'FAIL_SYS_USER_VALIDATE',
'FAIL_SYS_TOKEN_EXPIRED',
'FAIL_SYS_TOKEN_EXOIRED',
'FAIL_SYS_SESSION_EXPIRED',
'passport.goofish.com',
'mini_login',
'login',
)
ret_text_lower = ret_text.lower()
return any(keyword.lower() in ret_text_lower for keyword in auth_keywords)
return classify_order_history_api_error(ret_value, self.cookie_id_for_log).kind == 'session_expired'

def _set_runtime_cookie_state(self, cookies_dict: Dict[str, str]) -> bool:
normalized = {str(name): str(value) for name, value in cookies_dict.items() if str(name).strip()}
Expand Down Expand Up @@ -288,7 +347,7 @@ async def _request_order_page(self, page_number: int, allow_retry: bool = True)
logger.warning(f"【{self.cookie_id_for_log}】历史订单列表鉴权失败,Cookie 更新后重试第 {page_number} 页")
return await self._request_order_page(page_number, allow_retry=False)

raise RuntimeError(f"【{self.cookie_id_for_log}】历史订单列表 API 调用失败: {ret_value or res_json}")
raise classify_order_history_api_error(ret_value or res_json, self.cookie_id_for_log)

def _normalize_order_candidate(self, raw: Dict[str, Any]) -> Optional[Dict[str, Any]]:
if not isinstance(raw, dict):
Expand Down