From 62fcc4bfce4d0a8e1b15e707ddbb51e87c024d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=96=B0=E9=9B=A8?= Date: Thu, 12 Feb 2026 13:54:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E6=AF=8F=E6=97=A5=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0003_add_push_send_log_payload.py | 31 ++++++++++ server/app/api/v1/push.py | 33 +++++++++- server/app/db/models/push_send_log.py | 7 ++- server/app/tasks/push.py | 58 ++++++++++++++++-- server/celerybeat-schedule | Bin 12288 -> 12288 bytes server/celerybeat-schedule-shm | Bin 32768 -> 32768 bytes server/celerybeat-schedule-wal | Bin 238992 -> 675712 bytes 7 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 server/alembic/versions/0003_add_push_send_log_payload.py diff --git a/server/alembic/versions/0003_add_push_send_log_payload.py b/server/alembic/versions/0003_add_push_send_log_payload.py new file mode 100644 index 0000000..2c8f747 --- /dev/null +++ b/server/alembic/versions/0003_add_push_send_log_payload.py @@ -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") + diff --git a/server/app/api/v1/push.py b/server/app/api/v1/push.py index 266caf2..3a14821 100644 --- a/server/app/api/v1/push.py +++ b/server/app/api/v1/push.py @@ -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") diff --git a/server/app/db/models/push_send_log.py b/server/app/db/models/push_send_log.py index b340ba7..3449343 100644 --- a/server/app/db/models/push_send_log.py +++ b/server/app/db/models/push_send_log.py @@ -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="创建时间") diff --git a/server/app/tasks/push.py b/server/app/tasks/push.py index a9872dd..6d1f69f 100644 --- a/server/app/tasks/push.py +++ b/server/app/tasks/push.py @@ -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: diff --git a/server/celerybeat-schedule b/server/celerybeat-schedule index 782ef756c27cc0104daa77af9f6914a9a388beb7..df15f253a3b26e777fa9966fc3d13075d2d45aa5 100644 GIT binary patch delta 405 zcmZojXh_(=D!|Q?#=w7=e?9*lp0tgHVLT#ju8o2$QoRhRc_l@esl^Q}Q}vlQ`^au( zvF zl_VBt7wdtv=|Q!{!?kfbbFsf-;$}5sU|^1iStOVttCo?y*WQzBR(#hKL7Dhc{$*DQ1MU{F? zG{kv|o>JxHd-57wXBhZD@Sov-$$xXRpu$Fedj@W1DMm=p@i0p}|Qch}01sk&fV@XvxNH{(o7;43t`FZj2l>mtnguDO% delta 221 zcmZojXh_(=D!|Qa$-sY@e?9*lUdxSzn!Lg-#YHSqoiV9-B}JL3#SJV|W0^Kr$!ulh zXXa&KUtpk7GNx?as~;<$CsrR6=&w>#mBn>04@eVi2wiq diff --git a/server/celerybeat-schedule-shm b/server/celerybeat-schedule-shm index a285d7b33d522575ca6b79176a80401bb108e9b9..9fb6ee4066f412ab78904a1390fac25db623002d 100644 GIT binary patch delta 381 zcmb7=IWt3X6vcme3QDEQC!kR%Vh)nki>h05c~7&YmiSMqf=|8)fyjw z(HS%RlTm2>=A5}RckY}sb5&JRRY^&E$s|sLumMsdVkQZDlCvCM|6TCO`WCu>eDFPg zq&tZPGX8rnq7uY&zyI*N)|7WG97`xA>N32V^e?kf&LNKiiYUfR8Rb+`O)d2_(o8FD z*yyC2UhE7ogp89>#&I*rG_%a%#gD=w0ajQg#0FdJprLcX5ht8+!4=osM%@ORq<>nB zaxVE4VxojnEL2cM4RzuxO|)R8oesL_p^ttB8OFf~W4M@LiWxl2<6{ApC6)=Y#yXp9 Wv&$a)9CFMl=UfuzhKQ)NXa4{-BWywd delta 165 zcmZo@U}|V!s+V}A%K!qhK+MR%AYcWgnSppms#M*nmF(&Q1^@Q<-H+x~dmof}K6^?& vsp^47fdR?a!ZETl2#! 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 V5I_I{1Q0N%KoY&cANT16{vSA43m^ah literal 238992 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