You've already forked pruchase_jhw
支持补查订单
This commit is contained in:
+2
-4
@@ -9,10 +9,8 @@ authors = [
|
|||||||
{ name = "Antigravity Development Team" }
|
{ name = "Antigravity Development Team" }
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
# worker 自身已直接依赖 playwright,故 surugaya-common 无需 [browser] extra(extra 仅用于
|
# playwright 由 surugaya-common 的 [browser] extra 提供(>=1.54.0,<2.0.0),不再直接声明。
|
||||||
# 给没有 playwright 的消费者拉取 playwright),保持不带 extra 更简洁。
|
"surugaya-common[browser]>=0.2.0",
|
||||||
"surugaya-common>=0.2.0",
|
|
||||||
"playwright>=1.54.0,<2.0.0",
|
|
||||||
"pydantic>=2.7.0",
|
"pydantic>=2.7.0",
|
||||||
"pydantic-settings>=2.2.0",
|
"pydantic-settings>=2.2.0",
|
||||||
"loguru>=0.7.2",
|
"loguru>=0.7.2",
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class Settings(BaseSettings):
|
|||||||
redis_delay_zset_key: str = Field(default=redis_keys.TRADE_DELAY_ZSET, alias="REDIS_DELAY_ZSET_KEY")
|
redis_delay_zset_key: str = Field(default=redis_keys.TRADE_DELAY_ZSET, alias="REDIS_DELAY_ZSET_KEY")
|
||||||
redis_delay_poll_interval: int = Field(default=3600, alias="REDIS_DELAY_POLL_INTERVAL")
|
redis_delay_poll_interval: int = Field(default=3600, alias="REDIS_DELAY_POLL_INTERVAL")
|
||||||
redis_delay_batch_size: int = Field(default=10, alias="REDIS_DELAY_BATCH_SIZE")
|
redis_delay_batch_size: int = Field(default=10, alias="REDIS_DELAY_BATCH_SIZE")
|
||||||
|
redis_trade_fetch_stream_key: str = Field(default=redis_keys.TRADE_FETCH_STREAM, alias="REDIS_TRADE_FETCH_STREAM_KEY")
|
||||||
|
redis_trade_fetch_consumer_group: str = Field(default="surugaya_trade_fetch_group", alias="REDIS_TRADE_FETCH_CONSUMER_GROUP")
|
||||||
|
redis_trade_fetch_consumer_name: str = Field(default="surugaya_trade_fetch_worker", alias="REDIS_TRADE_FETCH_CONSUMER_NAME")
|
||||||
|
|
||||||
|
|
||||||
# 浏览器控制
|
# 浏览器控制
|
||||||
|
|||||||
+6
-1
@@ -19,6 +19,7 @@ from src.utils.logger import setup_logger
|
|||||||
from src.task_queue.dispatcher import TaskDispatcher
|
from src.task_queue.dispatcher import TaskDispatcher
|
||||||
from src.task_queue.delay_zset_listener import RedisZSetDelayListener
|
from src.task_queue.delay_zset_listener import RedisZSetDelayListener
|
||||||
from src.task_queue.listener import RedisStreamListener
|
from src.task_queue.listener import RedisStreamListener
|
||||||
|
from src.task_queue.trade_fetch_listener import TradeFetchStreamListener
|
||||||
from src.task_queue.redis_manager import get_redis_manager
|
from src.task_queue.redis_manager import get_redis_manager
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
@@ -53,6 +54,7 @@ async def main() -> None:
|
|||||||
dispatcher = TaskDispatcher()
|
dispatcher = TaskDispatcher()
|
||||||
stream_listener = RedisStreamListener(dispatcher)
|
stream_listener = RedisStreamListener(dispatcher)
|
||||||
delay_listener = RedisZSetDelayListener(dispatcher)
|
delay_listener = RedisZSetDelayListener(dispatcher)
|
||||||
|
trade_fetch_listener = TradeFetchStreamListener(dispatcher)
|
||||||
|
|
||||||
# 5. 优雅停机信号处理 (跨平台支持)
|
# 5. 优雅停机信号处理 (跨平台支持)
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
@@ -62,6 +64,7 @@ async def main() -> None:
|
|||||||
# 触发监听器停止的协程任务
|
# 触发监听器停止的协程任务
|
||||||
asyncio.create_task(stream_listener.stop())
|
asyncio.create_task(stream_listener.stop())
|
||||||
asyncio.create_task(delay_listener.stop())
|
asyncio.create_task(delay_listener.stop())
|
||||||
|
asyncio.create_task(trade_fetch_listener.stop())
|
||||||
|
|
||||||
# Windows 默认的 Proactor 事件循环不支持 add_signal_handler。
|
# Windows 默认的 Proactor 事件循环不支持 add_signal_handler。
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
@@ -78,7 +81,8 @@ async def main() -> None:
|
|||||||
try:
|
try:
|
||||||
stream_task = asyncio.create_task(stream_listener.listen())
|
stream_task = asyncio.create_task(stream_listener.listen())
|
||||||
delay_task = asyncio.create_task(delay_listener.listen())
|
delay_task = asyncio.create_task(delay_listener.listen())
|
||||||
await asyncio.gather(stream_task, delay_task)
|
trade_fetch_task = asyncio.create_task(trade_fetch_listener.listen())
|
||||||
|
await asyncio.gather(stream_task, delay_task, trade_fetch_task)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.warning("监听器轮询执行被事件循环取消。")
|
logger.warning("监听器轮询执行被事件循环取消。")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -88,6 +92,7 @@ async def main() -> None:
|
|||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
stream_listener.stop(),
|
stream_listener.stop(),
|
||||||
delay_listener.stop(),
|
delay_listener.stop(),
|
||||||
|
trade_fetch_listener.stop(),
|
||||||
get_browser_manager().close_all(),
|
get_browser_manager().close_all(),
|
||||||
get_redis_manager().close(),
|
get_redis_manager().close(),
|
||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from loguru import logger
|
|||||||
|
|
||||||
from src.config.settings import get_settings
|
from src.config.settings import get_settings
|
||||||
from src.task_queue.dispatcher import TaskDispatcher
|
from src.task_queue.dispatcher import TaskDispatcher
|
||||||
|
from src.task_queue.task_messages import build_trade_monitor_task
|
||||||
|
|
||||||
_CLAIM_DUE_MEMBERS_LUA = """
|
_CLAIM_DUE_MEMBERS_LUA = """
|
||||||
local key = KEYS[1]
|
local key = KEYS[1]
|
||||||
@@ -99,19 +100,7 @@ class RedisZSetDelayListener:
|
|||||||
data = json.loads(member_str)
|
data = json.loads(member_str)
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise ValueError("member JSON 不是对象结构")
|
raise ValueError("member JSON 不是对象结构")
|
||||||
now = int(time.time())
|
return build_trade_monitor_task(data.get("trade_code"))
|
||||||
task_data = {
|
|
||||||
"task_id": f"id_{now}",
|
|
||||||
"topic": "surugaya_trade_monitor",
|
|
||||||
"purchase_account": "trdian023@tokugen.jp",
|
|
||||||
"site_id": "7",
|
|
||||||
"record_video": "",
|
|
||||||
"payload": {
|
|
||||||
"trade_code": data.get("trade_code"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return task_data
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"延时队列 member 解析失败: {e} | member={member_str}")
|
raise ValueError(f"延时队列 member 解析失败: {e} | member={member_str}")
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,23 @@ class RedisStreamListener:
|
|||||||
- 连接异常时安全重连。
|
- 连接异常时安全重连。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dispatcher: TaskDispatcher) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
dispatcher: TaskDispatcher,
|
||||||
|
*,
|
||||||
|
stream_key: str | None = None,
|
||||||
|
group_name: str | None = None,
|
||||||
|
consumer_name: str | None = None,
|
||||||
|
) -> None:
|
||||||
self.settings = get_settings()
|
self.settings = get_settings()
|
||||||
self.dispatcher = dispatcher
|
self.dispatcher = dispatcher
|
||||||
self.redis: aioredis.Redis | None = None
|
self.redis: aioredis.Redis | None = None
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
# 从全局配置读取 Stream 相关参数
|
# 默认从全局配置读取 Stream 相关参数,子类/调用方可覆盖以监听其他 Stream
|
||||||
self.stream_key = self.settings.redis_stream_key
|
self.stream_key = stream_key or self.settings.redis_stream_key
|
||||||
self.group_name = self.settings.redis_consumer_group
|
self.group_name = group_name or self.settings.redis_consumer_group
|
||||||
self.consumer_name = self.settings.redis_consumer_name
|
self.consumer_name = consumer_name or self.settings.redis_consumer_name
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -69,7 +76,7 @@ class RedisStreamListener:
|
|||||||
await self.initialize()
|
await self.initialize()
|
||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
logger.info("Redis Stream 监听循环已启动。")
|
logger.info(f"Redis Stream 监听循环已启动,stream='{self.stream_key}'。")
|
||||||
|
|
||||||
# 1. 先恢复分配给本消费者但未 ACK 的 Pending 消息(崩溃重启清理)
|
# 1. 先恢复分配给本消费者但未 ACK 的 Pending 消息(崩溃重启清理)
|
||||||
await self._consume_pending_messages()
|
await self._consume_pending_messages()
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import time
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
# surugaya_trade_monitor 任务的固定执行账号与站点
|
||||||
|
TRADE_MONITOR_ACCOUNT = "trdian023@tokugen.jp"
|
||||||
|
TRADE_MONITOR_SITE_ID = "7"
|
||||||
|
|
||||||
|
|
||||||
|
def build_trade_monitor_task(trade_code: Any) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
构造骏河屋交易订单获取任务消息(TaskDispatcher 兼容结构)。
|
||||||
|
|
||||||
|
供延时队列(trade_delay zset)与补抓队列(trade_fetch stream)
|
||||||
|
两条入口复用,保证 topic / 账号等路由字段只维护一份。
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"task_id": f"id_{int(time.time())}",
|
||||||
|
"topic": "surugaya_trade_monitor",
|
||||||
|
"purchase_account": TRADE_MONITOR_ACCOUNT,
|
||||||
|
"site_id": TRADE_MONITOR_SITE_ID,
|
||||||
|
"record_video": "",
|
||||||
|
"payload": {
|
||||||
|
"trade_code": trade_code,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from src.config.settings import get_settings
|
||||||
|
from src.task_queue.dispatcher import TaskDispatcher
|
||||||
|
from src.task_queue.listener import RedisStreamListener
|
||||||
|
from src.task_queue.task_messages import build_trade_monitor_task
|
||||||
|
|
||||||
|
|
||||||
|
class TradeFetchStreamListener(RedisStreamListener):
|
||||||
|
"""
|
||||||
|
订单详情补抓 Stream 监听器。
|
||||||
|
|
||||||
|
消费 jp_surugaya API 侧 /api/trade 未命中订单详情时下发的
|
||||||
|
jp_surugaya_trade_fetch 消息(payload 仅含 trade_code),
|
||||||
|
包装为 surugaya_trade_monitor 任务后交给 TaskDispatcher。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, dispatcher: TaskDispatcher) -> None:
|
||||||
|
settings = get_settings()
|
||||||
|
super().__init__(
|
||||||
|
dispatcher,
|
||||||
|
stream_key=settings.redis_trade_fetch_stream_key,
|
||||||
|
group_name=settings.redis_trade_fetch_consumer_group,
|
||||||
|
consumer_name=settings.redis_trade_fetch_consumer_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_message(self, msg_id: bytes, raw_data: Dict[bytes, bytes]) -> Dict[str, Any]:
|
||||||
|
decoded_data = super()._parse_message(msg_id, raw_data)
|
||||||
|
task_data = build_trade_monitor_task(decoded_data.get("trade_code"))
|
||||||
|
# 保留 Stream 消息 ID,供任务完成后 ACK 使用
|
||||||
|
task_data["_stream_msg_id"] = decoded_data.get("_stream_msg_id")
|
||||||
|
return task_data
|
||||||
@@ -165,14 +165,13 @@ dependencies = [
|
|||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "opencv-python" },
|
{ name = "opencv-python" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "playwright" },
|
|
||||||
{ name = "pyautogui" },
|
{ name = "pyautogui" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyscreeze" },
|
{ name = "pyscreeze" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "redis" },
|
{ name = "redis" },
|
||||||
{ name = "surugaya-common" },
|
{ name = "surugaya-common", extra = ["browser"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -182,14 +181,13 @@ requires-dist = [
|
|||||||
{ name = "numpy", specifier = ">=2.4.6" },
|
{ name = "numpy", specifier = ">=2.4.6" },
|
||||||
{ name = "opencv-python", specifier = ">=4.13.0.92" },
|
{ name = "opencv-python", specifier = ">=4.13.0.92" },
|
||||||
{ name = "pillow", specifier = ">=12.2.0" },
|
{ name = "pillow", specifier = ">=12.2.0" },
|
||||||
{ name = "playwright", specifier = ">=1.54.0,<2.0.0" },
|
|
||||||
{ name = "pyautogui", specifier = ">=0.9.54" },
|
{ name = "pyautogui", specifier = ">=0.9.54" },
|
||||||
{ name = "pydantic", specifier = ">=2.7.0" },
|
{ name = "pydantic", specifier = ">=2.7.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.2.0" },
|
{ name = "pydantic-settings", specifier = ">=2.2.0" },
|
||||||
{ name = "pyscreeze", specifier = ">=1.0.1" },
|
{ name = "pyscreeze", specifier = ">=1.0.1" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0.1" },
|
{ name = "pyyaml", specifier = ">=6.0.1" },
|
||||||
{ name = "redis", specifier = "==8.0.0" },
|
{ name = "redis", specifier = "==8.0.0" },
|
||||||
{ name = "surugaya-common", specifier = ">=0.2.0", index = "https://git.jerryyan.net/api/packages/jp/pypi/simple" },
|
{ name = "surugaya-common", extras = ["browser"], specifier = ">=0.2.0", index = "https://git.jerryyan.net/api/packages/jp/pypi/simple" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -698,6 +696,11 @@ wheels = [
|
|||||||
{ url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.2.0/surugaya_common-0.2.0-py3-none-any.whl", hash = "sha256:140587df6b60abf1f1fc3b592d0668a096ddd42f5216ee16d4b83a95783b58cd" },
|
{ url = "https://git.jerryyan.net/api/packages/jp/pypi/files/surugaya-common/0.2.0/surugaya_common-0.2.0-py3-none-any.whl", hash = "sha256:140587df6b60abf1f1fc3b592d0668a096ddd42f5216ee16d4b83a95783b58cd" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
browser = [
|
||||||
|
{ name = "playwright" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user