mirror of
https://git.oceanpay.cc/danial/kami_apple_exchage.git
synced 2025-12-18 22:29:09 +00:00
本次提交将后端的任务队列系统从Celery迁移到了Arq,以支持基于协程的任务处理。主要改动包括: - 更新文档和配置文件,反映架构变化。 - 修改健康检查和服务初始化逻辑,以适应Arq的使用。 - 移除与Celery相关的代码,并添加Arq任务定义和调度器。 - 更新Dockerfile和相关脚本,确保Arq worker能够正确运行。 - 调整API和业务服务中的任务处理逻辑,移除对Celery的依赖。 这些改动旨在提高系统的异步处理能力和整体性能。
779 lines
32 KiB
Python
779 lines
32 KiB
Python
"""
|
||
Apple订单处理服务
|
||
集成OptimizedAppleOrderProcessor业务流程
|
||
专为Arq Worker环境设计的分布式订单处理服务
|
||
"""
|
||
|
||
import asyncio
|
||
import traceback
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from playwright.async_api import Page
|
||
|
||
from app.core.config import get_settings
|
||
from app.core.database import db_manager
|
||
from app.core.log import get_logger
|
||
from app.core.playwright_manager import DistributedPlaywrightManager
|
||
from app.core.state_manager import task_state_manager
|
||
from app.enums.task import OrderTaskStatus
|
||
from app.models.orders import OrderStatus, Orders
|
||
from app.repositories.order_repository import OrderRepository
|
||
from app.services.file_service import file_service
|
||
from app.services.gift_card_service import GiftCardService
|
||
|
||
settings = get_settings()
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
class AppleOrderProcessor:
|
||
"""
|
||
Apple订单处理器
|
||
集成了完整的Apple礼品卡订单处理流程
|
||
支持中断式礼品卡输入和实时进度跟踪
|
||
采用策略模式和责任链模式,提供清晰的代码结构
|
||
"""
|
||
|
||
def __init__(self, task_id: str, order_id: str):
|
||
self.order_id = order_id
|
||
self.task_id = task_id
|
||
self.order: Orders | None = 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_payment": "#rs-checkout-continue-button-bottom",
|
||
"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 = {
|
||
# 初始化阶段 (1-5%)
|
||
"初始化任务上下文": 1.0,
|
||
"加载订单信息": 3.0,
|
||
"初始化浏览器上下文": 5.0,
|
||
# 产品页面阶段 (6-15%)
|
||
"打开产品页面": 6.0,
|
||
"等待页面加载": 8.0,
|
||
"验证产品页面": 10.0,
|
||
"产品页面加载完成": 12.0,
|
||
"准备添加到购物车": 15.0,
|
||
# 购物车阶段 (16-25%)
|
||
"点击添加到购物车": 16.0,
|
||
"等待购物车响应": 18.0,
|
||
"验证购物车状态": 20.0,
|
||
"购物车操作完成": 22.0,
|
||
"准备设置邮编": 25.0,
|
||
# 邮编设置阶段 (26-35%)
|
||
"点击邮编编辑": 26.0,
|
||
"输入邮编信息": 28.0,
|
||
"应用邮编设置": 30.0,
|
||
"等待邮编确认": 32.0,
|
||
"邮编设置完成": 35.0,
|
||
# 结算流程阶段 (36-45%)
|
||
"进入结算流程": 36.0,
|
||
"等待结算页面": 38.0,
|
||
"选择游客登录": 40.0,
|
||
"等待登录确认": 42.0,
|
||
"继续配送流程": 45.0,
|
||
# 配送信息阶段 (46-65%)
|
||
"填写姓名信息": 46.0,
|
||
"填写邮箱地址": 48.0,
|
||
"填写电话号码": 50.0,
|
||
"填写街道地址": 52.0,
|
||
"保存配送信息": 54.0,
|
||
"继续到支付页面": 56.0,
|
||
"等待地址验证": 58.0,
|
||
"确认配送地址": 60.0,
|
||
"配送信息完成": 65.0,
|
||
# 礼品卡准备阶段 (66-75%)
|
||
"处理额外弹窗": 66.0,
|
||
"准备礼品卡输入": 68.0,
|
||
"等待礼品卡信息": 70.0,
|
||
"礼品卡接收完成": 72.0,
|
||
"准备输入礼品卡": 75.0,
|
||
# 礼品卡处理阶段 (76-85%)
|
||
"输入礼品卡号": 76.0,
|
||
"应用礼品卡": 78.0,
|
||
"等待礼品卡验证": 80.0,
|
||
"验证礼品卡状态": 82.0,
|
||
"处理礼品卡结果": 84.0,
|
||
"继续到审核页面": 85.0,
|
||
# 订单提交阶段 (86-95%)
|
||
"验证订单信息": 86.0,
|
||
"准备提交订单": 88.0,
|
||
"提交订单": 90.0,
|
||
"等待订单确认": 92.0,
|
||
"验证订单成功": 94.0,
|
||
"订单提交完成": 95.0,
|
||
# 订单完成阶段 (96-100%)
|
||
"提取订单号": 96.0,
|
||
"保存订单信息": 98.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._update_progress("加载订单信息")
|
||
await self._load_order_info()
|
||
await self._update_progress("初始化浏览器上下文")
|
||
|
||
# 使用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)
|
||
logger.info(f"{self.thread_prefix} 订单处理完成")
|
||
return result
|
||
|
||
except Exception as e:
|
||
error_msg = f"订单处理异常: {str(e)}"
|
||
logger.error(f"{self.thread_prefix} {error_msg}\n{traceback.format_exc()}")
|
||
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 != OrderStatus.PENDING:
|
||
raise ValueError(f"订单状态不正确: {self.order.status}")
|
||
|
||
# 更新订单状态为处理中
|
||
await order_repo.update_by_id(
|
||
self.order_id,
|
||
status=OrderStatus.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("页面已关闭")
|
||
|
||
# 产品页面阶段
|
||
await self._update_progress("打开产品页面")
|
||
await self._navigate_to_product_page(page)
|
||
await self._update_progress("等待页面加载")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("验证产品页面")
|
||
await page.wait_for_load_state("networkidle")
|
||
await self._update_progress("产品页面加载完成")
|
||
await self._update_progress("准备添加到购物车")
|
||
|
||
# 购物车阶段
|
||
await self._update_progress("点击添加到购物车")
|
||
await self._add_to_cart(page)
|
||
await self._update_progress("等待购物车响应")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("验证购物车状态")
|
||
await self._update_progress("购物车操作完成")
|
||
await self._update_progress("准备设置邮编")
|
||
|
||
# 邮编设置阶段
|
||
await self._update_progress("点击邮编编辑")
|
||
await self._handle_zipcode_setup(page)
|
||
await self._update_progress("等待邮编确认")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("邮编设置完成")
|
||
|
||
# 结算流程阶段
|
||
await self._update_progress("进入结算流程")
|
||
await self._checkout_other_payments(page)
|
||
await self._update_progress("等待结算页面")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("选择游客登录")
|
||
await self._continue_as_guest(page)
|
||
await self._update_progress("等待登录确认")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("继续配送流程")
|
||
await self._handle_payment_selection(page)
|
||
|
||
# 配送信息阶段
|
||
await self._update_progress("填写姓名信息")
|
||
await self._update_progress("填写邮箱地址")
|
||
await self._update_progress("填写电话号码")
|
||
await self._update_progress("填写街道地址")
|
||
await self._handle_shipping_info(page)
|
||
await self._update_progress("保存配送信息")
|
||
await self._update_progress("继续到支付页面")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("等待地址验证")
|
||
await self._update_progress("确认配送地址")
|
||
await self._update_progress("配送信息完成")
|
||
|
||
# 礼品卡准备阶段
|
||
await self._update_progress("处理额外弹窗")
|
||
await self._handle_gift_card_reset(page)
|
||
await self._update_progress("准备礼品卡输入")
|
||
await self._update_progress("等待礼品卡信息")
|
||
await self._handle_gift_card_process_enhanced(page)
|
||
await self._update_progress("礼品卡接收完成")
|
||
await self._update_progress("准备输入礼品卡")
|
||
|
||
# 礼品卡处理阶段
|
||
await self._update_progress("输入礼品卡号")
|
||
await self._update_progress("应用礼品卡")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("等待礼品卡验证")
|
||
await self._update_progress("验证礼品卡状态")
|
||
await self._update_progress("处理礼品卡结果")
|
||
await self._update_progress("继续到审核页面")
|
||
await self._continue_to_review(page)
|
||
|
||
# 订单提交阶段
|
||
await self._update_progress("验证订单信息")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("准备提交订单")
|
||
await self._update_progress("提交订单")
|
||
success = await self._handle_order_completion(page)
|
||
await self._update_progress("等待订单确认")
|
||
await page.wait_for_timeout(1000)
|
||
await self._update_progress("验证订单成功")
|
||
await self._update_progress("订单提交完成")
|
||
|
||
if success:
|
||
await self._update_progress("提取订单号")
|
||
order_info = await self._extract_order_info(page)
|
||
await self._update_progress("保存订单信息")
|
||
await self._update_progress("处理完成", OrderTaskStatus.SUCCESS)
|
||
|
||
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 _:
|
||
# 生成截图
|
||
try:
|
||
await file_service.save_screenshot(
|
||
f"{self.order_id}-error-{self.task_id}.png", await page.screenshot()
|
||
)
|
||
await file_service.save_export_file(
|
||
f"{self.order_id}-error-{self.task_id}.html", await page.content()
|
||
)
|
||
except Exception as _:
|
||
logger.error(
|
||
f"{self.thread_prefix} 保存错误页面失败: {traceback.format_exc()}"
|
||
)
|
||
error_msg = f"订单流程执行失败: {traceback.format_exc().strip()}"
|
||
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)
|
||
await page.wait_for_load_state("networkidle")
|
||
except Exception as nav_error:
|
||
raise Exception(f"{self.thread_prefix} 页面导航失败: {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 _click_element(
|
||
self,
|
||
page: Page,
|
||
selector: str,
|
||
selector_type: str = "css",
|
||
timeout: int = 10000,
|
||
wait: bool = True,
|
||
wait_stable: bool = False,
|
||
) -> 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()
|
||
if wait_stable:
|
||
await self._wait_for_element_stable(element)
|
||
|
||
if wait:
|
||
await page.wait_for_load_state("networkidle")
|
||
await page.wait_for_timeout(1000)
|
||
logger.debug(f"{self.thread_prefix} 成功点击元素: {selector}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"{self.thread_prefix} 点击元素失败 {selector}: {e}")
|
||
return False
|
||
|
||
async def _input_element(
|
||
self,
|
||
page: Page,
|
||
selector: str,
|
||
selector_type: str = "css",
|
||
value: str = "",
|
||
timeout: int = 10000,
|
||
):
|
||
"""统一的输入方法"""
|
||
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.fill(value)
|
||
logger.debug(f"{self.thread_prefix} 成功输入元素: {selector} 内容:{value}")
|
||
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} 添加邮箱")
|
||
if not await self._click_element(
|
||
page, self.selectors["zipcode_edit"], wait=False
|
||
):
|
||
raise Exception("无法点击点击邮编设置按钮")
|
||
if not await self._input_element(
|
||
page, self.selectors["zipcode_input"], value=self.order.user_data.zip_code
|
||
):
|
||
raise Exception("无法输入邮编")
|
||
if not await self._click_element(
|
||
page, self.selectors["zipcode_apply"], wait=False, wait_stable=True
|
||
):
|
||
raise Exception("无法应用邮箱")
|
||
logger.info(f"{self.thread_prefix} 已添加邮箱")
|
||
|
||
async def _checkout_other_payments(self, page: Page):
|
||
logger.info(f"{self.thread_prefix} 下单")
|
||
if not await self._click_element(
|
||
page, self.selectors["checkout_other_payments"], selector_type="id"
|
||
):
|
||
raise Exception("无法点击下单按钮")
|
||
logger.info(f"{self.thread_prefix} 已下单")
|
||
|
||
async def _continue_as_guest(self, page: Page):
|
||
logger.info(f"{self.thread_prefix} 下单")
|
||
if not await self._click_element(
|
||
page, self.selectors["guest_login"], selector_type="id", wait_stable=True
|
||
):
|
||
raise Exception("无法点击使用游客登录")
|
||
logger.info(f"{self.thread_prefix} 已下单")
|
||
|
||
async def _handle_payment_selection(self, page: Page):
|
||
"""处理支付方式选择"""
|
||
logger.info(f"{self.thread_prefix} 处理支付方式选择")
|
||
# 实际实现会包含支付方式选择的具体操作
|
||
if not await self._click_element(
|
||
page, self.selectors["continue_shipping"], wait_stable=True
|
||
):
|
||
raise Exception("无法点击使用游客登录")
|
||
logger.info(f"{self.thread_prefix} 已处理支付方式选择")
|
||
|
||
async def _handle_shipping_info(self, page: Page):
|
||
"""处理配送信息填写"""
|
||
logger.info(f"{self.thread_prefix} 处理配送信息")
|
||
# 实际实现会包含配送信息填写的具体操作
|
||
if not await self._input_element(
|
||
page,
|
||
self.selectors["first_name"],
|
||
value=self.order.user_data.first_name,
|
||
selector_type="id",
|
||
):
|
||
raise Exception("无法输入first name")
|
||
if not await self._input_element(
|
||
page,
|
||
self.selectors["last_name"],
|
||
value=self.order.user_data.last_name,
|
||
selector_type="id",
|
||
):
|
||
raise Exception("无法输入last name")
|
||
if not await self._input_element(
|
||
page,
|
||
self.selectors["email"],
|
||
value=self.order.user_data.email,
|
||
selector_type="id",
|
||
):
|
||
raise Exception("无法输入email")
|
||
clean_phone = (
|
||
self.order.user_data.phone.replace("+", "")
|
||
.replace("-", "")
|
||
.replace(" ", "")
|
||
)
|
||
# # 如果电话号码以1开头且长度为11位,移除开头的1
|
||
if clean_phone.startswith("1") and len(clean_phone) == 11:
|
||
clean_phone = clean_phone[1:]
|
||
if not await self._input_element(
|
||
page,
|
||
self.selectors["phone"],
|
||
value=clean_phone,
|
||
selector_type="id",
|
||
):
|
||
raise Exception("无法输入phone")
|
||
if not await self._input_element(
|
||
page,
|
||
self.selectors["street_address"],
|
||
value=self.order.user_data.street_address,
|
||
selector_type="id",
|
||
):
|
||
raise Exception("无法输入street_address")
|
||
await file_service.save_screenshot(
|
||
f"{self.order_id}-user-data-{self.task_id}.png", await page.screenshot()
|
||
)
|
||
if not await self._click_element(
|
||
page, self.selectors["continue_payment"], wait_stable=True
|
||
):
|
||
raise Exception("无法点击 Continue to payment")
|
||
if not await self._click_element(
|
||
page,
|
||
self.selectors["continue_selected_address"],
|
||
selector_type="id",
|
||
wait_stable=True,
|
||
):
|
||
raise Exception("无法点击 continue_selected_address")
|
||
logger.info(f"{self.thread_prefix} 已处理配送信息")
|
||
|
||
async def _handle_gift_card_reset(self, page: Page):
|
||
logger.info(f"{self.thread_prefix} 处理额外弹窗")
|
||
if not await self._click_element(
|
||
page,
|
||
self.selectors["gift_card_reset"],
|
||
selector_type="id",
|
||
wait_stable=True,
|
||
):
|
||
raise Exception("无法点击 Continue to payment")
|
||
logger.info(f"{self.thread_prefix} 已处理额外弹窗")
|
||
|
||
async def _handle_gift_card_process_enhanced(self, page: Page) -> None:
|
||
"""处理礼品卡流程(增强版,支持中断式输入)"""
|
||
logger.info(f"{self.thread_prefix} 等待礼品卡处理")
|
||
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 = "123354544545"
|
||
card_code, gift_card_id = await self._wait_for_gift_card_input()
|
||
#
|
||
if not card_code:
|
||
raise Exception("未收到礼品卡信息")
|
||
|
||
# 输入礼品卡号到页面
|
||
if not await self._fill_gift_card_form(page, card_code, gift_card_id):
|
||
raise Exception("无法输入礼品卡号")
|
||
|
||
if not await self._continue_to_review(page):
|
||
raise Exception("无法点击 Continue to review")
|
||
|
||
logger.info(f"{self.thread_prefix} 礼品卡处理成功")
|
||
|
||
except Exception as e:
|
||
raise e
|
||
|
||
async def _wait_for_gift_card_input(self) -> tuple[str, str]:
|
||
"""等待礼品卡信息输入"""
|
||
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.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, gift_card_id = await gift_card_service.get_card_code(
|
||
self.order_id
|
||
)
|
||
if card_code:
|
||
return card_code, gift_card_id
|
||
else:
|
||
logger.error(f"{self.thread_prefix} 礼品卡信息获取失败")
|
||
raise Exception("获取礼品卡信息失败")
|
||
|
||
# 检查超时
|
||
if asyncio.get_event_loop().time() - start_time > timeout:
|
||
logger.error(f"{self.thread_prefix} 等待礼品卡信息超时")
|
||
raise Exception("等待礼品卡信息超时")
|
||
|
||
# 等待1秒后再次检查
|
||
await asyncio.sleep(2)
|
||
|
||
async def _fill_gift_card_form(
|
||
self, page: Page, card_code: str, gift_card_id: str
|
||
) -> bool:
|
||
"""在页面中填写礼品卡表单"""
|
||
try:
|
||
logger.info(f"{self.thread_prefix} 填写礼品卡表单")
|
||
# 实际的礼品卡表单填写逻辑
|
||
logger.info(f"{self.thread_prefix} 填写礼品卡号: {card_code}")
|
||
# 具体的页面操作会在这里实现
|
||
if not await self._input_element(
|
||
page,
|
||
self.selectors["gift_card_input"],
|
||
value=card_code,
|
||
selector_type="id",
|
||
):
|
||
raise Exception("无法输入邮编")
|
||
if not await self._click_element(
|
||
page,
|
||
self.selectors["apply_gift_card"],
|
||
selector_type="id",
|
||
wait_stable=True,
|
||
):
|
||
raise Exception("无法点击 apply_gift_card")
|
||
content = await page.content()
|
||
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",
|
||
}
|
||
for tip, reason in failure_indicators.items():
|
||
if tip in content:
|
||
async with db_manager.get_async_session() as session:
|
||
gift_card_service = GiftCardService(session)
|
||
await gift_card_service.update_failed_reason(
|
||
gift_card_id, reason
|
||
)
|
||
|
||
raise Exception(reason)
|
||
|
||
logger.info(f"{self.thread_prefix} 填写礼品卡表单成功")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"{self.thread_prefix} 填写礼品卡表单失败: {e}")
|
||
return False
|
||
|
||
async def _continue_to_review(self, page: Page) -> bool:
|
||
try:
|
||
logger.info(f"{self.thread_prefix} 处理配送信息")
|
||
if not await self._click_element(
|
||
page,
|
||
self.selectors["continue_to_review"],
|
||
selector_type="id",
|
||
wait_stable=True,
|
||
):
|
||
raise Exception("无法点击 continue_to_review")
|
||
await page.wait_for_timeout(2000)
|
||
logger.info(f"{self.thread_prefix} 已处理配送信息")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"{self.thread_prefix} 处理配送信息失败: {e}")
|
||
return False
|
||
|
||
async def _handle_order_completion(self, page: Page) -> bool:
|
||
"""处理订单完成"""
|
||
try:
|
||
logger.info(f"{self.thread_prefix} 完成订单提交")
|
||
# 实际的订单完成逻辑
|
||
content = await page.content()
|
||
if (
|
||
"Please enter another form of payment to cover the remaining balance"
|
||
in content
|
||
):
|
||
raise Exception("礼品卡余额不足")
|
||
|
||
if "Place Your Order" not in content:
|
||
raise Exception("无法找到 Place Your Order 按钮 未知错误")
|
||
if not await self._click_element(
|
||
page,
|
||
self.selectors["place_order"],
|
||
selector_type="id",
|
||
wait_stable=True,
|
||
):
|
||
raise Exception("无法点击 place_order")
|
||
await page.wait_for_timeout(timeout=5000)
|
||
|
||
# 强制等待
|
||
max_wait_timeout = 20
|
||
while max_wait_timeout > 0:
|
||
if "You’re all set." not in await page.content():
|
||
logger.info(
|
||
f"正在等待订单提交成功...,倒计时:{max_wait_timeout}秒"
|
||
)
|
||
max_wait_timeout -= 1
|
||
await page.wait_for_timeout(1000)
|
||
|
||
try:
|
||
await file_service.save_screenshot(
|
||
f"{self.order_id}-{self.task_id}.png", await page.screenshot()
|
||
)
|
||
await file_service.save_export_file(
|
||
f"{self.order_id}-{self.task_id}.html", await page.content()
|
||
)
|
||
except Exception as _:
|
||
logger.error(
|
||
f"{self.thread_prefix} 保存订单页面失败: {traceback.format_exc()}"
|
||
)
|
||
|
||
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]:
|
||
"""提取订单信息"""
|
||
logger.info(f"{self.thread_prefix} 提取订单信息")
|
||
info = {}
|
||
try:
|
||
order_number_element = await page.wait_for_selector(
|
||
"#thankyou-container > div > div.rs-thankyou-headcontent > div > div > a"
|
||
)
|
||
if order_number_element:
|
||
order_number_text = await order_number_element.text_content()
|
||
order_number_text = order_number_text or ""
|
||
logger.info(f"订单号为: {order_number_text}")
|
||
order_number_link = await order_number_element.get_attribute("href")
|
||
info["order_number"] = order_number_text
|
||
info["order_url"] = order_number_link
|
||
return {}
|
||
except Exception as e:
|
||
logger.error(
|
||
f"{self.thread_prefix} 提取订单信息失败: {traceback.format_exc()}"
|
||
)
|
||
return {}
|
||
|
||
|
||
# 全局实例
|
||
playwright_manager = DistributedPlaywrightManager()
|