fix:每日推荐修复
This commit is contained in:
31
server/alembic/versions/0003_add_push_send_log_payload.py
Normal file
31
server/alembic/versions/0003_add_push_send_log_payload.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""add push_send_log payload snapshot
|
||||
|
||||
Revision ID: 0003_add_push_send_log_payload
|
||||
Revises: 0002_init_push_tables
|
||||
Create Date: 2026-02-12
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0003_add_push_send_log_payload"
|
||||
down_revision = "0002_init_push_tables"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("push_send_log", sa.Column("content_id", sa.Integer(), nullable=True, comment="推送文案内容 ID(可选)"))
|
||||
op.add_column("push_send_log", sa.Column("title", sa.String(length=128), nullable=True, comment="推送标题(可选)"))
|
||||
op.add_column("push_send_log", sa.Column("body", sa.Text(), nullable=True, comment="推送正文(可选)"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("push_send_log", "body")
|
||||
op.drop_column("push_send_log", "title")
|
||||
op.drop_column("push_send_log", "content_id")
|
||||
|
||||
@@ -153,6 +153,32 @@ async def register(req: PushRegisterRequest, db: AsyncSession = Depends(get_db))
|
||||
token.is_active = True
|
||||
token.last_seen_at = _ensure_utc(now)
|
||||
|
||||
# 额外:尽早写入/补齐时区与语言(用于按用户时区生成排程)
|
||||
# 说明:
|
||||
# - 用户首次授权后会立即调用 /register,但不一定马上进入“每日提醒”确认页
|
||||
# - 若 push_preferences 里 timezone 为空,会导致排程回退到 UTC,体验不符合预期
|
||||
if req.device_meta:
|
||||
tz = (req.device_meta.timezone or "").strip() or None
|
||||
loc = (req.device_meta.locale or "").strip() or None
|
||||
if tz or loc:
|
||||
qpref = select(PushPreference).where(PushPreference.client_user_id == req.client_user_id)
|
||||
rpref = await db.execute(qpref)
|
||||
pref = rpref.scalar_one_or_none()
|
||||
if pref is None:
|
||||
pref = PushPreference(
|
||||
client_user_id=req.client_user_id,
|
||||
enabled=False,
|
||||
times_per_day=0,
|
||||
timezone=tz,
|
||||
locale=loc,
|
||||
)
|
||||
db.add(pref)
|
||||
else:
|
||||
if tz and not (pref.timezone or "").strip():
|
||||
pref.timezone = tz
|
||||
if loc and not (pref.locale or "").strip():
|
||||
pref.locale = loc
|
||||
|
||||
await db.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -187,8 +213,11 @@ async def put_preferences(req: PushPreferencesRequest, db: AsyncSession = Depend
|
||||
else:
|
||||
pref.enabled = enabled
|
||||
pref.times_per_day = times
|
||||
pref.timezone = req.timezone
|
||||
pref.locale = req.locale
|
||||
# 注意:只在客户端显式传入时覆盖,避免把已保存的 timezone/locale 清空导致排程回退到 UTC
|
||||
if req.timezone is not None:
|
||||
pref.timezone = req.timezone
|
||||
if req.locale is not None:
|
||||
pref.locale = req.locale
|
||||
if req.user_profile is not None:
|
||||
pref.user_profile_json = req.user_profile.model_dump(mode="json")
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import Date, DateTime, Index, SmallInteger, String, Text, UniqueConstraint, func
|
||||
from sqlalchemy import Date, DateTime, Index, Integer, SmallInteger, String, Text, UniqueConstraint, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
@@ -33,5 +33,10 @@ class PushSendLog(Base):
|
||||
status: Mapped[str] = mapped_column(String(length=16), nullable=False, server_default="scheduled", comment="scheduled/sent/failed")
|
||||
error: Mapped[str | None] = mapped_column(Text, nullable=True, comment="失败原因(可选)")
|
||||
|
||||
# 发送内容快照(用于观测 + 去重)
|
||||
content_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="推送文案内容 ID(可选)")
|
||||
title: Mapped[str | None] = mapped_column(String(length=128), nullable=True, comment="推送标题(可选)")
|
||||
body: Mapped[str | None] = mapped_column(Text, nullable=True, comment="推送正文(可选)")
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now(), comment="创建时间")
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ def _pick_reco_locale(pref_locale: Optional[str]) -> str:
|
||||
|
||||
|
||||
def _pick_title(locale: str) -> str:
|
||||
return "每日提醒" if str(locale) == "tc" else "Daily Reminder"
|
||||
# 需求:tc 语言使用繁体标题
|
||||
return "每日推薦" if str(locale) == "tc" else "Daily Reminder"
|
||||
|
||||
|
||||
async def _send_expo_push(*, to: str, title: str, body: str, data: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
||||
@@ -288,17 +289,61 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
|
||||
# 关键:这里不能调用 tasks.reco.generate(内部会 asyncio.run),否则会嵌套事件循环崩溃。
|
||||
from app.tasks.reco import run_reco_payload_async
|
||||
|
||||
body = ""
|
||||
# 去重:同一用户同一天尽量不重复推送相同 content
|
||||
used_ids: list[int] = []
|
||||
try:
|
||||
reco_payload = await run_reco_payload_async(scene="push", user_profile=user_profile, k=1, locale=reco_locale)
|
||||
qused = (
|
||||
select(PushSendLog.content_id)
|
||||
.where(
|
||||
PushSendLog.client_user_id == client_user_id,
|
||||
PushSendLog.local_date == local_date,
|
||||
PushSendLog.content_id.is_not(None),
|
||||
PushSendLog.id != log.id,
|
||||
)
|
||||
.order_by(PushSendLog.slot_index.asc())
|
||||
)
|
||||
rused = await session.execute(qused)
|
||||
used_ids = [int(x) for x in rused.scalars().all() if x is not None]
|
||||
except Exception:
|
||||
used_ids = []
|
||||
|
||||
body = ""
|
||||
picked_content_id: int | None = None
|
||||
try:
|
||||
reco_payload = await run_reco_payload_async(
|
||||
scene="push",
|
||||
user_profile=user_profile,
|
||||
k=3,
|
||||
locale=reco_locale,
|
||||
already_recommended_ids=used_ids,
|
||||
)
|
||||
items = (reco_payload or {}).get("items") or []
|
||||
if items and isinstance(items, list):
|
||||
body = str(items[0].get("text") or "").strip()
|
||||
for it in items:
|
||||
if not isinstance(it, dict):
|
||||
continue
|
||||
cid = it.get("content_id")
|
||||
txt = str(it.get("text") or "").strip()
|
||||
if not txt:
|
||||
continue
|
||||
if cid is not None:
|
||||
try:
|
||||
cid_i = int(cid)
|
||||
except Exception:
|
||||
cid_i = None
|
||||
else:
|
||||
cid_i = None
|
||||
if cid_i is not None and cid_i in used_ids:
|
||||
continue
|
||||
picked_content_id = cid_i
|
||||
body = txt
|
||||
break
|
||||
except Exception:
|
||||
body = ""
|
||||
|
||||
if not body:
|
||||
body = "给自己一句温柔的话。"
|
||||
# tc 语言兜底文案使用繁体
|
||||
body = "給自己一句溫柔的話。" if reco_locale == "tc" else "给自己一句温柔的话。"
|
||||
|
||||
# 5) 发送
|
||||
expo_res = await _send_expo_push(
|
||||
@@ -329,6 +374,9 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
|
||||
log.status = "sent"
|
||||
log.sent_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
log.error = None
|
||||
log.title = title
|
||||
log.body = body
|
||||
log.content_id = picked_content_id
|
||||
await session.commit()
|
||||
return {"status": "sent", "expo": expo_res}
|
||||
except Exception as e:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user