Files
gitlab-instance-0a899031_cn…/backend/init_db.py
2026-06-07 23:18:53 +08:00

462 lines
15 KiB
Python

from __future__ import annotations
import asyncio
from typing import Any
from .pb_client import PocketBaseError, pb
from .seed_data import (
CITIES,
CITY_DETAILS,
CONTENT_ITEMS,
DISCUSSIONS,
GIGS,
MEETUPS,
PROFILES,
)
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}
def number(name: str, *, required: bool = False, only_int: bool = False) -> dict[str, Any]:
return {"name": name, "type": "number", "required": required, "onlyInt": only_int}
def boolean(name: str) -> dict[str, Any]:
return {"name": name, "type": "bool"}
def json_field(name: str) -> dict[str, Any]:
return {"name": name, "type": "json"}
def date(name: str) -> dict[str, Any]:
return {"name": name, "type": "date"}
COLLECTIONS: dict[str, list[dict[str, Any]]] = {
"cities": [
text("slug", required=True, max=80),
number("idNumber", only_int=True),
text("name", required=True, max=120),
text("nameEn", max=120),
text("country", max=80),
text("countryEmoji", max=16),
number("overallScore"),
number("costPerMonth", only_int=True),
number("internetSpeed", only_int=True),
text("safety", max=40),
text("liked", max=40),
number("temperature", only_int=True),
number("humidity", only_int=True),
number("nomadsNow", only_int=True),
text("description", max=500),
text("descriptionEn", max=500),
text("gradientFrom", max=32),
text("gradientTo", max=32),
text("icon", max=16),
json_field("tags"),
text("coverImage", max=500),
number("reviewsCount", only_int=True),
number("likedCount", only_int=True),
number("dislikedCount", only_int=True),
text("qualityOfLife", max=40),
text("familyScore", max=40),
text("communityScore", max=40),
number("nomadPercent"),
text("funLabel", max=40),
number("feelsLikeTemp", only_int=True),
number("acPercent", only_int=True),
number("lat"),
number("lng"),
text("region", max=80),
],
"profiles": [
text("userId", max=80),
text("email", max=255),
text("name", max=120),
number("age", only_int=True),
text("location", max=120),
json_field("tags"),
text("bio", max=1000),
text("photo", max=500),
],
"member_applications": [
text("userId", max=80),
text("email", max=255),
text("nickname", max=140),
text("gender", max=40),
text("single", max=40),
text("profession", max=140),
text("intro", max=1000),
text("wechat", max=140),
text("education", max=80),
text("graduationYear", max=20),
text("phone", max=40),
text("media", max=500),
boolean("isComplete"),
],
"memberships": [
text("userId", required=True, max=80),
text("email", max=255),
text("plan", max=40),
text("status", max=40),
date("expiresAt"),
],
"site_vip": [
text("user_id", required=True, max=80),
text("site_id", required=True, max=80),
text("order_id", max=120),
date("expires_at"),
],
"orders": [
text("orderId", required=True, max=80),
text("userId", max=120),
text("email", max=255),
number("totalFee", only_int=True),
text("type", max=60),
text("channel", max=40),
text("status", max=40),
text("returnUrl", max=500),
boolean("isNew"),
],
"payments": [
text("user_id", max=120),
text("type", max=60),
text("order_id", required=True, max=120),
number("amount", only_int=True),
number("total_fee", only_int=True),
text("channel", max=40),
],
"media_assets": [
text("userId", max=80),
text("url", required=True, max=500),
text("filename", max=255),
text("contentType", max=120),
number("size", only_int=True),
text("objectKey", max=500),
],
"meetups": [
text("city", required=True, max=120),
text("emoji", max=16),
date("date"),
text("time", max=20),
text("venue", max=255),
text("address", max=255),
text("description", max=1000),
number("rsvpCount", only_int=True),
text("gradientFrom", max=32),
text("gradientTo", max=32),
json_field("attendees"),
boolean("isUpcoming"),
text("status", max=40),
text("mode", max=40),
number("maxAttendees", only_int=True),
text("organizer", max=120),
text("accessLevel", max=40),
text("meetingProvider", max=80),
text("mirotalkRoom", max=160),
text("meetingUrl", max=500),
text("replayUrl", max=500),
json_field("features"),
],
"discussions": [
text("title", required=True, max=255),
text("author", max=120),
text("city", max=120),
json_field("tags"),
text("excerpt", max=1000),
number("replies", only_int=True),
number("views", only_int=True),
],
"gigs": [
text("title", required=True, max=255),
text("category", max=120),
number("budget", only_int=True),
text("location", max=120),
text("description", max=1000),
text("status", max=40),
],
"gig_applications": [
text("gigId", required=True, max=80),
text("userId", max=80),
text("applicant", max=255),
text("message", max=1000),
text("status", max=40),
],
"services": [
text("slug", required=True, max=120),
text("title", required=True, max=255),
text("titleEn", max=255),
text("icon", max=16),
text("description", max=1000),
text("descriptionEn", max=1000),
json_field("features"),
number("price", only_int=True),
text("priceLabel", max=120),
text("category", max=80),
text("status", max=40),
text("gradientFrom", max=32),
text("gradientTo", max=32),
],
"service_leads": [
text("serviceSlug", required=True, max=120),
text("userId", max=80),
text("email", max=255),
text("note", max=1000),
text("status", max=40),
],
"routes": [
text("slug", required=True, max=120),
text("title", required=True, max=255),
text("titleEn", max=255),
json_field("citySlugs"),
number("durationDays", only_int=True),
number("budget", only_int=True),
text("description", max=1000),
text("descriptionEn", max=1000),
json_field("stops"),
text("status", max=40),
],
"recommendation_logs": [
text("userId", max=80),
number("budget", only_int=True),
number("internet", only_int=True),
text("climate", max=80),
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),
text("title", max=255),
text("content", max=2000),
text("status", max=40),
],
"swipes": [text("userId", max=80), text("profileId", max=80), text("action", max=40)],
"content_items": [
text("slug", required=True, max=120),
text("type", required=True, max=40),
text("title", required=True, max=255),
text("titleEn", max=255),
text("subtitle", max=500),
text("subtitleEn", max=500),
text("description", max=2000),
text("descriptionEn", max=2000),
text("coverImage", max=500),
text("mediaUrl", max=500),
text("targetUrl", max=500),
text("ctaLabel", max=120),
text("ctaLabelEn", max=120),
json_field("citySlugs"),
json_field("relatedProfiles"),
json_field("chapters"),
number("durationMinutes", only_int=True),
number("sortOrder", only_int=True),
text("status", max=40),
text("authorName", max=120),
text("authorEmail", max=255),
text("submittedBy", max=80),
text("reviewStatus", max=40),
text("reviewNote", max=1000),
date("reviewedAt"),
json_field("submissionMeta"),
],
"city_details": [
text("citySlug", required=True, max=120),
json_field("guide"),
json_field("pros"),
json_field("reviews"),
json_field("cost"),
json_field("people"),
json_field("chat"),
json_field("photos"),
json_field("weather"),
json_field("trends"),
json_field("demographics"),
json_field("relatedContent"),
],
}
async def collection_exists(name: str) -> bool:
try:
await pb.request("GET", f"/api/collections/{name}", admin=True)
return True
except PocketBaseError:
return False
async def ensure_collection_fields(name: str, fields: list[dict[str, Any]]) -> None:
collection = await pb.request("GET", f"/api/collections/{name}", admin=True)
field_key = "fields" if "fields" in collection else "schema"
current_fields = collection.get(field_key) or []
current_names = {field.get("name") for field in current_fields}
missing_fields = [field for field in fields if field.get("name") not in current_names]
if not missing_fields:
return
await pb.request(
"PATCH",
f"/api/collections/{collection.get('id') or name}",
admin=True,
json={field_key: [*current_fields, *missing_fields]},
)
print(f"collection updated: {name} +{len(missing_fields)} fields")
async def create_collection(name: str, fields: list[dict[str, Any]]) -> None:
if await collection_exists(name):
await ensure_collection_fields(name, fields)
print(f"collection exists: {name}")
return
payload = {
"name": name,
"type": "base",
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"fields": fields,
}
await pb.request("POST", "/api/collections", admin=True, json=payload)
print(f"collection created: {name}")
async def seed_if_empty(collection: str, records: list[dict[str, Any]]) -> None:
data = await pb.list_records(collection, per_page=1)
if data.get("totalItems", 0) > 0:
print(f"seed skipped: {collection}")
return
for record in records:
payload = dict(record)
if collection == "meetups" and payload.get("date"):
payload["date"] = f"{payload['date']} 00:00:00.000Z"
payload.setdefault("status", "published")
await pb.create_record(collection, payload)
print(f"seeded {collection}: {len(records)} records")
async def upsert_seed(collection: str, key: str, records: list[dict[str, Any]]) -> None:
count = 0
for record in records:
payload = dict(record)
if collection == "meetups" and payload.get("date") and "T" not in str(payload["date"]):
payload["date"] = f"{payload['date']} 00:00:00.000Z"
payload.setdefault("status", "published")
await pb.upsert_by_field(collection, key, str(payload[key]), payload)
count += 1
print(f"upserted {collection}: {count} records")
async def main() -> None:
for name, fields in COLLECTIONS.items():
await create_collection(name, fields)
await upsert_seed("cities", "slug", CITIES)
await seed_if_empty("meetups", MEETUPS)
await seed_if_empty("discussions", DISCUSSIONS)
await seed_if_empty("gigs", GIGS)
await seed_if_empty("profiles", PROFILES)
await upsert_seed("meetups", "venue", ENHANCED_MEETUPS)
await upsert_seed("discussions", "title", ENHANCED_DISCUSSIONS)
await upsert_seed("gigs", "title", ENHANCED_GIGS)
await upsert_seed("profiles", "name", ENHANCED_PROFILES)
await upsert_seed("content_items", "slug", CONTENT_ITEMS)
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__":
asyncio.run(main())