学生选课功能

学生选课功能

数据库

新增一张学生选课表

前端

学生选课页面,把 Course 页面复制粘贴一份,命名为CourseList.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.number"
placeholder="请输入课程编号"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.teacher"
placeholder="请输入任课老师"
:prefix-icon="Search"
/>
<el-button type="info" plain style="margin: 0 0 0 10px" @click="reset"
>重置</el-button
>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="课程名称" width="180" />
<el-table-column prop="number" label="课程编号" width="180" />
<el-table-column prop="description" label="课程描述" width="240" />
<el-table-column prop="periods" label="课时" width="180" />
<el-table-column prop="teacher" label="任课教师" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="selectCourse(scope.row)"
>选课</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>

<div class="card">
<el-pagination
v-model:current-page="data.pageNum"
v-model:page-size="data.pageSize"
@current-change="handleCurrentChange"
background
layout="prev, pager, next"
:total="data.total"
/>
</div>
</div>
</template>

<script setup>
import { reactive } from "vue";
import { Search } from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage, ElMessageBox } from "element-plus";

const data = reactive({
name: "",
number: "",
teacher: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
teacher: data.teacher,
},
})
.then((res) => {
if (res.code === "200") {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};

// 加载一次 获取课程数据
load();

const handleCurrentChange = () => {
// 当翻页的时候重新加载数据
load();
};

const reset = () => {
data.name = "";
data.number = "";
data.teacher = "";
load();
};

const selectCourse = (row) => {
request
.post("/studentCourse/add", {
studentId: data.user.id,
name: row.name,
number: row.number,
courseId: row.id,
})
.then((res) => {
if (res.code === "200") {
ElMessage.success("选课成功");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Manager",
component: () => import("@/views/Manager.vue"),
redirect: "/home",
children: [
{
path: "home",
name: "Home",
component: () => import("@/views/manager/Home.vue"),
meta: { requiresAuth: true },
},
{
path: "course",
name: "Course",
component: () => import("@/views/manager/Course.vue"),
meta: { requiresAuth: true },
},
{
path: "student",
name: "Student",
component: () => import("@/views/manager/Student.vue"),
meta: { requiresAuth: true },
},
{
path: "person",
name: "Person",
component: () => import("@/views/manager/Person.vue"),
meta: { requiresAuth: true },
},
{
path: "courseList",
name: "CourseList",
component: () => import("@/views/manager/CourseList.vue"),
meta: { requiresAuth: true },
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
},
{
path: "/register",
name: "Register",
component: () => import("@/views/Register.vue"),
},
],
});

router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
const user = JSON.parse(localStorage.getItem("student-user"));
if (requiresAuth && !user) {
// 如果目标路由需要认证,并且用户未登录
next("/login"); // 跳转到登录页面
} else {
next(); // 如果目标路由不需要认证,或者用户已登录,则正常导航到目标路由
}
});

export default router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<template>
<div>
<div
style="height: 60px; background-color: #eae8e8; display: flex; align-items: center; border-bottom: 1px solid #c4c2c2"
>
<div style="flex: 1">
<div style="padding-left: 20px; display: flex; align-items: center">
<img src="@/assets/imgs/logo.png" alt="" style="width: 40px" />
<div style="font-weight: bold; font-size: 24px; margin-left: 5px">
学生信息管理系统
</div>
</div>
</div>
<div
style="width: fit-content; padding-right: 10px; display: flex; align-items: center;"
>
<img
:src="
user.avatar ||
'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
"
alt=""
style="width: 40px; height: 40px; border-radius: 50%"
/>
<span style="margin-left: 5px">{{ user.name }}</span>
</div>
</div>

<div style="display: flex">
<div
style="width: 200px; border-right: 1px solid #f3eeee; min-height: calc(100vh - 60px)"
>
<el-menu
router
style="border: none"
:default-active="$route.path"
:default-openeds="['/home', '2', '3']"
>
<el-menu-item index="/home">
<el-icon>
<HomeFilled />
</el-icon>
<span>系统首页</span>
</el-menu-item>

<el-sub-menu index="2" v-if="user.role === 'ADMIN'">
<template #title>
<el-icon>
<Management />
</el-icon>
<span>学生管理</span>
</template>
<el-menu-item index="/student">
<el-icon>
<UserFilled />
</el-icon>
<span>学生信息</span>
</el-menu-item>
</el-sub-menu>

<el-sub-menu index="3">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course" v-if="user.role === 'ADMIN'">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
<el-menu-item index="/courseList" v-if="user.role === 'STUDENT'">
<el-icon>
<Document />
</el-icon>
<span>学生选课</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person" v-if="user.role === 'STUDENT'">
<el-icon>
<User />
</el-icon>
<span>个人资料</span>
</el-menu-item>
<el-menu-item index="/login" @click="logout">
<el-icon>
<SwitchButton />
</el-icon>
<span>退出系统</span>
</el-menu-item>
</el-menu>
</div>

<div style="flex: 1; width: 0; background-color: #eaeaee; padding: 10px">
<router-view />
</div>
</div>
</div>
</template>

<script setup>
import { useRoute } from "vue-router";

const $route = useRoute();
console.log($route.path);

const logout = () => {
localStorage.removeItem("student-user");
};

const user = JSON.parse(localStorage.getItem("student-user") || "{}");
</script>

<style scoped>
.el-menu-item.is-active {
background-color: #c3d7d3 !important;
}

.el-menu-item:hover {
color: #0c98d5;
}

:deep(th) {
color: #333;
}
</style>

后端

选课实体类定义

在项目目录/model下创建一个studentCourse.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from model import Base
from model.student import Student


class StudentCourse(Base):
__tablename__ = "student_course"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
number: Mapped[str] = mapped_column(String(255), nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, nullable=False)

学生选课

接着实现学生选课的 api 部分,在/api下创建一个studentCourseApi.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi import APIRouter, Depends

from api import app
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.studentCourse import StudentCourseCreate
from service.studentCourseService import StudentCourseService

student_course_router = APIRouter(prefix="/studentCourse")


@student_course_router.post("/add", response_model=ResultModel)
async def add(student_course: StudentCourseCreate, db_session: Session = Depends(get_db_session)):
StudentCourseService.add_student_course(student_course, db_session)
return Result.success()


app.include_router(student_course_router)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

app = FastAPI()

# 跨域问题
origins = ["http://localhost:5173", "http://127.0.0.1:5173"] # 替换为你的前端应用的实际地址

app.add_middleware(
CORSMiddleware,
allow_origins=origins, # 允许跨域访问的来源域名列表
allow_credentials=True, # 是否允许携带cookie
allow_methods=["*"], # 允许的方法,默认包含常见的GET、POST等,"*"表示所有方法
allow_headers=["*"], # 允许的请求头,默认包含常见的Content-Type等,"*"表示所有请求头
)

from api import adminApi, exceptionHandler, courseApi, studentApi, fileApi, studentCourseApi

/servie下创建一个studentCourseService.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, and_

from common.utils import set_attrs
from exception.customException import StudentCourseExistException
from model import Session
from model.studentCourse import StudentCourseCreate, StudentCourse


class StudentCourseService:

@staticmethod
def add_student_course(student_course: StudentCourseCreate, db_session: Session):
query = select(StudentCourse).where(
and_(StudentCourse.studentId == student_course.studentId,
StudentCourse.courseId == student_course.courseId))
exist_student_course: StudentCourse = db_session.execute(query).scalar()
if exist_student_course:
raise StudentCourseExistException("课程已选过")
new_student_course = StudentCourse()
set_attrs(new_student_course, jsonable_encoder(student_course))
db_session.add(new_student_course)
db_session.commit()
return new_student_course
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from model import Base
from model.student import Student


class StudentCourse(Base):
__tablename__ = "student_course"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
number: Mapped[str] = mapped_column(String(255), nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, nullable=False)



class StudentCourseBase(BaseModel):
name: str
number: str
studentId: int
courseId: int


class StudentCourseCreate(StudentCourseBase):
pass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding:utf-8 -*-
# Author: Zachary

class UserNotFoundException(Exception):
def __init__(self, message: str):
self.message = message


class UserExistException(Exception):
def __init__(self, message: str):
self.message = message


class PasswordNotMatchException(Exception):
def __init__(self, message: str):
self.message = message


class TokenException(Exception):
def __init__(self, message: str):
self.message = message


class CourseExistException(Exception):
def __init__(self, message: str):
self.message = message


class CourseNotExistException(Exception):
def __init__(self, message: str):
self.message = message


class FileNotFoundException(Exception):
def __init__(self, message: str):
self.message = message


class StudentCourseExistException(Exception):
def __init__(self, message: str):
self.message = message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse

from api import app
from fastapi import Request

from common.result import Result
from exception.customException import UserNotFoundException, PasswordNotMatchException, TokenException, \
CourseExistException, CourseNotExistException, UserExistException, FileNotFoundException, \
StudentCourseExistException


@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
result = Result.error(code="500", msg=str(exc))
return JSONResponse(status_code=500, content=jsonable_encoder(result))


@app.exception_handler(UserNotFoundException)
async def user_not_fount_exception_handler(request: Request, exc: UserNotFoundException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(PasswordNotMatchException)
async def password_not_fount_exception_handler(request: Request, exc: PasswordNotMatchException):
result = Result.error(code="401", msg=exc.message)
return JSONResponse(status_code=401, content=jsonable_encoder(result))


@app.exception_handler(TokenException)
async def token_exception_handler(request: Request, exc: TokenException):
result = Result.error(code="401", msg=exc.message)
return JSONResponse(status_code=401, content=jsonable_encoder(result))


@app.exception_handler(CourseExistException)
async def course_exist_exception_handler(request: Request, exc: CourseExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))


@app.exception_handler(CourseNotExistException)
async def course_not_exist_exception_handler(request: Request, exc: CourseNotExistException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(UserExistException)
async def user_exist_exception_handler(request: Request, exc: UserExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))


@app.exception_handler(FileNotFoundException)
async def file_not_found_exception_handler(request: Request, exc: FileNotFoundException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(StudentCourseExistException)
async def student_course_exist_exception_handler(request: Request, exc: StudentCourseExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))

查看数据库有无数据插入

管理员/学生查看已选课程

这个需要做到:

  • 学生只看自己的
  • 管理员看全部的

前端

学生选课页面,将CourseList.vue复制粘贴一下作为StudentCourse.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.number"
placeholder="请输入课程编号"
:prefix-icon="Search"
/>
<el-button type="info" plain style="margin: 0 0 0 10px" @click="reset"
>重置</el-button
>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="课程名称" />
<el-table-column prop="number" label="课程编号" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="del(scope.row.id)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>

<div class="card">
<el-pagination
v-model:current-page="data.pageNum"
v-model:page-size="data.pageSize"
@current-change="handleCurrentChange"
background
layout="prev, pager, next"
:total="data.total"
/>
</div>
</div>
</template>

<script setup>
import { reactive } from "vue";
import { Search } from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage, ElMessageBox } from "element-plus";

const data = reactive({
name: "",
number: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
});

const load = () => {
let params = {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
};
if (data.user.role === "STUDENT") {
params.studentId = data.user.id;
}

request
.get("/studentCourse/selectPage", {
params: params,
})
.then((res) => {
if (res.code === "200") {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};

// 加载一次 获取学生选课数据
load();

const handleCurrentChange = () => {
// 当翻页的时候重新加载数据
load();
};

const reset = () => {
data.name = "";
data.number = "";
load();
};

const del = (id) => {};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Manager",
component: () => import("@/views/Manager.vue"),
redirect: "/home",
children: [
{
path: "home",
name: "Home",
component: () => import("@/views/manager/Home.vue"),
meta: { requiresAuth: true },
},
{
path: "course",
name: "Course",
component: () => import("@/views/manager/Course.vue"),
meta: { requiresAuth: true },
},
{
path: "student",
name: "Student",
component: () => import("@/views/manager/Student.vue"),
meta: { requiresAuth: true },
},
{
path: "person",
name: "Person",
component: () => import("@/views/manager/Person.vue"),
meta: { requiresAuth: true },
},
{
path: "courseList",
name: "CourseList",
component: () => import("@/views/manager/CourseList.vue"),
meta: { requiresAuth: true },
},
{
path: "studentCourse",
name: "StudentCourse",
component: () => import("@/views/manager/StudentCourse.vue"),
meta: { requiresAuth: true },
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
},
{
path: "/register",
name: "Register",
component: () => import("@/views/Register.vue"),
},
],
});

router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
const user = JSON.parse(localStorage.getItem("student-user"));
if (requiresAuth && !user) {
// 如果目标路由需要认证,并且用户未登录
next("/login"); // 跳转到登录页面
} else {
next(); // 如果目标路由不需要认证,或者用户已登录,则正常导航到目标路由
}
});

export default router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<template>
<div>
<div
style="height: 60px; background-color: #eae8e8; display: flex; align-items: center; border-bottom: 1px solid #c4c2c2"
>
<div style="flex: 1">
<div style="padding-left: 20px; display: flex; align-items: center">
<img src="@/assets/imgs/logo.png" alt="" style="width: 40px" />
<div style="font-weight: bold; font-size: 24px; margin-left: 5px">
学生信息管理系统
</div>
</div>
</div>
<div
style="width: fit-content; padding-right: 10px; display: flex; align-items: center;"
>
<img
:src="
user.avatar ||
'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
"
alt=""
style="width: 40px; height: 40px; border-radius: 50%"
/>
<span style="margin-left: 5px">{{ user.name }}</span>
</div>
</div>

<div style="display: flex">
<div
style="width: 200px; border-right: 1px solid #f3eeee; min-height: calc(100vh - 60px)"
>
<el-menu
router
style="border: none"
:default-active="$route.path"
:default-openeds="['/home', '2', '3']"
>
<el-menu-item index="/home">
<el-icon>
<HomeFilled />
</el-icon>
<span>系统首页</span>
</el-menu-item>

<el-sub-menu index="2" v-if="user.role === 'ADMIN'">
<template #title>
<el-icon>
<Management />
</el-icon>
<span>学生管理</span>
</template>
<el-menu-item index="/student">
<el-icon>
<UserFilled />
</el-icon>
<span>学生信息</span>
</el-menu-item>
</el-sub-menu>

<el-sub-menu index="3">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course" v-if="user.role === 'ADMIN'">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
<el-menu-item index="/courseList" v-if="user.role === 'STUDENT'">
<el-icon>
<Document />
</el-icon>
<span>学生选课</span>
</el-menu-item>
<el-menu-item index="/studentCourse">
<el-icon>
<Document />
</el-icon>
<span>选课记录</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person" v-if="user.role === 'STUDENT'">
<el-icon>
<User />
</el-icon>
<span>个人资料</span>
</el-menu-item>
<el-menu-item index="/login" @click="logout">
<el-icon>
<SwitchButton />
</el-icon>
<span>退出系统</span>
</el-menu-item>
</el-menu>
</div>

<div style="flex: 1; width: 0; background-color: #eaeaee; padding: 10px">
<router-view />
</div>
</div>
</div>
</template>

<script setup>
import { useRoute } from "vue-router";

const $route = useRoute();
console.log($route.path);

const logout = () => {
localStorage.removeItem("student-user");
};

const user = JSON.parse(localStorage.getItem("student-user") || "{}");
</script>

<style scoped>
.el-menu-item.is-active {
background-color: #c3d7d3 !important;
}

.el-menu-item:hover {
color: #0c98d5;
}

:deep(th) {
color: #333;
}
</style>

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Depends, Query

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.studentCourse import StudentCourseCreate, StudentCourseSearch
from service.studentCourseService import StudentCourseService

student_course_router = APIRouter(prefix="/studentCourse")


@student_course_router.post("/add", response_model=ResultModel)
async def add(student_course: StudentCourseCreate, db_session: Session = Depends(get_db_session)):
StudentCourseService.add_student_course(student_course, db_session)
return Result.success()


@student_course_router.get("/selectPage", response_model=ResultModel)
async def select_page(page: int = Query(1, ge=1, alias="pageNum", description="Page number"),
size: int = Query(5, gt=0, le=100, alias="pageSize", description="Page size"),
name: Optional[str] = Query(None, description="Student name"),
number: Optional[str] = Query(None, description="Student number"),
studentId: Optional[str] = Query(None, description="Student id"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
student_course_search = StudentCourseSearch(name=name, number=number, studentId=studentId)
student_list = StudentCourseService.select_page(student_course_search, db_session)
return Result.success(pageInfo.of(student_list))


app.include_router(student_course_router)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, and_, asc

from common.utils import set_attrs
from exception.customException import StudentCourseExistException
from model import Session
from model.studentCourse import StudentCourseCreate, StudentCourse, StudentCourseSearch


class StudentCourseService:

@staticmethod
def add_student_course(student_course: StudentCourseCreate, db_session: Session):
query = select(StudentCourse).where(
and_(StudentCourse.studentId == student_course.studentId,
StudentCourse.courseId == student_course.courseId))
exist_student_course: StudentCourse = db_session.execute(query).scalar()
if exist_student_course:
raise StudentCourseExistException("课程已选过")
new_student_course = StudentCourse()
set_attrs(new_student_course, jsonable_encoder(student_course))
db_session.add(new_student_course)
db_session.commit()
return new_student_course

@staticmethod
def select_page(student_course_search: StudentCourseSearch, db_session: Session):
query = select(StudentCourse).order_by(asc(StudentCourse.id))
if student_course_search.name:
query = query.where(StudentCourse.name.like(f"%{student_course_search.name}%"))
if student_course_search.number:
query = query.where(StudentCourse.number.like(f"%{student_course_search.number}%"))
if student_course_search.studentId:
query = query.where(StudentCourse.studentId == student_course_search.studentId)
result = db_session.execute(query).scalars().all()
return result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base
from model.student import Student


class StudentCourse(Base):
__tablename__ = "student_course"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
number: Mapped[str] = mapped_column(String(255), nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, nullable=False)



class StudentCourseBase(BaseModel):
name: str
number: str
studentId: int
courseId: int


class StudentCourseCreate(StudentCourseBase):
pass


class StudentCourseSearch(BaseModel):
name: str | None
number: str | None
studentId: int | None

从学生用户这里来看是挺好的,但是如果,多插入几个学生选课

从管理员的角度来看是这样的,并不能直接看出是哪位学生选了这门课

修改

我们给显示选课记录的这个表格,加一列学生姓名

学生选课表就需要追加一个表的关联,去关联一下学生表获取学生姓名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel
from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from model import Base
from model.student import Student


class StudentCourse(Base):
__tablename__ = "student_course"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
number: Mapped[str] = mapped_column(String(255), nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, ForeignKey('student.id'), nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, nullable=False)

student: Mapped[Student] = relationship(lazy=False, backref="student_course")


class StudentCourseBase(BaseModel):
name: str
number: str
studentId: int
courseId: int


class StudentCourseCreate(StudentCourseBase):
pass


class StudentCourseSearch(BaseModel):
name: str | None
number: str | None
studentId: int | None
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, and_, asc

from common.utils import set_attrs
from exception.customException import StudentCourseExistException
from model import Session
from model.student import Student
from model.studentCourse import StudentCourseCreate, StudentCourse, StudentCourseSearch


class StudentCourseService:

@staticmethod
def add_student_course(student_course: StudentCourseCreate, db_session: Session):
query = select(StudentCourse).where(
and_(StudentCourse.studentId == student_course.studentId,
StudentCourse.courseId == student_course.courseId))
exist_student_course: StudentCourse = db_session.execute(query).scalar()
if exist_student_course:
raise StudentCourseExistException("课程已选过")
new_student_course = StudentCourse()
set_attrs(new_student_course, jsonable_encoder(student_course))
db_session.add(new_student_course)
db_session.commit()
return new_student_course

@staticmethod
def select_page(student_course_search: StudentCourseSearch, db_session: Session):
query = select(StudentCourse, Student).join(StudentCourse.student).order_by(asc(StudentCourse.id))
if student_course_search.name:
query = query.where(StudentCourse.name.like(f"%{student_course_search.name}%"))
if student_course_search.number:
query = query.where(StudentCourse.number.like(f"%{student_course_search.number}%"))
if student_course_search.studentId:
query = query.where(StudentCourse.studentId == student_course_search.studentId)
result = db_session.execute(query).scalars().all()
return result

然后给前端这块 数据显示加一列,同时得是管理员的时候才显示这么一列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.number"
placeholder="请输入课程编号"
:prefix-icon="Search"
/>
<el-button type="info" plain style="margin: 0 0 0 10px" @click="reset"
>重置</el-button
>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="课程名称" />
<el-table-column prop="number" label="课程编号" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="del(scope.row.id)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>

<div class="card">
<el-pagination
v-model:current-page="data.pageNum"
v-model:page-size="data.pageSize"
@current-change="handleCurrentChange"
background
layout="prev, pager, next"
:total="data.total"
/>
</div>
</div>
</template>

<script setup>
import { reactive } from "vue";
import { Search } from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage, ElMessageBox } from "element-plus";

const data = reactive({
name: "",
number: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
});

const load = () => {
let params = {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
};
if (data.user.role === "STUDENT") {
params.studentId = data.user.id;
}

request
.get("/studentCourse/selectPage", {
params: params,
})
.then((res) => {
if (res.code === "200") {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};

// 加载一次 获取学生选课数据
load();

const handleCurrentChange = () => {
// 当翻页的时候重新加载数据
load();
};

const reset = () => {
data.name = "";
data.number = "";
load();
};

const del = (id) => {};
</script>

学生选课删除

现在实现删除学生所选课程

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.number"
placeholder="请输入课程编号"
:prefix-icon="Search"
/>
<el-button type="info" plain style="margin: 0 0 0 10px" @click="reset"
>重置</el-button
>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="课程名称" />
<el-table-column prop="number" label="课程编号" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="del(scope.row.id)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>

<div class="card">
<el-pagination
v-model:current-page="data.pageNum"
v-model:page-size="data.pageSize"
@current-change="handleCurrentChange"
background
layout="prev, pager, next"
:total="data.total"
/>
</div>
</div>
</template>

<script setup>
import { reactive } from "vue";
import { Search } from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage, ElMessageBox } from "element-plus";

const data = reactive({
name: "",
number: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
});

const load = () => {
let params = {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
};
if (data.user.role === "STUDENT") {
params.studentId = data.user.id;
}

request
.get("/studentCourse/selectPage", {
params: params,
})
.then((res) => {
if (res.code === "200") {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};

// 加载一次 获取学生选课数据
load();

const handleCurrentChange = () => {
// 当翻页的时候重新加载数据
load();
};

const reset = () => {
data.name = "";
data.number = "";
load();
};

const del = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/studentCourse/delete/" + id).then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
load();
} else {
ElMessage.error(res.msg);
}
});
})
.catch((err) => {
if (err.response?.data?.code === "401") {
localStorage.removeItem("student-user");
}
ElMessage.error(err.response?.data?.msg || err.message);
});
};
</script>

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Depends, Query

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.studentCourse import StudentCourseCreate, StudentCourseSearch
from service.studentCourseService import StudentCourseService

student_course_router = APIRouter(prefix="/studentCourse")


@student_course_router.post("/add", response_model=ResultModel)
async def add(student_course: StudentCourseCreate, db_session: Session = Depends(get_db_session)):
StudentCourseService.add_student_course(student_course, db_session)
return Result.success()


@student_course_router.get("/selectPage", response_model=ResultModel)
async def select_page(page: int = Query(1, ge=1, alias="pageNum", description="Page number"),
size: int = Query(5, gt=0, le=100, alias="pageSize", description="Page size"),
name: Optional[str] = Query(None, description="Student name"),
number: Optional[str] = Query(None, description="Student number"),
studentId: Optional[str] = Query(None, description="Student id"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
student_course_search = StudentCourseSearch(name=name, number=number, studentId=studentId)
student_list = StudentCourseService.select_page(student_course_search, db_session)
return Result.success(pageInfo.of(student_list))


@student_course_router.delete("/delete/{id}", response_model=ResultModel)
async def delete(id: int, db_session: Session = Depends(get_db_session)):
StudentCourseService.delete_by_id(id, db_session)
return Result.success()


app.include_router(student_course_router)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, and_, asc

from common.utils import set_attrs
from exception.customException import StudentCourseExistException
from model import Session
from model.student import Student
from model.studentCourse import StudentCourseCreate, StudentCourse, StudentCourseSearch


class StudentCourseService:

@staticmethod
def add_student_course(student_course: StudentCourseCreate, db_session: Session):
query = select(StudentCourse).where(
and_(StudentCourse.studentId == student_course.studentId,
StudentCourse.courseId == student_course.courseId))
exist_student_course: StudentCourse = db_session.execute(query).scalar()
if exist_student_course:
raise StudentCourseExistException("课程已选过")
new_student_course = StudentCourse()
set_attrs(new_student_course, jsonable_encoder(student_course))
db_session.add(new_student_course)
db_session.commit()
return new_student_course

@staticmethod
def select_page(student_course_search: StudentCourseSearch, db_session: Session):
query = select(StudentCourse, Student).join(StudentCourse.student).order_by(asc(StudentCourse.id))
if student_course_search.name:
query = query.where(StudentCourse.name.like(f"%{student_course_search.name}%"))
if student_course_search.number:
query = query.where(StudentCourse.number.like(f"%{student_course_search.number}%"))
if student_course_search.studentId:
query = query.where(StudentCourse.studentId == student_course_search.studentId)
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def delete_by_id(id, db_session):
exist_student_course: StudentCourse = check_student_course_exist(id, db_session)
db_session.delete(exist_student_course)
db_session.commit()
return exist_student_course


def check_student_course_exist(student_course_id: int, db_session: Session):
query = select(StudentCourse).where(StudentCourse.id == student_course_id)
exist_student_course: StudentCourse = db_session.execute(query).scalar()
if not exist_student_course:
raise StudentCourseNotExistException("选课记录不存在")
return exist_student_course

这种写法很冗余,大家自己想想可以怎么优化,我就不改了,或者后面有空改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# -*- coding:utf-8 -*-
# Author: Zachary

class UserNotFoundException(Exception):
def __init__(self, message: str):
self.message = message


class UserExistException(Exception):
def __init__(self, message: str):
self.message = message


class PasswordNotMatchException(Exception):
def __init__(self, message: str):
self.message = message


class TokenException(Exception):
def __init__(self, message: str):
self.message = message


class CourseExistException(Exception):
def __init__(self, message: str):
self.message = message


class CourseNotExistException(Exception):
def __init__(self, message: str):
self.message = message


class FileNotFoundException(Exception):
def __init__(self, message: str):
self.message = message


class StudentCourseExistException(Exception):
def __init__(self, message: str):
self.message = message


class StudentCourseNotExistException(Exception):
def __init__(self, message: str):
self.message = message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse

from api import app
from fastapi import Request

from common.result import Result
from exception.customException import UserNotFoundException, PasswordNotMatchException, TokenException, \
CourseExistException, CourseNotExistException, UserExistException, FileNotFoundException, \
StudentCourseExistException, StudentCourseNotExistException


@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
result = Result.error(code="500", msg=str(exc))
return JSONResponse(status_code=500, content=jsonable_encoder(result))


@app.exception_handler(UserNotFoundException)
async def user_not_fount_exception_handler(request: Request, exc: UserNotFoundException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(PasswordNotMatchException)
async def password_not_fount_exception_handler(request: Request, exc: PasswordNotMatchException):
result = Result.error(code="401", msg=exc.message)
return JSONResponse(status_code=401, content=jsonable_encoder(result))


@app.exception_handler(TokenException)
async def token_exception_handler(request: Request, exc: TokenException):
result = Result.error(code="401", msg=exc.message)
return JSONResponse(status_code=401, content=jsonable_encoder(result))


@app.exception_handler(CourseExistException)
async def course_exist_exception_handler(request: Request, exc: CourseExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))


@app.exception_handler(CourseNotExistException)
async def course_not_exist_exception_handler(request: Request, exc: CourseNotExistException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(UserExistException)
async def user_exist_exception_handler(request: Request, exc: UserExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))


@app.exception_handler(FileNotFoundException)
async def file_not_found_exception_handler(request: Request, exc: FileNotFoundException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))


@app.exception_handler(StudentCourseExistException)
async def student_course_exist_exception_handler(request: Request, exc: StudentCourseExistException):
result = Result.error(code="400", msg=exc.message)
return JSONResponse(status_code=400, content=jsonable_encoder(result))


@app.exception_handler(StudentCourseNotExistException)
async def student_course_not_exist_exception_handler(request: Request, exc: StudentCourseNotExistException):
result = Result.error(code="404", msg=exc.message)
return JSONResponse(status_code=404, content=jsonable_encoder(result))

测试

管理员删除

学生自己删除


更新: 2024-05-26 18:51:54
原文: https://www.yuque.com/zacharyblock/iacda/hyi625azhg5dant9