diff --git a/client/assets/images/home/Profile/widget/Widget2_en.png b/client/assets/images/home/Profile/widget/Widget2_en.png index c04db52..81eafe5 100644 Binary files a/client/assets/images/home/Profile/widget/Widget2_en.png and b/client/assets/images/home/Profile/widget/Widget2_en.png differ diff --git a/client/assets/images/home/Profile/widget/Widget2_tw.png b/client/assets/images/home/Profile/widget/Widget2_tw.png index 0cb5808..f095e8b 100644 Binary files a/client/assets/images/home/Profile/widget/Widget2_tw.png and b/client/assets/images/home/Profile/widget/Widget2_tw.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description1_en.png b/client/assets/images/home/Profile/widget/Widget_description1_en.png index 7ee26ca..51f20e5 100644 Binary files a/client/assets/images/home/Profile/widget/Widget_description1_en.png and b/client/assets/images/home/Profile/widget/Widget_description1_en.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description1_tw.png b/client/assets/images/home/Profile/widget/Widget_description1_tw.png index acd6c21..e2dcaf3 100644 Binary files a/client/assets/images/home/Profile/widget/Widget_description1_tw.png and b/client/assets/images/home/Profile/widget/Widget_description1_tw.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description2_en.png b/client/assets/images/home/Profile/widget/Widget_description2_en.png index 64a1625..bf686ae 100644 Binary files a/client/assets/images/home/Profile/widget/Widget_description2_en.png and b/client/assets/images/home/Profile/widget/Widget_description2_en.png differ diff --git a/client/assets/images/home/Profile/widget/Widget_description2_tw.png b/client/assets/images/home/Profile/widget/Widget_description2_tw.png index 31f48d7..dbab7de 100644 Binary files a/client/assets/images/home/Profile/widget/Widget_description2_tw.png and b/client/assets/images/home/Profile/widget/Widget_description2_tw.png differ diff --git a/client/ios/client/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/client/ios/client/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png index 1e1d338..959bb47 100644 Binary files a/client/ios/client/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png and b/client/ios/client/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/server/app/tasks/push.py b/server/app/tasks/push.py index 6d1f69f..d902caa 100644 --- a/server/app/tasks/push.py +++ b/server/app/tasks/push.py @@ -68,7 +68,11 @@ async def _send_expo_push(*, to: str, title: str, body: str, data: Optional[dict def _uniform_jitter_times(*, start: datetime, end: datetime, n: int) -> list[datetime]: """ - 将窗口均匀切分为 n 个区间,并在每段内随机取一个时间点(抖动)。 + 将窗口均匀切分为 n 个区间,并在每段内取“中点 + 受限抖动”的时间点。 + + 目的: + - 尽量均匀分布(避免相邻两条推送随机到非常接近的时间) + - 仍保留一定随机性,避免过于机械 """ if n <= 0: @@ -85,8 +89,15 @@ def _uniform_jitter_times(*, start: datetime, end: datetime, n: int) -> list[dat if seg <= 0: out.append(seg_start) continue - jitter = random.random() * seg - out.append(seg_start + timedelta(seconds=jitter)) + + # 受限抖动:在每段的 [25%, 75%] 区间内取点 + # 这样相邻两段的最小间隔为 50% 段长,能显著减少“随机挤在一起”。 + mid = seg_start + timedelta(seconds=seg * 0.5) + jitter = (random.random() - 0.5) * (seg * 0.5) # [-0.25*seg, +0.25*seg] + out.append(mid + timedelta(seconds=jitter)) + + # 保序(理论上天然有序,这里再保险) + out.sort() return out @@ -289,18 +300,22 @@ 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 - # 去重:同一用户同一天尽量不重复推送相同 content + # 去重:用户推送过的内容尽量不再推送 + # 说明: + # - 依赖 push_send_log.content_id(需先完成对应 DB 迁移) + # - 为避免历史过长导致 already_recommended_ids 过大,这里取“最近若干条已推送内容”近似全量去重 used_ids: list[int] = [] try: 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()) + # 优先排除最近发送过的内容 + .order_by(PushSendLog.local_date.desc(), PushSendLog.slot_index.desc()) + .limit(5000) ) rused = await session.execute(qused) used_ids = [int(x) for x in rused.scalars().all() if x is not None]