hku-class/backend/seed_demo.py
2026-04-27 09:21:20 +08:00

494 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Seed demo data for HKU ICB Class Hub.
Usage (inside backend container):
python seed_demo.py
Or from host:
docker compose exec backend python seed_demo.py
"""
import asyncio
import random
from datetime import datetime, timedelta, timezone
from sqlalchemy import select, func
from app.db.database import async_session, engine
from app.db.base import Base
from app.db.models import (
Class_, User, Timeline, TimelineLike, TimelineComment,
Schedule, Announcement, Resource, Notification,
ClassMembership, Vote, VoteOption, VoteResponse,
Assignment, AssignmentSubmission,
)
from app.core.auth import hash_password
# ── Demo data pools ──────────────────────────────────────────────────────────
STUDENTS = [
{"name": "张伟", "email": "zhangwei@demo.com", "student_id": "3001001"},
{"name": "李娜", "email": "lina@demo.com", "student_id": "3001002"},
{"name": "王芳", "email": "wangfang@demo.com", "student_id": "3001003"},
{"name": "刘洋", "email": "liuyang@demo.com", "student_id": "3001004"},
{"name": "陈明", "email": "chenming@demo.com", "student_id": "3001005"},
{"name": "杨秀英", "email": "yangxiuying@demo.com", "student_id": "3001006"},
{"name": "赵敏", "email": "zhaomin@demo.com", "student_id": "3001007"},
{"name": "黄强", "email": "huangqiang@demo.com", "student_id": "3001008"},
{"name": "周丽", "email": "zhouli@demo.com", "student_id": "3001009"},
{"name": "吴刚", "email": "wugang@demo.com", "student_id": "3001010"},
{"name": "徐静", "email": "xujing@demo.com", "student_id": "3001011"},
{"name": "孙磊", "email": "sunlei@demo.com", "student_id": "3001012"},
{"name": "马超", "email": "machao@demo.com", "student_id": "3001013"},
{"name": "朱婷", "email": "zhuting@demo.com", "student_id": "3001014"},
{"name": "胡建华", "email": "hujianhua@demo.com", "student_id": "3001015"},
]
CLASS_ADMIN = {
"name": "林教授",
"email": "linprof@demo.com",
}
INDUSTRIES = ["金融", "科技", "医疗", "教育", "制造业", "咨询", "互联网", "房地产", "消费品", "能源"]
COMPANIES = [
"腾讯科技", "阿里巴巴", "华为技术", "中国平安", "招商银行",
"字节跳动", "美团", "京东集团", "中信证券", "小米科技",
"百度", "网易", "滴滴出行", "拼多多", "比亚迪",
]
POSITIONS = [
"产品总监", "技术总监", "市场副总裁", "运营总监", "战略总监",
"投资总监", "事业部总经理", "首席架构师", "人力资源总监", "财务总监",
]
SKILLS_POOL = [
"战略规划", "团队管理", "数据分析", "产品设计", "市场营销",
"财务管理", "风险控制", "项目管理", "商业分析", "数字化转型",
"人工智能", "供应链管理", "品牌管理", "投融资", "企业并购",
]
TIMELINE_POSTS = [
{
"title": "开课第一天,期待已久的学习之旅!",
"content": "今天终于迎来了 HKU ICB 的开课日,见到了来自各行各业的同学们,非常期待接下来的学习时光。教授的讲解深入浅出,让人受益匪浅。",
},
{
"title": "小组讨论收获满满",
"content": "今天的小组讨论非常精彩,我们组的成员来自金融、科技和教育三个行业,不同的视角让我们对案例有了更全面的理解。大家碰撞出了很多精彩的火花!",
},
{
"title": "推荐一本好书《创新者的窘境》",
"content": "最近在读克莱顿·克里斯坦森的《创新者的窘境》,书中关于颠覆式创新的理论与课程中的战略管理内容非常契合,推荐给大家。",
},
{
"title": "企业参访活动回顾",
"content": "感谢学校组织的腾讯总部参访活动,深入了解了一家科技巨头的企业文化和创新机制。特别是他们敏捷开发流程和用户导向的产品理念,给我留下了深刻印象。",
},
{
"title": "期末项目组队啦",
"content": "我们正在组建期末项目的团队,主题是「传统企业数字化转型路径研究」,有兴趣的同学欢迎加入!目前团队已有三位同学,覆盖了金融、制造和咨询行业。",
},
{
"title": "学习心得:领导力与变革管理",
"content": "这周的领导力课程让我对变革管理有了全新的认识。科特的八步变革模型非常实用,结合我所在公司的实际案例,感觉可以直接应用。分享给同学们一起讨论。",
},
{
"title": "周末读书会邀约",
"content": "这周六下午在中环的咖啡厅组织一次读书分享会,我们计划讨论《从零到一》和《精益创业》两本书,欢迎有空的同学一起来交流。",
},
{
"title": "求职季来了,分享几个面试技巧",
"content": "最近在准备职业转型整理了一些高管面试的心得体会。最重要的三点1) 用数据说话2) 展示战略思维3) 体现文化匹配。希望对大家有帮助。",
},
]
SCHEDULE_DATA = [
{"type": "course", "title": "战略管理", "location": "HKU ICB 教室 A301", "desc": "教授:林教授\n课程内容:竞争战略分析框架", "day_offset": 7},
{"type": "course", "title": "财务分析与决策", "location": "HKU ICB 教室 B205", "desc": "教授:陈教授\n课程内容:企业财务报表分析", "day_offset": 14},
{"type": "course", "title": "数字营销战略", "location": "HKU ICB 教室 A301", "desc": "教授:王教授\n课程内容:数字时代的营销策略", "day_offset": 21},
{"type": "course", "title": "领导力与组织行为", "location": "HKU ICB 教室 C102", "desc": "教授:赵教授\n课程内容:变革管理与领导力发展", "day_offset": 28},
{"type": "deadline", "title": "小组项目提案截止", "location": None, "desc": "请提交小组项目的选题提案,包括研究背景、方法论和预期成果", "day_offset": 18},
{"type": "deadline", "title": "个人反思报告截止", "location": None, "desc": "提交不少于2000字的个人学习反思报告", "day_offset": 35},
{"type": "activity", "title": "企业参访:华为深圳总部", "location": "华为坂田基地", "desc": "了解华为的研发体系和企业文化,名额有限请提前报名", "day_offset": 25},
{"type": "activity", "title": "校友 networking 晚宴", "location": "港大校友会", "desc": "与往届校友交流职业发展经验", "day_offset": 32},
{"type": "course", "title": "创新与创业管理", "location": "HKU ICB 教室 A301", "desc": "教授:李教授\n课程内容:创新方法论与创业实践", "day_offset": 42},
{"type": "course", "title": "全球商业环境", "location": "HKU ICB 教室 B205", "desc": "教授:张教授\n课程内容:国际贸易与地缘经济", "day_offset": 49},
]
ANNOUNCEMENTS = [
{
"title": "2025年秋季学期注册通知",
"content": "各位同学2025年秋季学期注册现已开始。请于截止日期前完成选课和缴费。如有任何问题请联系教务处。\n\n注册截止日期2025年9月15日\n缴费截止日期2025年9月20日",
"is_pinned": True,
},
{
"title": "图书馆资源更新通知",
"content": "学院图书馆新增了 Harvard Business Review、McKinsey Quarterly 等数据库的访问权限。同学们可通过校园网直接访问,详细使用指南请查看学院官网。",
"is_pinned": False,
},
{
"title": "期末考试安排公布",
"content": "期末考试将于12月进行具体安排如下\n\n- 战略管理12月10日 09:00-12:00\n- 财务分析12月12日 14:00-17:00\n- 数字营销12月15日 09:00-12:00\n\n请同学们提前做好复习准备。",
"is_pinned": True,
},
{
"title": "校园 Wi-Fi 升级通知",
"content": "本周六9月20日校园网络将进行升级维护届时部分区域可能出现网络中断。预计维护时间为 22:00-次日 06:00给大家带来的不便敬请谅解。",
"is_pinned": False,
},
]
RESOURCES = [
{"title": "战略管理课程讲义 - 第一讲", "category": "course_material", "file_type": "pdf", "desc": "竞争战略分析框架基础"},
{"title": "财务分析模板", "category": "template", "file_type": "xlsx", "desc": "财务报表分析模板,包含三大报表"},
{"title": "数字营销案例集", "category": "case_study", "file_type": "pdf", "desc": "精选10个数字营销实战案例"},
{"title": "领导力自评工具", "category": "template", "file_type": "pdf", "desc": "领导力评估问卷及解读指南"},
{"title": "小组项目评分标准", "category": "course_material", "file_type": "pdf", "desc": "期末小组项目的详细评分标准"},
{"title": "商业模式画布模板", "category": "template", "file_type": "pptx", "desc": "Business Model Canvas 空白模板"},
{"title": "波特五力模型分析指南", "category": "reference", "file_type": "pdf", "desc": "Michael Porter 五力分析框架详解"},
{"title": "推荐书单 2025", "category": "reference", "file_type": "pdf", "desc": "教授推荐阅读书目清单"},
]
VOTES = [
{
"title": "期末聚餐地点投票",
"description": "请大家投票选出期末聚餐的地点,得票最高的地点将成为最终选择。",
"vote_type": "single",
"is_anonymous": False,
"options": ["中环·镛记酒家", "尖沙咀·海底捞", "铜锣湾·利苑酒家", "湾仔·名人坊"],
},
{
"title": "下次课程主题偏好调查",
"description": "我们希望了解大家对课程主题的偏好,以便安排后续的专题讲座。",
"vote_type": "multiple",
"is_anonymous": True,
"max_choices": 3,
"options": ["ESG与可持续发展", "Web3与区块链", "AI与大数据应用", "跨境投资与并购", "家族企业传承"],
},
{
"title": "企业参访意向",
"description": "选择你最希望参访的企业,我们将根据投票结果安排参访行程。",
"vote_type": "single",
"is_anonymous": False,
"options": ["字节跳动", "大疆创新", "比亚迪", "商汤科技"],
},
]
ASSIGNMENTS = [
{
"title": "个人战略分析报告",
"description": "选择一家上市公司,运用 SWOT 分析和波特五力模型撰写一份不少于3000字的战略分析报告。",
"deadline_days": 30,
},
{
"title": "小组商业计划书",
"description": "以小组为单位,提出一个创新商业想法,撰写完整的商业计划书,包括市场分析、财务预测和实施路线图。",
"deadline_days": 45,
},
{
"title": "课堂反思日志",
"description": "结合本学期所学内容撰写一篇个人学习反思日志总结关键收获和未来应用计划。不少于1500字。",
"deadline_days": 14,
},
]
COMMENTS = [
"写得太好了,很有共鸣!",
"感谢分享,收藏了",
"这个观点很有启发",
"期待更多分享",
"学习了,谢谢!",
"有同感,我们的经历很相似",
"补充一点,我觉得还可以从供应链角度分析",
"这个案例我之前也研究过,确实很经典",
]
async def seed():
"""Generate all demo data."""
# Ensure tables exist
from app.db.base import Base
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with async_session() as db:
# Check if data already exists
result = await db.execute(select(func.count(User.id)))
if result.scalar() > 1: # super_admin already exists
print("[!] Database already has data. Skipping seed.")
print(" To re-seed, delete the database file first.")
return
now = datetime.now(timezone.utc)
pwd_hash = hash_password("demo123")
# ── 1. Create class ──────────────────────────────────────────────
cls = Class_(
name="HKU ICB 企业管理研究生课程 2025",
cohort_year=2025,
description="香港大学中国商业学院企业管理研究生课程 2025 秋季班,汇聚来自各行各业的精英人才。",
invite_code="HKU2025",
)
db.add(cls)
await db.flush()
print(f"[+] Class: {cls.name} (invite code: {cls.invite_code})")
# ── 2. Create teacher ────────────────────────────────────────────
teacher = User(
email=CLASS_ADMIN["email"],
password_hash=pwd_hash,
name=CLASS_ADMIN["name"],
role="teacher",
status="approved",
industry="教育",
company="香港大学",
position="教授",
bio="香港大学中国商业学院教授,专注于战略管理和企业转型研究。",
wechat_id="lin_prof_hku",
)
db.add(teacher)
await db.flush()
db.add(
ClassMembership(
user_id=teacher.id,
class_id=cls.id,
membership_role="teacher",
)
)
print(f"[+] Teacher: {teacher.name} ({teacher.email})")
# ── 3. Create students ───────────────────────────────────────────
COMMITTEE_MAP = {0: "班长", 1: "副班长", 3: "学习委员", 5: "组织委员", 7: "宣传委员", 9: "文体委员"}
students = []
for i, s in enumerate(STUDENTS):
skills = random.sample(SKILLS_POOL, k=random.randint(2, 4))
user = User(
email=s["email"],
password_hash=pwd_hash,
name=s["name"],
student_id=s["student_id"],
role="student",
status="approved",
industry=INDUSTRIES[i % len(INDUSTRIES)],
company=COMPANIES[i % len(COMPANIES)],
position=POSITIONS[i % len(POSITIONS)],
skills_tags='["' + '", "'.join(skills) + '"]',
bio=f"{COMPANIES[i % len(COMPANIES)]}担任{POSITIONS[i % len(POSITIONS)]},拥有丰富的{INDUSTRIES[i % len(INDUSTRIES)]}行业经验。",
wechat_id=f"wx_{s['student_id']}",
phone=f"138{random.randint(10000000, 99999999)}",
)
db.add(user)
students.append(user)
await db.flush()
for user in students:
db.add(
ClassMembership(
user_id=user.id,
class_id=cls.id,
membership_role="student",
committee_role=COMMITTEE_MAP.get(i),
)
)
print(f"[+] {len(students)} students created (password: demo123)")
# ── 4. Timelines with likes and comments ─────────────────────────
all_users = [teacher] + students
for i, post_data in enumerate(TIMELINE_POSTS):
author = random.choice(all_users)
post = Timeline(
class_id=cls.id,
author_id=author.id,
title=post_data["title"],
content=post_data["content"],
created_at=now - timedelta(days=len(TIMELINE_POSTS) - i, hours=random.randint(0, 12)),
)
db.add(post)
await db.flush()
# Add random likes (3~10)
likers = random.sample(all_users, k=min(random.randint(3, 10), len(all_users)))
for liker in likers:
db.add(TimelineLike(post_id=post.id, user_id=liker.id))
# Add random comments (1~4)
commenters = random.sample(students, k=random.randint(1, 4))
for commenter in commenters:
db.add(TimelineComment(
post_id=post.id,
author_id=commenter.id,
content=random.choice(COMMENTS),
created_at=post.created_at + timedelta(hours=random.randint(1, 48)),
))
await db.flush()
print(f"[+] {len(TIMELINE_POSTS)} timeline posts with likes and comments")
# ── 6. Schedules ─────────────────────────────────────────────────
for sched in SCHEDULE_DATA:
start = now + timedelta(days=sched["day_offset"], hours=9)
end = start + timedelta(hours=3) if sched["type"] == "course" else None
s = Schedule(
class_id=cls.id,
type=sched["type"],
title=sched["title"],
start_time=start,
end_time=end,
location=sched["location"],
description=sched["desc"],
)
db.add(s)
await db.flush()
print(f"[+] {len(SCHEDULE_DATA)} schedules created")
# ── 7. Announcements ─────────────────────────────────────────────
for ann in ANNOUNCEMENTS:
a = Announcement(
class_id=cls.id,
author_id=admin.id,
title=ann["title"],
content=ann["content"],
is_pinned=ann["is_pinned"],
created_at=now - timedelta(days=random.randint(1, 30)),
)
db.add(a)
await db.flush()
print(f"[+] {len(ANNOUNCEMENTS)} announcements created")
# ── 8. Resources ─────────────────────────────────────────────────
for res in RESOURCES:
r = Resource(
class_id=cls.id,
uploader_id=admin.id,
title=res["title"],
description=res["desc"],
file_url=f"https://example.com/files/{res['title'].replace(' ', '_')}.{res['file_type']}",
file_type=res["file_type"],
file_size=random.randint(500_000, 15_000_000),
category=res["category"],
download_count=random.randint(5, 80),
)
db.add(r)
await db.flush()
print(f"[+] {len(RESOURCES)} resources created")
# ── 9. Votes with options and responses ──────────────────────────
for v_data in VOTES:
deadline = now + timedelta(days=random.randint(5, 20))
vote = Vote(
class_id=cls.id,
creator_id=admin.id,
title=v_data["title"],
description=v_data["description"],
vote_type=v_data["vote_type"],
is_anonymous=v_data["is_anonymous"],
max_choices=v_data.get("max_choices", 1),
deadline=deadline,
status="open",
)
db.add(vote)
await db.flush()
options = []
for j, opt_text in enumerate(v_data["options"]):
opt = VoteOption(
vote_id=vote.id,
content=opt_text,
sort_order=j,
)
db.add(opt)
options.append(opt)
await db.flush()
# Add random vote responses
voters = random.sample(students, k=min(random.randint(5, 12), len(students)))
for voter in voters:
chosen_count = 1 if v_data["vote_type"] == "single" else random.randint(1, v_data.get("max_choices", 1))
chosen_opts = random.sample(options, k=min(chosen_count, len(options)))
for chosen in chosen_opts:
db.add(VoteResponse(
vote_id=vote.id,
option_id=chosen.id,
voter_id=voter.id,
))
await db.flush()
print(f"[+] {len(VOTES)} votes with options and responses")
# ── 10. Assignments with submissions ──────────────────────────────
for asgn_data in ASSIGNMENTS:
asgn = Assignment(
class_id=cls.id,
creator_id=admin.id,
title=asgn_data["title"],
description=asgn_data["description"],
deadline=now + timedelta(days=asgn_data["deadline_days"]),
status="open",
)
db.add(asgn)
await db.flush()
# Some students already submitted
submitters = random.sample(students, k=random.randint(3, 8))
for submitter in submitters:
sub = AssignmentSubmission(
assignment_id=asgn.id,
student_id=submitter.id,
notes=f"已提交{asgn_data['title']},请老师查阅。",
file_url=f"https://example.com/submissions/{submitter.student_id}_{asgn.id}.pdf",
file_name=f"{submitter.name}_{asgn_data['title']}.pdf",
file_type="pdf",
file_size=random.randint(200_000, 5_000_000),
created_at=now - timedelta(days=random.randint(1, 10)),
)
# Grade some submissions
if random.random() > 0.5:
sub.grade = random.choice(["A", "A-", "B+", "B", "B+"])
sub.feedback = "分析到位,逻辑清晰,建议进一步深化数据支撑。"
sub.graded_at = now - timedelta(days=random.randint(0, 3))
db.add(sub)
await db.flush()
print(f"[+] {len(ASSIGNMENTS)} assignments with submissions")
# ── 11. Notifications ─────────────────────────────────────────────
notif_templates = [
{"type": "announcement", "title": "新公告发布", "content": "林教授发布了新公告「期末考试安排公布」"},
{"type": "assignment", "title": "新作业发布", "content": "林教授发布了新作业「个人战略分析报告」"},
{"type": "vote", "title": "新投票发布", "content": "林教授发起了投票「期末聚餐地点投票」"},
{"type": "timeline", "title": "动态互动", "content": "{name} 点赞了你的动态"},
{"type": "timeline", "title": "新评论", "content": "{name} 评论了你的动态"},
]
for student in students:
# Each student gets 2~4 notifications
num_notifs = random.randint(2, 4)
chosen_notifs = random.sample(notif_templates, k=min(num_notifs, len(notif_templates)))
for tmpl in chosen_notifs:
other = random.choice([s for s in students if s.id != student.id])
content = tmpl["content"].format(name=other.name)
n = Notification(
user_id=student.id,
type=tmpl["type"],
title=tmpl["title"],
content=content,
is_read=random.random() > 0.5,
created_at=now - timedelta(hours=random.randint(1, 72)),
)
db.add(n)
await db.flush()
print(f"[+] Notifications created for all students")
await db.commit()
print("\n✅ Demo data seeded successfully!")
print("" * 50)
print("Login credentials:")
print(f" Super Admin: admin@hkuicb.info / (from .env)")
print(f" Class Admin: {CLASS_ADMIN['email']} / demo123")
print(f" Students: *姓名拼音*@demo.com / demo123")
print(f" Invite code: HKU2025")
if __name__ == "__main__":
asyncio.run(seed())