""" 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 ─────────────────────────────────────────── 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", class_id=cls.id, industry=INDUSTRIES[i % len(INDUSTRIES)], company=COMPANIES[i % len(COMPANIES)], position=POSITIONS[i % len(POSITIONS)], committee_role=COMMITTEE_MAP.get(i), 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())