chore(scripts): 演示数据临时脚本(资讯4类×25条+活动40场,手动 add/del,不入种子迁移)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"content_ids": [
|
||||
"cont_demo_policy_000",
|
||||
"cont_demo_policy_001",
|
||||
"cont_demo_policy_002",
|
||||
"cont_demo_policy_003",
|
||||
"cont_demo_policy_004",
|
||||
"cont_demo_policy_005",
|
||||
"cont_demo_policy_006",
|
||||
"cont_demo_policy_007",
|
||||
"cont_demo_policy_008",
|
||||
"cont_demo_policy_009",
|
||||
"cont_demo_policy_010",
|
||||
"cont_demo_policy_011",
|
||||
"cont_demo_policy_012",
|
||||
"cont_demo_policy_013",
|
||||
"cont_demo_policy_014",
|
||||
"cont_demo_policy_015",
|
||||
"cont_demo_policy_016",
|
||||
"cont_demo_policy_017",
|
||||
"cont_demo_policy_018",
|
||||
"cont_demo_policy_019",
|
||||
"cont_demo_policy_020",
|
||||
"cont_demo_policy_021",
|
||||
"cont_demo_policy_022",
|
||||
"cont_demo_policy_023",
|
||||
"cont_demo_policy_024",
|
||||
"cont_demo_news_000",
|
||||
"cont_demo_news_001",
|
||||
"cont_demo_news_002",
|
||||
"cont_demo_news_003",
|
||||
"cont_demo_news_004",
|
||||
"cont_demo_news_005",
|
||||
"cont_demo_news_006",
|
||||
"cont_demo_news_007",
|
||||
"cont_demo_news_008",
|
||||
"cont_demo_news_009",
|
||||
"cont_demo_news_010",
|
||||
"cont_demo_news_011",
|
||||
"cont_demo_news_012",
|
||||
"cont_demo_news_013",
|
||||
"cont_demo_news_014",
|
||||
"cont_demo_news_015",
|
||||
"cont_demo_news_016",
|
||||
"cont_demo_news_017",
|
||||
"cont_demo_news_018",
|
||||
"cont_demo_news_019",
|
||||
"cont_demo_news_020",
|
||||
"cont_demo_news_021",
|
||||
"cont_demo_news_022",
|
||||
"cont_demo_news_023",
|
||||
"cont_demo_news_024",
|
||||
"cont_demo_skill_000",
|
||||
"cont_demo_skill_001",
|
||||
"cont_demo_skill_002",
|
||||
"cont_demo_skill_003",
|
||||
"cont_demo_skill_004",
|
||||
"cont_demo_skill_005",
|
||||
"cont_demo_skill_006",
|
||||
"cont_demo_skill_007",
|
||||
"cont_demo_skill_008",
|
||||
"cont_demo_skill_009",
|
||||
"cont_demo_skill_010",
|
||||
"cont_demo_skill_011",
|
||||
"cont_demo_skill_012",
|
||||
"cont_demo_skill_013",
|
||||
"cont_demo_skill_014",
|
||||
"cont_demo_skill_015",
|
||||
"cont_demo_skill_016",
|
||||
"cont_demo_skill_017",
|
||||
"cont_demo_skill_018",
|
||||
"cont_demo_skill_019",
|
||||
"cont_demo_skill_020",
|
||||
"cont_demo_skill_021",
|
||||
"cont_demo_skill_022",
|
||||
"cont_demo_skill_023",
|
||||
"cont_demo_skill_024",
|
||||
"cont_demo_dynamic_000",
|
||||
"cont_demo_dynamic_001",
|
||||
"cont_demo_dynamic_002",
|
||||
"cont_demo_dynamic_003",
|
||||
"cont_demo_dynamic_004",
|
||||
"cont_demo_dynamic_005",
|
||||
"cont_demo_dynamic_006",
|
||||
"cont_demo_dynamic_007",
|
||||
"cont_demo_dynamic_008",
|
||||
"cont_demo_dynamic_009",
|
||||
"cont_demo_dynamic_010",
|
||||
"cont_demo_dynamic_011",
|
||||
"cont_demo_dynamic_012",
|
||||
"cont_demo_dynamic_013",
|
||||
"cont_demo_dynamic_014",
|
||||
"cont_demo_dynamic_015",
|
||||
"cont_demo_dynamic_016",
|
||||
"cont_demo_dynamic_017",
|
||||
"cont_demo_dynamic_018",
|
||||
"cont_demo_dynamic_019",
|
||||
"cont_demo_dynamic_020",
|
||||
"cont_demo_dynamic_021",
|
||||
"cont_demo_dynamic_022",
|
||||
"cont_demo_dynamic_023",
|
||||
"cont_demo_dynamic_024"
|
||||
],
|
||||
"event_ids": [
|
||||
"E-DEMO-000",
|
||||
"E-DEMO-001",
|
||||
"E-DEMO-002",
|
||||
"E-DEMO-003",
|
||||
"E-DEMO-004",
|
||||
"E-DEMO-005",
|
||||
"E-DEMO-006",
|
||||
"E-DEMO-007",
|
||||
"E-DEMO-008",
|
||||
"E-DEMO-009",
|
||||
"E-DEMO-010",
|
||||
"E-DEMO-011",
|
||||
"E-DEMO-012",
|
||||
"E-DEMO-013",
|
||||
"E-DEMO-014",
|
||||
"E-DEMO-015",
|
||||
"E-DEMO-016",
|
||||
"E-DEMO-017",
|
||||
"E-DEMO-018",
|
||||
"E-DEMO-019",
|
||||
"E-DEMO-020",
|
||||
"E-DEMO-021",
|
||||
"E-DEMO-022",
|
||||
"E-DEMO-023",
|
||||
"E-DEMO-024",
|
||||
"E-DEMO-025",
|
||||
"E-DEMO-026",
|
||||
"E-DEMO-027",
|
||||
"E-DEMO-028",
|
||||
"E-DEMO-029",
|
||||
"E-DEMO-030",
|
||||
"E-DEMO-031",
|
||||
"E-DEMO-032",
|
||||
"E-DEMO-033",
|
||||
"E-DEMO-034",
|
||||
"E-DEMO-035",
|
||||
"E-DEMO-036",
|
||||
"E-DEMO-037",
|
||||
"E-DEMO-038",
|
||||
"E-DEMO-039"
|
||||
],
|
||||
"note": "demo_content_tmp.py 灌入的演示数据清单(del 时按此精确删除)"
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""【临时演示数据 · 手动运行 · 用完执行 del 清除】uv run python scripts/db/demo_content_tmp.py add|del
|
||||
|
||||
add —— 灌入演示数据:资讯 4 类别 × 25 条(大小图/官方·园区·OPC 混排)+ 活动 40 场(类型×线上线下×状态)
|
||||
del —— 仅删除本脚本灌入的那批演示数据(按记录在 manifest 里的 id 精确删除,不动其它数据)
|
||||
说明:id 全部确定性生成(cont_demo_* / E-DEMO-*),并同步写入 demo_content_tmp.ids.json 备查。
|
||||
不入种子、不进迁移;演示完执行 del 即可。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
from app.infrastructure.db import AsyncSessionLocal # noqa: E402
|
||||
from app.training.db import DB_PATH as TRAINING_DB # noqa: E402
|
||||
from app.infrastructure.models import ContentItem # noqa: E402
|
||||
from app.infrastructure.seed import utcnow_iso # noqa: E402
|
||||
|
||||
MANIFEST = Path(__file__).resolve().parent / "demo_content_tmp.ids.json"
|
||||
|
||||
# ── 生成参数 ────────────────────────────────────────────────────────────────
|
||||
CATS = ["policy", "news", "skill", "dynamic"]
|
||||
PER_CAT = 25 # 每类别条数(合计 100 条资讯)
|
||||
EVENTS_N = 40 # 活动场数
|
||||
SOURCES = [("official", "官方"), ("carrier", "园区运营方"), ("opc", "云南某某工作室")]
|
||||
TOPICS = {
|
||||
"policy": ["创业补贴", "创业担保贷款", "税收减免", "社保补贴", "场地扶持", "职称评定", "技能培训补贴", "青年见习", "创业导师", "园区入驻", "人才引进", "数字转型"],
|
||||
"news": ["平台上线", "公益课回顾", "园区动态", "OPC 会员风采", "合作签约", "赛事启动", "政策解读会", "创业者访谈", "月度数据", "生态共建", "算力升级", "社区活动"],
|
||||
"skill": ["AI 内容生产", "定价四步法", "路演 PPT", "财税入门", "私域运营", "短视频起号", "跨境选品", "合同避坑", "个人品牌", "效率工具", "数据分析", "直播带货"],
|
||||
"dynamic": ["新成员入驻", "企业动态", "平台周更", "榜单发布", "活动排期", "服务升级", "导师入库", "资源对接", "空间改造", "认证公示", "积分商城", "周年活动"],
|
||||
}
|
||||
HINT = "(演示数据,可在运营端删除)"
|
||||
|
||||
TITLE_POOL = ["实战指南", "入门手册", "案例拆解", "常见问题", "申报要点", "趋势观察", "工具盘点", "流程图解", "避坑清单", "一手复盘"]
|
||||
|
||||
_all_titles = [(cat, i) for cat in CATS for i in range(PER_CAT)]
|
||||
|
||||
|
||||
def _content_id(cat: str, i: int) -> str:
|
||||
return f"cont_demo_{cat}_{i:03d}"
|
||||
|
||||
|
||||
def _event_id(i: int) -> str:
|
||||
return f"E-DEMO-{i:03d}"
|
||||
|
||||
|
||||
def _iso(days_ago: float, hour: int = 9) -> str:
|
||||
from datetime import datetime, timedelta, timezone
|
||||
d = datetime.now(timezone.utc) - timedelta(days=days_ago)
|
||||
return d.replace(hour=hour, minute=(int(days_ago * 60) % 60), second=0, microsecond=0).isoformat()
|
||||
|
||||
|
||||
def _build_content() -> list[ContentItem]:
|
||||
items: list[ContentItem] = []
|
||||
for cat, i in _all_titles:
|
||||
topic = TOPICS[cat][i % len(TOPICS[cat])]
|
||||
title = f"{topic}·{TITLE_POOL[i % len(TITLE_POOL)]}{HINT}"
|
||||
source, pub = SOURCES[i % 3]
|
||||
mode = "big" if i % 3 == 0 else "small" # 1/3 大图、2/3 小图
|
||||
prio = 200 - i # 越新越靠前
|
||||
items.append(ContentItem(
|
||||
id=_content_id(cat, i), type=cat, title=title,
|
||||
summary=f"{topic}方向的演示摘要:覆盖要点、流程与常见疑问,帮助创业者快速上手。",
|
||||
body=("<p>本条为<b>演示数据</b>,用于验证资讯中心在各类别、各发布方与大小图模式下的展示效果。</p>"
|
||||
f"<p>类别:{cat} · 发布方:{pub} · 模式:{mode}</p>"
|
||||
"<p>删除方式:uv run python scripts/db/demo_content_tmp.py del</p>"),
|
||||
publisher_id="", publisher_name=pub, status="published", is_public=True,
|
||||
card_mode=mode, source=source, priority=prio,
|
||||
read_count=100 + i * 37, like_count=(i * 7) % 90, share_count=(i * 3) % 40,
|
||||
published_at=_iso(i * 0.4 + 1), created_at=_iso(i * 0.4 + 1), updated_at=utcnow_iso(),
|
||||
))
|
||||
return items
|
||||
|
||||
|
||||
def _build_events() -> list[dict]:
|
||||
evs = []
|
||||
types = [("free", "公益课"), ("salon", "沙龙")]
|
||||
modes = [("offline", "昆明市大学生创业园"), ("online", "视频号直播")]
|
||||
statuses = ["open", "open", "full", "done"]
|
||||
for i in range(EVENTS_N):
|
||||
etype, tlabel = types[i % 2]
|
||||
mode, loc = modes[(i // 2) % 2]
|
||||
status = statuses[i % 4]
|
||||
topic = TOPICS["skill"][(i * 5) % len(TOPICS["skill"])]
|
||||
evs.append({
|
||||
"id": _event_id(i), "type": etype, "mode": mode,
|
||||
"title": f"{topic}{tlabel}第 {i + 1} 场(演示)", "subtitle": f"{tlabel} · {'线上' if mode == 'online' else '线下'}场",
|
||||
"desc": "演示场次:覆盖类型×模式×状态的组合维度。删除方式:uv run python scripts/db/demo_content_tmp.py del",
|
||||
"location": loc, "host": "云超服 · OPC 培训",
|
||||
"image": f"https://picsum.photos/seed/opc-demo-{i}/600/400", "link": "",
|
||||
"start_at": _iso(-((i % 15) + 1), 14 if etype == "salon" else 19)[:19] + "+08:00",
|
||||
"duration_min": 120 if etype == "salon" else 90,
|
||||
"capacity": 30 if etype == "salon" else 60, "status": status,
|
||||
})
|
||||
return evs
|
||||
|
||||
|
||||
async def _add() -> None:
|
||||
items = _build_content()
|
||||
async with AsyncSessionLocal() as session:
|
||||
added = 0
|
||||
for it in items:
|
||||
if await session.get(ContentItem, it.id) is None:
|
||||
session.add(it)
|
||||
added += 1
|
||||
await session.commit()
|
||||
|
||||
conn = sqlite3.connect(TRAINING_DB)
|
||||
conn.row_factory = sqlite3.Row
|
||||
ev_added = 0
|
||||
try:
|
||||
for e in _build_events():
|
||||
dup = conn.execute("SELECT 1 FROM events WHERE id=:id", {"id": e["id"]}).fetchone()
|
||||
if dup:
|
||||
continue
|
||||
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)
|
||||
ev_added += 1
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
MANIFEST.write_text(json.dumps({
|
||||
"content_ids": [it.id for it in items],
|
||||
"event_ids": [e["id"] for e in _build_events()],
|
||||
"note": "demo_content_tmp.py 灌入的演示数据清单(del 时按此精确删除)",
|
||||
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"add 完成:资讯新增 {added}/{len(items)},活动新增 {ev_added}/{EVENTS_N};清单已写入 {MANIFEST.name}")
|
||||
|
||||
|
||||
async def _del() -> None:
|
||||
if not MANIFEST.exists():
|
||||
# 兜底:按确定性 id 规则删除
|
||||
content_ids = [_content_id(cat, i) for cat, i in _all_titles]
|
||||
event_ids = [_event_id(i) for i in range(EVENTS_N)]
|
||||
print("未找到 manifest,按确定性 id 规则删除")
|
||||
else:
|
||||
m = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
content_ids, event_ids = m["content_ids"], m["event_ids"]
|
||||
# 兼容清理:早期手工批次的演示 id(同样属于本脚本的演示数据)
|
||||
content_ids += ["cont_demo_pol_big_01", "cont_demo_pol_small_01", "cont_demo_pol_small_02",
|
||||
"cont_demo_news_big_01", "cont_demo_news_small_01", "cont_demo_news_big_02", "cont_demo_news_small_02",
|
||||
"cont_demo_skill_big_01", "cont_demo_skill_small_01", "cont_demo_skill_big_02", "cont_demo_skill_small_02",
|
||||
"cont_demo_dyn_big_01", "cont_demo_dyn_small_01", "cont_demo_dyn_small_02"]
|
||||
event_ids += ["E-S005", "E-F005"]
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
removed = 0
|
||||
for cid in content_ids:
|
||||
obj = await session.get(ContentItem, cid)
|
||||
if obj is not None:
|
||||
await session.delete(obj)
|
||||
removed += 1
|
||||
await session.commit()
|
||||
|
||||
conn = sqlite3.connect(TRAINING_DB)
|
||||
ev_removed = 0
|
||||
try:
|
||||
for eid in event_ids:
|
||||
cur = conn.execute("DELETE FROM events WHERE id=:id", {"id": eid})
|
||||
ev_removed += cur.rowcount
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
MANIFEST.unlink(missing_ok=True)
|
||||
print(f"del 完成:资讯删除 {removed}/{len(content_ids)},活动删除 {ev_removed}/{len(event_ids)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
if cmd == "add":
|
||||
asyncio.run(_add())
|
||||
elif cmd == "del":
|
||||
asyncio.run(_del())
|
||||
else:
|
||||
print(__doc__)
|
||||
Reference in New Issue
Block a user