import json from datetime import datetime, timezone from sqlalchemy import func, select from sqlalchemy.orm import Session import app.models # noqa: F401 - ensure models are registered from app.core.security import get_password_hash from app.db.session import sync_engine from app.models.audit_log import AuditLog from app.models.comment import Comment from app.models.favorite import Favorite from app.models.point_ledger import PointLedger from app.models.rating import Rating from app.models.report import Report from app.models.spot import Spot, SpotImage from app.models.tag import SpotTag, Tag from app.models.user import User DEMO_PASSWORD = "demo123456" def get_or_create(session: Session, model, filters: dict, defaults: dict | None = None): instance = session.execute(select(model).filter_by(**filters)).scalar_one_or_none() if instance is not None: return instance, False payload = dict(filters) if defaults: payload.update(defaults) instance = model(**payload) session.add(instance) session.flush() return instance, True def ensure_user( session: Session, *, phone: str, email: str, nickname: str, city: str, identity: str, role: str, ): user, _ = get_or_create( session, User, {"phone": phone}, { "email": email, "password_hash": get_password_hash(DEMO_PASSWORD), "nickname": nickname, "city": city, "identity": identity, "role": role, "is_active": True, "avatar_url": f"https://picsum.photos/seed/{phone[-4:]}/300/300", }, ) changed = False expected = { "email": email, "nickname": nickname, "city": city, "identity": identity, "role": role, "is_active": True, } for field, value in expected.items(): if getattr(user, field) != value: setattr(user, field, value) changed = True if not user.password_hash.startswith("$2"): user.password_hash = get_password_hash(DEMO_PASSWORD) changed = True if changed: session.add(user) session.flush() return user def ensure_tag(session: Session, *, name: str, category: str, usage_count: int): tag, _ = get_or_create( session, Tag, {"name": name}, { "category": category, "usage_count": usage_count, "is_active": True, }, ) tag.category = category tag.usage_count = usage_count tag.is_active = True session.add(tag) session.flush() return tag def ensure_spot( session: Session, *, title: str, city: str, longitude: float, latitude: float, description: str, transport: str, best_time: str, difficulty: str, audit_status: str, reject_reason: str | None, creator_id: int, ): spot, _ = get_or_create( session, Spot, {"title": title}, { "city": city, "description": description, "transport": transport, "best_time": best_time, "difficulty": difficulty, "audit_status": audit_status, "reject_reason": reject_reason, "creator_id": creator_id, "location": func.ST_SetSRID(func.ST_MakePoint(longitude, latitude), 4326), }, ) spot.city = city spot.description = description spot.transport = transport spot.best_time = best_time spot.difficulty = difficulty spot.audit_status = audit_status spot.reject_reason = reject_reason spot.creator_id = creator_id spot.location = func.ST_SetSRID(func.ST_MakePoint(longitude, latitude), 4326) session.add(spot) session.flush() return spot def ensure_spot_image( session: Session, *, spot_id: int, image_url: str, is_cover: bool, sort_order: int, audit_status: str = "approved", ): image, _ = get_or_create( session, SpotImage, {"spot_id": spot_id, "image_url": image_url}, { "is_cover": is_cover, "sort_order": sort_order, "audit_status": audit_status, }, ) image.is_cover = is_cover image.sort_order = sort_order image.audit_status = audit_status session.add(image) session.flush() return image def ensure_spot_tag(session: Session, *, spot_id: int, tag_id: int): get_or_create(session, SpotTag, {"spot_id": spot_id, "tag_id": tag_id}) def ensure_favorite(session: Session, *, user_id: int, spot_id: int): get_or_create(session, Favorite, {"user_id": user_id, "spot_id": spot_id}) def ensure_rating( session: Session, *, user_id: int, spot_id: int, score: int, short_comment: str, ): rating, _ = get_or_create( session, Rating, {"user_id": user_id, "spot_id": spot_id}, { "score": score, "short_comment": short_comment, }, ) rating.score = score rating.short_comment = short_comment session.add(rating) session.flush() return rating def ensure_comment( session: Session, *, spot_id: int, user_id: int, content: str, parent_id: int | None = None, audit_status: str = "approved", ): comment, _ = get_or_create( session, Comment, { "spot_id": spot_id, "user_id": user_id, "parent_id": parent_id, "content": content, }, {"audit_status": audit_status}, ) comment.audit_status = audit_status session.add(comment) session.flush() return comment def ensure_report( session: Session, *, reporter_id: int, target_type: str, target_id: int, reason: str, status: str, handler_id: int | None, conclusion: str | None, ): report, _ = get_or_create( session, Report, { "reporter_id": reporter_id, "target_type": target_type, "target_id": target_id, "reason": reason, }, { "status": status, "handler_id": handler_id, "conclusion": conclusion, "resolved_at": datetime.now(timezone.utc) if status != "pending" else None, }, ) report.status = status report.handler_id = handler_id report.conclusion = conclusion report.resolved_at = datetime.now(timezone.utc) if status != "pending" else None session.add(report) session.flush() return report def ensure_point_ledger( session: Session, *, user_id: int, change: int, balance: int, reason: str, ref_type: str | None, ref_id: int | None, ): ledger, _ = get_or_create( session, PointLedger, { "user_id": user_id, "change": change, "balance": balance, "reason": reason, "ref_type": ref_type, "ref_id": ref_id, }, ) session.add(ledger) session.flush() return ledger def ensure_audit_log( session: Session, *, operator_id: int, action: str, target_type: str, target_id: int | None, detail: dict | None, ): detail_text = json.dumps(detail, ensure_ascii=False) if detail is not None else None log, _ = get_or_create( session, AuditLog, { "operator_id": operator_id, "action": action, "target_type": target_type, "target_id": target_id, "detail": detail_text, }, ) session.add(log) session.flush() return log def refresh_spot_rating_stats(session: Session, spot: Spot): rows = session.execute(select(Rating).where(Rating.spot_id == spot.id)).scalars().all() if rows: spot.rating_count = len(rows) spot.avg_rating = round(sum(item.score for item in rows) / len(rows), 2) else: spot.rating_count = 0 spot.avg_rating = None session.add(spot) session.flush() def seed(): with Session(sync_engine) as session: admin = ensure_user( session, phone="13900000001", email="demo.admin@ciyuan.local", nickname="演示管理员", city="上海", identity="both", role="admin", ) moderator = ensure_user( session, phone="13900000002", email="demo.moderator@ciyuan.local", nickname="演示审核员", city="杭州", identity="both", role="moderator", ) coser = ensure_user( session, phone="13900000003", email="demo.coser@ciyuan.local", nickname="演示Coser", city="上海", identity="coser", role="user", ) photographer = ensure_user( session, phone="13900000004", email="demo.photo@ciyuan.local", nickname="演示摄影师", city="苏州", identity="photographer", role="user", ) traveler = ensure_user( session, phone="13900000005", email="demo.travel@ciyuan.local", nickname="演示旅拍用户", city="南京", identity="both", role="user", ) sakura = ensure_tag(session, name="演示-樱花", category="季节", usage_count=12) night = ensure_tag(session, name="演示-夜景", category="氛围", usage_count=8) cyber = ensure_tag(session, name="演示-赛博", category="风格", usage_count=6) hanfu = ensure_tag(session, name="演示-古风", category="风格", usage_count=10) street = ensure_tag(session, name="演示-街拍", category="题材", usage_count=9) spot_1 = ensure_spot( session, title="演示-上海静安樱花天桥", city="上海", longitude=121.4452, latitude=31.2336, description="春季樱花盛开时非常适合轻婚纱、JK 和日系角色外拍,桥面视野开阔。", transport="地铁 2 号线步行 8 分钟可达,附近可打车临停。", best_time="3 月下旬到 4 月上旬,清晨 7:00-9:00", difficulty="人流中等,需要提早到场占机位。", audit_status="approved", reject_reason=None, creator_id=coser.id, ) spot_2 = ensure_spot( session, title="演示-苏州工业园区玻璃连廊", city="苏州", longitude=120.7304, latitude=31.3241, description="现代感很强的玻璃连廊,适合赛博、都市未来风格拍摄。", transport="自驾更方便,园区停车位充足。", best_time="傍晚蓝调时刻到入夜后 1 小时", difficulty="夜间补光需求较高。", audit_status="approved", reject_reason=None, creator_id=photographer.id, ) spot_3 = ensure_spot( session, title="演示-南京城墙古风角楼", city="南京", longitude=118.8037, latitude=32.0647, description="城墙转角的古风点位,适合汉服、武侠、国风角色。", transport="地铁直达,步行约 10 分钟。", best_time="上午逆光柔和或傍晚金色时段", difficulty="游客较多,需要避开节假日。", audit_status="pending", reject_reason=None, creator_id=traveler.id, ) spot_4 = ensure_spot( session, title="演示-杭州废墟工业仓", city="杭州", longitude=120.1551, latitude=30.2741, description="老工业仓改造区域,墙体纹理丰富,适合废土和剧情向拍摄。", transport="建议包车前往,器材搬运更方便。", best_time="阴天全天都适合,层次更好。", difficulty="地面不平整,服装和脚下要注意。", audit_status="rejected", reject_reason="场地方临时封闭,当前不允许外拍。", creator_id=photographer.id, ) ensure_spot_image( session, spot_id=spot_1.id, image_url="https://picsum.photos/seed/demo-spot-1-cover/1200/800", is_cover=True, sort_order=0, ) ensure_spot_image( session, spot_id=spot_1.id, image_url="https://picsum.photos/seed/demo-spot-1-2/1200/800", is_cover=False, sort_order=1, ) ensure_spot_image( session, spot_id=spot_2.id, image_url="https://picsum.photos/seed/demo-spot-2-cover/1200/800", is_cover=True, sort_order=0, ) ensure_spot_image( session, spot_id=spot_3.id, image_url="https://picsum.photos/seed/demo-spot-3-cover/1200/800", is_cover=True, sort_order=0, audit_status="pending", ) ensure_spot_image( session, spot_id=spot_4.id, image_url="https://picsum.photos/seed/demo-spot-4-cover/1200/800", is_cover=True, sort_order=0, audit_status="rejected", ) ensure_spot_tag(session, spot_id=spot_1.id, tag_id=sakura.id) ensure_spot_tag(session, spot_id=spot_1.id, tag_id=street.id) ensure_spot_tag(session, spot_id=spot_2.id, tag_id=night.id) ensure_spot_tag(session, spot_id=spot_2.id, tag_id=cyber.id) ensure_spot_tag(session, spot_id=spot_3.id, tag_id=hanfu.id) ensure_spot_tag(session, spot_id=spot_4.id, tag_id=street.id) ensure_favorite(session, user_id=coser.id, spot_id=spot_2.id) ensure_favorite(session, user_id=photographer.id, spot_id=spot_1.id) ensure_favorite(session, user_id=traveler.id, spot_id=spot_1.id) ensure_rating( session, user_id=photographer.id, spot_id=spot_1.id, score=5, short_comment="樱花季氛围特别好,出片稳定。", ) ensure_rating( session, user_id=traveler.id, spot_id=spot_1.id, score=4, short_comment="人稍微有点多,但构图空间不错。", ) ensure_rating( session, user_id=coser.id, spot_id=spot_2.id, score=5, short_comment="夜景和反光材质非常适合赛博片。", ) refresh_spot_rating_stats(session, spot_1) refresh_spot_rating_stats(session, spot_2) refresh_spot_rating_stats(session, spot_3) refresh_spot_rating_stats(session, spot_4) comment_1 = ensure_comment( session, spot_id=spot_1.id, user_id=traveler.id, content="工作日早上人真的少很多,推荐 7 点前到。", ) ensure_comment( session, spot_id=spot_1.id, user_id=coser.id, parent_id=comment_1.id, content="收到,下次准备带反光板去拍。", ) ensure_comment( session, spot_id=spot_2.id, user_id=photographer.id, content="现场环境偏暗,建议准备外拍灯和脚架。", ) resolved_report = ensure_report( session, reporter_id=traveler.id, target_type="spot", target_id=spot_4.id, reason="演示上报:该地点当前施工封闭,信息需要更新。", status="resolved", handler_id=moderator.id, conclusion="已核实并驳回点位展示,等待重新开放。", ) pending_report = ensure_report( session, reporter_id=coser.id, target_type="comment", target_id=comment_1.id, reason="演示上报:评论里包含错误时间信息。", status="pending", handler_id=None, conclusion=None, ) ensure_point_ledger( session, user_id=coser.id, change=10, balance=10, reason="演示地点审核通过奖励", ref_type="spot_approved", ref_id=spot_1.id, ) ensure_point_ledger( session, user_id=photographer.id, change=6, balance=6, reason="演示评分与评论奖励", ref_type="engagement_bonus", ref_id=spot_2.id, ) ensure_point_ledger( session, user_id=traveler.id, change=3, balance=3, reason="演示有效反馈奖励", ref_type="report_reward", ref_id=spot_4.id, ) ensure_audit_log( session, operator_id=moderator.id, action="spot.approve", target_type="spot", target_id=spot_1.id, detail={"note": "演示数据:点位信息完整,允许展示"}, ) ensure_audit_log( session, operator_id=moderator.id, action="spot.reject", target_type="spot", target_id=spot_4.id, detail={"reason": "演示数据:场地方封闭"}, ) ensure_audit_log( session, operator_id=admin.id, action="report.resolve", target_type="report", target_id=resolved_report.id, detail={"result": "演示工单已关闭"}, ) ensure_audit_log( session, operator_id=admin.id, action="report.review", target_type="report", target_id=pending_report.id, detail={"result": "演示工单待处理"}, ) session.commit() print("Demo data ready.") print(f"Demo password for all demo users: {DEMO_PASSWORD}") for table, model in [ ("users", User), ("spots", Spot), ("spot_images", SpotImage), ("favorites", Favorite), ("point_ledger", PointLedger), ("audit_logs", AuditLog), ("comments", Comment), ("reports", Report), ("ratings", Rating), ("tags", Tag), ("spot_tags", SpotTag), ]: total = session.execute(select(func.count()).select_from(model)).scalar_one() print(f"{table}: {total}") if __name__ == "__main__": seed()