Compare commits

...
Author SHA1 Message Date
Orangeeee a21f166279 test。 2026-09-23 21:21:13 +08:00
Orangeeee 697a809a77 主文件README.md。 2026-09-23 20:34:34 +08:00
Orangeeee 57d0c3f95a 整体bug修改。 2026-09-23 20:30:51 +08:00
Orangeeee 483cc9cf28 整体bug修改。 2026-09-23 17:15:58 +08:00
Orangeeee 5871afc9aa 调整IP、端口地址。 2026-09-23 15:43:09 +08:00
Orangeeee af92c45e6a Merge remote-tracking branch 'origin/geyuan_last' into lon_students_merge_code_test 2026-09-23 14:36:48 +08:00
geyuan f697a8b634 加更改时提交空值不覆盖 2026-09-23 14:05:49 +08:00
Orangeeee 9e9d7e2606 更新统计模块查询入参。 2026-09-23 12:30:55 +08:00
Orangeeee 0fd0c4cd8b Merge remote-tracking branch 'origin/geyuan_last' into lon_students_merge_code_test 2026-09-23 12:30:28 +08:00
geyuan 8b26d3b239 添加一些注释 2026-09-23 12:23:41 +08:00
Coffee 2ce8fdacf6 添加statcalc 学生就业时常入参 2026-09-23 12:14:22 +08:00
Coffee 7d8d14fec1 Merge remote-tracking branch 'origin/lon_students_merge_code_test' into lon_students_merge_code_test 2026-09-23 12:09:59 +08:00
Coffee 6c0df6526e 添加statcalc top5入参 2026-09-23 12:09:49 +08:00
Orangeeee dbc3cd851f students模块bug修复更新 2026-09-23 11:15:13 +08:00
chen 55abde3c65 统计的dao 2026-09-23 11:00:36 +08:00
Orangeeee 069f8f364d class模块的readme.md更新。 2026-09-23 10:52:16 +08:00
Anhoa 1107260138 Merge remote-tracking branch 'origin/lon_students_merge_code_test' into lon_students_merge_code_test 2026-09-23 10:38:21 +08:00
Anhoa a3c54f3db5 read.me 2026-09-23 10:38:12 +08:00
Orangeeee 72f6012695 Merge remote-tracking branch 'origin/lon_students_merge_code_test_coffee' into lon_students_merge_code_test 2026-09-23 10:29:10 +08:00
Orangeeee 8493ad9769 Merge remote-tracking branch 'origin/geyuan_last' into lon_students_merge_code_test 2026-09-23 10:28:48 +08:00
geyuan fc522c14c3 调整一下请求体 2026-09-23 10:28:34 +08:00
Coffee e31970e696 11 2026-09-23 10:28:08 +08:00
Orangeeee e16c1b3307 更新调整stu模块bug。 2026-09-23 10:26:07 +08:00
Orangeeee 10b367a7d3 Merge remote-tracking branch 'origin/lon_students_merge_code_xioayang_test' into lon_students_merge_code_test 2026-09-23 10:26:04 +08:00
Orangeeee 4d9e889528 Merge remote-tracking branch 'origin/lon_students_merge_code_test_coffee' into lon_students_merge_code_test 2026-09-23 10:25:49 +08:00
Coffee 9922f4fe48 更新debug 2026-09-23 10:25:32 +08:00
chen 60a72dd297 模块解释 2026-09-23 10:24:03 +08:00
Orangeeee 30350d7f9c Merge remote-tracking branch 'origin/geyuan_last' into lon_students_merge_code_test 2026-09-23 10:12:51 +08:00
Orangeeee bc4d69b27c 更新调整stu模块bug。 2026-09-23 10:12:21 +08:00
geyuan 19298b5490 文字说明 2026-09-23 09:44:09 +08:00
Orangeeee d2464c7536 students/README_students.md更新,工具使用说明。 2026-09-23 04:35:03 +08:00
Orangeeee 5dc0c68ca2 统计分析所有测试通过。 2026-09-23 04:27:31 +08:00
Orangeeee 1ff0782399 前端测试通过,部分接口没有数据,运行没问题了。 2026-09-23 03:53:57 +08:00
Orangeeee 24a7da624b bug修复,前端测试。 2026-09-23 02:40:26 +08:00
Orangeeee 38b95eadf8 bug修复,前端测试。 2026-09-23 02:19:58 +08:00
Orangeeee 45dbd3e99c Merge remote-tracking branch 'origin/geyuan1' into lon_students_merge_code_test
# Conflicts:
#	class_management/dao/class_management_dao.py
#	class_management/main.py
#	students/README_students.md
2026-09-23 02:18:24 +08:00
Orangeeee efb8e2ebc3 Merge remote-tracking branch 'origin/lon_students_merge_code_test' into lon_students_merge_code_test 2026-09-23 01:03:09 +08:00
Orangeeee 642b70dd1c 新增启动时插入数据模块,新增前端页面接口,students模块更新README_students.md模块解释说明,主程序main.py更新数据模块接口。 2026-09-23 01:03:00 +08:00
geyuan b67e3cd547 修更新的bud 2026-09-23 00:50:59 +08:00
Anhoa 0709b47bfd Merge remote-tracking branch 'origin/lon_students_merge_code_test' into lon_students_merge_code_test 2026-09-22 23:35:16 +08:00
Anhoa fcca2490c7 删除add_class_dao的db.refresh 2026-09-22 23:13:36 +08:00
chen 079a7cf351 模块解释 2026-09-22 23:02:42 +08:00
Coffee 2dca7c7f76 更新debug 2026-09-22 23:02:42 +08:00
Orangeeee 566747b320 更新Student的README_students.md模块解释文件。 2026-09-22 23:02:42 +08:00
Orangeeee 699867b0af 优化了students_dao里add_student_dao的报错逻辑。 2026-09-22 23:02:42 +08:00
Orangeeee fa571a13da 优化了students_dao里add_student_dao的报错逻辑。 2026-09-22 23:02:42 +08:00
Coffee 5fddb78c27 更新db.refresh 2026-09-22 23:02:42 +08:00
Coffee b5883e450d 更新debug 2026-09-22 23:02:39 +08:00
Coffee dc243fb33f 更新外键 2026-09-22 23:01:59 +08:00
Anhoa 6e189d0c65 更改新增dao bug 2026-09-22 23:01:59 +08:00
chen f2e525b5a5 统计 2026-09-22 23:01:59 +08:00
Orangeeee a1d7adba94 全部更新确认,无基础错误,各自测试各自端口bug,暂时没有自动导入数据。 2026-09-22 23:01:59 +08:00
chen 7d5f352c69 统计 2026-09-22 23:01:56 +08:00
Orangeeee ffcb40db41 统计分析模块更新,待测试,统计分析模块更新,就业3个模块更新。 2026-09-22 23:01:21 +08:00
chen 75f23e39be dao改动 2026-09-22 23:01:21 +08:00
longtrying c1813ae700 删除 .env
以免频繁重新覆盖修改。
2026-09-22 23:01:21 +08:00
Anhoa 65d67e7cf6 增加了num的唯一键,修改了dao和api 2026-09-22 23:01:18 +08:00
Orangeeee c0880f9a44 统计分析模块更新,待测试,优化所有模块main.py文件,修复bug。 2026-09-22 22:59:50 +08:00
Orangeeee a4a55a9858 Merge remote-tracking branch 'origin/lon_students_merge_code_xioayang_test' into lon_students_merge_code_test 2026-09-22 22:57:00 +08:00
Orangeeee f5f9f43813 Merge remote-tracking branch 'origin/lon_students_merge_code_test_coffee' into lon_students_merge_code_test 2026-09-22 22:56:53 +08:00
Orangeeee 5e4a870547 Merge remote-tracking branch 'origin/geyuan' into lon_students_merge_code_test 2026-09-22 22:56:41 +08:00
Orangeeee 62f4fea198 更新Student的README_students.md模块解释文件。 2026-09-22 22:56:19 +08:00
geyuan 9c9e51d68f 改提交不上的bud 2026-09-22 22:55:17 +08:00
Coffee 7f58136a9f 更新debug 2026-09-22 22:40:48 +08:00
geyuan 334bb237b2 改提交不上的bud 2026-09-22 22:32:44 +08:00
chen 6515bd811c 模块解释 2026-09-22 22:07:46 +08:00
Orangeeee a27c326e94 优化了students_dao里add_student_dao的报错逻辑。 2026-09-22 21:57:56 +08:00
Orangeeee e76cee4bd6 优化了students_dao里add_student_dao的报错逻辑。 2026-09-22 21:42:44 +08:00
Orangeeee ff68a676c4 Merge remote-tracking branch 'origin/lon_students_merge_code_test_coffee' into lon_students_merge_code_test 2026-09-22 21:27:36 +08:00
Orangeeee ea9c85d7da Merge remote-tracking branch 'origin/geyuan' into lon_students_merge_code_test 2026-09-22 21:27:06 +08:00
Coffee 336467b96a 更新db.refresh 2026-09-22 21:23:40 +08:00
Anhoa 3b1a9d54a5 更改新增dao bug 2026-09-22 21:23:24 +08:00
geyuan 737d48babc 加电话号码的限制 2026-09-22 21:20:24 +08:00
Orangeeee ba2155643b Merge remote-tracking branch 'origin/geyuan' into lon_students_merge_code_test
# Conflicts:
#	class_management/main.py
2026-09-22 21:06:40 +08:00
Orangeeee a571243415 Merge remote-tracking branch 'origin/lon_students_merge_code_xioayang_test' into lon_students_merge_code_test 2026-09-22 21:05:34 +08:00
chen ff03df7d8d 统计 2026-09-22 21:03:15 +08:00
geyuan 17c739760f 文字说明 2026-09-22 21:01:35 +08:00
Coffee 8b50a337fa Merge remote-tracking branch 'origin/lon_students_merge_code_test_coffee' into lon_students_merge_code_test_coffee
# Conflicts:
#	stu_jiuye/CURD_api.py
2026-09-22 20:55:24 +08:00
Coffee 4604d3ba5f 更新外键 2026-09-22 20:51:20 +08:00
geyuan 748b9f751c 新增科目增加接口 2026-09-22 20:43:33 +08:00
Coffee c740efc022 更新debug 2026-09-22 19:44:35 +08:00
42 changed files with 5062 additions and 246 deletions
+2 -2
View File
@@ -8,8 +8,8 @@ __pycache__/
tests/
.git/
.github/
*.md
!README.md
#*.md
#!README.md
.env
.env.*
Dockerfile
+881
View File
@@ -0,0 +1,881 @@
# 教培系统架构设计与开发
本项目旨在开发一个基于FastAPI的学生管理系统,提供**学生基本信息管理、考核成绩管理和就业管理**三大核心功能模块。
系统将采用RESTful API设计,支持前后端分离架构。
## 痛点分析 - 沃林专项
- 部门系统与产品 <- 没有拳头产品 - 给外面演示的系统,产品
- 内部OA系统都没有接AI,用的是飞书大厂的平台
- 学员就业信息等还是纸质存储,没有信息化
- 拳头产品,**自身没有服务外部公司的团队与技术能力** - 目前**只局限培训**
- 教学课程开发 <- 课程体系太单一
- 只有入门-项目,不够灵活,满足专业3-5年的诉求
- 入门课程没有采用最新工具,**框架现代化API,很容易出去被pass - 外包等**
- 只有30-60人班级,没有**小班制进阶,专家培训**
- 客户群体拓展 <- 更多是跨行想入门的小白,
- 风险把控 <- 后付款的包就业模式,资金链如何把控?
## 需求分期
团队对需求管理按照模块化解耦拆分开发,按照三个阶段开发
1. 基础需求:按照开发任务,完成5 + 1 统计模块的开发
2. 特色需求:
- 数据源配置
- 鉴权管理
- 添加部门管理,顾问管理模块
- 添加用户认证模块
- 开发前端应用
- 开发更多的分析查询接口
- 增加AI智能体和知识库开发
3. 脚手架集成
### 第一阶段 - 基础需求
按照开发任务,完成5 + 1 统计模块的开发
- 学生
- 班级
- 老师
- 成绩
- 就业
- 统计分析
### 第二阶段 - 特色需求
- 数据源配置
- 鉴权管理
- 添加部门管理,顾问管理模块
- 添加用户认证模块
- 开发前端应用
- 开发更多的分析查询接口
- 增加AI智能体和知识库开发
### 第三阶段 - 脚手架集成
核心组件
- 服务状态监控 - utc时区[还有哪些可替代?]
- db ,中间件等组件是否活跃
- 自身服务是否活跃
- 认证模块: oauth2_scheme
-
-
## 业务拆分
> 业务范围划分
>
> - 实体
> - 学生基本信息管理
> - 考核成绩管理
> - 就业管理
> - 班级
> - 老师
> - 统计分析
## 数据架构设计
### 数据关系
- 学生:班级 = N: 1, 班级:老师 = 1 : 1 / [N:1], 学生: 成绩 = 1:N , 学生:就业 = 1:1,
- 统计分析:
### 表结构设计
```sql
-- 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 对聚合的支持数据库实现区别?
-
#### 小范围数据测试
```sql
-- 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
```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;
```
```md
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 基类
```python
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
```sql
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)` | 查某班未删除就业记录 |
```sql
-- 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` | 就业记录所属班级 |
```sql
-- 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 | 字符串自由填写 | 可选,公司名变化快,不一定适合字典 |
**判断标准**:取值**有限、稳定、需要跨表统一**的字段,适合字典化;取值**开放、频繁变化**的字段(如公司名、地址),不适合。
##### 通用字典表
```sql
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);
```
##### 每个维度独立建字典 - 适合高校+专业维度多 - 后期维护更专业!
```sql
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 - 采用
```sql
-- 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自增导致不一致问题
```sql
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 | 容易不一致、难统计 | 取值开放、无需统一 |
## 技术架构设计
### 构建环境适配
```bash
# 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 做架构优化
> - 添加业务模块:课程体系设计,
> -
> - 生成前端
> - 完善脚手架....
+42 -32
View File
@@ -1,6 +1,6 @@
# statcalc_api所有统计分析模块的api路由、接口
from fastapi import APIRouter , Depends , HTTPException
from fastapi import APIRouter , Depends , HTTPException , Query
from StatCalc.dao.statcalc_dao import *
from StatCalc.schema.statcalc_request import *
from databases import *
@@ -9,13 +9,13 @@ statcalc_api = APIRouter()
@statcalc_api.get("/statcalc/age"
,summary='人员信息查询'
,description=f'查询所有超过30岁的学员的信息。'
,description=f'查询所有大于等于输入年龄的学员的信息。'
)
def get_students(db=Depends(get_db)):
result = get_age_30( db=db )
if result:
return result
raise HTTPException(status_code=404, detail="没有找到符合条件的学生")
def get_students( age:int = 0 ,db=Depends(get_db)):
result = get_age_30( age=age , db=db )
if result is None or len(result) == 0:
raise HTTPException(status_code=404, detail="没有找到符合条件的学生")
return result
@statcalc_api.get("/statcalc/students"
,summary='人数统计'
@@ -30,62 +30,72 @@ def get_students(headcount:get_student_dao
raise HTTPException(status_code=404, detail="没有找到符合条件的学生")
@statcalc_api.get("/statcalc/scores"
,summary='80分以上成绩学生信息查询'
,description=f'查询每次考试成绩都在80分以上的学生的编号,姓名和成绩。'
,summary='优秀成绩学生信息查询'
,description=f'查询所有科目考试成绩都在80分以上的学生。'
)
def get_students(db=Depends(get_db)):
result = get_score_80( db=db )
def get_excellent(sco:int = Query(80,ge=0, le=100,description="优秀成绩设定")
, db=Depends(get_db)
):
result = get_excellent_students(sco=sco, db=db )
if result:
return result
raise HTTPException(status_code=404, detail="没有找到符合条件的学生")
@statcalc_api.get("/statcalc/fail"
,summary='不及格成绩查询'
,description=f'查询有两次以上不及格的学生的姓名,班级和不及格成绩。'
,description=f'查询有两次以上低于输入分数的学生的姓名,班级和不及格成绩。'
)
def get_students( db=Depends(get_db) ):
result = get_score_80( db=db )
def get_students(sco:int = Query(60,ge=0, le=100,description="不及格成绩设定")
, db=Depends(get_db) ):
result = get_score_60( sco=sco ,db=db )
if result:
return result
raise HTTPException(status_code=404, detail="没有找到符合条件的学生")
@statcalc_api.get('/statcalc/average', summary='统计每次考试每个班级的平均分')
def get_class_avg(db=Depends(get_db)):
result = get_class_avg_dao(db)
def get_class_avg( class_id:int = Query(1,ge=1, le=3,description="查询班级")
, db=Depends(get_db)):
result = get_class_avg_dao( class_id , db)
if not result:
return []
return result
from stu_jiuye.model import *
from students.model.students_model import Students
from class_management.model.class_management_model import ClassInfo
@statcalc_api.get('/statcalc/top',summary='就业薪资Top5')
def salarytop5(db=Depends(get_db)):
q = db.query(Employments,Company.employment_company) \
.join(Company, Employments.company_id == Company.id) \
.filter(Employments.is_deleted == 0)\
.order_by(Employments.employment_salary.desc())\
.limit(5) .all()
return [{'学生姓名':i.sname,'学生班级编号':i.class_num,'就业时间':i.offer_recived_time,'公司':employment_company} for i,employment_company in q]
def salarytop5(num:int,db=Depends(get_db)):
q = db.query(Employments, Company.employment_company, Students.name.label('sname'), ClassInfo.name.label('class_num')) \
.join(Company, Employments.company_id == Company.id) \
.join(Students, Employments.stuid == Students.id) \
.join(ClassInfo, Employments.class_id == ClassInfo.id) \
.filter(Employments.is_deleted == 0) \
.order_by(Employments.employment_salary.desc()) \
.limit(num).all()
# q是4元组 (Employments对象, company_name, sname, class_num)
return [{'学生姓名': row[2], '学生班级编号': row[3], '就业时间': str(row[0].offer_recived_time) if row[0].offer_recived_time else None, '公司': row[1]} for row in q]
@statcalc_api.get('/statcalc/job',summary='学生就业时长')
def worktime(db=Depends(get_db)):
q = db.query(Employments)\
.filter(Employments.is_deleted==0)\
.all()
def worktime(stuid:int,db=Depends(get_db)):
q = db.query(Employments, Students.name.label('sname')) \
.join(Students, Employments.stuid == Students.id) \
.filter(Employments.stuid == stuid,Employments.is_deleted == 0) \
.all()
return [
{'学生姓名':i.sname
,'就业时常':f'{i.offer_recived_time-i.employment_open_time}'
if i.offer_recived_time and i.employment_open_time else '暂无数据'} for i in q
{'学生姓名': row[1],
'就业时常':f'{row[0].offer_recived_time-row[0].employment_open_time}'
if row[0].offer_recived_time and row[0].employment_open_time else '暂无数据'} for row in q
]
@statcalc_api.get('/statcalc/avgjob',summary='学生平均就业时长')
def avgworktime(db=Depends(get_db)):
q = db.query(Employments)\
q = db.query(Employments) \
.filter(Employments.employment_open_time.isnot(None)
,Employments.offer_recived_time.isnot(None)
,Employments.is_deleted==0)\
,Employments.is_deleted==0) \
.all()
if not q:
return {'平均就业时常': '暂无数据'}
+52 -29
View File
@@ -1,15 +1,12 @@
from pygments.lexers import sql
from sentry_sdk.integrations import sqlalchemy
from StatCalc.schema.statcalc_request import *
from fastapi import HTTPException
from sqlalchemy import *
from students.model.students_model import Students
def get_age_30(db):
q = db.query(Students).filter(Students.age>=30).all()
def get_age_30( age , db):
q = db.query(Students).filter(Students.age>=age).all()
return q
@@ -17,52 +14,78 @@ from class_management.model.class_management_model import ClassInfo
def get_class_count(g , db):
if g == "班级人数":
q = (db.query(ClassInfo.id,ClassInfo.name,func.count(ClassInfo.id) )
q = (db.query(ClassInfo.id,ClassInfo.name,func.count(ClassInfo.id).label('count') )
.join(Students,Students.class_id==ClassInfo.id)
.group_by(ClassInfo.id,ClassInfo.name,Students.sex)
.all())
elif g == "男女人数":
q = (db.query(Students.sex,func.count(ClassInfo.id) )
.join(Students,Students.class_id==ClassInfo.id)
.group_by(Students.sex)
.all())
q = (db.query(Students.sex, func.count(Students.id).label('count'))
.group_by(Students.sex)
.all())
elif g == "班级中男女人数":
q = (db.query(ClassInfo.id,ClassInfo.name,Students.sex,func.count(ClassInfo.id) )
q = (db.query(ClassInfo.id,ClassInfo.name,Students.sex,func.count(ClassInfo.id).label('count') )
.join(Students,Students.class_id==ClassInfo.id)
.group_by(ClassInfo.id,ClassInfo.name,Students.sex)
.all())
else:
return False
return q
return [row._asdict() for row in q]
from scores.model.score_model import Score
def get_score_80(db):
sq = ( db.query(Students.id)
.join(Score, Score.sid == Students.id)
.filer(Score.score < 80 )
.group_by(Students.id)
.having(func.count(Score.id) > 2 )
.sq()
)
q = ( db.query(Students.name,ClassInfo.name,Score.score )
.join(ClassInfo, ClassInfo.id == Students.class_id)
.join(Score, Score.sid == Students.id)
.filer(Students.id.in_(sq),Score.score < 80)
.all()
)
return q
def get_excellent_students(sco,db):
"""查询所有科目都在80分以上的优秀学生"""
# 先查总科目数
total_subjects = db.execute(text("SELECT COUNT(*) FROM subject")).scalar()
def get_class_avg_dao(db):
# 子查询:找出成绩>=80分的科目数等于总科目数的学生
sq = db.query(Students.id) \
.join(Score, Score.sid == Students.id) \
.filter(Score.score >= sco) \
.group_by(Students.id) \
.having(func.count(Score.id) == total_subjects) \
.subquery()
# 主查询:返回优秀学生信息
q = db.query(Students.id, Students.name, ClassInfo.name.label('class_name'), Score.score) \
.join(ClassInfo, ClassInfo.id == Students.class_id) \
.join(Score, Score.sid == Students.id) \
.filter(Students.id.in_(sq.select()), Score.score >= sco) \
.all()
return [row._asdict() for row in q]
from scores.model.score_model import Score
def get_score_60(sco ,db):
"""查询有两次以上不及格(<60分)的学生"""
# 子查询:找出不及格超过2门的学生ID
sq = db.query(Students.id) \
.join(Score, Score.sid == Students.id) \
.filter(Score.score < sco) \
.group_by(Students.id) \
.having(func.count(Score.id) > 2) \
.subquery()
# 主查询:返回不及格学生信息
q = db.query(Students.name, ClassInfo.name.label('class_name'), Score.score) \
.join(ClassInfo, ClassInfo.id == Students.class_id) \
.join(Score, Score.sid == Students.id) \
.filter(Students.id.in_(sq.select()), Score.score < sco) \
.all()
return [row._asdict() for row in q]
def get_class_avg_dao(class_id , db):
results = (db.query(Score.num,
Score.cid,
ClassInfo.name.label("class_name"),
func.avg(Score.score).label("avg_score"))
.join(ClassInfo, Score.cid == ClassInfo.id)
.filter(Score.is_deleted == 0)
.group_by(Score.num, Score.cid, ClassInfo.name)
.group_by(Score.num, Score.cid, ClassInfo.name,ClassInfo.id)
.having(ClassInfo.id == class_id)
.order_by(func.avg(Score.score).desc())
.all())
-2
View File
@@ -1,7 +1,5 @@
from fastapi import FastAPI,APIRouter
from StatCalc.api.statcalc_api import statcalc_api
from databases import *
from contextlib import asynccontextmanager
StatCalc_API = APIRouter()
StatCalc_API.include_router(statcalc_api)
-2
View File
@@ -1,7 +1,5 @@
# students_schema:请求、响应的模型框架
from pydantic import BaseModel , Field , field_validator
from datetime import datetime
import enum
class get_student_dao(str , enum.Enum):
+18 -6
View File
@@ -10,23 +10,35 @@
2.教师信息的新增\
3.教师信息的更新\
4.教师信息的逻辑删除\
4.1输入id查\
4.2输入任意信息查\
5.学科信息的新增\
**dao数据筛选文件**\
1.查询 get_tea_dao:\
2.新增 add_tea_dao:\
3.更新 update_tea_dao:\
4.逻辑删除 delete_tea_dao:\
4.根据教师id逻辑删除 delete_tea_dao:\
5.任意信息逻辑删除\
6.科目信息的新增\
**tea_model数据库建表文件**\
1.教师表\
2.科目表\
字段名:id、name、sex、class_id、sub(学科)\
home_place specialty(专业)、start_time(入职时间)\
work_experience、collage、creat_date、update_date\
is_delete、delete_time\
is_delete、delete_time、phone\
2.学科表\
字段名:id,name(科目名字)
字段名:id,name(科目名字)\
**schema 请求体、响应体**\
请求体:设置了两个,一个带自增组件用于查询\
一个不带自增组件,新增用;\
响应体:用于返回给前端
教师表\
请求体:\
1.查询、任意逻辑删除调用
2.新增、更改时调用\
响应体:用于返回给前端\
科目表:\
1.请求体:新增科目时调用
+35 -13
View File
@@ -3,19 +3,27 @@ from Teachers.schema import *
from Teachers.database import *
from Teachers.model.tea_model import *
from Teachers.dao.tea_dao import *
from Teachers.schema.tea_request import TeaRequest,NewTeaRequest,NewTeaRequest1
from Teachers.schema.tea_request import TeaRequest,NewTeaRequest,NewTeaRequest1,SubRequest
from Teachers.schema.tea_response import TeaResponse
tea_api = APIRouter()
@tea_api.post('/tea',response_model=TeaResponse,summary='新增老师',description='输入新增的老师信息,除去姓名外可以为空')
@tea_api.post('/tea'
# ,response_model=TeaResponse
,summary='新增老师'
,description='输入新增的老师信息,除去姓名外可以为空')
def add_tea(tea: NewTeaRequest=Depends(),db=Depends(get_db)):
r = add_tea_dao(tea.model_dump(),db)
r = add_tea_dao(tea.model_dump(exclude_unset=True),db)
if not r:
raise HTTPException(status_code=500,detail='添加失败!')
return TeaResponse(totals=1,data=tea.model_dump())
@tea_api.get('/tea',response_model=TeaResponse,summary='查询老师',description='任意选择输入的信息数量,不输入则查询全部老师')
def get_tea(t:NewTeaRequest1=Depends(),db=Depends(get_db)): #开启会话窗口
return r #TeaResponse(totals=1,data=tea.model_dump())
# r
@tea_api.get('/tea'
,response_model=TeaResponse
,summary='查询老师'
,description='任意选择输入的信息数量,不输入则查询全部老师')
def get_tea(t:TeaRequest=Depends(),db=Depends(get_db)): #开启会话窗口
r = get_tea_dao(t.model_dump(),db=db)
if not r:
raise HTTPException(status_code=404,detail='老师不存在')
@@ -27,13 +35,19 @@ def delete_tea(id:int|None,db=Depends(get_db)):
raise HTTPException(status_code=500,detail='删除失败!')
return {'detail':'删除成功!'}
@tea_api.put('/tea/{id}',summary='更新老师信息',description='输入更改的信息,可以为空')
def update_tea(id:int,tea:NewTeaRequest,db=Depends(get_db)):
t = tea.model_dump(exclude_unset= True)
r = update_tea_dao(id=id,t=t,db=db)
if not r:
raise HTTPException(status_code=404,detail='老师不存在')
return {'detail':'更新成功'}
@tea_api.put('/tea/{id}', summary='更新老师信息', description='输入更改的信息,可以为空')
def update_tea(id: int, tea: NewTeaRequest = Depends(), db=Depends(get_db)):
t = tea.model_dump(exclude_unset=True)
print(t)
r = update_tea_dao(id=id, t=t, db=db)
if r == 'NOT_FOUND':
raise HTTPException(status_code=404, detail='老师不存在')
if r == 'NO_CHANGE':
raise HTTPException(status_code=400, detail='没有要更新的内容')
if isinstance(r, str) and r.startswith('ERROR'):
raise HTTPException(status_code=400, detail=r)
#先确认是字符串再调用字符串方法判断是不是以错误开头,是就报错给前端
return {'detail': '更新成功'}
#根据任意信息删除
@tea_api.delete('/tea',summary='根据任意信息逻辑删除',description='输入任意信息删除')
@@ -43,4 +57,12 @@ def delete_other_tea_dao1(t:TeaRequest=Depends(),db=Depends(get_db)):
raise HTTPException(status_code=404,detail='没有找到符合条件的老师')
return{'code':200,'data':row1}
#新增科目
@tea_api.post('/subject',summary='新增科目',description='输入新增的科目')
def add_sub(t:SubRequest=Depends(),db=Depends(get_db)): #开启会话窗口
r = add_subject_dao(t.model_dump(),db=db)
if r=='已存在':
raise HTTPException(status_code=500,detail='科目已存在')
return TeaResponse(totals=len(r), data=r)
+45 -16
View File
@@ -1,4 +1,6 @@
from Teachers.model.tea_model import Tea
from sys import excepthook
from Teachers.model.tea_model import Tea,Sub
from datetime import datetime
#增加
@@ -7,19 +9,21 @@ def add_tea_dao(t,db):
not_delete = db.query(Tea).filter(Tea.phone ==t.get('phone'),Tea.is_delete == False).first()
if not_delete:
print('该老师已存在')
raise Exception('老师已存在')
raise Exception('该老师已存在,不可重复添加')
existing = db.query(Tea).filter(Tea.phone == t.get('phone'),Tea.is_delete == True).first()
if existing:
for key,value in t.items():
if hasattr(existing,key) and value is not None and value != '':
#hasattr(existing,key)统计有没有key,返回值是True或False
setattr(existing,key,value)
#将value的值赋值到existing的key中,配合循环,让所有输入的信息都覆盖原信息
existing.is_delete = False
db.commit()
return '恢复修改成功'
return '新增成功-修订数据'
t1 = Tea(**t)
db.add(t1)
db.commit()
return True
return '新增成功'
except Exception as e:
db.rollback()
print('新增失败原因',e)
@@ -27,15 +31,22 @@ def add_tea_dao(t,db):
#更新
def update_tea_dao(id,t, db):
def update_tea_dao(id, t, db):
try:
row = db.query(Tea).filter(Tea.id == id,Tea.is_delete ==False ).update(t)
except:
db.rollback()
return False
else:
if not t:
return 'NO_CHANGE'
obj = db.query(Tea).filter(Tea.id == id, Tea.is_delete == False).first()
if not obj:
return 'NOT_FOUND'
for k, v in t.items():
if hasattr(obj, k) and v is not None and v != '':
setattr(obj, k, v)
db.commit()
return row
return 'OK'
except Exception as e:
db.rollback()
print('更新失败原因', repr(e))
return f'ERROR: {e}'
#查询
def get_tea_dao(t,db):
@@ -48,8 +59,8 @@ def get_tea_dao(t,db):
query = query.filter(Tea.sex == t.get('sex') )
if t.get('class_id'):
query = query.filter(Tea.class_id == t.get('class_id') )
if t.get('sub'):
query = query.filter(Tea.sub == t.get('sub') )
if t.get('subject_id'):
query = query.filter(Tea.subject_id == t.get('subject_id') )
if t.get('home_place'):
query = query.filter(Tea.home_place == t.get('home_place') )
if t.get('specialty'):
@@ -65,7 +76,7 @@ def get_tea_dao(t,db):
if t.get('phone'):
query = query.filter(Tea.phone == t.get('phone') )
r = query.all()
return [{'id':i.id,'name':i.name,'class_id':i.class_id,'sub':i.sub,'sex':i.sex,'home_place':i.home_place
return [{'id':i.id,'name':i.name,'class_id':i.class_id,'sub':i.subject_id,'sex':i.sex,'home_place':i.home_place
,'specialty':i.specialty,'start_time':i.start_time,'work_experience':i.work_experience
,'collage':i.collage,'phone':i.phone} for i in r]
@@ -96,8 +107,8 @@ def delete_other_tea_dao(t,db):
query1 = query1.filter(Tea.sex == t.get('sex'))
if t.get('class_id'):
query1 = query1.filter(Tea.class_id == t.get('class_id'))
if t.get('sub'):
query1 = query1.filter(Tea.sub == t.get('sub'))
if t.get('subject_id'):
query1 = query1.filter(Tea.subject_id == t.get('subject_id'))
if t.get('home_place'):
query1 = query1.filter(Tea.home_place == t.get('home_place'))
if t.get('collage'):
@@ -122,3 +133,21 @@ def delete_other_tea_dao(t,db):
def add_subject_dao(t,db):
try:
t1 = db.query(Sub).filter(Sub.name == t.get('name')).first()
if t1:
return'已存在'
s1 = Sub(**t)
db.add(s1)
db.commit()
return '[s1]'
except InterruptedError as e:
db.rollback()
print('失败的原因',e)
return False
except Exception as e:
db.rollback()
return f'{e}'
+1
View File
@@ -24,4 +24,5 @@ def get_db():
yield db
finally:
db.close()
#建表
Base.metadata.create_all(engine1)
+1 -1
View File
@@ -15,7 +15,7 @@ if __name__ == "__main__":
Base.metadata.create_all(engine1)
import uvicorn
uvicorn.run('main:app', host='0.0.0.0', port=55000)
uvicorn.run('main:app', host='0.0.0.0', port=56000)
"""
IPv4 地址 . . . . . . . . . . . . : 192.168.1.51
子网掩码 . . . . . . . . . . . . : 255.255.255.0
-3
View File
@@ -16,17 +16,14 @@ class Tea(Base_all):
,autoincrement=True
,comment='id')
name = Column(String(50)
,nullable=False
,comment='姓名')
sex = Column(Enum(Gender),default='未知'
,comment='性别')
class_id = Column(Integer
,nullable=False
,comment='班级id')
phone = Column(String(50))
subject_id = Column(Integer
, ForeignKey('subject.id')
,nullable=False
,comment='教学科目')
home_place = Column(String(50)
,comment='家乡')
+10 -7
View File
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel,Field
from Teachers.model.tea_model import *
#查询使用请求体,都可以为空
@@ -7,8 +7,8 @@ class TeaRequest(BaseModel):
name: str|None = None
sex: Gender|None = None
class_id: int | None = None
sub: str|None = None
phone: str|None = None
subject_id: str|None = None
phone: str | None = None
home_place: str|None = None
collage: str|None = None
education: str|None = None
@@ -22,9 +22,9 @@ class NewTeaRequest(BaseModel):
name: str | None = None
sex: Gender|None = None
class_id: int | None = None
sub: str | None = None
subject_id: int | None = None
home_place: str | None = None
phone: str
phone: str | None = Field(default=None, min_length=11, max_length=11)
collage: str | None = None
education: str | None = None
work_experience: str | None = None
@@ -35,11 +35,14 @@ class NewTeaRequest1(BaseModel):
name: str | None = None
sex: Gender|None = None
class_id: int | None = None
sub: str | None = None
subject_id: int | None = None
home_place: str | None = None
phone: str|None = None
phone: str | None = Field(default=None, min_length=11, max_length=11)
collage: str | None = None
education: str | None = None
work_experience: str | None = None
start_time: datetime | None = None
specialty: str | None = None
class SubRequest(BaseModel):
name: str
@@ -16,7 +16,6 @@ def add_class(cla:ClassCreate,db=Depends(get_db)):
raise HTTPException(status_code=400,detail='新增失败,班级已存在')
return ClassResponse(detail='新增成功',totals=1,data=c_dict)
#更新接口
@class_api.put("/{class_id}",summary='修改班级信息')
def update_class(class_id:int
+7 -1
View File
@@ -4,6 +4,12 @@ from sqlalchemy.orm import Session
#新增班级
def add_class_dao(c,db):
try:
exist_class = db.query(ClassInfo).filter(
ClassInfo.num == c["num"],
ClassInfo.is_delete == 0
).first()
if exist_class:
return False
c1 = ClassInfo(**c)
db.add(c1)
except:
@@ -11,9 +17,9 @@ def add_class_dao(c,db):
return False
else:
db.commit()
db.refresh(c1)
return True
#更新班级信息
def update_class_dao(cid,update_data,db):
try:
+2 -2
View File
@@ -11,9 +11,9 @@ DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "3306")
DB_NAME = os.getenv("DB_NAME", "student_manage_system")
class_management_rul = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
class_management_url = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
engine = create_engine(class_management_rul)
engine = create_engine(class_management_url)
SessionLocal = sessionmaker(bind=engine,autocommit=False, autoflush=False)
Base = declarative_base()
+7 -5
View File
@@ -3,16 +3,18 @@ from class_management.api.class_management_api import class_api
from class_management.model.class_management_model import ClassInfo
from class_management.database import Base,engine
# 合并接口
Classes_API = APIRouter()
Classes_API.include_router(class_api)
# 自测接口
app = FastAPI()
app.include_router(class_api)
if __name__ =='__main__':
# 自测接口
app = FastAPI()
app.include_router(class_api)
Base.metadata.create_all(engine)
import uvicorn
@@ -22,16 +22,16 @@ class ClassInfo(Base_all):
,comment='班级名称'
)
head_teacher_id = Column(Integer
,ForeignKey('teacher.id')
,ForeignKey('teachers.id')
,comment='班主任编号'
)
coach_teacher_id = Column(Integer
,ForeignKey('teacher.id')
,ForeignKey('teachers.id')
,comment='授课老师编号'
)
tutor_teacher_id = Column(Integer
,ForeignKey('teacher.id')
,ForeignKey('teachers.id')
,comment='助教老师编号'
)
class_start_time = Column(Date
+206
View File
@@ -0,0 +1,206 @@
========================================
Class Management 班级管理模块
========================================
基于 FastAPI + SQLAlchemy + MySQL 的班级管理模块,
提供班级的增删改查、多字段分页查询、逻辑删除等能力。
----------------------------------------
一、目录结构
----------------------------------------
class_management/
api/
class_management_api.py # 路由层:定义 HTTP 接口
dao/
class_management_dao.py # 数据访问层:封装数据库操作
model/
class_management_model.py # ORM 模型:映射数据库表
schema/
class_management_request.py # Pydantic 模型:请求/响应体
database.py # 数据库连接、Session、Base
分层职责:
api 接收请求、参数校验、返回响应,不直接写 SQL
dao 封装 CRUD、事务控制,不处理 HTTP 状态码
model 定义表结构、字段约束,不写业务逻辑
schema 定义请求/响应格式,不碰数据库
----------------------------------------
二、数据表结构
----------------------------------------
表名:classes
字段 类型 约束 说明
------------------ ----------- ---------------------- ------------------
id Integer PK, Auto Increment 班级主键
num String(50) NOT NULL, UNIQUE 班级编号
name String(50) NOT NULL 班级名称
head_teacher_id Integer FK -> teachers.id 班主任编号
coach_teacher_id Integer FK -> teachers.id 授课老师编号
tutor_teacher_id Integer FK -> teachers.id 助教老师编号
class_start_time Date 可空 开班日期
class_end_time Date 可空 结课日期
is_delete Integer 默认 0 逻辑删除 0未删 1已删
create_time DATETIME 默认 now 创建时间
update_time DATETIME 默认 now, onupdate now 更新时间
注意:head_teacher_id / coach_teacher_id / tutor_teacher_id 是外键,
插入前需保证 teachers 表中存在对应 id。
----------------------------------------
三、API 接口
----------------------------------------
基础前缀:/classes
[1] 新增班级
POST /classes
请求体:
{
"num": "C001",
"name": "Python01班",
"head_teacher_id": 1,
"coach_teacher_id": 2,
"tutor_teacher_id": 3,
"class_start_time": "2025-01-01",
"class_end_time": "2025-06-30"
}
字段说明:
num string 班级编号,唯一
name string 班级名称
head_teacher_id int 班主任 ID
coach_teacher_id int 授课老师 ID
tutor_teacher_id int 助教老师 ID
class_start_time date 格式 YYYY-MM-DD
class_end_time date 格式 YYYY-MM-DD
响应:
{
"code": 200,
"detail": "新增成功",
"totals": 1,
"data": { ... }
}
错误:
400 班级编号已存在
[2] 根据 ID 查询班级
GET /classes/{class_id}
响应:
{
"code": 200,
"detail": "OK",
"totals": 1,
"data": {
"id": 1,
"num": "C001",
"name": "Python01班",
"head_teacher_id": 1,
"coach_teacher_id": 2,
"tutor_teacher_id": 3,
"class_start_time": "2025-01-01",
"class_end_time": "2025-06-30"
}
}
错误:
404 该班级不存在或已删除
[3] 多字段分页查询
GET /classes
Query 参数:
num string 班级编号(精确匹配)
name string 班级名称(精确匹配)
head_teacher_id int 班主任 ID
coach_teacher_id int 授课老师 ID
tutor_teacher_id int 助教老师 ID
class_start_time date 开班日期
class_end_time date 结课日期
page int 页码,默认 1,>=1
page_size int 每页条数,默认 5,1~20
请求示例:
GET /classes?name=Python01班&page=1&page_size=5
响应:
{
"code": 200,
"detail": "OK",
"totals": 1,
"data": [
{ "id": 1, "num": "C001", "name": "Python01班", ... }
]
}
注意:当前为内存分页,先查出所有匹配记录再切片。
数据量大时建议改成 DB 层 offset/limit。
[4] 修改班级信息
PUT /classes/{class_id}
请求体(只传要改的字段):
{
"head_teacher_id": 5,
"class_end_time": "2025-12-31"
}
响应:
{
"code": 200,
"detail": "修改成功",
"data": { "id": 1 }
}
错误:
400 没有传入要修改的字段
404 班级不存在,修改失败
[5] 逻辑删除班级
DELETE /classes/{class_id}
响应:
{
"code": 200,
"detail": "删除成功",
"data": { "id": 1 }
}
错误:
404 班级不存在,删除失败
说明:逻辑删除,只把 is_delete 置为 1,不物理删除记录。
----------------------------------------
五、约定与规范
----------------------------------------
返回值约定:
DAO 函数 成功返回 失败返回 API 判断
------------------ ---------- ------------ ------------------
add_class_dao None 错误字符串 if res:
update_class_dao int >= 1 0 if not row_count:
delete_class_dao int >= 1 0 if not del_rows:
get_class_dao list [] if not res:
逻辑删除:
所有查询默认过滤 is_delete = 0,
删除操作用 UPDATE is_delete = 1,不做物理删除。
分页:
page 从 1 开始
page_size 范围 1~20
返回 totals 为总条数,data 为当前页数据
模块负责人:cyf
最后更新:2026-09-22
+2137
View File
File diff suppressed because it is too large Load Diff
+246
View File
@@ -0,0 +1,246 @@
"""
测试数据初始化脚本
使用纯SQL直接向数据库插入测试数据,不修改任何原有项目文件
用法: python init_test_data.py
"""
from datetime import date
from databases import engine_all, Session
from sqlalchemy import text
Session_ = Session()
def seed():
print("=== 开始插入测试数据 ===\n")
# ── 1. 科目 ──────────────────────────────────────────
subjects = ["Python程序设计", "数据结构", "数据库原理", "人工智能导论", "计算机网络", "操作系统"]
sub_ids = {}
for name in subjects:
r = Session_.execute(text("SELECT id FROM subject WHERE name = :name"), {"name": name}).fetchone()
if r:
sub_ids[name] = r[0]
else:
result = Session_.execute(text("INSERT INTO subject (name) VALUES (:name)"), {"name": name})
sub_ids[name] = result.lastrowid
print(f" [科目] {name}")
Session_.commit()
# ── 2. 教师 ──────────────────────────────────────────
teachers = [
("张伟", "m", sub_ids["Python程序设计"], 1, "北京", "Python", "博士", "清华", date(2018, 3, 1), "6年"),
("李芳", "f", sub_ids["数据结构"], 1, "上海", "算法", "博士", "北大", date(2019, 9, 1), "4年"),
("王强", "m", sub_ids["数据库原理"], 2, "广州", "MySQL", "硕士", "中山大学", date(2020, 2, 1), "3年"),
("陈静", "f", sub_ids["人工智能导论"], 2, "深圳", "机器学习", "博士", "华南理工", date(2017, 7, 1), "7年"),
("刘洋", "m", sub_ids["计算机网络"], 3, "杭州", "网络", "硕士", "浙江大学", date(2021, 1, 1), "2年"),
("赵敏", "f", sub_ids["操作系统"], 3, "南京", "Linux", "博士", "南京大学", date(2019, 3, 1), "5年"),
]
teacher_ids = []
for t in teachers:
phone = f"1380000{len(teacher_ids):04d}"
r = Session_.execute(
text("SELECT id FROM teachers WHERE phone = :phone AND is_delete = 0"),
{"phone": phone}
).fetchone()
if r:
teacher_ids.append(r[0])
else:
result = Session_.execute(text("""
INSERT INTO teachers (name, sex, class_id, phone, subject_id, home_place, specialty,
education, collage, start_time, work_experience, is_delete)
VALUES (:name, :sex, :class_id, :phone, :subject_id, :home_place, :specialty,
:education, :collage, :start_time, :work_experience, 0)
"""), {
"name": t[0], "sex": t[1], "class_id": t[3], "phone": phone,
"subject_id": t[2], "home_place": t[4], "specialty": t[5],
"education": t[6], "collage": t[7], "start_time": t[8], "work_experience": t[9],
})
teacher_ids.append(result.lastrowid)
print(f" [教师] {t[0]} ({t[1]}) - {t[5]}")
Session_.commit()
# ── 3. 班级 ──────────────────────────────────────────
classes = [
("CS202401", "计算机科学2401班", teacher_ids[0], teacher_ids[1], teacher_ids[2], date(2024, 9, 1), date(2028, 6, 30)),
("CS202402", "计算机科学2402班", teacher_ids[3], teacher_ids[4], teacher_ids[5], date(2024, 9, 1), date(2028, 6, 30)),
("AI202501", "人工智能2501班", teacher_ids[0], teacher_ids[3], teacher_ids[5], date(2025, 9, 1), date(2029, 6, 30)),
]
class_ids = []
for c in classes:
r = Session_.execute(
text("SELECT id FROM classes WHERE num = :num AND is_delete = 0"),
{"num": c[0]}
).fetchone()
if r:
class_ids.append(r[0])
else:
result = Session_.execute(text("""
INSERT INTO classes (num, name, head_teacher_id, coach_teacher_id, tutor_teacher_id,
class_start_time, class_end_time, is_delete)
VALUES (:num, :name, :head_teacher_id, :coach_teacher_id, :tutor_teacher_id,
:class_start_time, :class_end_time, 0)
"""), {
"num": c[0], "name": c[1], "head_teacher_id": c[2],
"coach_teacher_id": c[3], "tutor_teacher_id": c[4],
"class_start_time": c[5], "class_end_time": c[6],
})
class_ids.append(result.lastrowid)
print(f" [班级] {c[1]}")
Session_.commit()
# ── 4. 学生 ──────────────────────────────────────────
first_names = ["志强", "丽华", "建国", "美玲", "文博", "晓燕", "浩然", "雨桐", "子轩", "思涵",
"俊杰", "雅琪", "宇航", "欣怡", "天翊", "诗涵", "一诺", "浩宇", "梓萱", "博文"]
home_places = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"]
colleges = ["清华大学", "北京大学", "复旦大学", "上海交通大学", "浙江大学", "南京大学", "武汉大学", "华中科技大学"]
specialties = ["计算机科学", "软件工程", "数据科学", "人工智能"]
student_count = 0
for i in range(20):
class_idx = i % 3
enrollment = date(2024, 9, 1) if class_idx != 2 else date(2025, 9, 1)
graduate = date(2028, 6, 30) if class_idx != 2 else date(2029, 6, 30)
phone = f"1390000{1000+i}"
r = Session_.execute(
text("SELECT id FROM students WHERE phone = :phone AND is_deleted = 0"),
{"phone": phone}
).fetchone()
if r:
continue
Session_.execute(text("""
INSERT INTO students (num, name, age, sex, home_Place, college, specialty,
enrollment_time, graduate_time, education,
class_id, advisor_id, phone, is_deleted)
VALUES (:num, :name, :age, :sex, :home_Place, :college, :specialty,
:enrollment_time, :graduate_time, '本科',
:class_id, :advisor_id, :phone, 0)
"""), {
"num": f"2024{1001+i}",
"name": first_names[i],
"age": 18 + (i % 4),
"sex": "男" if i % 2 == 0 else "女",
"home_Place": home_places[i % 8],
"college": colleges[i % 8],
"specialty": specialties[i % 4],
"enrollment_time": enrollment,
"graduate_time": graduate,
"class_id": class_ids[class_idx],
"advisor_id": teacher_ids[i % 6],
"phone": phone,
})
student_count += 1
print(f" [学生] {first_names[i]} (2024{1001+i}) - {'男' if i%2==0 else '女'} - 班级{class_idx+1}")
Session_.commit()
# ── 5. 成绩 ──────────────────────────────────────────
all_students = Session_.execute(
text("SELECT id, class_id FROM students WHERE is_deleted = 0")
).fetchall()
all_subjects = Session_.execute(text("SELECT id FROM subject")).fetchall()
score_count = 0
for idx, st in enumerate(all_students):
for subj_idx, sub in enumerate(all_subjects):
score_val = 60 + (idx * 7 + subj_idx * 13) % 41
# 让前3名学生部分课程不及格(用于统计接口测试)
if idx < 3 and subj_idx < 2:
score_val = 30 + (idx * 5) % 25
existing = Session_.execute(
text("SELECT id FROM scores WHERE sid = :sid AND cid = :cid AND num = 1 AND t_subject = :t_subject AND is_deleted = 0"),
{"sid": st[0], "cid": st[1], "t_subject": sub[0]}
).fetchone()
if not existing:
Session_.execute(text("""
INSERT INTO scores (sid, cid, num, score, t_subject, is_deleted)
VALUES (:sid, :cid, 1, :score, :t_subject, 0)
"""), {"sid": st[0], "cid": st[1], "score": score_val, "t_subject": sub[0]})
score_count += 1
print(f" [成绩] 为 {len(all_students)} 名学生插入了 {score_count} 条成绩记录")
Session_.commit()
# ── 6. 公司地址 ────────────────────────────────────────
addresses = ["北京市海淀区", "上海市浦东新区", "深圳市南山区", "杭州市西湖区", "广州市天河区"]
address_ids = []
for addr in addresses:
r = Session_.execute(
text("SELECT id FROM Address WHERE somewhere = :where"),
{"where": addr}
).fetchone()
if r:
address_ids.append(r[0])
else:
result = Session_.execute(
text("INSERT INTO Address (somewhere) VALUES (:where)"),
{"where": addr}
)
address_ids.append(result.lastrowid)
print(f" [地址] {addr}")
Session_.commit()
# ── 7. 公司 ────────────────────────────────────────────
companies = [
("字节跳动", address_ids[2]),
("阿里巴巴", address_ids[3]),
("腾讯", address_ids[2]),
("华为", address_ids[4]),
("百度", address_ids[0]),
]
company_ids = []
for comp in companies:
r = Session_.execute(
text("SELECT id FROM Company WHERE employment_company = :name"),
{"name": comp[0]}
).fetchone()
if r:
company_ids.append(r[0])
else:
result = Session_.execute(
text("INSERT INTO Company (employment_company, address_id) VALUES (:name, :addr_id)"),
{"name": comp[0], "addr_id": comp[1]}
)
company_ids.append(result.lastrowid)
print(f" [公司] {comp[0]}")
Session_.commit()
# ── 8. 就业信息 ────────────────────────────────────────
employment_data = [
(all_students[0][0], all_students[0][1], company_ids[0], 35000, date(2028, 3, 1), date(2028, 4, 15)),
(all_students[1][0], all_students[1][1], company_ids[1], 32000, date(2028, 3, 1), date(2028, 5, 1)),
(all_students[2][0], all_students[2][1], company_ids[2], 30000, date(2028, 3, 1), date(2028, 4, 20)),
(all_students[3][0], all_students[3][1], company_ids[3], 28000, date(2028, 3, 1), date(2028, 5, 10)),
(all_students[4][0], all_students[4][1], company_ids[4], 25000, date(2028, 3, 1), date(2028, 6, 1)),
(all_students[5][0], all_students[5][1], company_ids[0], 33000, date(2028, 3, 1), date(2028, 4, 25)),
(all_students[6][0], all_students[6][1], company_ids[1], 29000, date(2028, 3, 1), None),
(all_students[7][0], all_students[7][1], company_ids[2], 31000, date(2028, 3, 1), date(2028, 5, 5)),
]
emp_count = 0
for emp in employment_data:
r = Session_.execute(
text("SELECT id FROM Employments WHERE stuid = :stuid AND is_deleted = 0"),
{"stuid": emp[0]}
).fetchone()
if not r:
Session_.execute(text("""
INSERT INTO Employments (stuid, class_id, company_id, employment_salary,
employment_open_time, offer_recived_time, is_deleted)
VALUES (:stuid, :class_id, :company_id, :salary, :open_time, :offer_time, 0)
"""), {
"stuid": emp[0], "class_id": emp[1], "company_id": emp[2],
"salary": emp[3], "open_time": emp[4], "offer_time": emp[5],
})
emp_count += 1
print(f" [就业] 插入 {emp_count} 条就业记录")
Session_.commit()
# ── 完成 ───────────────────────────────────────────────
Session_.close()
print("\n=== 测试数据初始化完成!共插入:")
print(f" • 科目: {len(subjects)} 门")
print(f" • 教师: {len(teachers)} 位")
print(f" • 班级: {len(classes)} 个")
print(f" • 学生: {student_count} 名")
print(f" • 成绩: {score_count} 条")
print(f" • 公司: {len(companies)} 家")
print(f" • 就业: {emp_count} 条")
if __name__ == "__main__":
seed()
+21 -8
View File
@@ -6,15 +6,26 @@ from students.main import Students_API
from Teachers.main import Teachers_API
from StatCalc.main import StatCalc_API
from databases import *
from seed_data import seed_test_data
app = FastAPI(title="学生管理系统")
from fastapi.middleware.cors import CORSMiddleware
app.include_router(Classes_API,tags=["classes"])
app.include_router(Scores_API,tags=["scores"])
app.include_router(Shtudent_Jiuye,tags=["Employments"])
app.include_router(Students_API,tags=["students"])
app.include_router(Teachers_API,tags=["teachers"])
app.include_router(StatCalc_API,tags=["StatCalc"])
# 配置CORS - 接前端
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有来源
allow_credentials=True,
allow_methods=["*"], # 允许所有方法
allow_headers=["*"], # 允许所有头部
)
app.include_router(Classes_API,tags=["班级管理模块"])
app.include_router(Scores_API,tags=["学生考核成绩管理模块"])
app.include_router(Shtudent_Jiuye,tags=["学生就业管理模块"])
app.include_router(Students_API,tags=["学生管理模块"])
app.include_router(Teachers_API,tags=["老师管理模块"])
app.include_router(StatCalc_API,tags=["统计分析模块"])
if __name__ == "__main__":
@@ -23,10 +34,12 @@ if __name__ == "__main__":
def main():
print("Hello from student-manage-system!")
seed_test_data()
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=59000)
uvicorn.run("main:app", host="0.0.0.0", port=58000)
"""
IPv4 地址 . . . . . . . . . . . . : 192.168.6.40
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.6.1
"""
"""
+61
View File
@@ -0,0 +1,61 @@
#scores模块解释
##项目层
web框架:FastAPI(生成接口)
ORM:sqlalchemy
校验:MYSQL(通过PyMysql驱动)
运行:uvicorn,端口12346
用FastAPI显露HTTP接口 ,对MysQL的scores表进行一个增,删,改,查的动作,删除是软删除,
api是接口层,成绩信息的增加,删除,更新,查询
dao是数据操作层,查询get_scores_dao
更新add_scores_dao
增加update_scores_dao
删除delete_scores_dao
统计查询80分以上的get_top_students_dao
查询连续两次不及格的get_failing_students_dao
班级平均分get_class_avg_dao
model是数据库表模型层,字段信息 id *pk*
sid 链接学生表的学生ID
cid 链接班级表的班级ID
num 考试序次
score 考试成绩
t_subject 链接科目表的科目ID
create_date 创建数据的时间
update_date 更新数据的是时间
is_deleted 逻辑删除
deleted_date 删除的时间
schema是请求体和响应体模型层, 查询的ScoreQuery
更新,增加,删除的ScoreRequest
databases是数据库定义配置文件, 链接引擎对象db_url,链接地址engine,
建表的基类Base,数据库会话管理Session
会话实例def get_db()
main是主程序文件,将配置文件的接口导入,创建接口运行
+11 -7
View File
@@ -14,15 +14,19 @@ def get_scores(score:ScoreQuery=Depends(),db=Depends(get_db)):
return aa
raise HTTPException(status_code=404,detail='不存在')
@score_api.post('/Score',response_model=ScoreResponse
@score_api.post('/Score'
,summary='成绩添加')
def add_scores(score:ScoreRequest=Depends(),db=Depends(get_db)):
a = score.model_dump()
# print(a,type(a))
aa = add_scores_dao(b=a,db=db)
if aa:
return ScoreResponse(data=a)
raise HTTPException(status_code=500,detail='不可以')
try:
a = score.model_dump()
# print(a,type(a))
aa = add_scores_dao(b=a,db=db)
if aa:
return aa
except ValueError as e:
raise HTTPException(status_code=400, detail="数据库已有该成绩")
except Exception as e:
raise HTTPException(status_code=500, detail="数据库错误")
@score_api.put('/Score',summary='成绩更新',description=f'查询条件为学号,班级号,考试序次,考试科目,然后更改这位同学此次科目的成绩')
def update_scores(score:ScoreRequest,sid:int,cid:int,num:int,t_subject:int,db=Depends(get_db)):
+37 -9
View File
@@ -1,3 +1,5 @@
from scores.model.score_model import Score
from datetime import datetime
@@ -28,19 +30,45 @@ def get_scores_dao(s,db):
def add_scores_dao(b,db):
try:
aa = db.query(Score).filter(Score.t_subject==b['t_subject']).all()
aa = db.query(Score).filter(Score.sid == b['sid']
,Score.cid == b['cid']
,Score.t_subject == b['t_subject']
,Score.is_deleted == 0
).first()
if aa:
raise ValueError
else:
z = Score(**b)
db.add(z)
deleted = db.query(Score).filter(
Score.sid == b['sid'],
Score.cid == b['cid'],
Score.t_subject == b['t_subject'],
Score.is_deleted == 1
).first()
if deleted.is_deleted and deleted.is_deleted == True:
deleted.sid = b['sid']
deleted.cid = b['cid']
deleted.num = b['num']
deleted.score = b['score']
deleted.t_subject = b['t_subject']
deleted.create_date = datetime.now()
deleted.update_date = datetime.now()
deleted.is_deleted = False
deleted.deleted_date = None
db.commit()
db.refresh(deleted) # 从数据库再拉一次数据
return b
z = Score(**b)
db.add(z)
db.commit()
db.refresh(z)
return z
except Exception as e:
db.rollback()
print(e)
return False
else:
db.commit()
return True
# print(e)
raise e
def update_scores_dao(sid:int,cid:int,num:int,t_subject:int,update_data:dict,db):
if not update_data:
+1 -3
View File
@@ -50,11 +50,9 @@ def get_failing_students_dao(db):
.order_by(ClassInfo.id, Score.num)
.all())
return [{"sid": r.sid,
return [{
"name": r.name,
"class_name": r.class_name,
"num": r.num,
"t_subject": r.tsub,
"score": r.score,}
for r in results]
+4 -3
View File
@@ -1,7 +1,7 @@
from fastapi import FastAPI,APIRouter
from scores.api.score_api import score_api
from scores.database import engine,Base
from scores.api.score_api import score_api
from scores.api.statistic_api import statistic_api
# 合并接口
Scores_API = APIRouter()
@@ -9,11 +9,12 @@ Scores_API.include_router(score_api)
# 自测接口
app = FastAPI(tags=['成绩'], title='成绩管理系统')
app.include_router(score_api)
app.include_router(statistic_api)
if __name__ == '__main__':
app.include_router(score_api)
Base.metadata.create_all(engine)
import uvicorn
+4 -4
View File
@@ -1,12 +1,12 @@
from pydantic import BaseModel
from pydantic import BaseModel,Field
class ScoreRequest(BaseModel):
# id:int
sid:int
cid:int
sid:int = Field(ge=1, le=20, description="学生ID:1-20")
cid:int = Field(ge=1, le=8, description="班级ID:1-8")
num:int
score:int
t_subject:int
t_subject:int = Field(ge=1, le=6, description="科目ID:1-6")
class ScoreQuery(BaseModel):
id:int|None = None
+260
View File
@@ -0,0 +1,260 @@
"""
种子数据初始化模块
在 main.py 启动时自动调用,向数据库注入测试数据
幂等设计:已存在数据则跳过,不重复插入
"""
from datetime import date
from sqlalchemy import text
from databases import engine_all, Base_all, Session
def seed_test_data():
"""启动时自动注入测试数据(幂等)"""
db = Session()
try:
# 临时关闭外键检查,避免 class_management_model.py 的 FK bug
db.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
# 导入所有模型以获取 metadata(触发 Base_all.metadata.create_all)
import students.model.students_model
import Teachers.model.tea_model
import scores.model.score_model
import stu_jiuye.model
# 创建所有表(临时关闭外键检查)
db.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
Base_all.metadata.create_all(bind=engine_all)
# 手动建 classes 表(因 FK bug 无法通过 ORM 建表)
db.execute(text("""
CREATE TABLE IF NOT EXISTS classes (
id INT AUTO_INCREMENT PRIMARY KEY,
num VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(50) NOT NULL,
head_teacher_id INT,
coach_teacher_id INT,
tutor_teacher_id INT,
class_start_time DATE,
class_end_time DATE,
is_delete INT DEFAULT 0,
create_time DATETIME,
update_time DATETIME,
FOREIGN KEY (head_teacher_id) REFERENCES teachers(id),
FOREIGN KEY (coach_teacher_id) REFERENCES teachers(id),
FOREIGN KEY (tutor_teacher_id) REFERENCES teachers(id)
)
"""))
# 恢复外键检查
db.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
db.commit()
# 幂等检查:科目表有数据就跳过全部
if db.execute(text("SELECT COUNT(*) FROM subject")).scalar() > 0:
return
print(" [种子] 开始注入测试数据...")
# ── 1. 科目 ──────────────────────────────────────
subjects = ["Python程序设计", "数据结构", "数据库原理",
"人工智能导论", "计算机网络", "操作系统"]
sub_ids = {}
for name in subjects:
result = db.execute(
text("INSERT INTO subject (name) VALUES (:name)"),
{"name": name}
)
sub_ids[name] = result.lastrowid
# ── 2. 教师 ──────────────────────────────────────
teachers = [
("张伟", "m", 1, sub_ids["Python程序设计"],
"北京", "Python", "博士", "清华", date(2018, 3, 1), "6年"),
("李芳", "f", 1, sub_ids["数据结构"],
"上海", "算法", "博士", "北大", date(2019, 9, 1), "4年"),
("王强", "m", 2, sub_ids["数据库原理"],
"广州", "MySQL", "硕士", "中山大学", date(2020, 2, 1), "3年"),
("陈静", "f", 2, sub_ids["人工智能导论"],
"深圳", "机器学习", "博士", "华南理工", date(2017, 7, 1), "7年"),
("刘洋", "m", 3, sub_ids["计算机网络"],
"杭州", "网络", "硕士", "浙江大学", date(2021, 1, 1), "2年"),
("赵敏", "f", 3, sub_ids["操作系统"],
"南京", "Linux", "博士", "南京大学", date(2019, 3, 1), "5年"),
]
teacher_ids = []
for t in teachers:
phone = f"1380000{len(teacher_ids):04d}"
result = db.execute(text("""
INSERT INTO teachers (name, sex, class_id, phone, subject_id,
home_place, specialty, education, collage,
start_time, work_experience, is_delete)
VALUES (:name, :sex, :class_id, :phone, :subject_id,
:home_place, :specialty, :education, :collage,
:start_time, :work_experience, 0)
"""), {
"name": t[0], "sex": t[1], "class_id": t[2],
"phone": phone, "subject_id": t[3],
"home_place": t[4], "specialty": t[5],
"education": t[6], "collage": t[7],
"start_time": t[8], "work_experience": t[9],
})
teacher_ids.append(result.lastrowid)
# ── 3. 班级 ──────────────────────────────────────
classes = [
("CS202401", "计算机科学2401班", teacher_ids[0],
teacher_ids[1], teacher_ids[2],
date(2024, 9, 1), date(2028, 6, 30)),
("CS202402", "计算机科学2402班", teacher_ids[3],
teacher_ids[4], teacher_ids[5],
date(2024, 9, 1), date(2028, 6, 30)),
("AI202501", "人工智能2501班", teacher_ids[0],
teacher_ids[3], teacher_ids[5],
date(2025, 9, 1), date(2029, 6, 30)),
]
class_ids = []
for c in classes:
result = db.execute(text("""
INSERT INTO classes (num, name, head_teacher_id, coach_teacher_id,
tutor_teacher_id, class_start_time,
class_end_time, is_delete)
VALUES (:num, :name, :head_teacher_id, :coach_teacher_id,
:tutor_teacher_id, :class_start_time,
:class_end_time, 0)
"""), {
"num": c[0], "name": c[1],
"head_teacher_id": c[2], "coach_teacher_id": c[3],
"tutor_teacher_id": c[4],
"class_start_time": c[5], "class_end_time": c[6],
})
class_ids.append(result.lastrowid)
# ── 4. 学生 ──────────────────────────────────────
first_names = ["志强", "丽华", "建国", "美玲", "文博", "晓燕",
"浩然", "雨桐", "子轩", "思涵",
"俊杰", "雅琪", "宇航", "欣怡", "天翊",
"诗涵", "一诺", "浩宇", "梓萱", "博文"]
home_places = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"]
colleges = ["清华大学", "北京大学", "复旦大学", "上海交通大学",
"浙江大学", "南京大学", "武汉大学", "华中科技大学"]
specialties = ["计算机科学", "软件工程", "数据科学", "人工智能"]
for i in range(20):
class_idx = i % 3
enrollment = date(2024, 9, 1) if class_idx != 2 else date(2025, 9, 1)
graduate = date(2028, 6, 30) if class_idx != 2 else date(2029, 6, 30)
phone = f"1390000{1000 + i}"
# 前3个学生年龄设大一些,用于30岁以上统计测试
age = 30 + i if i < 3 else 18 + (i % 4)
db.execute(text("""
INSERT INTO students (num, name, age, sex, home_Place, college,
specialty, enrollment_time, graduate_time,
education, class_id, advisor_id, phone, is_deleted)
VALUES (:num, :name, :age, :sex, :home_Place, :college,
:specialty, :enrollment_time, :graduate_time,
'ben', :class_id, :advisor_id, :phone, 0)
"""), {
"num": f"2024{1001 + i}",
"name": first_names[i],
"age": age,
"sex": "m" if i % 2 == 0 else "f",
"home_Place": home_places[i % 8],
"college": colleges[i % 8],
"specialty": specialties[i % 4],
"enrollment_time": enrollment,
"graduate_time": graduate,
"class_id": class_ids[class_idx],
"advisor_id": teacher_ids[i % 6],
"phone": phone,
})
# ── 5. 成绩 ──────────────────────────────────────
all_students = db.execute(
text("SELECT id, class_id FROM students WHERE is_deleted = 0")
).fetchall()
all_subjects = db.execute(
text("SELECT id FROM subject")
).fetchall()
for idx, st in enumerate(all_students):
for subj_idx, sub in enumerate(all_subjects):
# 前3名学生设大年龄(用于30岁以上统计)
# 前2名学生所有科目80分以上(用于优秀成绩统计)
# 最后2名学生部分科目不及格(用于不及格统计)
if idx < 2:
# 优秀学生:所有科目85-95分
score_val = 85 + (idx * 5 + subj_idx * 3) % 11
elif idx >= 18:
# 不及格学生:部分科目50-60分
if subj_idx < 3:
score_val = 50 + (idx * 3 + subj_idx) % 11
else:
score_val = 75 + (idx * 2 + subj_idx) % 16
else:
score_val = 60 + (idx * 7 + subj_idx * 13) % 41
db.execute(text("""
INSERT INTO scores (sid, cid, num, score, t_subject, is_deleted)
VALUES (:sid, :cid, 1, :score, :t_subject, 0)
"""), {
"sid": st[0], "cid": st[1],
"score": score_val, "t_subject": sub[0],
})
# ── 6. 地址 ──────────────────────────────────────
addresses = ["北京市海淀区", "上海市浦东新区", "深圳市南山区",
"杭州市西湖区", "广州市天河区"]
address_ids = []
for addr in addresses:
result = db.execute(
text("INSERT INTO Address (somewhere) VALUES (:where)"),
{"where": addr}
)
address_ids.append(result.lastrowid)
# ── 7. 公司 ──────────────────────────────────────
companies = ["字节跳动", "阿里巴巴", "腾讯", "华为", "百度"]
company_ids = []
for idx, comp_name in enumerate(companies):
result = db.execute(
text("INSERT INTO Company (employment_company) VALUES (:name)"),
{"name": comp_name}
)
company_ids.append(result.lastrowid)
# ── 8. 就业信息 ───────────────────────────────────
# 添加更多就业记录,确保统计接口有数据
employment_data = []
for i in range(min(15, len(all_students))):
company_idx = i % len(company_ids)
address_idx = i % len(address_ids)
salary = 25000 + (i * 2000) % 15000
# 前10个学生有offer时间,后5个没有(测试不同情况)
offer_time = date(2028, 4, 15 + i) if i < 10 else None
employment_data.append((
all_students[i][0], all_students[i][1], company_ids[company_idx],
address_ids[address_idx], salary, date(2028, 3, 1), offer_time
))
for idx, emp in enumerate(employment_data):
db.execute(text("""
INSERT INTO Employments (stuid, class_id, company_id, address_id,
employment_salary, employment_open_time,
offer_recived_time, is_deleted)
VALUES (:stuid, :class_id, :company_id, :addr_id, :salary,
:open_time, :offer_time, 0)
"""), {
"stuid": emp[0], "class_id": emp[1],
"company_id": emp[2], "addr_id": emp[3],
"salary": emp[4], "open_time": emp[5], "offer_time": emp[6],
})
db.commit()
print(f" [种子] 完成!插入 {len(subjects)} 科目、{len(teachers)} 教师、"
f"{len(classes)} 班级、20 学生、{len(all_students)*len(all_subjects)} 成绩、"
f"{len(companies)} 公司、{len(employment_data)} 就业")
except Exception as e:
db.rollback()
print(f" [种子] 失败: {e}")
raise
finally:
db.close()
+5 -5
View File
@@ -15,10 +15,10 @@ def get_stuinfo( #查询学生就业信息
,min_salary:int|None=Query(None,description='最小薪资范围')
,max_salary:int|None=Query(None,description='最大薪资范围')
,somewhere:str|None=Query(None,description='公司地区')
,employment_open_start: date | None = Query(None,description='就业开放起始时间')
,employment_open_end: date | None = Query(None,description='就业开放结束时间')
,offer_recived_start: date | None = Query(None,description='offer下发起始时间')
,offer_recived_end: date | None = Query(None,description='offer下发结束时间')
,employment_open_start: date|None = Query(None,description='就业开放起始时间')
,employment_open_end: date|None = Query(None,description='就业开放结束时间')
,offer_recived_start: date|None = Query(None,description='offer下发起始时间')
,offer_recived_end: date|None = Query(None,description='offer下发结束时间')
,db=Depends(get_db)
): #查询条件
r=get_emp_dao( #调用函数
@@ -93,7 +93,7 @@ def delete_stuinfo( #删除模块
): #根据id选择对象
rows = delete_emp_dao(id, db) #调用删除函数
if not rows:
raise HTTPException(status_code=500, detail='没有删除!') #失败返回错误信息
raise HTTPException(status_code=404, detail='未找到该就业记录!') #失败返回错误信息
return {'code': 200, 'totals': rows, 'detail': '删除成功'} #成功返回信息
'''
+16 -15
View File
@@ -2,6 +2,7 @@
from stu_jiuye.model import Employments,Address,Company
from students.model.students_model import *
from class_management.model.class_management_model import *
def add_employment_dao(o,db): #新增就业信息函数
try:
# r = db.query(Employments).filter(Employments.id == o['id'] ).all() #
@@ -18,6 +19,7 @@ def add_employment_dao(o,db): #新增就业信息函数
return False
else:
db.commit() #提交事务,永久保存
db.refresh(o1)
return True
def add_company_dao(o,db): #添加公司信息函数
@@ -34,6 +36,7 @@ def add_company_dao(o,db): #添加公司信息函数
return False
else:
db.commit() #提交事务,永久保存
db.refresh(o1)
return True
def add_address_dao(o,db): #添加地址区域信息函数
try:
@@ -49,6 +52,7 @@ def add_address_dao(o,db): #添加地址区域信息函数
return False
else:
db.commit() #提交事务,永久保存
db.refresh(o1)
return True
@@ -82,24 +86,23 @@ def get_emp_dao( #获取学生
# ,page
# ,page_size
): #根据选择条件获取信息
q = db.query(Employments,Company.employment_company,Address.somewhere) \
.outerjoin(Company, Students.id == Employments.stuid) \
.outerjoin(Company, ClassInfo.id == Employments.class_id)\
.outerjoin(Company,Employments.company_id==Company.id)\
.outerjoin(Address,Company.address_id==Address.id)\
.filter(Employments.is_deleted == 0) #连接公司表和地址表,筛选未被删除信息
q = (db.query(Employments, Students.name, Company.employment_company, Address.somewhere)
.outerjoin(Students, Students.id == Employments.stuid)
.outerjoin(ClassInfo, ClassInfo.id == Employments.class_id)
.outerjoin(Company,Employments.company_id==Company.id)
.outerjoin(Address,Employments.address_id==Address.id)
.filter(Employments.is_deleted == 0)) #连接公司表和地址表,筛选未被删除信息
# if stuid:
# q = q.filter( Employments.id == stuid )
if sname: #根据学生姓名筛选
q = q.filter( Students.name == sname )
if class_id:
q = q.filter(ClassInfo.id == class_id)
q = q.filter(Employments.class_id == class_id)
if somewhere:
q = q.filter(Address.somewhere.like(f'%{somewhere}%'))
if employment_company: #根据公司姓名筛选
q=q.filter(Company.employment_company.like(f'%{employment_company}%'))
# r = q.offset( (page-1)*page_size ).limit( page_size ).all()
if min_salary is not None: #根据最小薪资筛选
q = q.filter(Employments.employment_salary >= min_salary)
if max_salary is not None: ##根据最大薪资筛选
@@ -116,20 +119,18 @@ def get_emp_dao( #获取学生
r= q.all()
if r:
return [ {"id": i.id,
'学生姓名':i.sname,
'学生姓名':sname or '-',
'学生班级编号':i.class_id,
"公司": employment_company or '未就业',
"公司区域":somewhere or '-',
# "company_id": i.company_id,
"薪资":i.employment_salary or '-',
# "address_id": i.address_id,
"就业开放时间":i.employment_open_time.strftime('%Y-%m-%d') ,
"offer下发时间":i.offer_recived_time.strftime('%Y-%m-%d')
"就业开放时间":i.employment_open_time.strftime('%Y-%m-%d') if i.employment_open_time else '-',
"offer下发时间":i.offer_recived_time.strftime('%Y-%m-%d') if i.offer_recived_time else '-'
# , "创建时间": i.create_time,
# "更新时间": i.update_time,
# "是否删除":i.is_deleted
} for i,sname,class_id,employment_company,somewhere in r ] #返回信息列表
} for i,sname,employment_company,somewhere in r ] #返回信息列表
return {"code": 200, "totals": len(r), "data": r}
def delete_emp_dao(id,db): #删除函数
try:
+7 -7
View File
@@ -19,12 +19,14 @@ class Employments(Base_all): #学生
class_id = Column(Integer
, ForeignKey('classes.id')
) #学生班级,冗余字段
company_id = Column(Integer #公司id,外键匹配公司表
company_id = Column(Integer # 公司id,外键匹配公司表
, ForeignKey('Company.id')
, nullable=True
)
address_id = Column(Integer # 地址id,匹配地址表
, ForeignKey('Address.id') # 外键地址表id
, nullable=False # 非空约束
)
employment_salary = Column(Integer, nullable=True) #薪资
employment_open_time = Column(DATE, nullable=True) #就业开放时间
@@ -49,10 +51,7 @@ class Company(Base_all): #公司
employment_company = Column(String(50) #公司姓名
, nullable=False #非空约束
)
address_id = Column(Integer #地址id,匹配地址表
, ForeignKey('Address.id') #外键地址表id
, nullable=False #非空约束
)
create_time = Column(DATETIME
, default=datetime.now
)
@@ -71,6 +70,7 @@ class Address(Base_all):
somewhere=Column(String(50) #地址区域
, nullable=False
)
create_time = Column(DATETIME
, default=datetime.now
)
+3 -3
View File
@@ -4,10 +4,10 @@ from pydantic import BaseModel
class PostRequest(BaseModel): #学生就业信息请求体模型
# compname: str|None=None
sname: str|None=None
stuid: int|None=None
class_id: int|None=None
company_id:int|None=None
# address_id: int|None=None
address_id: int|None=None
employment_salary: int|None=None
employment_open_time:date|None=None
offer_recived_time:date|None=None
@@ -21,7 +21,7 @@ class EmpResponse(BaseModel): #学生就
class CompRequest(BaseModel): #公司信息请求体模型
employment_company:str
address_id: int
# address_id: int
class CompResponse(BaseModel): #公司信息响应体
code: int = 200
+377
View File
@@ -0,0 +1,377 @@
# 学生就业信息管理系统 —— 就业模块项目文档
## 一、模块概述
本模块是「学生就业信息管理系统」的核心业务模块,负责管理学生就业状态的全生命周期。围绕就业信息(Employments)、公司信息(Company)、地址信息(Address)三张核心表,并与学生表(students)、班级表(classes)建立外键关联,提供增删改查(CRUD)与统计分析接口。
**技术栈:**
| 项 | 选型 |
| ------ | --------------------- |
| Web 框架 | FastAPI |
| ORM | SQLAlchemy 2.x |
| 数据库 | MySQL 8.x(驱动 PyMySQL) |
| 数据校验 | Pydantic v2 |
| 服务启动 | Uvicorn |
## 二、目录结构
```
student\_manage\_system/
├── main.py # 程序入口:注册全模块路由、建表(\_\_main\_\_ 中执行)
├── databases.py # 根数据库配置:engine\_all / Base\_all / Session / get\_db
└── stu\_jiuye/ # 就业模块业务包
&#x20; ├── main.py # 路由组装(Shtudent\_Jiuye);独立自测 app
&#x20; ├── database.py # 模块级数据库配置(engine / Base / Session / get\_db)
&#x20; ├── model.py # ORM 模型:Employments / Company / Address
&#x20; ├── request.py # Pydantic 请求体 / 响应体模型
&#x20; ├── dao.py # 数据访问层(纯数据库操作)
&#x20; └── CURD\_api.py # 路由层(API 接口定义)
```
**分层职责:**
| 层 | 文件 | 职责 |
| ----- | ------------ | --------------------------- |
| 路由层 | CURD\_api.py | 接收 HTTP 请求,参数校验,调用 DAO,返回响应 |
| 数据访问层 | dao.py | 封装 SQLAlchemy 查询与事务,向上提供函数 |
| 模型层 | model.py | 定义表结构与外键关系 |
| 校验层 | request.py | 定义请求体 / 响应体的字段与类型 |
| 配置层 | database.py | 数据库 URL、引擎、Session 工厂 |
## 三、数据表设计
### 1. Employments(就业信息表)
| 字段 | 类型 | 约束 | 说明 |
| ---------------------- | -------- | ------------------------- | ------------------ |
| id | Integer | PK, 自增 | 主键 |
| stuid | Integer | FK → students.id | 学生 ID |
| class\_id | Integer | FK → classes.id | 班级 ID |
| company\_id | Integer | FK → Company.id, 可空 | 就业公司 |
| address\_id | Integer | FK → Address.id, NOT NULL | 就业地址 |
| employment\_salary | Integer | 可空 | 就业薪资 |
| employment\_open\_time | DATE | 可空 | 就业开放时间 |
| offer\_recived\_time | DATE | 可空 | Offer 下发时间 |
| create\_time | DateTime | default=now | 创建时间 |
| update\_time | DateTime | default=now, onupdate=now | 更新时间 |
| is\_deleted | Integer | default=0 | 软删除标记(0 正常 / 1 已删) |
### 2. Company(公司表)
| 字段 | 类型 | 约束 | 说明 |
| ------------------- | ---------- | ------------------------- | ----- |
| id | Integer | PK, 自增 | 主键 |
| employment\_company | String(50) | NOT NULL | 公司名称 |
| create\_time | DateTime | default=now | 创建时间 |
| update\_time | DateTime | default=now, onupdate=now | 更新时间 |
| is\_deleted | Integer | default=0 | 软删除标记 |
### 3. Address(地址表)
| 字段 | 类型 | 约束 | 说明 |
| ------------ | ---------- | ------------------------- | --------------- |
| id | Integer | PK, 自增 | 主键 |
| somewhere | String(50) | NOT NULL | 地址区域,如 "深圳市南山区" |
| create\_time | DateTime | default=now | 创建时间 |
| update\_time | DateTime | default=now, onupdate=now | 更新时间 |
| is\_deleted | Integer | default=0 | 软删除标记 |
### 表关系图
```
students (1) ───< (N) Employments (N) >─── (1) Company
classes (1) ───< (N) | (N) >─── (1) Address
```
* 一个学生可有多个就业记录,一条就业记录对应一个学生(通过 stuid 关联学生表)
* 一条就业记录对应一个公司(company\_id)与一个地址(address\_id)
* 通过多表 JOIN 可从就业记录直接获取学生姓名、班级名称、公司名称与地址信息
**设计说明:姓名 / 班级不冗余,走 JOIN 获取**
与 "在就业表冗余学生姓名" 的方案不同,本模块选择通过 `stuid`、`class_id` 外键关联学生表与班级表,查询时动态 JOIN 获取姓名与班级名。代价是查询多一次 JOIN;好处是学生姓名 / 班级变更后无需同步,数据天然一致。
## 四、API 功能清单(CRUD)
接口定义于 `CURD_api.py` 的 `CURD` 路由,由根 `../main.py` 以 `Shtudent_Jiuye` 挂载。
### 📌 新增(Create)
| 方法 | 路径 | 功能 | 请求体 |
| ---- | -------------------- | -------- | ----------- |
| POST | /emp/company/address | 新增地址区域 | AddRequest |
| POST | /emp/company | 新增公司 | CompRequest |
| POST | /emp | 新增学生就业信息 | PostRequest |
> 注意:由于外键依赖,建议按 Address → Company → Employments 的顺序添加。
### 📌 查询(Read)
| 方法 | 路径 | 功能 | 查询参数 |
| --- | ---- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET | /emp | 多条件查询就业信息 | sname、class\_id、employment\_company、min\_salary、max\_salary、somewhere、employment\_open\_start、employment\_open\_end、offer\_recived\_start、offer\_recived\_end(全部可选,均不传则查询全部) |
> 就业薪资 Top5、就业时长、平均就业时长等统计接口已迁移至统计分析模块(StatCalc):
>
> `/statcalc/top`
>
> 、
>
> `/statcalc/job`
>
> 、
>
> `/statcalc/avgjob`
>
> 。
### 📌 更新(Update)
| 方法 | 路径 | 功能 | 参数 |
| --- | --------- | ------ | ----------------------------------- |
| PUT | /emp?id=X | 修改就业信息 | Query:id;Body:PostRequest(支持部分字段更新) |
### 📌 删除(Delete)
| 方法 | 路径 | 功能 | 参数 |
| ------ | --------- | ------- | ------- |
| DELETE | /emp/{id} | 软删除就业信息 | Path:id |
**删除说明**:本模块采用软删除(`is_deleted = 1`),数据不会从物理表中移除,便于审计与恢复。
## 五、核心流程示例
### 查询流程(GET /emp)
```
前端 → CURD\_api.get\_stuinfo()
&#x20; ↓ 收集 10 个可选查询条件
&#x20; ↓ 调用 get\_emp\_dao()
&#x20; ↓ DAO 五表 OUTER JOIN:Employments + Students + ClassInfo + Company + Address
&#x20; ↓ 动态拼接 filter(姓名/班级/公司/薪资范围/地址/时间范围)
&#x20; ↓ 统一过滤 is\_deleted = 0
&#x20; ↓ 返回 list\[dict](含学生姓名、班级名称、公司、区域、薪资、时间)
&#x20; ↓ 结果为空 → 404「名单不存在」
```
### 添加流程(POST /emp)
```
前端 → CURD\_api.add\_stuinfo()
&#x20; ↓ p.model\_dump() 转 dict
&#x20; ↓ add\_employment\_dao()
&#x20; ↓ o.pop('id' / 'create\_time' / 'update\_time' / 'is\_deleted') 去掉自增主键与自动字段
&#x20; ↓ Employments(\*\*o) 实例化
&#x20; ↓ db.add → db.commit → db.refresh
&#x20; ↓ 失败 → db.rollback → 返回 500
```
### 更新流程(PUT /emp?id=X)
```
前端 → CURD\_api.updata\_stuinfo()
&#x20; ↓ 请求体排除未传字段(exclude\_unset)
&#x20; ↓ 为空 → 400「没有需要更新的字段」
&#x20; ↓ update\_emp\_dao() 按 id 执行 update
&#x20; ↓ 影响 0 行 → 404「未找到对应记录」
```
### 删除流程(DELETE /emp/{id})
```
前端 → CURD\_api.delete\_stuinfo()
&#x20; ↓ delete\_emp\_dao() 按 id 执行 UPDATE is\_deleted = 1(软删除)
&#x20; ↓ 影响 0 行 → 500「没有删除」
```
## 六、项目亮点
1. **清晰的分层架构**
路由(CURD\_api.py)、数据访问(dao.py)、模型(model.py)、校验(request.py)各司其职,便于单元测试与维护。
2. **全链路软删除**
Employments、Company、Address 三张表均有 `is_deleted` 字段,删除统一走 `UPDATE is_deleted = 1`,数据可追溯、可恢复;查询时统一过滤 `is_deleted == 0`。
3. **动态多条件组合查询**
`get_emp_dao` 使用链式 filter 动态拼接查询条件,任一条件为空自动跳过,一个接口覆盖 10 种查询维度(姓名 / 班级 / 公司 / 薪资区间 / 地址 / 开放时间区间 / Offer 时间区间)。
4. **统一时间戳审计**
所有表均带 `create_time` / `update_time`,`onupdate=datetime.now` 让修改自动记录时间,为审计、报表提供数据基础。
5. **请求体与响应体分离**
request.py 为每类资源定义独立请求体(PostRequest / CompRequest / AddRequest)与响应体(EmpResponse / CompResponse / AddResponse),前端契约清晰,Swagger 自动生成接口文档。
6. **多表 JOIN 关联查询**
就业记录与学生、班级、公司、地址四张表通过外键关联,一次查询即可返回完整就业信息,无需前端二次请求。
7. **外键约束保证数据完整性**
stuid /class\_id/company\_id /address\_id 全部建立外键,杜绝 "就业记录指向不存在学生 / 公司" 的脏数据。
## 七、待添加的新功能 与 已知问题
### 🎯 短期(可快速落地)
| 功能 | 说明 | 涉及改动 |
| ------- | ------------------------------------------------ | ---------------------------------------------- |
| 分页查询 | /emp 支持 page /page\_size,返回 total 与 items | dao.py 加 .offset ().limit (),request.py 加分页请求体 |
| 薪资区间校验 | 确保 min\_salary <= max\_salary | request.py 加 @model\_validator |
| 数据去重 | 同一学生同一公司只保留一条有效就业记录 | add\_employment\_dao 前置查询 + 唯一约束 |
| 级联删除 | 删公司 / 地址时同步软删其下就业记录 | dao.py 加级联更新逻辑 |
| 外键存在性校验 | 新增时校验 stuid /company\_id/address\_id 真实存在,给出明确错误 | dao.py 前置查询 |
### 🚀 中期(需要设计)
| 功能 | 说明 |
| ---------- | --------------------------------- |
| 批量导入 | 支持 Excel/CSV 批量导入就业信息 |
| 数据导出 | 查询结果一键导出为 Excel |
| JWT 鉴权 | 管理员 / 普通用户角色分离,保护写接口 |
| 操作日志 | 记录每一次增删改操作的操作人与时间 |
| Alembic 迁移 | 用 Alembic 管理表结构变更,告别手动 DROP TABLE |
### 🌟 长期(数据价值)
| 功能 | 说明 |
| ------ | ----------------------- |
| 就业数据看板 | 按班级 / 公司 / 薪资维度统计,前端可视化 |
| 薪资分布分析 | 直方图、分位数、同比环比 |
| 就业率追踪 | 结合学生表计算班级 / 专业就业率 |
| 缓存层 | Redis 缓存 Top5、统计类接口 |
| 异步任务 | Celery 处理导入 / 导出等耗时任务 |
### ⚠️ 当前已知问题(开发中待修复)
| 问题 | 位置 | 影响 | 建议修复 |
| ----------------- | -------------------------------------------------------- | -------------------- | ------------------------- |
| 查询结果按 5 变量解包 3 元组 | dao.py get\_emp\_dao | 查到数据即 ValueError 崩溃 | select 补齐 5 列,或解包变量与列数一致 |
| 空日期直接 strftime | dao.py 返回段 | 时间为空时 AttributeError | 判空后再格式化 |
| 删除 0 行返回 500 | CURD\_api.py delete\_stuinfo | 状态码语义错误 | 改 404 |
| 查询空结果返回 404 | CURD\_api.py get\_stuinfo | 正常空结果被当异常 | 改 200 + 空列表 + totals |
| 更新未过滤已删除记录 | dao.py update\_emp\_dao | 可修改已软删数据 | filter 加 is\_deleted == 0 |
| SQL 全量打印 | database.py echo=True | 开发噪音 / 性能损耗 | 生产环境关闭 |
| 命名不统一 | Shtudent\_Jiuye / updata\_stuinfo / offer\_recived\_time | 可读性差 | 统一规范命名 |
## 八、运行方式
```
\# 1. 安装依赖
pip install fastapi uvicorn sqlalchemy pymysql pydantic python-dotenv
\# 2. 配置数据库(项目根目录 .env 中 DB\_USER / DB\_PASSWORD / DB\_HOST / DB\_PORT / DB\_NAME)
\# 3. 启动服务(根入口,含全模块)
python main.py
\# 或
uvicorn main:app --host 127.0.0.1 --port 59000 --reload
\# 4. 单独调试就业模块
uvicorn stu\_jiuye.main:app --host 127.0.0.1 --port 12395 --reload
\# 5. 访问 Swagger 文档
http://127.0.0.1:59000/docs
```
## 九、注意事项
1. **表结构变更需删表重建**:`Base.metadata.create_all(engine)` 只建不改,模型加字段后需 DROP TABLE 或引入 Alembic。
2. **建表时机**:当前建表逻辑在根 `../main.py` 的 `__main__` 块中执行;直接用 `uvicorn main:app` 启动时不会建表,全新环境需先执行一次 `python main.py`(或后续将建表移入 FastAPI lifespan)。
3. **添加顺序有外键依赖**:先建学生 / 班级 / 公司 / 地址,再添加就业记录;Employments 的 stuid /class\_id/company\_id /address\_id 均受外键约束。
4. **软删除后查询自动过滤**:`get_emp_dao` 已统一过滤 `is_deleted == 0`,被删除的记录不会再出现在列表里。
5. **删除接口用 Path 参数**:`DELETE /emp/{id}`,不要写成 `/emp?id=X`。
6. **echo=True 仅限开发**:生产环境请关掉,避免刷屏与性能损耗。
+270
View File
@@ -0,0 +1,270 @@
# 学生就业信息管理系统 —— 就业模块项目文档
## 一、模块概述
本模块是「学生就业信息管理系统」的核心业务模块,负责管理学生就业状态的全生命周期。围绕就业信息(Employments)、公司信息(Company)、地址信息(Address)三张核心表,并与学生表(students)、班级表(classes)建立外键关联,提供增删改查(CRUD)与统计分析接口。
**技术栈:**
| 项 | 选型 |
|---|---|
| Web 框架 | FastAPI |
| ORM | SQLAlchemy 2.x |
| 数据库 | MySQL 8.x(驱动 PyMySQL) |
| 数据校验 | Pydantic v2 |
| 服务启动 | Uvicorn |
## 二、目录结构
```
student_manage_system/
├── main.py # 程序入口:注册全模块路由、建表(__main__ 中执行)
├── databases.py # 根数据库配置:engine_all / Base_all / Session / get_db
└── stu_jiuye/ # 就业模块业务包
├── main.py # 路由组装(Shtudent_Jiuye);独立自测 app
├── database.py # 模块级数据库配置(engine / Base / Session / get_db)
├── model.py # ORM 模型:Employments / Company / Address
├── request.py # Pydantic 请求体 / 响应体模型
├── dao.py # 数据访问层(纯数据库操作)
└── CURD_api.py # 路由层(API 接口定义)
```
**分层职责:**
| 层 | 文件 | 职责 |
|---|---|---|
| 路由层 | CURD_api.py | 接收 HTTP 请求,参数校验,调用 DAO,返回响应 |
| 数据访问层 | dao.py | 封装 SQLAlchemy 查询与事务,向上提供函数 |
| 模型层 | model.py | 定义表结构与外键关系 |
| 校验层 | request.py | 定义请求体 / 响应体的字段与类型 |
| 配置层 | database.py | 数据库 URL、引擎、Session 工厂 |
## 三、数据表设计
### 1. Employments(就业信息表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | Integer | PK, 自增 | 主键 |
| stuid | Integer | FK → students.id | 学生 ID |
| class_id | Integer | FK → classes.id | 班级 ID |
| company_id | Integer | FK → Company.id, 可空 | 就业公司 |
| address_id | Integer | FK → Address.id, NOT NULL | 就业地址 |
| employment_salary | Integer | 可空 | 就业薪资 |
| employment_open_time | DATE | 可空 | 就业开放时间 |
| offer_recived_time | DATE | 可空 | Offer 下发时间 |
| create_time | DateTime | default=now | 创建时间 |
| update_time | DateTime | default=now, onupdate=now | 更新时间 |
| is_deleted | Integer | default=0 | 软删除标记(0 正常 / 1 已删) |
### 2. Company(公司表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | Integer | PK, 自增 | 主键 |
| employment_company | String(50) | NOT NULL | 公司名称 |
| create_time | DateTime | default=now | 创建时间 |
| update_time | DateTime | default=now, onupdate=now | 更新时间 |
| is_deleted | Integer | default=0 | 软删除标记 |
### 3. Address(地址表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | Integer | PK, 自增 | 主键 |
| somewhere | String(50) | NOT NULL | 地址区域,如"深圳市南山区" |
| create_time | DateTime | default=now | 创建时间 |
| update_time | DateTime | default=now, onupdate=now | 更新时间 |
| is_deleted | Integer | default=0 | 软删除标记 |
### 表关系图
```
students (1) ───< (N) Employments (N) >─── (1) Company
classes (1) ───< (N) | (N) >─── (1) Address
```
- 一个学生可有多个就业记录,一条就业记录对应一个学生(通过 stuid 关联学生表)
- 一条就业记录对应一个公司(company_id)与一个地址(address_id)
- 通过多表 JOIN 可从就业记录直接获取学生姓名、班级名称、公司名称与地址信息
**设计说明:姓名/班级不冗余,走 JOIN 获取**
与"在就业表冗余学生姓名"的方案不同,本模块选择通过 `stuid`、`class_id` 外键关联学生表与班级表,查询时动态 JOIN 获取姓名与班级名。代价是查询多一次 JOIN;好处是学生姓名/班级变更后无需同步,数据天然一致。
## 四、API 功能清单(CRUD)
接口定义于 `stu_jiuye/CURD_api.py` 的 `CURD` 路由,由根 `main.py` 以 `Shtudent_Jiuye` 挂载。
### 📌 新增(Create)
| 方法 | 路径 | 功能 | 请求体 |
|---|---|---|---|
| POST | /emp/company/address | 新增地址区域 | AddRequest |
| POST | /emp/company | 新增公司 | CompRequest |
| POST | /emp | 新增学生就业信息 | PostRequest |
> 注意:由于外键依赖,建议按 Address → Company → Employments 的顺序添加。
### 📌 查询(Read)
| 方法 | 路径 | 功能 | 查询参数 |
|---|---|---|---|
| GET | /emp | 多条件查询就业信息 | sname、class_id、employment_company、min_salary、max_salary、somewhere、employment_open_start、employment_open_end、offer_recived_start、offer_recived_end(全部可选,均不传则查询全部) |
> 就业薪资 Top5、就业时长、平均就业时长等统计接口已迁移至统计分析模块(StatCalc):`/statcalc/top`、`/statcalc/job`、`/statcalc/avgjob`。
### 📌 更新(Update)
| 方法 | 路径 | 功能 | 参数 |
|---|---|---|---|
| PUT | /emp?id=X | 修改就业信息 | Query:id;Body:PostRequest(支持部分字段更新) |
### 📌 删除(Delete)
| 方法 | 路径 | 功能 | 参数 |
|---|---|---|---|
| DELETE | /emp/{id} | 软删除就业信息 | Path:id |
**删除说明**:本模块采用软删除(`is_deleted = 1`),数据不会从物理表中移除,便于审计与恢复。
## 五、核心流程示例
### 查询流程(GET /emp)
```
前端 → CURD_api.get_stuinfo()
↓ 收集 10 个可选查询条件
↓ 调用 get_emp_dao()
↓ DAO 五表 OUTER JOIN:Employments + Students + ClassInfo + Company + Address
↓ 动态拼接 filter(姓名/班级/公司/薪资范围/地址/时间范围)
↓ 统一过滤 is_deleted = 0
↓ 返回 list[dict](含学生姓名、班级名称、公司、区域、薪资、时间)
↓ 结果为空 → 404「名单不存在」
```
### 添加流程(POST /emp)
```
前端 → CURD_api.add_stuinfo()
↓ p.model_dump() 转 dict
↓ add_employment_dao()
↓ o.pop('id' / 'create_time' / 'update_time' / 'is_deleted') 去掉自增主键与自动字段
↓ Employments(**o) 实例化
↓ db.add → db.commit → db.refresh
↓ 失败 → db.rollback → 返回 500
```
### 更新流程(PUT /emp?id=X)
```
前端 → CURD_api.updata_stuinfo()
↓ 请求体排除未传字段(exclude_unset)
↓ 为空 → 400「没有需要更新的字段」
↓ update_emp_dao() 按 id 执行 update
↓ 影响 0 行 → 404「未找到对应记录」
```
### 删除流程(DELETE /emp/{id})
```
前端 → CURD_api.delete_stuinfo()
↓ delete_emp_dao() 按 id 执行 UPDATE is_deleted = 1(软删除)
↓ 影响 0 行 → 500「没有删除」
```
## 六、项目亮点
1. **清晰的分层架构**
路由(CURD_api.py)、数据访问(dao.py)、模型(model.py)、校验(request.py)各司其职,便于单元测试与维护。
2. **全链路软删除**
Employments、Company、Address 三张表均有 `is_deleted` 字段,删除统一走 `UPDATE is_deleted = 1`,数据可追溯、可恢复;查询时统一过滤 `is_deleted == 0`。
3. **动态多条件组合查询**
`get_emp_dao` 使用链式 filter 动态拼接查询条件,任一条件为空自动跳过,一个接口覆盖 10 种查询维度(姓名/班级/公司/薪资区间/地址/开放时间区间/Offer 时间区间)。
4. **统一时间戳审计**
所有表均带 `create_time` / `update_time`,`onupdate=datetime.now` 让修改自动记录时间,为审计、报表提供数据基础。
5. **请求体与响应体分离**
request.py 为每类资源定义独立请求体(PostRequest / CompRequest / AddRequest)与响应体(EmpResponse / CompResponse / AddResponse),前端契约清晰,Swagger 自动生成接口文档。
6. **多表 JOIN 关联查询**
就业记录与学生、班级、公司、地址四张表通过外键关联,一次查询即可返回完整就业信息,无需前端二次请求。
7. **外键约束保证数据完整性**
stuid / class_id / company_id / address_id 全部建立外键,杜绝"就业记录指向不存在学生/公司"的脏数据。
## 七、待添加的新功能 与 已知问题
### 🎯 短期(可快速落地)
| 功能 | 说明 | 涉及改动 |
|---|---|---|
| 分页查询 | /emp 支持 page / page_size,返回 total 与 items | dao.py 加 .offset().limit(),request.py 加分页请求体 |
| 薪资区间校验 | 确保 min_salary <= max_salary | request.py 加 @model_validator |
| 数据去重 | 同一学生同一公司只保留一条有效就业记录 | add_employment_dao 前置查询 + 唯一约束 |
| 级联删除 | 删公司/地址时同步软删其下就业记录 | dao.py 加级联更新逻辑 |
| 外键存在性校验 | 新增时校验 stuid / company_id / address_id 真实存在,给出明确错误 | dao.py 前置查询 |
### 🚀 中期(需要设计)
| 功能 | 说明 |
|---|---|
| 批量导入 | 支持 Excel/CSV 批量导入就业信息 |
| 数据导出 | 查询结果一键导出为 Excel |
| JWT 鉴权 | 管理员 / 普通用户角色分离,保护写接口 |
| 操作日志 | 记录每一次增删改操作的操作人与时间 |
| Alembic 迁移 | 用 Alembic 管理表结构变更,告别手动 DROP TABLE |
### 🌟 长期(数据价值)
| 功能 | 说明 |
|---|---|
| 就业数据看板 | 按班级/公司/薪资维度统计,前端可视化 |
| 薪资分布分析 | 直方图、分位数、同比环比 |
| 就业率追踪 | 结合学生表计算班级/专业就业率 |
| 缓存层 | Redis 缓存 Top5、统计类接口 |
| 异步任务 | Celery 处理导入/导出等耗时任务 |
###
| | | | |
|---|---|---|---|
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
## 八、运行方式
```bash
# 1. 安装依赖
pip install fastapi uvicorn sqlalchemy pymysql pydantic python-dotenv
# 2. 配置数据库(项目根目录 .env 中 DB_USER / DB_PASSWORD / DB_HOST / DB_PORT / DB_NAME)
# 3. 启动服务(根入口,含全模块)
python main.py
# 或
uvicorn main:app --host 127.0.0.1 --port 59000 --reload
# 4. 单独调试就业模块
uvicorn stu_jiuye.main:app --host 127.0.0.1 --port 12395 --reload
# 5. 访问 Swagger 文档
http://127.0.0.1:59000/docs
```
## 九、注意事项
1. **表结构变更需删表重建**:`Base.metadata.create_all(engine)` 只建不改,模型加字段后需 DROP TABLE 或引入 Alembic。
2. **建表时机**:当前建表逻辑在根 `main.py` 的 `__main__` 块中执行;直接用 `uvicorn main:app` 启动时不会建表,全新环境需先执行一次 `python main.py`(或后续将建表移入 FastAPI lifespan)。
3. **添加顺序有外键依赖**:先建学生/班级/公司/地址,再添加就业记录;Employments 的 stuid / class_id / company_id / address_id 均受外键约束。
4. **软删除后查询自动过滤**:`get_emp_dao` 已统一过滤 `is_deleted == 0`,被删除的记录不会再出现在列表里。
5. **删除接口用 Path 参数**:`DELETE /emp/{id}`,不要写成 `/emp?id=X`。
6. **echo=True 仅限开发**:生产环境请关掉,避免刷屏与性能损耗。
+200 -15
View File
@@ -1,4 +1,6 @@
# students模块解释
## 组名:江山代有人才秃
## 口号:干就完了
## 项目层:
web框架:FastAPI(自动生成接口)
@@ -16,48 +18,231 @@ ORM:sqlalchemy
## main:学生管理系统的主程序文件、测试接口
1. 下面所有配置文件的接口全部导入并建表
2. 创建fastapi接口并本地运行
2. 创建合并接口APIRouter
3. 创建fastapi接口并本地运行
## databases:链接数据库配置文件
1. 链接本地mysql,实例化mysql,创建引擎,创建基类,实例化数据库会话
2. 创建函数,调用数据库会话
3. 因为后期多代码合并,每个人的databases都不同链接也不同,所以导入了.env文件
1. 这样在每个人有差异的数据可以直接保存在本地,不用反复同步相互覆盖了
2. 另外一个原因就是model层大家建表要使用同一个基类
1. 所以接入了.env文件随时入参
2. 这样更加方便,而且高频同步和拉取也不用重新配置
## 分层代码书写顺序:
1. model层:
1. 确认你的业务需要哪些字段
2. 确认小组的字段名、表名、库名一致
3. 确认主键、唯一键、字段数据类型、外键等关键字内容
2. 请求体
1. 搞清楚业务需求,需要用到哪些字段
2. 增删改查的具体逻辑
3. api
1. 写好接收变量
2. 装饰器设定
3. 输出规则
4. 一次写好基本上就不会再改了
4. dao
1. 工作量最大的层,所有的业务逻辑都写在这里面
2. 所有的业务逻辑都在这个层里
## api.students_api:接口文件
### 学生表students的功能接口:
1. 学生信息的查询
1. 仅保存入参标量,代码都在逻辑层,建好了基本不用修改,方便维护
2. 学生信息的新增
1. 仅保存入参标量,代码都在逻辑层,建好了基本不用修改,方便维护
3. 学生信息的更新
1. 仅保存入参标量,代码都在逻辑层,建好了基本不用修改,方便维护
4. 学生信息的逻辑删除
1. 仅保存入参标量,代码都在逻辑层,建好了基本不用修改,方便维护
## dao.students_dao:数据筛选文件
### 数据库的逻辑筛选文件:
1. 查询:get_student_dao
1. 筛选所有非空内容,过滤逻辑删除数据,输出学生完整字段
2. 新增:add_student_dao
1. 关键字段限制
2. 过滤各种报错,反馈报错问题
3. 无报错即可全部插入
4. 注意逻辑删除的数据,其实也会占用唯一键,所以有唯一键重复时,直接拉出旧数据进行清空翻新。
3. 更新:update_student_dao
1. 主要过滤掉没有输入的字段,除非输入才可以被覆盖,不能每次更新清空全部内容
2. 唯一字段过滤,比如电话号码,在业务逻辑上电话号码不能重复
4. 逻辑删除:delete_student_dao
1. 逻辑删除可以用所有字段进行精确或者范围删除
2. 但是不可以全为空删除我的整个库
## model.students_model:数据库建表文件
1. 数据表建表代码
2. 字段信息: sid **PK**
, student_name
, student_phone
, class_name
, job_open_time
, offer_issue_time
, company_name
, salary
, create_date
, update_date
, is_deleted
, deleted_date
, **待建FK:** class_id/teacher_id/score_id/company_id/
2. 字段信息: `id`:***PK***
`num`:***UNI***
`name`
`age`
`sex`:*ENUM*
`home_Place`
`college`
`specialty`
`enrollment_time`
`graduate_time`
`education`:*ENUM*
***外键字段***
`class_id`:***FK***
`advisor_id`:***FK***
***额外字段***
`phone`:***UNI***
***通用字段***
`create_date`
`update_date`
`is_deleted`
`deleted_date`
## schema.students_request:请求体、数据过滤
请求体根据业务要求有不同调整,方便后续维护和更改需求
1. 查询:StudentsQuery
1. 所有字段都可以进行查询
2. 随便填哪个都可以进行精确查询或者范围查询
3. 全为空即查询全部
2. 新增:StudentsResponse
1. 新增有必填项、不可重复项等,保证数据干净
3. 更新:StudentsUpdate
1. 以ID作为查询字段,更新其他内容
2. 会过滤掉重复值
4. 逻辑删除:StudentsDelete
1. 所有字段都可以进行删除
2. 随便填哪个都可以进行精确删除或者范围删除
3. 不可全字段为空
## StatCalc:统计分析模块
- 除了没有model以外没有什么太大的区别
- 因为接口都有具体需求,所以直接一键呼出,不用填写筛选内容
1. 请求体
1. 搞清楚业务需求,需要用到哪些字段
2. 增删改查的具体逻辑
2. api
1. 写好接收变量
2. 装饰器设定
3. 输出规则
4. 一次写好基本上就不会再改了
3. dao
1. 工作量最大的层,所有的业务逻辑都写在这里面
2. 所有的业务逻辑都在这个层里
## 学生管理系统修改报错记录
### 1. 合并修改记录:
1. main.py
1. 所有子模块中的main新增了APIRouter,用来拉到主main进行运行
2. stu_jiuye
1. stu_jiuye\main.py
1. 新增为加粗倾斜处
2. from ***sut_jiuye***.文件名 import 调用名
2. CURD_api.py
1. 新增为加粗倾斜处
2. from ***sut_jiuye***.文件名 import 调用名
2. databases.py
1. 所有人的db_url账号密码IP端口和名称全部存入了.env文件
2. 导入了load_dotenv进行调用
1. from dotenv import load_dotenv
2. load_dotenv( )
3. DB_USER = os.getenv("DB_USER", "root")
4. DB_PASSWORD = os.getenv("DB_PASSWORD", "")
5. DB_HOST = os.getenv("DB_HOST", "localhost")
6. DB_PORT = os.getenv("DB_PORT", "3306")
7. DB_NAME = os.getenv("DB_NAME", "student_manage_system")
3. 修改了部分url名称,自行检查
3. class文件夹下导入修改
1. 新增为加粗倾斜处
1. from ***class_management***.文件名 import 调用名
1. 改动文件:
1. class_management_api.py
2. class_management_dao.py
3. class_management_model.py
### 2. 合并完成
1. 新增.env文件,设置好账号密码等
1. 可以一次运行创建好所有表格
2. 但是注意,没办法创建库,也就是student_manage_system
3. 如果没有建库的话会报错
2. 表外键写完之后,可以用SQL语句删除所有表
1. 再重新运行***main.py***文件
1. 这个main.py特指最外层的运行主程序main.py
### 3. 分支测试
1. 阶段完成后
1. 如果不擅长保存版本,可以在自己的分支下面再建新分支
2. 方便切换版本和查询bug,理解逻辑,以免修改后bug更多了
3. 完成后可以将子分支变基到主线
4. 变基(覆盖)完成后可以继续使用子分支进行修改整合
2. 注意云端仓库,没有修改的文件是不会反复上传的
1. 所以看距离时间最近的基本都是被修改过的
2. 可以用来进行版本对比
### 4. 外键创建
1. 外键必须要来自同一个基类,不然会报错
1. 新增主程序的databases.py文件,统一基类
### 5. 外键报错
1. 在新建字段时,依赖主表的外键必须要已经建立,不然在提交时都会报错
2. 所以新增接口的报错逻辑需要特别优化,将各个阶段可能报错的内容都捕获下来
## hermes agent协作内容:
### 1. 测试数据脚本 — init_test_data.py/seed_data.py
直接正常运行主程序文件 main.py 即可插入
直接插入前提:
1. 保证数据库 student_manage_system 存在
2. 数据表字段对应
3. 建议直接删库建库 students\StudentsQuery.sql 里有相应需求的语句
向数据库插入了完整的测试数据,没有修改任何原有项目文件:
数据类型
数量
科目 6 门(Python程序设计、数据结构、数据库原理等)
教师 6 位(张伟、李芳、王强、陈静、刘洋、赵敏)
班级 3 个(CS2401、CS2402、AI2501)
学生 20 名(涵盖3个班级,男女各半)
成绩 120 条(每名学生6门课,含部分不及格记录)
公司 5 家(字节跳动、阿里巴巴、腾讯、华为、百度)
就业信息 8 条(含薪资、offer时间等)
### 2. 前端页面 - frontend/index.html - 双击运行
单文件完整前端,包含:
首页:问候 Banner + 快捷卡片(查课表/查成绩/校园卡/报修)+ 校园动态信息流
学生管理:表格列表、搜索过滤、新增/编辑/删除(含二次确认弹窗)
成绩查询:学期切换 Tab、成绩卡片展示、Chart.js 柱状图
教师管理:教师列表、搜索、新增
就业管理:就业信息列表、状态标签(已就业/待就业)
个人中心:蓝色背景头像区 + 功能菜单列表
交互特性:
✅ 卡片点击缩放反馈(95% 回弹,150ms)
✅ 页面切换从右侧滑入(300ms)
✅ 删除二次确认弹窗
✅ Toast 提示(成功/失败/信息)
✅ 鼠标悬停按钮/卡片颜色加深、阴影加深
✅ 隔行变色表格 + 表头排序
启动后端后直接浏览器打开 frontend/index.html 即可使用。
### 移植
只动几个文件,都不需要改代码:
移动的文件:
seed_data.py → 新项目的根目录
init_test_data.py → 新项目的根目录
frontend/ → 新项目的根目录(整个文件夹)
要改的唯一参数: frontend/index.html
const API_BASE = 'http://localhost:59000';
改成新项目实际运行的端口即可。
其他都不用动:
init_test_data.py 完全独立,连接的是 databases.py 里的 .env 配置,新项目只要 DB 信息对就能直接跑
frontend/index.html 纯静态页面,不依赖任何项目代码
如果新项目用了不同的端口(比如 8000),改那一行就行
+23 -10
View File
@@ -10,16 +10,29 @@ drop database student_manage_system;
drop table students;
select * from students limit 50;
select * from students limit 200;
desc students;
select * from scores limit 200;
select * from address limit 200;
select * from classes limit 200;
select * from company limit 200;
select * from employments limit 200;
select * from teachers limit 200;
select * from subject limit 200;
select *
from cast(student_manage_system as BINARY)
where age >30
join on
group by
having
order by
limit
# select *
# from cast(student_manage_system as BINARY)
# where age >30
# join on
# group by
# having
# order by
# limit
+9 -9
View File
@@ -1,6 +1,6 @@
# students_api所有students的api路由、接口
from fastapi import APIRouter , Depends , HTTPException
from fastapi import APIRouter , Depends , HTTPException , Body
from students.dao.students_dao import *
from students.model.students_model import *
from students.schema.students_request import *
@@ -27,15 +27,15 @@ def get_students( stu:StudentsQuery = Depends()
@s_api.post("/students"
,summary='新增学生资料'
,description=f'学生ID自动分配,学生姓名、学生手机号和班级是必填项,不能为空。\n '
f'时间格式:YYYY-MM-DD'
,response_model=StudentsResponse )
def add_student( stu:StudentsResponse = Depends()
f'注意事项:\n'
f'时间格式:YYYY-MM-DD\n'
f'班级ID和顾问ID必须已经存在才可以绑定'
)
def add_student( stu:StudentsResponse =Depends()
, db=Depends(get_db)
):
s_dict = stu.model_dump(exclude_unset=True)
result = add_student_dao( s=s_dict , db=db )
if not result:
raise HTTPException(status_code=500, detail='手机号已有学生使用或必填项为空。')
return result
@s_api.put("/students"
@@ -43,7 +43,7 @@ def add_student( stu:StudentsResponse = Depends()
,description=f'请输入需要更新信息的学生ID,学生手机号不可重复,填写即更新,为空即不更新。\n '
f'时间格式:YYYY-MM-DD'
,response_model=StudentsUpdate )
def get_students( stu:StudentsUpdate = Depends()
def update_students( stu:StudentsUpdate =Depends()
, db=Depends(get_db)
):
s_dict = stu.model_dump(exclude_unset=True)
@@ -54,10 +54,10 @@ def get_students( stu:StudentsUpdate = Depends()
@s_api.delete("/students"
,summary='删除学生资料'
,description=f'输入需要删除的学生信息,删除条件任选,但不可全为空,工资筛选能删除大于等于输入工资的所有人。\n '
,description=f'输入需要删除的学生信息,删除条件任选,但不可全为空。\n '
f'时间格式:YYYY-MM-DD'
,response_model=list[StudentsDelete] ) # 逻辑删除/软删除
def delete_students( stu:StudentsDelete = Depends()
def delete_students( stu:StudentsDelete =Depends()
, db=Depends(get_db)
):
s_dict = stu.model_dump(exclude_unset=True)
+30 -11
View File
@@ -62,9 +62,20 @@ def add_student_dao( s , db):
phone = s.get("phone")
s1_deleted = db.query( Students ).filter( Students.phone == phone ).first()
if s1_deleted and s1_deleted.is_deleted == False:
return False
raise ValueError("该手机号已存在有效学生记录,请勿重复添加")
if not phone:
return False
raise ValueError("手机号不能为空")
if not s.get("class_id") or not s.get("advisor_id"):
raise ValueError("班级ID和顾问ID不能为空")
# 查询 classes 表确认 class_id 存在
from Teachers.model.tea_model import Tea
s1_class_id = db.query(Tea).filter(Tea.id == s.get("class_id")).first()
# 查询 teachers 表确认 advisor_id 存在
s1_advisor_id = db.query(Tea).filter(Tea.id == s.get("advisor_id")).first()
if not s1_class_id:
raise ValueError(f"班级ID {s.get('class_id')} 不存在,请先创建班级")
if not s1_advisor_id:
raise ValueError(f"顾问ID {s.get('advisor_id')} 不存在,请先创建顾问")
if s1_deleted and s1_deleted.is_deleted == True:
s1_deleted.name = s.get("name")
s1_deleted.age = s.get("age")
@@ -88,14 +99,22 @@ def add_student_dao( s , db):
db.commit()
db.refresh(s1_deleted) # 从数据库再拉一次数据
return s1_deleted
s1 = Students( **s )
s1 = Students(**s)
# 显式设置时间,避免 SQLAlchemy 字典构建时跳过 default
now = datetime.now()
s1.create_date = now
s1.update_date = now
db.add(s1)
except Exception:
db.commit()
except ValueError as e:
# 业务校验异常:回滚并返回错误信息
db.rollback()
return {"code": 400, "msg": str(e)}
except Exception as e:
# 这里可以加日志
db.rollback()
return False
return {"code": 500, "msg": f"系统异常: {str(e)}"}
else:
db.commit()
db.refresh(s1)
return s1
@@ -114,7 +133,7 @@ def update_student_dao( s , db):
if not s1_id:
raise HTTPException(status_code=404, detail=f'学生不存在:id={s.get("id")}')
if s.get("num") is not None:
s1_id.name = s.get("num")
s1_id.num = s.get("num")
if s.get("name") is not None:
s1_id.name = s.get("name")
if s.get("age") is not None:
@@ -140,15 +159,15 @@ def update_student_dao( s , db):
s1_id.advisor_id = s.get("advisor_id")
# 额外字段
if s.get("phone") is not None:
s1_id.student_phone = s.get("phone")
s1_id.phone = s.get("phone")
s1_id.update_date = datetime.now()
db.commit()
except HTTPException:
raise
except Exception:
db.rollback()
raise
else:
s1_id.update_date = datetime.now()
db.commit()
db.refresh(s1_id) # 从数据库再拉一次数据
return s1_id
@@ -198,7 +217,7 @@ def delete_student_dao( s , db):
if s.get('class_id'):
q = q.filter(Students.class_id == s.get("class_id") )
if s.get('advisor_id'):
q = q.filter(Students.advisor_id == s.get("v") )
q = q.filter(Students.advisor_id == s.get("advisor_id") )
if s.get("phone"):
q = q.filter(Students.phone == s.get("phone") )
s1_delete = q.all()
+2 -2
View File
@@ -30,7 +30,7 @@ class Students(Base_all):
, comment='年龄'
, nullable=False)
sex = Column(Enum(SexEnum, values_callable=lambda e: [i.value for i in e])
sex = Column(Enum(SexEnum)
, comment='性别'
, nullable=False)
@@ -49,7 +49,7 @@ class Students(Base_all):
graduate_time = Column(DATETIME
, comment='毕业时间')
education = Column(Enum(EducationEnum, values_callable=lambda e: [i.value for i in e])
education = Column(Enum(EducationEnum)
, comment='学历'
, nullable=True)
+12 -10
View File
@@ -1,6 +1,6 @@
# students_schema:请求、响应的模型框架
from pydantic import BaseModel , Field , field_validator
from pydantic import BaseModel , Field
from datetime import datetime
import enum
@@ -8,6 +8,12 @@ class SexEnum(str , enum.Enum):
m = '男'
f = '女'
class EducationEnum(str , enum.Enum):
z = '专科'
ben = '本科'
s = '硕士'
bo = '博士'
# 查询
class StudentsQuery(BaseModel):
id:int | None = None
@@ -20,18 +26,14 @@ class StudentsQuery(BaseModel):
specialty:str | None = None
enrollment_time:datetime | None = None
graduate_time:datetime | None = None
education:str | None = None
education:EducationEnum | None = None
# 外键字段
class_id:int | None = None
advisor_id:int | None = None
# 额外字段
phone:str | None = None
class EducationEnum(str , enum.Enum):
z = '专科'
ben = '本科'
s = '硕士'
bo = '博士'
# 新建
class StudentsResponse(BaseModel):
@@ -47,7 +49,7 @@ class StudentsResponse(BaseModel):
education:EducationEnum
# 外键字段
class_id:int = Field(..., gt=0, description='班级ID')
advisor_id:int | None = None
advisor_id:int = Field(..., gt=0, description='班级ID')
# 额外字段
phone:str = Field(...,min_length=11,max_length=11,description='学生手机号')
@@ -63,7 +65,7 @@ class StudentsUpdate(BaseModel):
specialty:str | None = None
enrollment_time:datetime | None = None
graduate_time:datetime | None = None
education:str | None = None
education:EducationEnum | None = None
# 外键字段
class_id:int | None = None
advisor_id:int | None = None
@@ -82,7 +84,7 @@ class StudentsDelete(BaseModel):
specialty:str | None = None
enrollment_time:datetime | None = None
graduate_time:datetime | None = None
education:str | None = None
education:EducationEnum | None = None
# 外键字段
class_id:int | None = None
advisor_id:int | None = None
+14
View File
@@ -0,0 +1,14 @@
def f(f1)
def a(c):
def b(a1):
c = c + a1
return b
@a(d)
def c(a1):
return "hi"
print(c(1))