fix:更新定时任务push

This commit is contained in:
吕新雨
2026-02-11 13:50:02 +08:00
parent 402cbf90eb
commit eef5210c99
5 changed files with 112 additions and 58 deletions

View File

@@ -46,7 +46,7 @@ function getApiBaseUrl(env: AppRuntimeEnv): string {
return getOptionalEnv('EXPO_PUBLIC_API_BASE_URL_PROD', getOptionalEnv('EXPO_PUBLIC_API_BASE_URL_LOCAL', 'https://api.damer.fun'));
}
export const API_BASE_URL = getApiBaseUrl(APP_ENV);
export const API_BASE_URL = getApiBaseUrl(APPpai qa
/**
* 调试:打印环境变量注入结果(仅开发环境)

View File

@@ -218,17 +218,27 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
return {"status": "noop", "reason": "no_log"}
if str(log.status) == "sent":
return {"status": "noop", "reason": "already_sent"}
if str(log.status) != "scheduled":
# 例如 failed/skipped/sending:不再重复尝试
return {"status": "noop", "reason": f"not_scheduled:{log.status}"}
if str(log.status) not in ("scheduled", "sending"):
# 例如 failed/skipped不再重复尝试
return {"status": "noop", "reason": f"not_retryable:{log.status}"}
# 原子抢占:避免同一条 scheduled 被重复发送(例如 ETA 任务丢失后被补偿重投递)
# 只有从 scheduled -> sending 抢占成功的任务才能继续执行。
# 原子抢占:避免重复发送
# - scheduled正常抢占 scheduled -> sending
# - sending如果长时间卡在 sending进程崩溃/网络异常等),允许“超时接管”继续执行
now_utc_naive = datetime.now(timezone.utc).replace(tzinfo=None)
steal_cutoff = now_utc_naive - timedelta(minutes=10)
res = await session.execute(
update(PushSendLog)
.where(
PushSendLog.id == log.id,
PushSendLog.status == "scheduled",
(
(PushSendLog.status == "scheduled")
| (
(PushSendLog.status == "sending")
& (PushSendLog.sent_at.is_(None))
& (PushSendLog.scheduled_at <= steal_cutoff)
)
),
)
.values(status="sending", error=None)
)
@@ -262,6 +272,7 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
await session.commit()
return {"status": "failed", "reason": "no_active_token"}
try:
# 4) 生成文案(复用推荐模块 push 场景)
reco_locale = str(normalize_locale(_pick_reco_locale(pref.locale)))
title = _pick_title(reco_locale)
@@ -274,12 +285,12 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
build_user_profile_from_questionnaire(QuestionnaireAnswersV1_2()).model_dump()
)
# 直接复用 reco 的 Celery 任务实现(同步函数)
from app.tasks.reco import generate as reco_generate
# 关键:这里不能调用 tasks.reco.generate内部会 asyncio.run否则会嵌套事件循环崩溃。
from app.tasks.reco import run_reco_payload_async
reco_payload = reco_generate(scene="push", user_profile=user_profile.model_dump(), k=1, locale=reco_locale)
body = ""
try:
reco_payload = await run_reco_payload_async(scene="push", user_profile=user_profile, k=1, locale=reco_locale)
items = (reco_payload or {}).get("items") or []
if items and isinstance(items, list):
body = str(items[0].get("text") or "").strip()
@@ -290,18 +301,12 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
body = "给自己一句温柔的话。"
# 5) 发送
try:
expo_res = await _send_expo_push(
to=str(token.push_token),
title=title,
body=body,
data={"client_user_id": client_user_id, "scene": "push"},
)
except Exception as e:
log.status = "failed"
log.error = f"send_failed:{type(e).__name__}"
await session.commit()
return {"status": "failed", "error": str(e)}
# 6) 解析 Expo 回执,必要时停用 token
try:
@@ -326,6 +331,12 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
log.error = None
await session.commit()
return {"status": "sent", "expo": expo_res}
except Exception as e:
# 兜底:任何未预期异常都不要让状态卡在 sending
log.status = "failed"
log.error = f"unexpected:{type(e).__name__}"
await session.commit()
return {"status": "failed", "error": str(e)}
@shared_task(name="tasks.push.send_scheduled")
@@ -356,7 +367,11 @@ def requeue_overdue(*, grace_seconds: int = 300, limit: int = 200) -> dict[str,
async with AsyncSessionLocal() as session:
q = (
select(PushSendLog)
.where(PushSendLog.status == "scheduled", PushSendLog.scheduled_at <= cutoff)
.where(
PushSendLog.status.in_(("scheduled", "sending")),
PushSendLog.sent_at.is_(None),
PushSendLog.scheduled_at <= cutoff,
)
.order_by(PushSendLog.scheduled_at.asc())
.limit(int(limit))
)

View File

@@ -53,6 +53,45 @@ async def _run_reco_async(
)
async def run_reco_payload_async(
*,
scene: Scene,
user_profile: UserProfileV1_2,
already_recommended_ids: Optional[list[Any]] = None,
touched_or_viewed_ids: Optional[list[Any]] = None,
k: Optional[int] = None,
now: Optional[datetime] = None,
locale: Optional[str] = None,
) -> dict[str, Any]:
"""
在“已有事件循环”内运行推荐并返回 payload。
用途:
- 供 Push 等 async 任务内部调用,避免 `asyncio.run()` 嵌套导致 RuntimeError
- 也便于未来在 API/任务间复用
"""
effective_now = _ensure_now(now)
effective_locale = _ensure_locale(locale)
# k 默认按场景(与 generate 保持一致)
if k is None:
k_i = 30 if scene == "feed" else 1
else:
k_i = int(k)
result = await _run_reco_async(
scene=scene,
user_profile=user_profile,
already_recommended_ids=list(already_recommended_ids or []),
touched_or_viewed_ids=list(touched_or_viewed_ids or []),
k=int(k_i),
now=effective_now,
locale=effective_locale,
)
return result.model_dump()
def _run_reco_sync(
*,
scene: Scene,

Binary file not shown.

Binary file not shown.