63cec4143809628c5a104822ce476ef9818c8090
教培系统架构设计与开发
本项目旨在开发一个基于FastAPI的学生管理系统,提供学生基本信息管理、考核成绩管理和就业管理三大核心功能模块。
系统将采用RESTful API设计,支持前后端分离架构。
需求分期
团队对需求管理按照模块化解耦拆分开发,按照三个阶段开发
-
基础需求:按照开发任务,完成5 + 1 统计模块的开发
-
特色需求:
-
数据源配置
-
鉴权管理
-
添加部门管理,顾问管理模块
-
添加用户认证模块
-
开发前端应用
-
开发更多的分析查询接口
-
增加AI智能体和知识库开发
-
-
脚手架集成
第一阶段 - 基础需求
按照开发任务,完成5 + 1 统计模块的开发
- 学生
- 班级
- 老师
- 成绩
- 就业
- 统计分析
第二阶段 - 特色需求
-
数据源配置
-
鉴权管理
-
添加部门管理,顾问管理模块
-
添加用户认证模块
-
开发前端应用
-
开发更多的分析查询接口
-
增加AI智能体和知识库开发
第三阶段 - 脚手架集成
核心组件
- 服务状态监控 - utc时区[还有哪些可替代?]
- db ,中间件等组件是否活跃
- 自身服务是否活跃
-
认证模块: oauth2_scheme
业务拆分
业务范围划分
- 实体
- 学生基本信息管理
- 考核成绩管理
- 就业管理
- 班级
- 老师
- 统计分析
数据架构设计
数据关系
- 学生:班级 = N: 1, 班级:老师 = 1 : 1 / [N:1], 学生: 成绩 = 1:N , 学生:就业 = 1:1,
- 统计分析:
表结构设计
-- create database student_management_system;
-- use student_management_system;
create database sms;
use sms;
-- students
create table if not exists students(
id int auto_increment primary key,
num varchar(30) not null comment '学生编号',
name varchar(30) not null default '',
age int not null default 18,
sex tinyint not null default 0 comment '0未知 1男 2女',
home_place varchar(50) null comment '家乡',
college varchar(50) null comment '毕业院校',
specialty varchar(50) null comment '专业',
enrollment_time datetime null comment '入学时间',
graduate_time datetime null comment '毕业时间',
education varchar(50) null comment '学历',
-- 外键字段
class_id int not null default 0,
advisor_id int null comment '顾问id',
-- advisor_num int null comment '顾问编号', -- join teacher
-- 通用字段
create_time datetime not null default current_timestamp,
update_time datetime not null default current_timestamp on update current_timestamp,
is_deleted tinyint not null default 0,
delete_time datetime null comment '删除时间,记录软删除时间'
);
-- teachers
create table if not exists teachers(
id int auto_increment primary key,
num varchar(30) not null comment '老师编号',
name varchar(30) not null,
age int not null,
sex tinyint not null default 0 comment '0未知 1男 2女',
home_place varchar(50) null comment '家乡',
college varchar(50) null comment '毕业院校',
specialty varchar(50) null comment '专业',
enrollment_time datetime null comment '入学时间',
graduate_time datetime null comment '毕业时间',
education varchar(50) null comment '学历',
-- 额外信息:工作经验/教学评级 ,教学方向:应用,项目,算法;
work_experience text null comment '工作经验',
coach_area varchar(50) null comment '教学方向:应用,项目,算法',
-- 通用字段
create_time datetime not null default current_timestamp,
update_time datetime not null default current_timestamp on update current_timestamp,
is_deleted tinyint not null default 0,
delete_time datetime null comment '删除时间,记录软删除时间'
);
-- classes
create table if not exists classes(
id int auto_increment primary key,
num varchar(30) not null comment '班级编号',
name varchar(30) not null comment '班级名称',
class_start_time datetime null comment '开课时间',
head_teacher_id int null comment '班主任',
-- head_teacher_num int null comment '班主任编号',
coach_teacher_id int null comment '授课老师',
-- coach_teacher_num int null comment '授课老师编号',
tutor_teacher_id int null comment '助教老师',
-- tutor_teacher_num int null comment '助教老师编号',
-- 通用字段
create_time datetime not null default current_timestamp,
update_time datetime not null default current_timestamp on update current_timestamp,
is_deleted tinyint not null default 0,
delete_time datetime null comment '删除时间,记录软删除时间'
);
-- scores
create table if not exists scores(
id int auto_increment primary key,
sid int not null,
-- snum varchar(30) not null,
num varchar(30) not null comment '考核序次',
score decimal(10,2) not null,
-- 通用字段
create_time datetime not null default current_timestamp,
update_time datetime not null default current_timestamp on update current_timestamp,
is_deleted tinyint not null default 0,
delete_time datetime null comment '删除时间,记录软删除时间'
);
-- employment
create table if not exists employment(
id int auto_increment primary key,
sid int not null,
-- snum varchar(30) not null,
-- sname varchar(30) not null,
class_id int not null,
-- class_num varchar(30) not null comment '班级编号',
employment_open_time datetime null comment '就业开放时间',
offer_recived_time datetime null comment 'offer下发时间',
employment_company varchar(50) null comment '就业公司',
employment_salary decimal(20,5) null comment '就业薪资',
-- 通用字段
create_time datetime not null default current_timestamp,
update_time datetime not null default current_timestamp on update current_timestamp,
is_deleted tinyint not null default 0,
delete_time datetime null comment '删除时间,记录软删除时间'
);
造数据压测
qa
- cout / max 对聚合的支持数据库实现区别?
小范围数据测试
-- use student_management_system;
use sms;
-- ==================== 清空旧数据(按依赖倒序) ====================
set foreign_key_checks = 0;
truncate table employment;
truncate table scores;
truncate table students;
truncate table classes;
truncate table teachers;
set foreign_key_checks = 1;
-- ==================== teachers ====================
insert into teachers
(num, name, age, sex, home_place, college, specialty,
enrollment_time, graduate_time, education, work_experience, coach_area)
values
('T001', '张伟', 35, 1, '北京', '清华大学', '计算机科学与技术',
'2008-09-01', '2012-06-30', '本科', '10年Java教学经验', '应用'),
('T002', '李娜', 32, 2, '上海', '复旦大学', '软件工程',
'2010-09-01', '2014-06-30', '硕士', '8年前端教学经验', '项目'),
('T003', '王强', 40, 1, '广州', '中山大学', '人工智能',
'2004-09-01', '2008-06-30', '博士', '12年算法教学经验', '算法'),
('T004', '赵敏', 28, 2, '深圳', '浙江大学', '数据科学',
'2014-09-01', '2018-06-30', '硕士', '5年Python教学经验', '应用'),
('T005', '陈刚', 38, 1, '成都', '电子科技大学', '通信工程',
'2006-09-01', '2010-06-30', '本科', '11年嵌入式教学经验', '项目');
-- ==================== classes ====================
insert into classes
(num,name, class_start_time, head_teacher_id, coach_teacher_id, tutor_teacher_id)
values
('C2024001','C2024001', '2024-03-01 09:00:00', 1, 2, 4),
('C2024002','C2024001', '2024-04-01 09:00:00', 2, 3, 5),
('C2024003','C2024003', '2024-05-01 09:00:00', 3, 1, 4);
-- ==================== students ====================
insert into students
(num, name, age, sex, home_place, college, specialty,
enrollment_time, graduate_time, education, class_id, advisor_id)
values
('S2024001', '刘洋', 22, 1, '北京', '北京大学', '计算机科学与技术',
'2020-09-01', '2024-06-30', '本科', 1, 1),
('S2024002', '孙丽', 23, 2, '上海', '上海交通大学', '软件工程',
'2019-09-01', '2023-06-30', '本科', 1, 1),
('S2024003', '周杰', 24, 1, '广州', '华南理工大学', '人工智能',
'2018-09-01', '2022-06-30', '硕士', 1, 2),
('S2024004', '吴敏', 21, 2, '深圳', '深圳大学', '数据科学',
'2021-09-01', '2025-06-30', '本科', 2, 3),
('S2024005', '郑浩', 25, 1, '成都', '四川大学', '通信工程',
'2017-09-01', '2021-06-30', '本科', 2, 4),
('S2024006', '冯雪', 22, 2, '杭州', '浙江大学', '计算机科学与技术',
'2020-09-01', '2024-06-30', '本科', 2, 5),
('S2024007', '黄磊', 23, 1, '南京', '南京大学', '软件工程',
'2019-09-01', '2023-06-30', '本科', 3, 1),
('S2024008', '徐婷', 24, 2, '武汉', '武汉大学', '人工智能',
'2018-09-01', '2022-06-30', '硕士', 3, 2),
('S2024009', '马超', 21, 1, '西安', '西安交通大学', '数据科学',
'2021-09-01', '2025-06-30', '本科', 3, 3),
('S2024010', '朱琳', 22, 2, '天津', '南开大学', '通信工程',
'2020-09-01', '2024-06-30', '本科', 3, 4);
-- ==================== scores ====================
insert into scores (sid, num, score) values
-- 刘洋
(1, '第一次考核', 85.50), (1, '第二次考核', 90.00), (1, '第三次考核', 88.00),
-- 孙丽
(2, '第一次考核', 92.00), (2, '第二次考核', 87.50), (2, '第三次考核', 91.00),
-- 周杰
(3, '第一次考核', 78.00), (3, '第二次考核', 82.50), (3, '第三次考核', 80.00),
-- 吴敏
(4, '第一次考核', 95.00), (4, '第二次考核', 93.50), (4, '第三次考核', 96.00),
-- 郑浩
(5, '第一次考核', 70.00), (5, '第二次考核', 75.50), (5, '第三次考核', 72.00),
-- 冯雪
(6, '第一次考核', 88.00), (6, '第二次考核', 90.50), (6, '第三次考核', 89.00),
-- 黄磊
(7, '第一次考核', 82.00), (7, '第二次考核', 85.00), (7, '第三次考核', 83.50),
-- 徐婷
(8, '第一次考核', 91.00), (8, '第二次考核', 94.00), (8, '第三次考核', 92.50),
-- 马超
(9, '第一次考核', 76.00), (9, '第二次考核', 79.50), (9, '第三次考核', 77.00),
-- 朱琳
(10, '第一次考核', 89.00), (10, '第二次考核', 86.50), (10, '第三次考核', 90.00);
-- 提供不及格数据
insert into students
(num, name, age, sex, home_place, college, specialty,
enrollment_time, graduate_time, education, class_id, advisor_id)
values
('S2026001', '牧遥', 22, 1, '北京', '北京大学', '计算机科学与技术',
'2020-09-01', '2024-06-30', '本科', 1, 1);
insert into scores (sid, num, score) values
-- 牧遥
(11, '第一次考核', 56.50), (11, '第二次考核', 30.00), (11, '第三次考核', 88.00);
-- ==================== employment ====================
insert into employment
(sid, class_id, employment_open_time, offer_recived_time,
employment_company, employment_salary)
values
(1, 1, '2024-06-01 00:00:00', '2024-07-15 10:00:00', '字节跳动', 25000.00000),
(2, 1, '2024-06-01 00:00:00', '2024-07-20 14:30:00', '腾讯', 28000.00000),
(3, 1, '2024-06-01 00:00:00', '2024-08-01 09:00:00', '阿里巴巴', 30000.00000),
(4, 2, '2024-07-01 00:00:00', '2024-08-10 11:00:00', '美团', 24000.00000),
(5, 2, '2024-07-01 00:00:00', null, null, null),
(6, 2, '2024-07-01 00:00:00', '2024-08-15 16:00:00', '京东', 26000.00000),
(7, 3, '2024-08-01 00:00:00', '2024-09-05 10:30:00', '百度', 27000.00000),
(8, 3, '2024-08-01 00:00:00', '2024-09-10 15:00:00', '华为', 32000.00000),
(9, 3, '2024-08-01 00:00:00', null, null, null),
(10, 3, '2024-08-01 00:00:00', '2024-09-20 09:30:00', '小米', 23000.00000);
统计大盘 - 示例sql
-- 基本信息统计
select * from students where age > 30;
select class_id, sex, count(*) from students group by class_id,sex;
-- 成绩统计
select s.num,s.name, sc.score from students s left join scores sc on s.id = sc.sid where sc.score > 80;
-- 两次不及格 -> CTE 更好点: <- todo | 代码实现角度:?
select s.name 学生名称,c.name 班级名称 ,tmp.score 学生成绩
from
students as s, classes as c, (select sid,score from scores where score < 60 group by sid, score) as tmp ,
(select sid from (select sid,score from scores where score < 60 group by sid, score) as tmp
group by sid having count(*) >= 2
) as sid_table
where s.id = sid_table.sid
and s.class_id = c.id
and s.id = tmp.sid;
-- 或者:全量join,然后2次group by
--
select s.class_id, sc.num, avg(sc.score)
from students s left join scores sc
on s.id = sc.sid
group by sc.num, s.class_id
order by avg(sc.score) desc;
-- 就业统计
select s.name, c.name,
e.employment_open_time, e.offer_recived_time,
e.employment_company,e.employment_salary
from students s
left join classes c on s.class_id = c.id
left join employment e on s.id = e.sid
order by e.employment_salary desc
limit 5;
select s.name, c.name,
e.employment_open_time, e.offer_recived_time,
e.employment_company,e.employment_salary,
(e.offer_recived_time - e.employment_open_time) as duration
from students s
left join classes c on s.class_id = c.id
left join employment e on s.id = e.sid;
select s.class_id, avg(e.offer_recived_time -e.employment_open_time)
from students s
left join classes c on s.class_id = c.id
left join employment e on s.id = e.sid
where
e.employment_open_time is not null
group by s.class_id;
employment_open_time datetime null comment '就业开放时间',
offer_recived_time datetime null comment 'offer下发时间',
employment_company varchar(50) null comment '就业公司',
employment_salary decimal(20,5) null comment '就业薪资',
学生表 - 数据量瓶颈是多少? - 千万?亿级
分库分表 - 考虑历史数据与最新数据
表结构优化
通用字段方案
框架层 - ORM 基类
from sqlalchemy import Column, Integer, String, DATETIME, create_engine, Boolean
from datetime import datetime
# 定义基类模板,解决通用字段
class Template():
id = Column( Integer # 声明字段的数据类型
, primary_key=True # 声明是主键
, autoincrement=True # 声明是自增主键
, comment='订单编号,自增主键' # 注释
)
create_datetime = Column(DATETIME
, default=datetime.now # 默认值是当前时间
)
update_datetime = Column(DATETIME
, default=datetime.now # 首次创建的时间跟更新时间一致
, onupdate=datetime.now
)
is_deleted = Column(Boolean, default=False )
delete_datetime = Column(DATETIME, nullable=True)
triger deletetime
delimiter $$
create trigger trg_students_update_time
before update on students
for each row
begin
-- 只在业务字段变化时更新 update_time,纯软删除操作不更新
if not (old.is_deleted <=> new.is_deleted)
and old.sname <=> new.sname
and old.age <=> new.age then
set new.update_time = old.update_time;
else
set new.update_time = current_timestamp;
end if;
end$$
delimiter ;
索引与约束
索引同步
| 表 | 索引名 | 类型 | 字段 | 对应查询场景 |
|---|---|---|---|---|
| students | PRIMARY | 主键 | id |
按主键查 |
| uk_snum | 唯一 | snum |
按学号精确查 | |
| idx_class_id | 普通 | class_id |
按班级查学生列表 | |
| idx_advisor_id | 普通 | advisor_id |
按顾问查学生 | |
| idx_class_deleted | 联合 | (class_id, is_deleted) |
查某班未删除学生 | |
| teachers | PRIMARY | 主键 | id |
按主键查 |
| uk_tnum | 唯一 | tnum |
按老师编号精确查 | |
| classes | PRIMARY | 主键 | id |
按主键查 |
| uk_class_num | 唯一 | class_num |
按班级编号精确查 | |
| idx_head_teacher | 普通 | head_teacher_id |
按班主任查班级 | |
| idx_coach_teacher | 普通 | coach_teacher_id |
按授课老师查班级 | |
| idx_tutor_teacher | 普通 | tutor_teacher_id |
按助教查班级 | |
| scores | PRIMARY | 主键 | id |
按主键查 |
| uk_sid_score | 唯一 | (sid, score_num) |
防止同一学生同一考核序次重复录入 | |
| idx_snum | 普通 | snum |
按学号查成绩 | |
| idx_sid | 普通 | sid |
按学生 ID 查成绩 | |
| employment | PRIMARY | 主键 | id |
按主键查 |
| uk_sid | 唯一 | sid |
一个学生只能有一条就业记录 | |
| idx_class_id | 普通 | class_id |
按班级统计就业 | |
| idx_snum | 普通 | snum |
按学号查就业 | |
| idx_class_deleted | 联合 | (class_id, is_deleted) |
查某班未删除就业记录 |
-- alter table students drop primary key -- auto_increment 列 必须是 key
-- alter table students drop index idx_email
-- ==================== students ====================
alter table students
add unique key uk_snum (num),
add key idx_class_id (class_id),
add key idx_advisor_id (advisor_id),
add key idx_class_deleted (class_id, is_deleted);
-- ==================== teachers ====================
alter table teachers
add unique key uk_tnum (num);
-- ==================== classes ====================
alter table classes
add unique key uk_class_num (num),
add key idx_head_teacher (head_teacher_id),
add key idx_coach_teacher (coach_teacher_id),
add key idx_tutor_teacher (tutor_teacher_id);
-- ==================== scores ====================
alter table scores
add unique key uk_sid_score (sid, num),
-- add key idx_snum (snum),
add key idx_sid (sid);
-- ==================== employment ====================
alter table employment
add unique key uk_sid (sid),
add key idx_class_id (class_id),
-- add key idx_snum (snum),
add key idx_class_deleted (class_id, is_deleted);
外键约束
| 表 | 字段 | 引用 | 含义 |
|---|---|---|---|
| students | class_id |
classes.id |
学生所属班级 |
| students | advisor_id |
teachers.id |
学生的顾问老师 |
| classes | head_teacher_id |
teachers.id |
班主任 |
| classes | coach_teacher_id |
teachers.id |
授课老师 |
| classes | tutor_teacher_id |
teachers.id |
助教老师 |
| scores | sid |
students.id |
成绩所属学生 |
| employment | sid |
students.id |
就业记录所属学生 |
| employment | class_id |
classes.id |
就业记录所属班级 |
-- SET FOREIGN_KEY_CHECKS = 0; -- 临时关闭
-- 批量导入
-- SET FOREIGN_KEY_CHECKS = 1;
-- ==================== students ====================
alter table students
add constraint fk_students_class
foreign key (class_id) references classes(id)
on delete restrict on update cascade,
-- on delete set null on update cascade,
add constraint fk_students_advisor
foreign key (advisor_id) references teachers(id)
on delete set null on update cascade;
-- ==================== classes ====================
alter table classes
add constraint fk_classes_head_teacher
foreign key (head_teacher_id) references teachers(id)
on delete set null on update cascade,
add constraint fk_classes_coach_teacher
foreign key (coach_teacher_id) references teachers(id)
on delete set null on update cascade,
add constraint fk_classes_tutor_teacher
foreign key (tutor_teacher_id) references teachers(id)
on delete set null on update cascade;
-- ==================== scores ====================
alter table scores
add constraint fk_scores_student
foreign key (sid) references students(id)
on delete cascade on update cascade;
-- on delete set null on update cascade;
-- ==================== employment ====================
alter table employment
add constraint fk_employment_student
foreign key (sid) references students(id)
on delete cascade on update cascade,
-- on delete set null on update cascade,
add constraint fk_employment_class
foreign key (class_id) references classes(id)
on delete restrict on update cascade;
-- on delete set null on update cascade;
字典表: 解决字段规范问题
education、college、specialty
| 字段 | 所在表 | 问题 | 字典化收益 |
|---|---|---|---|
sex |
students, teachers | 用 int 存,取值含义靠注释 |
统一“0未知/1男/2女” |
education |
students, teachers | 字符串自由填写 | 统一“本科/硕士/博士”等 |
college |
students, teachers | 字符串自由填写 | 统一院校名称 |
specialty |
students, teachers | 字符串自由填写 | 统一专业名称 |
coach_area |
teachers | text 存“应用/项目/算法” |
统一教学方向 |
employment_company |
employment | 字符串自由填写 | 可选,公司名变化快,不一定适合字典 |
判断标准:取值有限、稳定、需要跨表统一的字段,适合字典化;取值开放、频繁变化的字段(如公司名、地址),不适合。
通用字典表
create table if not exists dict_items (
id int auto_increment primary key,
dict_type varchar(50) not null comment '字典类型:sex, education, college, specialty',
dict_code varchar(50) not null comment '字典编码,存到业务表的值',
dict_label varchar(100) not null comment '显示名称',
sort_order int not null default 0 comment '排序',
is_enabled tinyint not null default 1 comment '是否启用',
create_time datetime not null default current_timestamp,
update_time datetime not null default current_timestamp on update current_timestamp,
is_deleted tinyint not null default 0,
delete_time datetime not null default null,
unique key uk_type_code (dict_type, dict_code),
key idx_type (dict_type)
) engine=InnoDB default charset=utf8mb4;
insert into dict_items (dict_type, dict_code, dict_label, sort_order) values
('sex', '0', '未知', 1),
('sex', '1', '男', 2),
('sex', '2', '女', 3),
('education', 'bachelor', '本科', 1),
('education', 'master', '硕士', 2),
('education', 'phd', '博士', 3),
('coach_area', 'app', '应用', 1),
('coach_area', 'project', '项目', 2),
('coach_area', 'algorithm', '算法', 3);
每个维度独立建字典 - 适合高校+专业维度多 - 后期维护更专业!
create table if not exists dict_colleges (
id int auto_increment primary key,
college_code varchar(50) not null,
college_name varchar(100) not null,
is_enabled tinyint not null default 1,
unique key uk_code (college_code)
) engine=InnoDB default charset=utf8mb4;
create table if not exists dict_specialties (
id int auto_increment primary key,
specialty_code varchar(50) not null,
specialty_name varchar(100) not null,
college_id int not null comment '所属学院',
is_enabled tinyint not null default 1,
unique key uk_code (specialty_code),
key idx_college (college_id)
) engine=InnoDB default charset=utf8mb4;
-- 全国高校,专业数据
业务表引用
dict_code - 采用
-- students.education 存 'bachelor',而不是 '本科'
select s.sname, d.dict_label as education_name
from students s
left join dict_items d
on d.dict_type = 'education'
and d.dict_code = s.education
and d.is_deleted = 0;
id - 简单,但是迁移 - 跨环境id自增导致不一致问题
select s.* ,
from students s left join dict_items d
on s.college = d.id
注意事项
软删除的字典项,业务表还在引用 <- 字典表更新 - 轻易不再动
- 如果某个字典项被软删除,但业务表里还有引用它的记录,查询时会 JOIN 不到,显示为空。
- 解决方案:字典项不轻易删除,只设
is_enabled = 0(禁用),禁用后不允许新记录引用,但历史记录仍能 JOIN 到业务表引用字典表的
dict_code,不建议加数据库外键
因为
dict_code是字符串,且字典表可能被缓存到应用层,加外键会限制字典的灵活性。用应用层校验:插入业务记录前,先检查
dict_code是否存在于字典表。字典数据量大了怎么办?
- 如果字典项很多(比如全国高校列表),
dict_items一张表会变得很大。- 解决方案:按
dict_type分表,或者把大字典(如高校)单独建表。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 字典表 | 可动态维护、跨表统一 | 需 JOIN、需维护 | 取值有限且需要统一 |
| 枚举(ENUM) | 数据库层面约束 | 改枚举要 DDL、不灵活 | 取值极少且永不变 |
| 直接存字符串 | 简单、无 JOIN | 容易不一致、难统计 | 取值开放、无需统一 |
技术架构设计
构建环境适配
# uv 统一
pip install uv
uv init
uv add fastapi[standard] pymsql sqlalchemy # 添加依赖
# 运行测试
uv run uvicorn main:app --reload # uv run fastapi dev
# 其他平台: sync 同步即可
uv sync
基础架构适配
- 后端框架:
- web route / api docs / 数据校验 : FastAPI
- ORM 框架: pymysql, sqlarchemy
- 日志系统:loguru,
- 数据库:多数据源适配
- dev : sqlite3
- prod: MySQL, PG
- 前端框架:
- vue3
脚手架设计 - 单体架构
分布式架构 - 微服务
方向:
- 把学生模块拆分出来
AI-agent接入
核心
- 让agent 做架构优化
- 添加业务模块:课程体系设计,
- 生成前端
- 完善脚手架....
Languages
Python
100%