chore(seed): 种子脚本去 sqlite 直连 + 大厅/社区演示种子 + 大厅单测
- scripts/db/seed.py:培训种子与 accounts→users 迁移改经 training 数据层(双方言) - scripts/db/seed_hall_community.py:人才/岗位/服务/帖子演示数据(幂等,离线执行) - tests/test_hall.py:人才档案/帖子点赞评论计数/register 报名状态机覆盖
This commit is contained in:
+23
-26
@@ -24,27 +24,23 @@ log = logging.getLogger("db.seed")
|
||||
|
||||
|
||||
def _seed_training() -> None:
|
||||
"""培训种子(唯一总库 app.db):排期 + 在线课程(幂等)。
|
||||
"""培训种子(统一总库):排期 + 在线课程(幂等)。
|
||||
|
||||
不再向旧 ``accounts`` 表种子账号 —— 平台规范账号已由 platform_seed 写入
|
||||
``users``(如 u_demo_01/pine),旧 accounts 为「多余账号仓」已被清理
|
||||
(见 scripts/db/cleanup_accounts.py),此处不再重复造账号。
|
||||
经 training 数据层(SQLAlchemy,方言由 DATABASE_URL 决定)读写,
|
||||
不再直连 sqlite3。不向旧 ``accounts`` 表种子账号 —— 平台规范账号由
|
||||
platform_seed 写入 ``users``。
|
||||
"""
|
||||
import sqlite3
|
||||
from app.training import db as tdb
|
||||
conn = sqlite3.connect(tdb.DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
if conn.execute("SELECT COUNT(*) AS c FROM events").fetchone()["c"] == 0:
|
||||
if not tdb.list_all("events"):
|
||||
for e in tdb.SEED_EVENTS:
|
||||
conn.execute("INSERT INTO events (id,type,mode,title,subtitle,desc,location,host,image,link,start_at,duration_min,capacity,status) VALUES (:id,:type,:mode,:title,:subtitle,:desc,:location,:host,:image,:link,:start_at,:duration_min,:capacity,:status)", e)
|
||||
if conn.execute("SELECT COUNT(*) AS c FROM courses").fetchone()["c"] == 0:
|
||||
tdb.insert("events", e)
|
||||
if not tdb.list_all("courses"):
|
||||
for c in tdb.SEED_COURSES:
|
||||
conn.execute("INSERT INTO courses (id,category,level,title,subtitle,desc,price,status,start_at,end_at,venue,quota,image,host,created_at) VALUES (:id,:category,:level,:title,:subtitle,:desc,:price,:status,:start_at,:end_at,:venue,:quota,:image,:host,:created_at)", c)
|
||||
conn.commit()
|
||||
log.info("培训种子完成(账号/排期/课程)")
|
||||
finally:
|
||||
conn.close()
|
||||
tdb.insert("courses", c)
|
||||
log.info("培训种子完成(排期/课程)")
|
||||
except Exception as e: # noqa: BLE001
|
||||
log.warning("培训种子跳过(表可能未迁移):%s", e)
|
||||
|
||||
|
||||
async def _migrate_accounts_to_users(db) -> None:
|
||||
@@ -54,18 +50,20 @@ async def _migrate_accounts_to_users(db) -> None:
|
||||
且并入全局唯一账号体系。迁移/种子由用户执行(铁律),禁止运行态调用。
|
||||
"""
|
||||
import secrets
|
||||
import sqlite3
|
||||
from app.training import db as tdb
|
||||
from app.api.routers.auth import _ensure_opc_identity
|
||||
|
||||
conn = sqlite3.connect(tdb.DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
migrated = created = 0
|
||||
try:
|
||||
for row in conn.execute("SELECT * FROM accounts"):
|
||||
username = (row["username"] or "").strip()
|
||||
wxid = (row["wxid"] or "").strip()
|
||||
phone = (row["phone"] or "").strip()
|
||||
rows = tdb.list_all("accounts")
|
||||
except Exception as e: # noqa: BLE001
|
||||
log.warning("accounts 读取失败,跳过迁移:%s", e)
|
||||
return
|
||||
try:
|
||||
for row in rows:
|
||||
username = (row.get("username") or "").strip()
|
||||
wxid = (row.get("wxid") or "").strip()
|
||||
phone = (row.get("phone") or "").strip()
|
||||
if not username and not wxid and not phone:
|
||||
continue
|
||||
user = (await db.users.get_by_username(username)) if username else None
|
||||
@@ -78,7 +76,7 @@ async def _migrate_accounts_to_users(db) -> None:
|
||||
uname = (username or phone or f"wx_{wxid[:24]}")
|
||||
user = await db.users.create(
|
||||
username=uname, password=secrets.token_hex(16),
|
||||
nickname=(row["name"] or uname), avatar=(row["avatar"] or ""),
|
||||
nickname=(row.get("name") or uname), avatar=(row.get("avatar") or ""),
|
||||
phone=phone, wx_mini_openid=wxid or "",
|
||||
wx_openid=(f"wx_{wxid[:24]}" if wxid else ""),
|
||||
source="mini_program",
|
||||
@@ -88,12 +86,11 @@ async def _migrate_accounts_to_users(db) -> None:
|
||||
created += 1
|
||||
await _ensure_opc_identity(db, user["id"])
|
||||
# 迁移即接管:删掉旧 accounts 行,避免同一逻辑账号落在两套表(多余账号)。
|
||||
conn.execute("DELETE FROM accounts WHERE id=?", (row["id"],))
|
||||
tdb.delete_row("accounts", row["id"])
|
||||
migrated += 1
|
||||
conn.commit()
|
||||
log.info("accounts→users 迁移完成:共 %s 条,新建 %s 条(旧 accounts 行已清理)", migrated, created)
|
||||
finally:
|
||||
conn.close()
|
||||
tdb.reset_engine()
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""大厅与社区演示种子(非运行态,幂等)—— 0035 迁移后执行。
|
||||
|
||||
用法:uv run python scripts/db/seed_hall_community.py
|
||||
灌入:人才档案 / 岗位 / OPC 服务 / 社区帖子演示数据(挂到演示账号 u_demo_01 / u_opc_01)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
from app.infrastructure.hall_repositories import (
|
||||
CommunityPostRepository,
|
||||
JobRepository,
|
||||
OpcServiceRepository,
|
||||
TalentProfileRepository,
|
||||
)
|
||||
from app.infrastructure.repositories import Database, utcnow_iso
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("db.seed_hall")
|
||||
|
||||
|
||||
TALENTS = [
|
||||
{"user_id": "u_demo_01", "talent_type": "opc", "display_name": "阿岩",
|
||||
"headline": "AI 应用开发 · 全栈 · 昆明", "region": "昆明",
|
||||
"bio": "专注 AI 应用落地:智能客服、内容生成、小程序全栈。带过 3 个创业团队技术线。",
|
||||
"fields_json": json.dumps(["AI应用", "技术开发"], ensure_ascii=False),
|
||||
"skills_json": json.dumps(["Python", "FastAPI", "小程序", "Prompt工程"], ensure_ascii=False),
|
||||
"work_years": 6, "cases_count": 12},
|
||||
{"user_id": "u_opc_01", "talent_type": "opc", "display_name": "小玉",
|
||||
"headline": "品牌设计 · 短视频剪辑 · 大理", "region": "大理",
|
||||
"bio": "服务过 30+ 农特产品牌的视觉与内容,擅长用 AI 把设计产能放大。",
|
||||
"fields_json": json.dumps(["设计创意", "音视频"], ensure_ascii=False),
|
||||
"skills_json": json.dumps(["PS", "AI绘画", "剪辑"], ensure_ascii=False),
|
||||
"work_years": 4, "cases_count": 30},
|
||||
]
|
||||
|
||||
JOBS = [
|
||||
{"company_name": "云南某某甲方企业", "title": "小程序开发工程师", "category": "技术",
|
||||
"description": "负责公司电商小程序的功能迭代与性能优化。", "requirement": "1 年以上小程序开发经验,熟悉 Taro 或原生。",
|
||||
"tags": "小程序,前端", "salary_min": 6000, "salary_max": 10000, "location": "昆明·五华",
|
||||
"work_type": "fulltime", "headcount": 1, "contact": "hr@example.cn"},
|
||||
{"company_name": "大理文旅传媒", "title": "短视频运营", "category": "运营",
|
||||
"description": "负责文旅账号的内容策划与短视频运营。", "requirement": "有短视频账号运营案例者优先。",
|
||||
"tags": "短视频,文旅", "salary_min": 4500, "salary_max": 8000, "location": "大理",
|
||||
"work_type": "fulltime", "headcount": 2, "contact": "hr@wl.cn"},
|
||||
]
|
||||
|
||||
SERVICES = [
|
||||
{"opc_id": "u_demo_01", "opc_name": "阿岩", "title": "企业官网 + AI 客服一站式搭建",
|
||||
"category": "软件开发", "description": "官网设计开发 + 接入 AI 智能客服,两周交付,含一年维护。",
|
||||
"price": 8000, "delivery_days": 14, "tags": "官网,AI客服,全栈", "status": "published"},
|
||||
{"opc_id": "u_opc_01", "opc_name": "小玉", "title": "农特产品牌全套视觉(Logo+包装+详情页)",
|
||||
"category": "设计创意", "description": "面向云南农特产品牌:Logo、包装、电商详情页一套齐。",
|
||||
"price": 5000, "delivery_days": 10, "tags": "Logo,包装,详情页", "status": "published"},
|
||||
]
|
||||
|
||||
POSTS = [
|
||||
{"author_name": "阿岩", "topic": "经验", "title": "一个人接单两年,我踩过的 5 个坑",
|
||||
"content": "从报价到合同到尾款……每一条都是真金白银换来的经验,欢迎补充。", "pinned": True},
|
||||
{"author_name": "小玉", "topic": "资源", "title": "整理了一波云南本地可用的 AI 工具折扣",
|
||||
"content": "算力、设计、剪辑三类,评论区自取,需要邀请码的私信我。"},
|
||||
{"author_name": "李同学", "topic": "求助", "title": "应届生想入行超级个体,先从什么方向开始?",
|
||||
"content": "市场营销专业,会一点剪辑和文案,求过来人指条路。"},
|
||||
]
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
db = Database()
|
||||
try:
|
||||
users = {u["username"]: u for u in []} # 占位:直接按 id 取
|
||||
demo = await db.users.get_by_id("u_demo_01") or await db.users.get_by_username("u_demo_01") or {}
|
||||
opc = await db.users.get_by_id("u_opc_01") or await db.users.get_by_username("opc01") or {}
|
||||
talent_repo = TalentProfileRepository(db.session)
|
||||
job_repo = JobRepository(db.session)
|
||||
svc_repo = OpcServiceRepository(db.session)
|
||||
post_repo = CommunityPostRepository(db.session)
|
||||
now = utcnow_iso()
|
||||
|
||||
for t in TALENTS:
|
||||
uid = t["user_id"]
|
||||
user = demo if uid == "u_demo_01" else (opc or None)
|
||||
if user is None:
|
||||
continue # 演示账号不存在则跳过该档案
|
||||
patch = dict(t)
|
||||
patch.pop("user_id")
|
||||
patch["talentType"] = patch.pop("talent_type")
|
||||
patch["displayName"] = patch.get("displayName") or user.get("nickname")
|
||||
patch["avatar"] = user.get("avatar", "")
|
||||
patch["display_name"] = patch["displayName"]
|
||||
patch["fields"] = json.loads(patch.pop("fields_json"))
|
||||
patch["skills"] = json.loads(patch.pop("skills_json"))
|
||||
await talent_repo.upsert(uid, patch)
|
||||
log.info("人才档案种子完成")
|
||||
|
||||
if not await job_repo.list(status="", q="小程序开发工程师"):
|
||||
for j in JOBS:
|
||||
await job_repo.create(status="published", published_at=now, **j)
|
||||
log.info("岗位种子完成")
|
||||
|
||||
if not await svc_repo.list(q="企业官网"):
|
||||
for s in SERVICES:
|
||||
owner = demo if s["opc_id"] == "u_demo_01" else opc
|
||||
fields = {k: v for k, v in s.items() if k not in ("opc_id", "opc_name", "status")}
|
||||
await svc_repo.create(status="published",
|
||||
opc_id=owner.get("id") or s["opc_id"],
|
||||
opc_name=owner.get("nickname") or s["opc_name"], **fields)
|
||||
log.info("OPC 服务种子完成")
|
||||
|
||||
if not await post_repo.list(status=""):
|
||||
author = demo or opc
|
||||
for p in POSTS:
|
||||
await post_repo.create(author_id=author.get("id"), avatar=author.get("avatar", ""),
|
||||
status="published", pinned=p.get("pinned", False),
|
||||
**{k: v for k, v in p.items() if k != "pinned"})
|
||||
log.info("社区帖子种子完成")
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run())
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""大厅与社区自动化验证(0035)。
|
||||
|
||||
覆盖:人才档案 upsert / 帖子点赞评论计数 / TaskService.register 报名状态机
|
||||
(报名成功、重复拒绝、名额上限、选定转 assigned)。
|
||||
临时 SQLite 隔离,不依赖外部库。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from app.infrastructure.hall_repositories import (
|
||||
CommunityCommentRepository,
|
||||
CommunityLikeRepository,
|
||||
CommunityPostRepository,
|
||||
TalentProfileRepository,
|
||||
)
|
||||
from app.infrastructure.models import Task
|
||||
from app.infrastructure.repositories import Database
|
||||
from app.services.task_service import TaskService
|
||||
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def db(tmp_path, monkeypatch):
|
||||
url = f"sqlite+aiosqlite:///{tmp_path}/hall.db"
|
||||
d = Database(db_url=url)
|
||||
from app.infrastructure.db import Base
|
||||
async with d._engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield d
|
||||
await d.close()
|
||||
|
||||
|
||||
async def _mk_task(db: Database, **kw) -> dict:
|
||||
t = Task(id="task_hall_1", title="报名测试任务", mode="register", status="published",
|
||||
register_quota=kw.pop("register_quota", 2), c_visible=True,
|
||||
publisher_name="测试发包方")
|
||||
for k, v in kw.items():
|
||||
setattr(t, k, v)
|
||||
db.session.add(t)
|
||||
await db.session.commit()
|
||||
return {"id": t.id, "status": t.status, "mode": t.mode, "register_quota": t.register_quota,
|
||||
"visibility": "public", "park_id": "", "assign_type": "", "eligibility": "{}"}
|
||||
|
||||
|
||||
ACTOR = {"id": "u_actor", "username": "actor", "nickname": "接单人"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_talent_profile_upsert_and_list(db):
|
||||
repo = TalentProfileRepository(db.session)
|
||||
await repo.upsert("u_t1", {"displayName": "阿岩", "talentType": "opc",
|
||||
"headline": "AI 开发", "fields": ["AI应用"], "skills": ["Python"]})
|
||||
got = await repo.get("u_t1")
|
||||
assert got["displayName"] == "阿岩" and got["fields"] == ["AI应用"]
|
||||
rows = await repo.list(q="AI")
|
||||
assert any(r["userId"] == "u_t1" for r in rows)
|
||||
# 下架后列表不可见
|
||||
await repo.set_published("u_t1", False)
|
||||
assert not any(r["userId"] == "u_t1" for r in await repo.list())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_community_post_like_comment_counts(db):
|
||||
posts = CommunityPostRepository(db.session)
|
||||
likes = CommunityLikeRepository(db.session)
|
||||
cmts = CommunityCommentRepository(db.session)
|
||||
p = await posts.create(author_id="u_a", author_name="作者", title="第一帖",
|
||||
content="内容", topic="交流", status="published")
|
||||
pid = p["id"]
|
||||
await likes.add("post", pid, "u1")
|
||||
await likes.add("post", pid, "u2")
|
||||
await posts.inc_likes(pid, 2)
|
||||
assert (await posts.get(pid))["likeCount"] == 2
|
||||
await likes.remove("post", pid, "u1")
|
||||
await posts.inc_likes(pid, -1)
|
||||
assert (await posts.get(pid))["likeCount"] == 1
|
||||
await cmts.create(pid, "u2", "评论人", "赞一个")
|
||||
await posts.inc_comments(pid, 1)
|
||||
assert (await posts.get(pid))["commentCount"] == 1
|
||||
assert len(await cmts.list_for_post(pid)) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_flow(db):
|
||||
svc = TaskService(db)
|
||||
task = await _mk_task(db, register_quota=2)
|
||||
claim = await svc.register(task["id"], ACTOR)
|
||||
assert claim["status"] == "registered" and claim["claim_source"] == "register"
|
||||
with pytest.raises(Exception) as e:
|
||||
await svc.register(task["id"], ACTOR)
|
||||
assert getattr(e.value, "status_code", 0) == 400 # 重复报名拒绝
|
||||
other = {"id": "u_b", "username": "b", "nickname": "乙"}
|
||||
await svc.register(task["id"], other)
|
||||
third = {"id": "u_c", "username": "c", "nickname": "丙"}
|
||||
with pytest.raises(Exception) as e2:
|
||||
await svc.register(task["id"], third)
|
||||
assert getattr(e2.value, "status_code", 0) == 400 # 名额已满
|
||||
# 选定乙 → assigned + 任务 claimed,其余 withdrawn
|
||||
claims = {c["claimer_user_id"]: c for c in await db.task_claims.list_by_task(task["id"])}
|
||||
await svc.register_select(task["id"], claims["u_b"]["id"], ACTOR)
|
||||
assert (await db.task_claims.get(claims["u_b"]["id"]))["status"] == "assigned"
|
||||
assert (await db.task_claims.get(claims["u_actor"]["id"]))["status"] == "withdrawn"
|
||||
assert (await db.tasks.get(task["id"]))["status"] == "claimed"
|
||||
Reference in New Issue
Block a user