From 75b81ca8de8f039ca9ade78b8084fbf4ed63a8cb Mon Sep 17 00:00:00 2001 From: Pine Date: Sat, 29 Aug 2026 00:30:16 +0800 Subject: [PATCH] =?UTF-8?q?chore(scripts):=20=E6=BC=94=E7=A4=BA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=B8=B4=E6=97=B6=E8=84=9A=E6=9C=AC=EF=BC=88=E8=B5=84?= =?UTF-8?q?=E8=AE=AF4=E7=B1=BB=C3=9725=E6=9D=A1+=E6=B4=BB=E5=8A=A840?= =?UTF-8?q?=E5=9C=BA=EF=BC=8C=E6=89=8B=E5=8A=A8=20add/del=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=85=A5=E7=A7=8D=E5=AD=90=E8=BF=81=E7=A7=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/db/demo_content_tmp.ids.json | 147 ++++++++++++++++++++++ scripts/db/demo_content_tmp.py | 182 +++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 scripts/db/demo_content_tmp.ids.json create mode 100644 scripts/db/demo_content_tmp.py diff --git a/scripts/db/demo_content_tmp.ids.json b/scripts/db/demo_content_tmp.ids.json new file mode 100644 index 0000000..5811448 --- /dev/null +++ b/scripts/db/demo_content_tmp.ids.json @@ -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 时按此精确删除)" +} \ No newline at end of file diff --git a/scripts/db/demo_content_tmp.py b/scripts/db/demo_content_tmp.py new file mode 100644 index 0000000..062e576 --- /dev/null +++ b/scripts/db/demo_content_tmp.py @@ -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=("

本条为演示数据,用于验证资讯中心在各类别、各发布方与大小图模式下的展示效果。

" + f"

类别:{cat} · 发布方:{pub} · 模式:{mode}

" + "

删除方式:uv run python scripts/db/demo_content_tmp.py del

"), + 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__)