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.is_active = True
|
||||||
token.last_seen_at = _ensure_utc(now)
|
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()
|
await db.commit()
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@@ -187,8 +213,11 @@ async def put_preferences(req: PushPreferencesRequest, db: AsyncSession = Depend
|
|||||||
else:
|
else:
|
||||||
pref.enabled = enabled
|
pref.enabled = enabled
|
||||||
pref.times_per_day = times
|
pref.times_per_day = times
|
||||||
pref.timezone = req.timezone
|
# 注意:只在客户端显式传入时覆盖,避免把已保存的 timezone/locale 清空导致排程回退到 UTC
|
||||||
pref.locale = req.locale
|
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:
|
if req.user_profile is not None:
|
||||||
pref.user_profile_json = req.user_profile.model_dump(mode="json")
|
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 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 sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.db.base import Base
|
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")
|
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="失败原因(可选)")
|
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="创建时间")
|
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:
|
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]:
|
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),否则会嵌套事件循环崩溃。
|
# 关键:这里不能调用 tasks.reco.generate(内部会 asyncio.run),否则会嵌套事件循环崩溃。
|
||||||
from app.tasks.reco import run_reco_payload_async
|
from app.tasks.reco import run_reco_payload_async
|
||||||
|
|
||||||
body = ""
|
# 去重:同一用户同一天尽量不重复推送相同 content
|
||||||
|
used_ids: list[int] = []
|
||||||
try:
|
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 []
|
items = (reco_payload or {}).get("items") or []
|
||||||
if items and isinstance(items, list):
|
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:
|
except Exception:
|
||||||
body = ""
|
body = ""
|
||||||
|
|
||||||
if not body:
|
if not body:
|
||||||
body = "给自己一句温柔的话。"
|
# tc 语言兜底文案使用繁体
|
||||||
|
body = "給自己一句溫柔的話。" if reco_locale == "tc" else "给自己一句温柔的话。"
|
||||||
|
|
||||||
# 5) 发送
|
# 5) 发送
|
||||||
expo_res = await _send_expo_push(
|
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.status = "sent"
|
||||||
log.sent_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
log.sent_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
log.error = None
|
log.error = None
|
||||||
|
log.title = title
|
||||||
|
log.body = body
|
||||||
|
log.content_id = picked_content_id
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return {"status": "sent", "expo": expo_res}
|
return {"status": "sent", "expo": expo_res}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user