From 1986db6c3470aa04c93932eaf89f70a723530a0f Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 12 Apr 2026 20:00:20 +0800 Subject: [PATCH] 1 --- backend/seed_demo.py | 491 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 backend/seed_demo.py diff --git a/backend/seed_demo.py b/backend/seed_demo.py new file mode 100644 index 0000000..2ddb929 --- /dev/null +++ b/backend/seed_demo.py @@ -0,0 +1,491 @@ +""" +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, + StudentRoster, 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 class admin ──────────────────────────────────────── + admin = User( + email=CLASS_ADMIN["email"], + password_hash=pwd_hash, + name=CLASS_ADMIN["name"], + role="class_admin", + status="approved", + class_id=cls.id, + industry="教育", + company="香港大学", + position="教授", + bio="香港大学中国商业学院教授,专注于战略管理和企业转型研究。", + wechat_id="lin_prof_hku", + ) + db.add(admin) + await db.flush() + print(f"[+] Class Admin: {admin.name} ({admin.email})") + + # ── 3. Create students ─────────────────────────────────────────── + 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", + class_id=cls.id, + 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() + print(f"[+] {len(students)} students created (password: demo123)") + + # ── 4. Student roster ──────────────────────────────────────────── + for s in students: + roster = StudentRoster( + class_id=cls.id, + student_id=s.student_id, + name=s.name, + status="registered", + user_id=s.id, + ) + db.add(roster) + await db.flush() + print(f"[+] {len(students)} roster entries created") + + # ── 5. Timelines with likes and comments ───────────────────────── + all_users = [admin] + 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())