265 lines
12 KiB
Python
265 lines
12 KiB
Python
"""灌入演示数据:顾问 / 老师 / 班级 / 学生 / 成绩 / 就业。
|
||
|
||
用法:
|
||
python -m app.scripts.seed_data # 已有数据就跳过
|
||
python -m app.scripts.seed_data --reset # 清空业务数据后重灌
|
||
|
||
数据是"设计过"的,不是随机糊上去的:
|
||
* 4 个班、每班 12~16 人,男女比例有差异,方便看 2.6.1 的性别分布;
|
||
* 5 次考核,成绩按"个人基础 + 波动"生成,必然产生:
|
||
- 每次都在线以上的学霸(2.6.2 有结果)
|
||
- 多次不及格的重点关注对象(红线预警有数据)
|
||
- 成绩大起大落的人(2.7.2 波动分析有排名)
|
||
* 就业数据让各班就业率、薪资区间都拉开档次(2.6.3 / 漏斗图有东西看)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import random
|
||
from datetime import date, timedelta
|
||
|
||
from app.core.database import SessionLocal, engine, ensure_database_exists
|
||
from app.dao.advisor_dao import AdvisorDao
|
||
from app.dao.clazz_dao import ClazzDao
|
||
from app.dao.student_dao import StudentDao
|
||
from app.dao.teacher_dao import TeacherDao
|
||
from app.model import Advisor, Base, Clazz, Employment, Score, Student, Teacher
|
||
from app.service.student_service import StudentService
|
||
|
||
random.seed(20260916) # 固定种子,每次灌出来的数据一样,便于对比
|
||
|
||
SURNAMES = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜"
|
||
GIVEN = [
|
||
"伟", "芳", "娜", "敏", "静", "强", "磊", "洋", "艳", "勇", "军", "杰", "娟", "涛", "明",
|
||
"超", "秀英", "霞", "平", "刚", "桂英", "文轩", "雨欣", "子豪", "思远", "梓涵", "浩宇",
|
||
"若曦", "嘉怡", "天佑", "梦琪", "俊杰", "欣怡", "家豪", "雅静",
|
||
]
|
||
CITIES = ["广东深圳", "湖南长沙", "江西南昌", "广西南宁", "湖北武汉", "四川成都", "河南郑州", "福建福州", "安徽合肥", "山东济南"]
|
||
SCHOOLS = [
|
||
"深圳职业技术学院", "长沙民政职业技术学院", "江西现代职业技术学院", "南宁职业技术学院",
|
||
"武汉船舶职业技术学院", "成都航空职业技术学院", "黄河水利职业技术学院",
|
||
"福建信息职业技术学院", "安徽机电职业技术学院", "山东商业职业技术学院",
|
||
]
|
||
MAJORS = ["软件技术", "计算机应用技术", "大数据技术", "人工智能技术应用", "计算机网络技术", "数字媒体技术", "电子商务"]
|
||
EDUCATIONS = ["大专", "大专", "大专", "本科", "中专"]
|
||
COMPANIES = [
|
||
"深圳华为技术", "腾讯科技(深圳)", "字节跳动", "广州网易", "杭州阿里巴巴", "比亚迪股份",
|
||
"中兴通讯", "深圳大疆创新", "京东科技", "美团", "小米通讯", "海康威视", "OPPO 广东",
|
||
"深信服科技", "金蝶软件", "用友网络", "软通动力", "中软国际",
|
||
]
|
||
CITY_WORK = ["深圳", "广州", "杭州", "北京", "上海", "成都", "武汉", "东莞"]
|
||
POSITIONS = ["Java 开发工程师", "前端开发工程师", "测试工程师", "大数据开发工程师", "运维工程师", "算法工程师", "产品助理"]
|
||
|
||
CLASS_PLAN = [
|
||
{"class_no": "JAVA202601", "name": "Java 就业 2026 一班", "direction": "Java", "capacity": 40, "count": 16},
|
||
{"class_no": "WEB202602", "name": "Web 前端 2026 一班", "direction": "Web", "capacity": 35, "count": 14},
|
||
{"class_no": "BIGDATA202603", "name": "大数据 2026 一班", "direction": "BigData", "capacity": 30, "count": 12},
|
||
{"class_no": "AI202604", "name": "人工智能 2026 一班", "direction": "AI", "capacity": 25, "count": 10},
|
||
]
|
||
|
||
TEACHER_PLAN = [
|
||
("陈志远", 1, "高级讲师", "Java", "2019-03-01"),
|
||
("林晓雯", 2, "讲师", "Web 前端", "2020-07-15"),
|
||
("黄建国", 1, "教研组长", "大数据", "2017-09-01"),
|
||
("苏婉清", 2, "讲师", "人工智能", "2021-04-20"),
|
||
("郑海涛", 1, "高级讲师", "Java", "2018-11-05"),
|
||
]
|
||
|
||
ADVISOR_PLAN = [
|
||
("李慧敏", 2, "招生一部", "13900000001"),
|
||
("王志强", 1, "招生二部", "13900000002"),
|
||
("周雅丽", 2, "就业服务部", "13900000003"),
|
||
]
|
||
|
||
EXAM_NAMES = ["阶段一考试", "阶段二考试", "阶段三考试", "阶段四考试", "结课答辩"]
|
||
|
||
|
||
def clear_business_data(db) -> None:
|
||
for model in (Employment, Score, Student, Clazz, Teacher, Advisor):
|
||
db.query(model).delete()
|
||
from app.model import class_teachers
|
||
|
||
db.execute(class_teachers.delete())
|
||
db.commit()
|
||
print("已清空业务数据(账号保留)")
|
||
|
||
|
||
def seed(reset: bool = False) -> None:
|
||
ensure_database_exists()
|
||
Base.metadata.create_all(bind=engine)
|
||
|
||
with SessionLocal() as db:
|
||
if reset:
|
||
clear_business_data(db)
|
||
elif StudentDao.count(db) > 0:
|
||
print("已有学生数据,跳过灌数据(要重灌请加 --reset)")
|
||
return
|
||
|
||
# ---------------- 顾问 ----------------
|
||
advisors = []
|
||
for idx, (name, gender, dept, phone) in enumerate(ADVISOR_PLAN, start=1):
|
||
advisor = Advisor(
|
||
advisor_no=f"A{idx:04d}", name=name, gender=gender, dept=dept, phone=phone,
|
||
email=f"advisor{idx}@wolin.com",
|
||
)
|
||
db.add(advisor)
|
||
advisors.append(advisor)
|
||
db.flush()
|
||
|
||
# ---------------- 老师 ----------------
|
||
teachers = []
|
||
for idx, (name, gender, title, subject, hire) in enumerate(TEACHER_PLAN, start=1):
|
||
teacher = Teacher(
|
||
teacher_no=f"T2026{idx:03d}", name=name, gender=gender, title=title, subject=subject,
|
||
hire_date=date.fromisoformat(hire), phone=f"1380000{idx:04d}",
|
||
email=f"teacher{idx}@wolin.com",
|
||
)
|
||
db.add(teacher)
|
||
teachers.append(teacher)
|
||
db.flush()
|
||
|
||
# ---------------- 班级 ----------------
|
||
classes = []
|
||
for idx, plan in enumerate(CLASS_PLAN):
|
||
klass = Clazz(
|
||
class_no=plan["class_no"],
|
||
name=plan["name"],
|
||
direction=plan["direction"],
|
||
open_date=date(2026, 3, 2) + timedelta(days=idx * 7),
|
||
close_date=date(2026, 9, 30) + timedelta(days=idx * 7),
|
||
classroom=f"A{201 + idx}",
|
||
capacity=plan["capacity"],
|
||
status=1,
|
||
head_teacher_id=teachers[idx % len(teachers)].id,
|
||
advisor_id=advisors[idx % len(advisors)].id,
|
||
description=f"{plan['direction']} 方向就业班,{plan['count']} 人",
|
||
)
|
||
# 授课老师:本方向 + 一位公共课老师
|
||
klass.teachers = [teachers[idx % len(teachers)], teachers[(idx + 1) % len(teachers)]]
|
||
db.add(klass)
|
||
classes.append(klass)
|
||
db.flush()
|
||
|
||
# ---------------- 学生 ----------------
|
||
students: list[Student] = []
|
||
used_names: set[str] = set()
|
||
for c_idx, (klass, plan) in enumerate(zip(classes, CLASS_PLAN)):
|
||
for i in range(plan["count"]):
|
||
while True:
|
||
name = random.choice(SURNAMES) + random.choice(GIVEN)
|
||
if name not in used_names:
|
||
used_names.add(name)
|
||
break
|
||
gender = 1 if random.random() < 0.62 else 2
|
||
age = random.randint(19, 27)
|
||
enroll = date(2026, 3, 2) + timedelta(days=c_idx * 7 + i)
|
||
student = Student(
|
||
stu_no=StudentService.build_stu_no(db, klass.id, enroll),
|
||
name=name,
|
||
gender=gender,
|
||
birth_date=date(2026 - age, random.randint(1, 12), random.randint(1, 28)),
|
||
birth_date_estimated=0,
|
||
native_place=random.choice(CITIES),
|
||
graduate_school=random.choice(SCHOOLS),
|
||
major=random.choice(MAJORS),
|
||
education=random.choice(EDUCATIONS),
|
||
enroll_date=enroll,
|
||
graduate_date=enroll + timedelta(days=random.randint(500, 900)),
|
||
phone=f"1{random.randint(3, 9)}{random.randint(10**8, 10**9 - 1)}",
|
||
class_id=klass.id,
|
||
advisor_id=advisors[(c_idx + i) % len(advisors)].id,
|
||
status=1,
|
||
)
|
||
db.add(student)
|
||
db.flush() # 立刻落库,保证下一个学号能看到它
|
||
students.append(student)
|
||
db.flush()
|
||
|
||
# ---------------- 成绩 ----------------
|
||
score_count = 0
|
||
for student in students:
|
||
# 每个人的"基础水平",决定他是学霸还是重点关注对象
|
||
base = random.gauss(76, 12)
|
||
volatility = random.choice([2, 3, 4, 6, 9, 13]) # 有人稳、有人大起大落
|
||
for seq in range(1, 6):
|
||
value = max(20.0, min(100.0, random.gauss(base, volatility)))
|
||
if random.random() < 0.06: # 偶尔缺考/失手
|
||
value = max(20.0, value - random.randint(15, 30))
|
||
db.add(
|
||
Score(
|
||
stu_id=student.id,
|
||
exam_seq=seq,
|
||
exam_name=EXAM_NAMES[seq - 1],
|
||
exam_date=date(2026, 3, 2) + timedelta(days=seq * 45 + random.randint(-3, 3)),
|
||
score=round(value, 1),
|
||
flag=1 if value < 60 else 0,
|
||
remark="系统生成" if value < 60 else None,
|
||
)
|
||
)
|
||
score_count += 1
|
||
db.flush()
|
||
|
||
# ---------------- 就业 ----------------
|
||
emp_count = 0 # 已拿到 offer
|
||
open_count = 0 # 只开放了就业、还没 offer
|
||
OFFER_RATE = [0.88, 0.79, 0.67, 0.50] # 就业率按班级拉开差距:Java 班最猛,AI 班最慢
|
||
SALARY_BASE = [13500, 11500, 14000, 15500]
|
||
for student in students:
|
||
klass_idx = next(i for i, c in enumerate(classes) if c.id == student.class_id)
|
||
rate = OFFER_RATE[klass_idx % len(OFFER_RATE)]
|
||
if random.random() > rate:
|
||
# 没就业的学生里,一部分"已开放就业",状态推到"进入就业"
|
||
if random.random() < 0.6:
|
||
open_d = date(2026, 9, 30) + timedelta(days=random.randint(0, 20))
|
||
db.add(Employment(stu_id=student.id, class_id=student.class_id, open_date=open_d))
|
||
student.status = 2
|
||
open_count += 1
|
||
continue
|
||
|
||
open_d = date(2026, 9, 30) + timedelta(days=random.randint(0, 25))
|
||
wait = random.randint(5, 70)
|
||
offer_d = open_d + timedelta(days=wait)
|
||
|
||
# 薪资:Java/大数据偏高,AI 班样本少但更极端
|
||
salary_base = SALARY_BASE[klass_idx % len(SALARY_BASE)]
|
||
salary = max(6000, int(random.gauss(salary_base, 3200)))
|
||
if random.random() < 0.08:
|
||
salary = int(salary * random.uniform(1.3, 1.6)) # 少数高薪 offer
|
||
|
||
db.add(
|
||
Employment(
|
||
stu_id=student.id,
|
||
class_id=student.class_id,
|
||
open_date=open_d,
|
||
offer_date=offer_d,
|
||
company=random.choice(COMPANIES),
|
||
salary=salary,
|
||
position=random.choice(POSITIONS),
|
||
city=random.choice(CITY_WORK),
|
||
remark="就业老师推荐" if random.random() < 0.3 else None,
|
||
)
|
||
)
|
||
student.status = 3
|
||
emp_count += 1
|
||
if random.random() < 0.75:
|
||
# 拿到 offer 的学生大多有对应阶段的成绩
|
||
student.graduate_date = offer_d + timedelta(days=random.randint(20, 60))
|
||
|
||
db.commit()
|
||
|
||
print("演示数据灌入完成:")
|
||
print(f" 顾问 {len(advisors)} 人")
|
||
print(f" 老师 {len(teachers)} 人")
|
||
print(f" 班级 {len(classes)} 个")
|
||
print(f" 学生 {len(students)} 人")
|
||
print(f" 成绩 {score_count} 条")
|
||
print(f" 就业记录 {emp_count + open_count} 条(已拿 offer {emp_count} / 仅开放就业 {open_count})")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser(description="灌入沃林学生管理系统演示数据")
|
||
parser.add_argument("--reset", action="store_true", help="先清空业务数据再灌")
|
||
args = parser.parse_args()
|
||
seed(reset=args.reset)
|