'd'
This commit is contained in:
@@ -16,6 +16,58 @@ from .seed_data import (
|
||||
from .enhanced_seed import ENHANCED_DISCUSSIONS, ENHANCED_GIGS, ENHANCED_MEETUPS, ENHANCED_PROFILES, ROUTES, SERVICES
|
||||
|
||||
|
||||
NOTIFICATION_SEEDS: list[dict[str, Any]] = [
|
||||
{
|
||||
"title": "欢迎来到游牧中国消息中心",
|
||||
"body": "这里会集中显示活动提醒、赏金任务、服务咨询、匹配动态、城市更新和系统公告。你可以筛选、已读、归档,也可以从消息直接跳转到对应功能。",
|
||||
"category": "system",
|
||||
"priority": "high",
|
||||
"targetUserId": "",
|
||||
"audience": "all",
|
||||
"actionLabel": "查看仪表盘",
|
||||
"actionUrl": "/dashboard",
|
||||
"entityType": "onboarding",
|
||||
"entityId": "message-center",
|
||||
"icon": "🔔",
|
||||
"status": "published",
|
||||
"channels": ["in_app"],
|
||||
"metadata": {"source": "seed", "tone": "welcome"},
|
||||
},
|
||||
{
|
||||
"title": "本周同城活动已更新",
|
||||
"body": "大理、成都、深圳等城市新增了线下聚会,你可以在活动页查看时间、地点和报名人数。",
|
||||
"category": "meetup",
|
||||
"priority": "normal",
|
||||
"targetUserId": "",
|
||||
"audience": "all",
|
||||
"actionLabel": "查看活动",
|
||||
"actionUrl": "/meetups",
|
||||
"entityType": "meetup",
|
||||
"entityId": "weekly",
|
||||
"icon": "🍹",
|
||||
"status": "published",
|
||||
"channels": ["in_app"],
|
||||
"metadata": {"source": "seed"},
|
||||
},
|
||||
{
|
||||
"title": "赏金墙有新任务可接",
|
||||
"body": "城市资料补充、共享办公调研、活动摄影等任务已经开放,接单后会生成站内通知记录。",
|
||||
"category": "gig",
|
||||
"priority": "normal",
|
||||
"targetUserId": "",
|
||||
"audience": "all",
|
||||
"actionLabel": "去接任务",
|
||||
"actionUrl": "/gigs",
|
||||
"entityType": "gig",
|
||||
"entityId": "open-board",
|
||||
"icon": "💰",
|
||||
"status": "published",
|
||||
"channels": ["in_app"],
|
||||
"metadata": {"source": "seed"},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def text(name: str, *, required: bool = False, max: int = 0) -> dict[str, Any]:
|
||||
return {"name": name, "type": "text", "required": required, "max": max}
|
||||
|
||||
@@ -205,6 +257,41 @@ COLLECTIONS: dict[str, list[dict[str, Any]]] = {
|
||||
json_field("tags"),
|
||||
json_field("resultSlugs"),
|
||||
],
|
||||
"notifications": [
|
||||
text("title", required=True, max=255),
|
||||
text("body", max=2000),
|
||||
text("category", max=80),
|
||||
text("priority", max=40),
|
||||
text("targetUserId", max=80),
|
||||
text("audience", max=40),
|
||||
text("actionLabel", max=120),
|
||||
text("actionUrl", max=500),
|
||||
text("entityType", max=80),
|
||||
text("entityId", max=120),
|
||||
text("icon", max=16),
|
||||
text("status", max=40),
|
||||
json_field("channels"),
|
||||
json_field("metadata"),
|
||||
date("expiresAt"),
|
||||
],
|
||||
"notification_receipts": [
|
||||
text("notificationId", required=True, max=80),
|
||||
text("userId", required=True, max=80),
|
||||
date("readAt"),
|
||||
date("archivedAt"),
|
||||
date("dismissedAt"),
|
||||
boolean("pinned"),
|
||||
],
|
||||
"notification_preferences": [
|
||||
text("userId", required=True, max=80),
|
||||
text("email", max=255),
|
||||
json_field("channels"),
|
||||
json_field("categories"),
|
||||
text("quietHoursStart", max=10),
|
||||
text("quietHoursEnd", max=10),
|
||||
text("digest", max=40),
|
||||
boolean("allowMarketing"),
|
||||
],
|
||||
"feedback": [
|
||||
text("type", max=40),
|
||||
text("email", max=255),
|
||||
@@ -319,6 +406,7 @@ async def main() -> None:
|
||||
await upsert_seed("city_details", "citySlug", CITY_DETAILS)
|
||||
await upsert_seed("services", "slug", SERVICES)
|
||||
await upsert_seed("routes", "slug", ROUTES)
|
||||
await upsert_seed("notifications", "title", NOTIFICATION_SEEDS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
440
backend/main.py
440
backend/main.py
@@ -11,7 +11,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import Response as FastAPIResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .pb_client import PocketBaseError, pb, pb_quote
|
||||
from .settings import get_settings
|
||||
@@ -88,7 +88,7 @@ class DiscussionPayload(BaseModel):
|
||||
title: str
|
||||
author: str = "NomadCNA"
|
||||
city: str = ""
|
||||
tags: list[str] = []
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
excerpt: str = ""
|
||||
|
||||
|
||||
@@ -106,11 +106,36 @@ class ServiceLeadPayload(BaseModel):
|
||||
note: str = ""
|
||||
|
||||
|
||||
class NotificationCreatePayload(BaseModel):
|
||||
title: str
|
||||
body: str = ""
|
||||
category: str = "system"
|
||||
priority: str = "normal"
|
||||
targetUserId: str = ""
|
||||
audience: str = "all"
|
||||
actionLabel: str = ""
|
||||
actionUrl: str = ""
|
||||
entityType: str = ""
|
||||
entityId: str = ""
|
||||
icon: str = "🔔"
|
||||
channels: list[str] = Field(default_factory=lambda: ["in_app"])
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class NotificationPreferencePayload(BaseModel):
|
||||
channels: dict[str, bool] = Field(default_factory=dict)
|
||||
categories: dict[str, bool] = Field(default_factory=dict)
|
||||
quietHoursStart: str = "22:00"
|
||||
quietHoursEnd: str = "08:00"
|
||||
digest: str = "daily"
|
||||
allowMarketing: bool = False
|
||||
|
||||
|
||||
class RecommendationPayload(BaseModel):
|
||||
budget: int = 6000
|
||||
internet: int = 50
|
||||
climate: str = "any"
|
||||
tags: list[str] = []
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
limit: int = 12
|
||||
|
||||
|
||||
@@ -218,6 +243,147 @@ def public_record(record: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in record.items() if not k.startswith("@")}
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.000Z")
|
||||
|
||||
|
||||
def default_notification_preferences(user_id: str, email: str = "") -> dict[str, Any]:
|
||||
return {
|
||||
"userId": user_id,
|
||||
"email": email,
|
||||
"channels": {"in_app": True, "email": False, "wechat": False},
|
||||
"categories": {
|
||||
"system": True,
|
||||
"meetup": True,
|
||||
"gig": True,
|
||||
"match": True,
|
||||
"service": True,
|
||||
"community": True,
|
||||
"city": True,
|
||||
},
|
||||
"quietHoursStart": "22:00",
|
||||
"quietHoursEnd": "08:00",
|
||||
"digest": "daily",
|
||||
"allowMarketing": False,
|
||||
}
|
||||
|
||||
|
||||
def normalize_notification_preferences(record: dict[str, Any] | None, user_id: str, email: str = "") -> dict[str, Any]:
|
||||
defaults = default_notification_preferences(user_id, email)
|
||||
if not record:
|
||||
return defaults
|
||||
public = public_record(record)
|
||||
return {
|
||||
**defaults,
|
||||
**public,
|
||||
"channels": {**defaults["channels"], **(public.get("channels") or {})},
|
||||
"categories": {**defaults["categories"], **(public.get("categories") or {})},
|
||||
}
|
||||
|
||||
|
||||
async def notification_preferences_for_user(user_id: str, user: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
record = await pb.first_record("notification_preferences", filter=f'userId={pb_quote(user_id)}')
|
||||
return normalize_notification_preferences(record, user_id, user.get("email", "") if user else "")
|
||||
|
||||
|
||||
def notification_is_muted(item: dict[str, Any], preferences: dict[str, Any]) -> bool:
|
||||
category = item.get("category") or "system"
|
||||
categories = preferences.get("categories") or {}
|
||||
channels = preferences.get("channels") or {}
|
||||
item_channels = item.get("channels") or ["in_app"]
|
||||
category_enabled = bool(categories.get(category, True))
|
||||
channel_enabled = any(bool(channels.get(channel, False)) for channel in item_channels)
|
||||
return not category_enabled or not channel_enabled
|
||||
|
||||
|
||||
async def create_notification(
|
||||
*,
|
||||
title: str,
|
||||
body: str = "",
|
||||
category: str = "system",
|
||||
priority: str = "normal",
|
||||
target_user_id: str = "",
|
||||
audience: str = "all",
|
||||
action_label: str = "",
|
||||
action_url: str = "",
|
||||
entity_type: str = "",
|
||||
entity_id: str = "",
|
||||
icon: str = "🔔",
|
||||
channels: list[str] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
try:
|
||||
return await pb.create_record(
|
||||
"notifications",
|
||||
{
|
||||
"title": title,
|
||||
"body": body,
|
||||
"category": category,
|
||||
"priority": priority,
|
||||
"targetUserId": target_user_id,
|
||||
"audience": audience,
|
||||
"actionLabel": action_label,
|
||||
"actionUrl": action_url,
|
||||
"entityType": entity_type,
|
||||
"entityId": entity_id,
|
||||
"icon": icon,
|
||||
"status": "published",
|
||||
"channels": channels or ["in_app"],
|
||||
"metadata": metadata or {},
|
||||
},
|
||||
)
|
||||
except PocketBaseError:
|
||||
return None
|
||||
|
||||
|
||||
async def receipt_for(notification_id: str, user_id: str) -> dict[str, Any] | None:
|
||||
if not user_id:
|
||||
return None
|
||||
return await pb.first_record(
|
||||
"notification_receipts",
|
||||
filter=f'notificationId={pb_quote(notification_id)} && userId={pb_quote(user_id)}',
|
||||
)
|
||||
|
||||
|
||||
async def upsert_receipt(notification_id: str, user_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
existing = await receipt_for(notification_id, user_id)
|
||||
base = {"notificationId": notification_id, "userId": user_id, "pinned": False}
|
||||
if existing:
|
||||
return await pb.update_record("notification_receipts", existing["id"], payload)
|
||||
return await pb.create_record("notification_receipts", {**base, **payload})
|
||||
|
||||
|
||||
async def decorated_notifications_for_user(
|
||||
user_id: str,
|
||||
preferences: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
preferences = preferences or await notification_preferences_for_user(user_id)
|
||||
data = await pb.list_records("notifications", filter='status="published"', per_page=100)
|
||||
notifications = [
|
||||
public_record(item)
|
||||
for item in data.get("items", [])
|
||||
if item.get("audience", "all") == "all" or item.get("targetUserId") == user_id
|
||||
]
|
||||
receipts = (await pb.list_records("notification_receipts", filter=f'userId={pb_quote(user_id)}', per_page=200)).get("items", [])
|
||||
receipt_by_notification = {receipt.get("notificationId"): public_record(receipt) for receipt in receipts}
|
||||
decorated = []
|
||||
for item in notifications:
|
||||
receipt = receipt_by_notification.get(item["id"], {})
|
||||
archived = bool(receipt.get("archivedAt"))
|
||||
decorated.append(
|
||||
{
|
||||
**item,
|
||||
"receipt": receipt,
|
||||
"read": bool(receipt.get("readAt")),
|
||||
"archived": archived,
|
||||
"pinned": bool(receipt.get("pinned")),
|
||||
"muted": notification_is_muted(item, preferences),
|
||||
}
|
||||
)
|
||||
decorated.sort(key=lambda item: (item.get("pinned", False), item.get("created", "")), reverse=True)
|
||||
return decorated
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health() -> dict[str, Any]:
|
||||
return {"ok": True, "service": "NomadCNA FastAPI", "pb": settings.pb_url}
|
||||
@@ -408,6 +574,19 @@ async def join(payload: JoinPayload, request: Request) -> dict[str, Any]:
|
||||
await pb.update_record("profiles", profile["id"], profile_payload)
|
||||
else:
|
||||
await pb.create_record("profiles", profile_payload)
|
||||
await create_notification(
|
||||
title="成员资料已更新",
|
||||
body="你的加入资料已经保存,后续匹配、活动推荐和城市建议会基于这份资料联动。",
|
||||
category="system",
|
||||
priority="normal",
|
||||
target_user_id=user["id"],
|
||||
audience="user",
|
||||
action_label="查看资料",
|
||||
action_url="/join",
|
||||
entity_type="member_application",
|
||||
entity_id=record["id"],
|
||||
icon="👤",
|
||||
)
|
||||
return {"ok": True, "record": record, "user_id": user["id"]}
|
||||
|
||||
|
||||
@@ -555,6 +734,171 @@ async def content_detail(slug: str) -> dict[str, Any]:
|
||||
return {"ok": True, "item": public_record(item), "cities": cities}
|
||||
|
||||
|
||||
@app.get("/api/notifications")
|
||||
async def list_notifications(
|
||||
request: Request,
|
||||
category: str = "all",
|
||||
status: str = "active",
|
||||
q: str = "",
|
||||
muted: str = "include",
|
||||
) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
preferences = await notification_preferences_for_user(user_id, user)
|
||||
all_items = await decorated_notifications_for_user(user_id, preferences)
|
||||
items = all_items
|
||||
if muted == "exclude":
|
||||
items = [item for item in items if not item.get("muted")]
|
||||
elif muted == "only":
|
||||
items = [item for item in items if item.get("muted")]
|
||||
if category != "all":
|
||||
items = [item for item in items if item.get("category") == category]
|
||||
if status == "unread":
|
||||
items = [item for item in items if not item.get("read") and not item.get("archived") and not item.get("muted")]
|
||||
elif status == "read":
|
||||
items = [item for item in items if item.get("read") and not item.get("archived")]
|
||||
elif status == "archived":
|
||||
items = [item for item in items if item.get("archived")]
|
||||
else:
|
||||
items = [item for item in items if not item.get("archived")]
|
||||
if q.strip():
|
||||
needle = q.strip().lower()
|
||||
items = [
|
||||
item
|
||||
for item in items
|
||||
if needle in str(item.get("title", "")).lower() or needle in str(item.get("body", "")).lower()
|
||||
]
|
||||
unread = sum(1 for item in all_items if not item.get("read") and not item.get("archived") and not item.get("muted"))
|
||||
categories: dict[str, int] = {}
|
||||
muted_count = 0
|
||||
for item in all_items:
|
||||
if not item.get("archived"):
|
||||
categories[item.get("category", "system")] = categories.get(item.get("category", "system"), 0) + 1
|
||||
if item.get("muted"):
|
||||
muted_count += 1
|
||||
return {
|
||||
"ok": True,
|
||||
"items": items,
|
||||
"unread": unread,
|
||||
"categories": categories,
|
||||
"muted": muted_count,
|
||||
"preferences": preferences,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/notifications/unread-count")
|
||||
async def notification_unread_count(request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
preferences = await notification_preferences_for_user(user_id, user)
|
||||
items = await decorated_notifications_for_user(user_id, preferences)
|
||||
return {"ok": True, "count": sum(1 for item in items if not item.get("read") and not item.get("archived") and not item.get("muted"))}
|
||||
|
||||
|
||||
@app.post("/api/notifications")
|
||||
async def create_notification_endpoint(payload: NotificationCreatePayload, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
target_user_id = payload.targetUserId or (user["id"] if user and payload.audience == "user" else "")
|
||||
record = await create_notification(
|
||||
title=payload.title,
|
||||
body=payload.body,
|
||||
category=payload.category,
|
||||
priority=payload.priority,
|
||||
target_user_id=target_user_id,
|
||||
audience=payload.audience,
|
||||
action_label=payload.actionLabel,
|
||||
action_url=payload.actionUrl,
|
||||
entity_type=payload.entityType,
|
||||
entity_id=payload.entityId,
|
||||
icon=payload.icon,
|
||||
channels=payload.channels,
|
||||
metadata=payload.metadata,
|
||||
)
|
||||
if not record:
|
||||
raise HTTPException(status_code=500, detail="通知创建失败")
|
||||
return {"ok": True, "record": public_record(record)}
|
||||
|
||||
|
||||
@app.post("/api/notifications/{notification_id}/read")
|
||||
async def mark_notification_read(notification_id: str, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
receipt = await upsert_receipt(notification_id, user_id, {"readAt": utc_now()})
|
||||
return {"ok": True, "receipt": public_record(receipt)}
|
||||
|
||||
|
||||
@app.post("/api/notifications/{notification_id}/unread")
|
||||
async def mark_notification_unread(notification_id: str, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
receipt = await upsert_receipt(notification_id, user_id, {"readAt": None, "archivedAt": None})
|
||||
return {"ok": True, "receipt": public_record(receipt)}
|
||||
|
||||
|
||||
@app.post("/api/notifications/{notification_id}/archive")
|
||||
async def archive_notification(notification_id: str, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
receipt = await upsert_receipt(notification_id, user_id, {"archivedAt": utc_now(), "readAt": utc_now()})
|
||||
return {"ok": True, "receipt": public_record(receipt)}
|
||||
|
||||
|
||||
@app.post("/api/notifications/{notification_id}/restore")
|
||||
async def restore_notification(notification_id: str, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
receipt = await upsert_receipt(notification_id, user_id, {"archivedAt": None, "dismissedAt": None})
|
||||
return {"ok": True, "receipt": public_record(receipt)}
|
||||
|
||||
|
||||
@app.post("/api/notifications/{notification_id}/pin")
|
||||
async def pin_notification(notification_id: str, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
existing = await receipt_for(notification_id, user_id)
|
||||
pinned = not bool(existing and existing.get("pinned"))
|
||||
receipt = await upsert_receipt(notification_id, user_id, {"pinned": pinned})
|
||||
return {"ok": True, "receipt": public_record(receipt), "pinned": pinned}
|
||||
|
||||
|
||||
@app.post("/api/notifications/mark-all-read")
|
||||
async def mark_all_notifications_read(request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
preferences = await notification_preferences_for_user(user_id, user)
|
||||
items = await decorated_notifications_for_user(user_id, preferences)
|
||||
count = 0
|
||||
for item in items:
|
||||
if not item.get("read") and not item.get("archived") and not item.get("muted"):
|
||||
await upsert_receipt(item["id"], user_id, {"readAt": utc_now()})
|
||||
count += 1
|
||||
return {"ok": True, "count": count}
|
||||
|
||||
|
||||
@app.get("/api/notifications/preferences")
|
||||
async def notification_preferences(request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
return {"ok": True, "record": await notification_preferences_for_user(user_id, user)}
|
||||
|
||||
|
||||
@app.put("/api/notifications/preferences")
|
||||
async def update_notification_preferences(payload: NotificationPreferencePayload, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
record_payload = {
|
||||
"userId": user_id,
|
||||
"email": user.get("email", "") if user else "",
|
||||
**payload.model_dump(),
|
||||
}
|
||||
existing = await pb.first_record("notification_preferences", filter=f'userId={pb_quote(user_id)}')
|
||||
if existing:
|
||||
record = await pb.update_record("notification_preferences", existing["id"], record_payload)
|
||||
else:
|
||||
record = await pb.create_record("notification_preferences", record_payload)
|
||||
return {"ok": True, "record": public_record(record)}
|
||||
|
||||
|
||||
def city_recommendation_score(city: dict[str, Any], payload: RecommendationPayload) -> tuple[float, list[str]]:
|
||||
score = float(city.get("overallScore", 0)) * 20
|
||||
reasons: list[str] = []
|
||||
@@ -663,6 +1007,19 @@ async def create_service_lead(payload: ServiceLeadPayload, request: Request) ->
|
||||
"status": "new",
|
||||
},
|
||||
)
|
||||
await create_notification(
|
||||
title=f"服务咨询已提交:{service.get('title', payload.serviceSlug)}",
|
||||
body="我们已经记录你的服务咨询,后续可以按城市、预算、路线和会员状态继续跟进。",
|
||||
category="service",
|
||||
priority="normal",
|
||||
target_user_id=user["id"] if user else "",
|
||||
audience="user" if user else "all",
|
||||
action_label="查看服务",
|
||||
action_url="/services",
|
||||
entity_type="service_lead",
|
||||
entity_id=record["id"],
|
||||
icon=service.get("icon") or "🧭",
|
||||
)
|
||||
return {"ok": True, "record": public_record(record), "service": public_record(service)}
|
||||
|
||||
|
||||
@@ -709,6 +1066,18 @@ async def create_meetup(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
payload.setdefault("rsvpCount", 0)
|
||||
payload.setdefault("attendees", [])
|
||||
record = await pb.create_record("meetups", payload)
|
||||
await create_notification(
|
||||
title=f"新活动已提交:{payload.get('city', '同城')} · {payload.get('venue', '活动地点')}",
|
||||
body=str(payload.get("description") or "新的同城活动已经进入活动列表,审核后可对外展示。"),
|
||||
category="meetup",
|
||||
priority="normal",
|
||||
audience="all",
|
||||
action_label="查看活动",
|
||||
action_url="/meetups",
|
||||
entity_type="meetup",
|
||||
entity_id=record["id"],
|
||||
icon=payload.get("emoji") or "🍹",
|
||||
)
|
||||
return {"ok": True, "record": record}
|
||||
|
||||
|
||||
@@ -730,6 +1099,19 @@ async def create_discussion(payload: DiscussionPayload) -> dict[str, Any]:
|
||||
"views": 0,
|
||||
},
|
||||
)
|
||||
await create_notification(
|
||||
title=f"新讨论:{payload.title}",
|
||||
body=payload.excerpt or f"{payload.author} 在 {payload.city or '社区'} 发起了一个新话题。",
|
||||
category="community",
|
||||
priority="normal",
|
||||
audience="all",
|
||||
action_label="参与讨论",
|
||||
action_url="/discuss",
|
||||
entity_type="discussion",
|
||||
entity_id=record["id"],
|
||||
icon="🔥",
|
||||
metadata={"city": payload.city, "tags": payload.tags},
|
||||
)
|
||||
return {"ok": True, "record": public_record(record)}
|
||||
|
||||
|
||||
@@ -751,6 +1133,18 @@ async def create_gig(payload: GigPayload, request: Request) -> dict[str, Any]:
|
||||
"status": "open",
|
||||
},
|
||||
)
|
||||
await create_notification(
|
||||
title=f"赏金墙新任务:{payload.title}",
|
||||
body=f"{payload.location} · {payload.category} · ¥{payload.budget}。{payload.description}",
|
||||
category="gig",
|
||||
priority="high" if payload.budget >= 1000 else "normal",
|
||||
audience="all",
|
||||
action_label="查看任务",
|
||||
action_url="/gigs",
|
||||
entity_type="gig",
|
||||
entity_id=record["id"],
|
||||
icon="💰",
|
||||
)
|
||||
return {"ok": True, "record": public_record(record), "user": public_record(user) if user else None}
|
||||
|
||||
|
||||
@@ -772,6 +1166,19 @@ async def apply_gig(gig_id: str, request: Request) -> dict[str, Any]:
|
||||
},
|
||||
)
|
||||
updated = await pb.update_record("gigs", gig_id, {"status": "in_progress"})
|
||||
await create_notification(
|
||||
title=f"任务已被接单:{gig.get('title', '赏金任务')}",
|
||||
body=f"{applicant} 已接下这个任务,任务状态已进入进行中。",
|
||||
category="gig",
|
||||
priority="high",
|
||||
target_user_id=user["id"] if user else "",
|
||||
audience="user" if user else "all",
|
||||
action_label="查看赏金墙",
|
||||
action_url="/gigs",
|
||||
entity_type="gig_application",
|
||||
entity_id=application["id"],
|
||||
icon="✅",
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "已接单",
|
||||
@@ -792,12 +1199,39 @@ async def save_swipe(payload: SwipePayload, request: Request) -> dict[str, Any]:
|
||||
_, user = await current_user(request)
|
||||
user_id = user["id"] if user else "anonymous"
|
||||
record = await pb.create_record("swipes", {"userId": user_id, "profileId": payload.profileId, "action": payload.action})
|
||||
if payload.action in {"like", "right", "superlike"}:
|
||||
profile = await pb.first_record("profiles", filter=f'id={pb_quote(payload.profileId)}')
|
||||
await create_notification(
|
||||
title=f"已喜欢 {profile.get('name', '一位成员') if profile else '一位成员'}",
|
||||
body="这条社交动作已经记录,后续可以扩展成互相喜欢、私信和同城约见提醒。",
|
||||
category="match",
|
||||
priority="normal",
|
||||
target_user_id=user_id if user_id != "anonymous" else "",
|
||||
audience="user" if user_id != "anonymous" else "all",
|
||||
action_label="继续匹配",
|
||||
action_url="/dating",
|
||||
entity_type="swipe",
|
||||
entity_id=record["id"],
|
||||
icon="💚",
|
||||
)
|
||||
return {"ok": True, "record": record}
|
||||
|
||||
|
||||
@app.post("/api/feedback")
|
||||
async def feedback(payload: FeedbackPayload) -> dict[str, Any]:
|
||||
record = await pb.create_record("feedback", {**payload.model_dump(), "status": "new"})
|
||||
await create_notification(
|
||||
title=f"反馈已收到:{payload.title}",
|
||||
body=payload.content[:220],
|
||||
category="system",
|
||||
priority="normal",
|
||||
audience="all",
|
||||
action_label="查看反馈页",
|
||||
action_url="/feedback",
|
||||
entity_type="feedback",
|
||||
entity_id=record["id"],
|
||||
icon="💡",
|
||||
)
|
||||
return {"ok": True, "record": record}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user