Files
kami_apple_exchage/backend/app/services/playwright_service.py
danial 5c486e34d3 docs(项目): 添加项目文档并进行代码调整
- 新增 CODEBUDDY.md、GEMINI.md、GEMINI_CN.md 等项目文档
- 更新 Dockerfile 和其他配置文件
- 优化部分代码结构,如 orders.py、tasks.py 等
- 新增 .dockerignore 文件
2025-09-12 19:38:24 +08:00

470 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Apple订单处理服务
集成OptimizedAppleOrderProcessor业务流程
专为Celery Worker环境设计的分布式订单处理服务
"""
import asyncio
import traceback
from typing import Any
from datetime import datetime
from playwright.async_api import Page
from app.core.config import get_settings
from app.core.log import get_logger
from app.models.orders import OrderResultStatus
from app.core.state_manager import task_state_manager
from app.repositories.order_repository import OrderRepository
from app.core.database import db_manager
from app.services.gift_card_service import GiftCardService
from app.enums.task import OrderTaskStatus
from app.core.playwright_manager import DistributedPlaywrightManager
settings = get_settings()
logger = get_logger(__name__)
class AppleOrderProcessor:
"""
Apple订单处理器
集成了完整的Apple礼品卡订单处理流程
支持中断式礼品卡输入和实时进度跟踪
采用策略模式和责任链模式,提供清晰的代码结构
"""
def __init__(self, order_id: str):
self.order_id = order_id
self.task_id = f"order_{order_id}"
self.order = None
self.thread_prefix = f"[订单{order_id}]"
# Apple网站元素选择器配置
self.selectors = {
"add_to_cart": "#add-to-cart",
"zipcode_edit": "//button[@data-autom='bag-zipcode-edit-cold']",
"zipcode_input": ".form-textbox-input.form-textbox-number-input",
"zipcode_apply": "//button[@data-autom='bag-zipcode-apply']",
"checkout_other_payments": "shoppingCart.actions.checkoutOtherPayments",
"guest_login": "signIn.guestLogin.guestLogin",
"continue_shipping": "#rs-checkout-continue-button-bottom",
"first_name": "checkout.shipping.addressSelector.newAddress.address.firstName",
"last_name": "checkout.shipping.addressSelector.newAddress.address.lastName",
"street_address": "checkout.shipping.addressSelector.newAddress.address.street",
"email": "checkout.shipping.addressContactEmail.address.emailAddress",
"phone": "checkout.shipping.addressContactPhone.address.fullDaytimePhone",
"continue_selected_address": "checkout.shipping.addressVerification.selectedAddress.continueWithSelectedAddress",
"gift_card_reset": "checkout.billing.billingOptions.selectedBillingOptions.giftCard.giftCardInput.resetFields",
"gift_card_input": "checkout.billing.billingOptions.selectedBillingOptions.giftCard.giftCardInput.giftCard",
"apply_gift_card": "checkout.billing.billingOptions.selectedBillingOptions.giftCard.giftCardInput.applyGiftCard",
"continue_review": "rs-checkout-continue-button-bottom",
"place_order": "rs-checkout-continue-button-bottom",
"order_number": "#thankyou-container > div > div.rs-thankyou-headcontent > div > div > a",
}
# 失败指示器配置
self.failure_indicators = {
"Please use an Apple Gift Card that has been purchased in United States": "礼品卡地区限制",
"You have entered an invalid gift card": "无效礼品卡",
"Please enter a valid PIN": "无效卡号",
"We could not process your gift card": "礼品卡处理错误",
"This gift card has a zero balance": "礼品卡余额为0",
"Please enter another form of payment to cover the remaining balance": "礼品卡余额不足",
}
# 进度步骤定义
self.progress_steps = {
"初始化上下文": 5.0,
"打开产品页面": 10.0,
"添加到购物车": 15.0,
"设置邮编": 20.0,
"选择支付方式": 25.0,
"填写配送信息": 35.0,
"等待礼品卡信息": 40.0,
"处理礼品卡": 60.0,
"继续到审核页面": 80.0,
"提交订单": 90.0,
"提取订单信息": 95.0,
"处理完成": 100.0,
}
async def process_order(self) -> dict[str, Any]:
"""
处理订单的主入口方法
使用上下文管理器确保资源正确清理
"""
logger.info(f"{self.thread_prefix} 开始处理订单")
try:
# 初始化任务状态
await self._update_progress("初始化上下文", OrderTaskStatus.RUNNING)
# 获取订单信息
await self._load_order_info()
# 使用Playwright上下文管理器
async with playwright_manager.get_order_context(
self.order_id
) as context_info:
page = context_info["page"]
# 设置页面超时
page.set_default_timeout(60000)
# # 执行订单处理流程
# result = await self._execute_order_flow(page)
result = {}
if result.get("success"):
logger.info(f"{self.thread_prefix} 订单处理成功")
return result
else:
logger.error(
f"{self.thread_prefix} 订单处理失败: {result.get('error')}"
)
return result
except Exception as e:
error_msg = f"订单处理异常: {str(e)}"
logger.error(f"{self.thread_prefix} {error_msg}\n{traceback.format_exc()}")
# 更新任务状态为失败
await task_state_manager.fail_task(self.task_id, error_msg)
# 更新订单状态
await self._update_order_failure(error_msg)
return {"success": False, "error": error_msg, "order_id": self.order_id}
async def _load_order_info(self):
"""加载订单信息"""
async with db_manager.get_async_session() as session:
order_repo = OrderRepository(session)
self.order = await order_repo.get_order_with_full_details(self.order_id)
if not self.order:
raise ValueError(f"订单不存在: {self.order_id}")
if self.order.status != OrderResultStatus.PENDING:
raise ValueError(f"订单状态不正确: {self.order.status}")
# 更新订单状态为处理中
await order_repo.update_by_id(
self.order_id,
status=OrderResultStatus.PROCESSING,
updated_at=datetime.now(),
)
logger.info(f"{self.thread_prefix} 订单信息加载成功")
async def _update_progress(
self, step_name: str, status: OrderTaskStatus | None = None
):
"""更新任务进度"""
progress = self.progress_steps.get(step_name, 0.0)
if status:
await task_state_manager.set_task_state(
self.task_id,
status,
progress=progress,
order_id=self.order_id,
current_step=step_name,
)
else:
await task_state_manager.update_task_progress(self.task_id, progress)
logger.info(f"{self.thread_prefix} {step_name} ({progress}%)")
async def _execute_order_flow(self, page: Page) -> dict[str, Any]:
"""执行订单处理流程"""
try:
# 检查页面是否有效
if page.is_closed():
raise Exception("页面已关闭")
# 步骤1: 打开产品页面
await self._update_progress("打开产品页面")
await self._navigate_to_product_page(page)
# 步骤2: 添加到购物车
await self._update_progress("添加到购物车")
await self._add_to_cart(page)
# 步骤3: 设置邮编
await self._update_progress("设置邮编")
await self._handle_zipcode_setup(page)
# 步骤4: 选择支付方式
await self._update_progress("选择支付方式")
await self._handle_payment_selection(page)
# 步骤5: 填写配送信息
await self._update_progress("填写配送信息")
await self._handle_shipping_info(page)
# 步骤6: 处理礼品卡(中断式输入)
await self._update_progress("等待礼品卡信息")
gift_card_data = await self._handle_gift_card_process_enhanced(page)
if not gift_card_data:
return {
"success": False,
"error": "礼品卡处理失败",
"order_id": self.order_id,
}
# 步骤7: 完成订单
await self._update_progress("提交订单")
success = await self._handle_order_completion(page, gift_card_data)
if success:
await self._update_progress("处理完成", OrderTaskStatus.SUCCESS)
order_info = await self._extract_order_info(page)
await self._update_order_success(order_info)
return {
"success": True,
"order_id": self.order_id,
"order_number": order_info.get("order_number"),
"order_url": order_info.get("order_url"),
"processed_at": datetime.now().isoformat(),
}
else:
return {
"success": False,
"error": "订单完成失败",
"order_id": self.order_id,
}
except Exception as e:
error_msg = f"订单流程执行失败: {str(e)}"
logger.error(f"{self.thread_prefix} {error_msg}")
return {"success": False, "error": error_msg, "order_id": self.order_id}
async def _navigate_to_product_page(self, page: Page):
"""打开产品页面"""
if not self.order or not self.order.links or not self.order.links.url:
raise ValueError("订单中没有产品链接信息")
product_url = self.order.links.url
logger.info(f"{self.thread_prefix} 打开产品页面: {product_url}")
try:
await page.goto(product_url, wait_until="networkidle", timeout=30000)
await page.wait_for_timeout(2000)
except Exception as nav_error:
raise Exception(f"页面导航失败: {str(nav_error)}")
async def _add_to_cart(self, page: Page):
"""添加到购物车"""
if not await self._click_element(page, self.selectors["add_to_cart"]):
raise Exception("无法点击添加到购物车按钮")
await page.wait_for_timeout(2000)
logger.info(f"{self.thread_prefix} 已添加到购物车")
async def _update_order_failure(self, error_msg: str):
"""更新订单状态为失败"""
async with db_manager.get_async_session() as session:
order_repo = OrderRepository(session)
await order_repo.update_by_id(
self.order_id,
status=OrderResultStatus.FAILURE,
failure_reason=error_msg,
completed_at=datetime.now(),
)
async def _update_order_success(self, order_info: dict[str, Any]):
"""更新订单状态为成功"""
async with db_manager.get_async_session() as session:
order_repo = OrderRepository(session)
await order_repo.update_by_id(
self.order_id,
status=OrderResultStatus.SUCCESS,
order_number=order_info.get("order_number"),
final_order_url=order_info.get("order_url"),
completed_at=datetime.now(),
)
# 辅助方法实现
async def _click_element(
self,
page: Page,
selector: str,
selector_type: str = "css",
timeout: int = 30000,
) -> bool:
"""统一的元素点击方法"""
try:
if page.is_closed():
logger.error(f"{self.thread_prefix} 页面已关闭,无法操作元素")
return False
full_selector = self._build_selector(selector, selector_type)
try:
element = await page.wait_for_selector(full_selector, timeout=timeout)
if not element:
logger.error(f"{self.thread_prefix} 元素未找到: {selector}")
return False
except asyncio.TimeoutError:
logger.error(f"{self.thread_prefix} 等待元素超时: {selector}")
return False
await element.click()
await self._wait_for_element_stable(element)
logger.debug(f"{self.thread_prefix} 成功点击元素: {selector}")
return True
except Exception as e:
logger.error(f"{self.thread_prefix} 点击元素失败 {selector}: {e}")
return False
def _build_selector(self, selector: str, selector_type: str) -> str:
"""构建完整的选择器字符串"""
if selector_type == "id":
return f"#{selector.replace('.', '\\.')}"
elif selector_type == "xpath":
return f"xpath={selector}"
else:
return selector
async def _wait_for_element_stable(self, element, max_wait: int = 10) -> None:
"""等待元素状态稳定(非禁用状态)"""
try:
for _ in range(max_wait):
if not await element.is_disabled():
break
await asyncio.sleep(1)
except Exception:
pass # 忽略状态检查错误
# 简化的处理方法,实际实现需要根据业务需求完善
async def _handle_zipcode_setup(self, page: Page):
"""处理邮编设置"""
logger.info(f"{self.thread_prefix} 处理邮编设置")
# 实际实现会包含邮编相关的具体操作
pass
async def _handle_payment_selection(self, page: Page):
"""处理支付方式选择"""
logger.info(f"{self.thread_prefix} 处理支付方式选择")
# 实际实现会包含支付方式选择的具体操作
pass
async def _handle_shipping_info(self, page: Page):
"""处理配送信息填写"""
logger.info(f"{self.thread_prefix} 处理配送信息")
# 实际实现会包含配送信息填写的具体操作
pass
async def _handle_gift_card_process_enhanced(
self, page: Page
) -> dict[str, Any] | None:
"""处理礼品卡流程(增强版,支持中断式输入)"""
try:
# 更新任务状态为等待礼品卡
await task_state_manager.set_task_state(
self.task_id,
OrderTaskStatus.WAITING_GIFT_CARD,
order_id=self.order_id,
message="等待用户提供礼品卡信息",
)
logger.info(f"{self.thread_prefix} 等待礼品卡信息输入")
# 等待礼品卡信息最多等待10分钟
card_code = await self._wait_for_gift_card_input()
if not card_code:
return None
# 输入礼品卡号到页面
if not await self._fill_gift_card_form(page, card_code):
return None
logger.info(f"{self.thread_prefix} 礼品卡处理成功")
return {"card_code": card_code}
except Exception as e:
logger.error(f"{self.thread_prefix} 礼品卡处理异常: {e}")
return None
async def _wait_for_gift_card_input(self) -> str | None:
"""等待礼品卡信息输入"""
timeout = 600 # 10分钟
start_time = asyncio.get_event_loop().time()
while True:
# 检查是否已接收到礼品卡信息
task_state = await task_state_manager.get_task_state(self.task_id)
if (
task_state
and task_state.get("status") == OrderTaskStatus.GIFT_CARD_RECEIVED
):
logger.info(f"{self.thread_prefix} 已接收到礼品卡信息")
# 获取礼品卡信息
async with db_manager.get_async_session() as db:
gift_card_service = GiftCardService(db)
card_code = await gift_card_service.get_card_code(self.order_id)
if card_code:
return card_code
else:
logger.error(f"{self.thread_prefix} 礼品卡信息获取失败")
return None
# 检查超时
if asyncio.get_event_loop().time() - start_time > timeout:
logger.error(f"{self.thread_prefix} 等待礼品卡信息超时")
await task_state_manager.fail_task(self.task_id, "等待礼品卡信息超时")
return None
# 等待1秒后再次检查
await asyncio.sleep(1)
async def _fill_gift_card_form(self, page: Page, card_code: str) -> bool:
"""在页面中填写礼品卡表单"""
try:
# 实际的礼品卡表单填写逻辑
logger.info(
f"{self.thread_prefix} 填写礼品卡号: {card_code[:4]}****{card_code[-4:]}"
)
# 具体的页面操作会在这里实现
return True
except Exception as e:
logger.error(f"{self.thread_prefix} 填写礼品卡表单失败: {e}")
return False
async def _handle_order_completion(
self, page: Page, gift_card_data: dict[str, Any]
) -> bool:
"""处理订单完成"""
try:
logger.info(f"{self.thread_prefix} 完成订单提交")
# 实际的订单完成逻辑
return True
except Exception as e:
logger.error(f"{self.thread_prefix} 订单完成失败: {e}")
return False
async def _extract_order_info(self, page: Page) -> dict[str, Any]:
"""提取订单信息"""
try:
# 生成模拟的订单信息
order_number = (
f"APPLE_{self.order_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
)
order_url = "https://apple.com/order/123"
logger.info(f"{self.thread_prefix} 订单号: {order_number}")
return {"order_number": order_number, "order_url": order_url}
except Exception as e:
logger.error(f"{self.thread_prefix} 提取订单信息失败: {e}")
return {}
# 全局实例
playwright_manager = DistributedPlaywrightManager()