Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fcc64f6e3 | ||
|
|
9d6829eceb | ||
|
|
4838bcef4b |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 143 KiB |
@@ -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'));
|
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(APPpai qa
|
export const API_BASE_URL = getApiBaseUrl(APP_ENV);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调试:打印环境变量注入结果(仅开发环境)
|
* 调试:打印环境变量注入结果(仅开发环境)
|
||||||
|
|||||||
@@ -14,6 +14,53 @@ router = APIRouter(prefix="/v1/legal", tags=["legal"])
|
|||||||
|
|
||||||
INTERNAL_SENTINEL = "__internal__"
|
INTERNAL_SENTINEL = "__internal__"
|
||||||
|
|
||||||
|
# 技术支持页文案(EN / TC)
|
||||||
|
SUPPORT_MD = """Dear Mama | Technical Support
|
||||||
|
Last updated: February 2026
|
||||||
|
|
||||||
|
If you need technical support for Dear Mama, please contact us:
|
||||||
|
- Support email: leitinglan@project-c.org
|
||||||
|
|
||||||
|
Support scope (including but not limited to):
|
||||||
|
- App installation or update issues
|
||||||
|
- App crashes, freezes, or abnormal behavior
|
||||||
|
- Notification or widget related issues
|
||||||
|
- Difficulty accessing privacy policy or terms pages
|
||||||
|
|
||||||
|
To help us process your request faster, please include:
|
||||||
|
- Device model and OS version
|
||||||
|
- App version
|
||||||
|
- A brief issue description and occurrence time
|
||||||
|
- Screenshots or screen recording (if available)
|
||||||
|
|
||||||
|
Service commitment:
|
||||||
|
- We generally respond within 3-5 business days
|
||||||
|
- Emergency availability may vary by holidays and local time
|
||||||
|
|
||||||
|
---
|
||||||
|
Dear Mama|技術支援
|
||||||
|
最後更新日期:2026 年 2 月
|
||||||
|
|
||||||
|
如需 Dear Mama 的技術支援,請透過以下方式聯絡我們:
|
||||||
|
- 支援信箱:leitinglan@project-c.org
|
||||||
|
|
||||||
|
支援範圍(包含但不限於):
|
||||||
|
- App 安裝或更新問題
|
||||||
|
- App 閃退、卡頓或異常行為
|
||||||
|
- 通知或小組件相關問題
|
||||||
|
- 隱私政策或使用條款頁面無法開啟
|
||||||
|
|
||||||
|
為了加快處理,建議提供:
|
||||||
|
- 裝置型號與系統版本
|
||||||
|
- App 版本號
|
||||||
|
- 問題描述與發生時間
|
||||||
|
- 截圖或螢幕錄影(如有)
|
||||||
|
|
||||||
|
服務承諾:
|
||||||
|
- 一般會在 3-5 個工作日內回覆
|
||||||
|
- 節假日或時區差異時,回覆時間可能延長
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# 当前多语言仅支持 EN / TC(繁体)
|
# 当前多语言仅支持 EN / TC(繁体)
|
||||||
ResolvedLang = Literal["en", "tc"]
|
ResolvedLang = Literal["en", "tc"]
|
||||||
@@ -129,3 +176,17 @@ async def get_terms_of_use(request: Request) -> HTMLResponse:
|
|||||||
page = render_as_simple_html(title=title, content=content)
|
page = render_as_simple_html(title=title, content=content)
|
||||||
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/support", response_class=HTMLResponse)
|
||||||
|
async def get_support_page(request: Request) -> HTMLResponse:
|
||||||
|
"""
|
||||||
|
技术支持页面(用于 App 审核的可访问 URL)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
accept_language = request.headers.get("accept-language")
|
||||||
|
lang = _resolve_lang(accept_language)
|
||||||
|
content, resolved = choose_content_by_lang(SUPPORT_MD, lang)
|
||||||
|
title = "Dear Mama | Technical Support" if resolved == "en" else "Dear Mama|技術支援"
|
||||||
|
page = render_as_simple_html(title=title, content=content)
|
||||||
|
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
def _uniform_jitter_times(*, start: datetime, end: datetime, n: int) -> list[datetime]:
|
||||||
"""
|
"""
|
||||||
将窗口均匀切分为 n 个区间,并在每段内随机取一个时间点(抖动)。
|
将窗口均匀切分为 n 个区间,并在每段内取“中点 + 受限抖动”的时间点。
|
||||||
|
|
||||||
|
目的:
|
||||||
|
- 尽量均匀分布(避免相邻两条推送随机到非常接近的时间)
|
||||||
|
- 仍保留一定随机性,避免过于机械
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if n <= 0:
|
if n <= 0:
|
||||||
@@ -85,8 +89,15 @@ def _uniform_jitter_times(*, start: datetime, end: datetime, n: int) -> list[dat
|
|||||||
if seg <= 0:
|
if seg <= 0:
|
||||||
out.append(seg_start)
|
out.append(seg_start)
|
||||||
continue
|
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
|
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),否则会嵌套事件循环崩溃。
|
# 关键:这里不能调用 tasks.reco.generate(内部会 asyncio.run),否则会嵌套事件循环崩溃。
|
||||||
from app.tasks.reco import run_reco_payload_async
|
from app.tasks.reco import run_reco_payload_async
|
||||||
|
|
||||||
# 去重:同一用户同一天尽量不重复推送相同 content
|
# 去重:用户推送过的内容尽量不再推送
|
||||||
|
# 说明:
|
||||||
|
# - 依赖 push_send_log.content_id(需先完成对应 DB 迁移)
|
||||||
|
# - 为避免历史过长导致 already_recommended_ids 过大,这里取“最近若干条已推送内容”近似全量去重
|
||||||
used_ids: list[int] = []
|
used_ids: list[int] = []
|
||||||
try:
|
try:
|
||||||
qused = (
|
qused = (
|
||||||
select(PushSendLog.content_id)
|
select(PushSendLog.content_id)
|
||||||
.where(
|
.where(
|
||||||
PushSendLog.client_user_id == client_user_id,
|
PushSendLog.client_user_id == client_user_id,
|
||||||
PushSendLog.local_date == local_date,
|
|
||||||
PushSendLog.content_id.is_not(None),
|
PushSendLog.content_id.is_not(None),
|
||||||
PushSendLog.id != log.id,
|
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)
|
rused = await session.execute(qused)
|
||||||
used_ids = [int(x) for x in rused.scalars().all() if x is not None]
|
used_ids = [int(x) for x in rused.scalars().all() if x is not None]
|
||||||
|
|||||||
@@ -202,6 +202,7 @@
|
|||||||
- 后端新增内置协议内容页:
|
- 后端新增内置协议内容页:
|
||||||
- `GET /v1/legal/privacy`
|
- `GET /v1/legal/privacy`
|
||||||
- `GET /v1/legal/terms`
|
- `GET /v1/legal/terms`
|
||||||
|
- `GET /v1/legal/support`(技术支持页面,提供审核可用的公开支持信息)
|
||||||
- 客户端新增协议接口封装 `client/src/services/legalApi.ts`
|
- 客户端新增协议接口封装 `client/src/services/legalApi.ts`
|
||||||
- 客户端工程化:新增统一 HTTP 封装 `client/src/utils/http.ts`(baseURL/超时/JSON/统一错误),并将 `legalApi.ts` / `recoApi.ts` 接入
|
- 客户端工程化:新增统一 HTTP 封装 `client/src/utils/http.ts`(baseURL/超时/JSON/统一错误),并将 `legalApi.ts` / `recoApi.ts` 接入
|
||||||
- 客户端接入两处入口:`app/(splash)/splash.tsx`、`components/home/ProfileModal.tsx`
|
- 客户端接入两处入口:`app/(splash)/splash.tsx`、`components/home/ProfileModal.tsx`
|
||||||
|
|||||||