Initial project commit
This commit is contained in:
@@ -0,0 +1,650 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user