FastAPI 实战汇总版

学生信息管理系统——前后端分离

技术栈

后端:FastAPI + SQLAlchemy

前端:Vue3 + Element-plus

数据库:MySQL

所需工具

Pycharm、WebStorm、Postman

项目功能

管理员

  • 登录
  • 学生管理
  • 课程管理
  • 选课管理
  • 成绩管理

学生

  • 登录、注册
  • 查看、修改个人信息
  • 查看课程、选课
  • 查看成绩
  • 查看、修改评价

环境介绍

开发环境版本:

Python3.10.11、Node.js16.9.0、Vue3、MySQL5.7.31

官方文档

Node.js:https://nodejs.org/en

Vue:https://cn.vuejs.org/

Vite:https://cn.vitejs.dev/guide/

Element-Plus:https://element-plus.org/zh-CN/

FastAPI:https://fastapi.tiangolo.com/zh/

SQLAlchemy:https://docs.sqlalchemy.org/en/20/dialects/mysql.html#module-sqlalchemy.dialects.mysql.mysqldb

字节 Icon 库:https://iconpark.oceanengine.com/official

图片素材库:https://iconscout.com/

项目初始化

前端 Vue

安装 Node.js

进入官网 https://nodejs.org/download/release/v16.19.0/安装好 node.js

安装 vue-cli

然后使用npm install -g @vue/cli命令安装 vue-cli

创建 vue 项目

使用 vite 进行构建

通过npm create vite@latest studentfontend -- --template vue构建前端项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  ~ cd Documents/PythonCode
➜ PythonCode mkdir studentProject
➜ PythonCode cd studentProject
➜ studentProject npm create vite@latest studentfontend -- --template vue

Scaffolding project in /Users/zachary/Documents/PythonCode/studentProject/studentfontend...

Done. Now run:

cd studentfontend
npm install
npm run dev

➜ studentProject cd studentfontend
➜ studentfontend ls
README.md package.json src
index.html public vite.config.js

npm 源

如果 npm 命令运行的时候很卡的话,尝试换源

npm config set registry [https://registry.npmmirror.com](https://registry.npmmirror.com)

打开 vue 项目

使用 WebStorm 打开创建好的前端项目

先什么都不做,尝试把项目运行起来

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
➜  studentfontend npm install
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: '@vitejs/plugin-vue@5.0.4',
npm WARN EBADENGINE required: { node: '^18.0.0 || >=20.0.0' },
npm WARN EBADENGINE current: { node: 'v16.19.0', npm: '8.19.3' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'rollup@4.12.1',
npm WARN EBADENGINE required: { node: '>=18.0.0', npm: '>=8.0.0' },
npm WARN EBADENGINE current: { node: 'v16.19.0', npm: '8.19.3' }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'vite@5.1.5',
npm WARN EBADENGINE required: { node: '^18.0.0 || >=20.0.0' },
npm WARN EBADENGINE current: { node: 'v16.19.0', npm: '8.19.3' }
npm WARN EBADENGINE }

up to date in 649ms
➜ studentfontend npm run dev

> studentfontend@0.0.0 dev
> vite


VITE v5.1.5 ready in 814 ms

➜ Local: http://127.0.0.1:5173/
➜ Network: use --host to expose
➜ press h + enter to show help

项目整改

现在我们把这个项目做好看些,为了学生信息管理系统做准备的,

需要把路由写好,同时修改主题色,还有 request http 的请求封装,选择一个自己喜欢的页面标题及 icon

index.html

修改项目下的 index.html 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>学生信息管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

css&imgs

删除/assets下的vue.svg

/src/assets路径下创建一个cssimgs目录

在创建好的/css目录下分别创建global.cssindex.scss

全局 css 样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
* {
box-sizing: border-box;
}

body {
margin: 0;
padding: 0;
color: #252424;
}

a {
text-decoration: none;
}

.card {
background-color: rgb(255, 255, 255);
border-radius: 5px;
padding: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

主题色配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: (
"primary": (
"base": #0a7fce
),
"success": (
"base": #08b41f
),
"warning": (
"base": #e8af56
),
"danger": (
"base": #ef3030
),
"info": (
"base": #5d66ea
)
)
);

路由设置

/src路径下创建一个router目录,在其中创建一个index.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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"),
},
],
},
],
});

export default router;

request 请求

/src路径下创建一个utils目录,在其中创建一个request.js文件

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
import { ElMessage } from "element-plus";
import router from "../router";
import axios from "axios";

const request = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 30000, // 后台接口超时时间设置
});

// request 拦截器
// 可以自请求发送前对请求做一些处理
request.interceptors.request.use(
(config) => {
config.headers["Content-Type"] = "application/json;charset=utf-8";
return config;
},
(error) => {
return Promise.reject(error);
}
);

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
(response) => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === "blob") {
return res;
}
// 兼容服务端返回的字符串数据
if (typeof res === "string") {
res = res ? JSON.parse(res) : res;
}
// 当权限验证不通过的时候给出提示
if (res.code === "401") {
ElMessage.error(res.msg);
router.push("/login");
}
return res;
},
(error) => {
console.log("err" + error);
return Promise.reject(error);
}
);

export default request;

views

/src路径下的/components修改为/views目录,将其中的HelloWorld.vue修改成Manager.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
<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="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
alt=""
style="width: 40px; height: 40px"
/>
<span style="margin-left: 5px">管理员</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']"
>
<el-menu-item index="/home">
<el-icon><HomeFilled /></el-icon>
<span>系统首页</span>
</el-menu-item>
<el-menu-item index="/person">
<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");
};
</script>

<style scoped>
.el-menu-item.is-active {
background-color: #c3d7d3 !important;
}
.el-menu-item:hover {
color: #0c98d5;
}
:deep(th) {
color: #333;
}
</style>

接着在/src/views路径下创建一个manager目录,在其中创建一个Home.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
<template>
<div class="welcome-container">
<div class="custom-card bg-white shadow rounded p-4 mb-5">
<h2 class="text-center text-primary">欢迎来到本系统</h2>
<p class="text-center mt-3">
您好 <span :style="{ color: '#116ca9' }">{{ user.username }}</span
>,祝您使用愉快!
</p>
</div>
</div>
</template>

<script setup>
import request from "@/utils/request";

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

<style scoped>
.welcome-container {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 100px); /* 根据实际项目需要调整高度 */
}

.custom-card {
max-width: 400px;
border: none;
}
</style>

App.vue

修改项目中的/src/App.vue

1
2
3
<template>
<RouterView />
</template>

main.js

修改项目中的/src/main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import ElementPlus from "element-plus";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";

import "@/assets/css/global.css";

const app = createApp(App);

app.use(router);
app.use(ElementPlus, {
locale: zhCn,
});
app.mount("#app");

for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}

.env

在项目路径下创建两个文件.env.development.env.production

1
VITE_BASE_URL='http://localhost:9090'
1
VITE_BASE_URL='http://:9090'

json 配置

修改项目路径下的文件:package.json

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
{
"name": "studentfontend",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.2",
"element-plus": "^2.4.2",
"sass": "^1.69.5",
"unplugin-element-plus": "^0.8.0",
"vue": "^3.3.4",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.4.11"
}
}

vite.config.js

修改项目路径下的文件:vite.config.js

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
import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";

import ElementPlus from "unplugin-element-plus/vite";

export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver({ importStyle: "sass" })],
}),
Components({
resolvers: [ElementPlusResolver({ importStyle: "sass" })],
}),

ElementPlus({
useSource: true,
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/assets/css/index.scss" as *;
`,
},
},
},
});

修改一下 logo 和标题的 icon

分别放在/public/favicon.ico/src/assets/imgs/logo.png路径下

删除/public/vite.svg

可以去这个网站下载https://iconscout.com/icons

https://iconscout.com/free-icon/library-2642818,将该图片保存为/src/assets/imgs/logo.png

或者https://iconscout.com/free-icon/student-79

https://iconscout.com/free-icon/student-reading-2909468,保存为/public/favicon.ico

运行

命令运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  studentfontend npm install

added 93 packages, removed 1 package, and changed 5 packages in 16s
➜ studentfontend npm run dev

> studentfontend@0.0.0 dev
> vite


VITE v4.5.2 ready in 1169 ms

➜ Local: http://127.0.0.1:5173/
➜ Network: use --host to expose
➜ press h to show help

一键运行

通过配置一个启动项,便捷运行项目


后端 FastAPI

创建空项目

新建一个项目进入 FastAPI 的世界

先创建项目文件夹

1
2
3
4
5
6
7
8
➜  studentProject ll
total 0
drwxr-xr-x@ 16 zachary staff 512B 3 10 20:25 studentfontend
➜ studentProject mkdir studentbackend
➜ studentProject ll
total 0
drwxr-xr-x@ 2 zachary staff 64B 3 10 20:41 studentbackend
drwxr-xr-x@ 16 zachary staff 512B 3 10 20:25 studentfontend

然后使用 Pycharm 创建一个空项目

依赖包安装

需要安装一个 FastAPI 的依赖包

命令安装

pip install fastapi[all]

requirements.txt 安装

1
fastapi[all]

实现一个 api

创建

项目目录下创建一个main.py

1
2
3
4
5
6
7
8
9
10
11
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def hello():
return {"message": "Hello World"}

运行

需要使用 fastapi 提供的一个uvicornASGI 网关服务器来启动 api 服务

命令运行

uvicorn main:app --reload

一键运行
  • 配置 main 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf-8 -*-
# Author: Zachary
import uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def hello():
return {"message": "Hello World"}


if __name__ == "__main__":
uvicorn.run("main:app", reload=True)

  • 启动配置项

以上两种方式均可以实现,具体看个人习惯

项目整改

.env

这个文件用于存放环境变量,包括项目的运行 ip、端口号等,后面的数据库环境变量也在这里存放

1
2
HOST = "localhost"
PORT = "9090"

common

在项目路径下创建一个/common的 package,用于实现公共类或者公共方法

环境变量配置文件

/common包下创建一个config.py文件,实现对环境变量的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
import os
from pathlib import Path
from dotenv import load_dotenv


class Config:
def __init__(self):
dotenv_path = Path(__file__).parent.parent / ".env"
load_dotenv(dotenv_path=dotenv_path)
self._env = dict(os.environ)

@property
def env(self):
return self._env


config = Config()

/common包下创建一个constant.py文件,用于配置常量

1
2
3
4
5
6
7
# -*- coding:utf-8 -*-
# Author: Zachary

from common.config import config

HOST = config.env.get("HOST")
PORT = config.env.get("PORT")
返回类 Result

实现一下 api 的通用返回类,在/common包下创建一个result.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
27
28
29
30
31
32
33
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel


class ResultBase:
code: str
msg: str
data: dict


class ResultModel(BaseModel, ResultBase):
pass


class Result(ResultBase):

def __init__(self, code, msg, data):
self.code = code
self.msg = msg
self.data = data

@classmethod
def success(cls, data: object = None, code: str = "200", msg: str = "success"):
if not data:
data = {}
return cls(code, msg, data)

@classmethod
def error(cls, data: object = None, code: str = "500", msg: str = "error"):
if not data:
data = {}
return cls(code, msg, data)

exception

创建一个/exception的 package 用于自定义异常

api

创建一个/api的 package,用于创建后端的 API,实际就是 Controller 层

init.py

用于创建 FastAPI 应用初始化

1
2
3
4
5
6
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI

app = FastAPI()
adminApi.py

创建一个 hello fastapi 接口测试一下项目

1
2
3
4
5
6
7
8
9
10
# -*- coding:utf-8 -*-
# Author: Zachary

from api import app
from common.result import Result


@app.get("/")
async def hello():
return Result.success()

然后新增了一个 api 文件之后需要给 api 的init.py 说明一下新增了一个 api 文件

1
2
3
4
5
6
7
8
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI

app = FastAPI()

from api import adminApi

service

创建一个/service的 package 用于实现 Service 层的业务代码

model

创建一个/model的 package 用于实现数据库的映射类

main

用于启动 FastAPI 的主入口程序

1
2
3
4
5
6
7
8
9
10
# -*- coding:utf-8 -*-
# Author: Zachary

from api import app
import uvicorn

from common.constant import HOST, PORT

if __name__ == '__main__':
uvicorn.run("main:app", host=HOST, port=int(PORT), reload=True)

运行

直接运行 main.py 文件即可

出现这个就是访问成功啦

至此,我们的前端后端项目就都初始化好了


管理员登录

前端

Login

首先绘制一个登录页面

参考 element-plus 官网提供的表单https://element-plus.org/zh-CN/component/form.html

在项目路径/src/views下创建一个Login.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
<template>
<div>
<div class="login-container">
<div style="width: 420px" class="login-box">
<div class="title">学生信息管理系统 - 登录</div>
<el-form :model="data.form">
<el-form-item>
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Avatar"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item>
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%;font-size: 18px"
>登 录</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:35px;text-align: right;font-size: 15px">
还没有账号?请<a href="/register">注册</a>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { reactive } from "vue";

const data = reactive({
form: {},
});
</script>

<style scoped>
.login-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.login-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</style>

背景图片

添加一张登录背景图片到/src/assets/imags/login_background.png

https://iconscout.com/free-illustration/children-are-doing-chemical-experiments-10946611

路由添加

/src/router/index.js中添加一下上面这个Login的路由

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
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"),
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
},
],
});

export default router;

通过http://localhost:5173/login可以访问到登录页面

表单校验和登录

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
<template>
<div>
<div class="login-container">
<div style="width: 420px" class="login-box">
<div class="title">学生信息管理系统 - 登录</div>
<el-form :model="data.form" ref="formRef" :rules="rules">
<el-form-item prop="username">
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Avatar"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
show-password
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width: 100%;font-size: 18px"
@click="login"
>登 录</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:35px;text-align: right;font-size: 15px">
还没有账号?请<a href="/register">注册</a>
</div>
</div>
</div>
</div>
</template>

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

const data = reactive({
form: {},
});

const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const login = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.post("/login", data.form)
.then((res) => {
if (res.code === "200") {
localStorage.setItem("student-user", JSON.stringify(res.data));
ElMessage.success("登录成功");
router.push("/home");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
ElMessage.error(err.response?.data?.msg || err.message);
});
}
});
};
</script>

<style scoped>
.login-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.login-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</style>

后端

数据库

IDE 连接 MySQL

创建库

首先在数据库中创建一个 student_info 库

创建 admin 表

插入一条数据

数据库配置

查看 sqlalchemy 官方文档https://docs.sqlalchemy.org/en/20/dialects/mysql.html#module-sqlalchemy.dialects.mysql.mysqldb

requirements.txt

通过 sqlalchemy 连接数据库需要增加两个驱动

1
2
3
fastapi[all]
mysqlclient==2.1.1
SQLAlchemy==2.0.23

.env

在.env 文件下添加数据库的配置信息

1
2
3
4
5
6
7
8
9
HOST = "localhost"
PORT = "9090"

MYSQL_DIALECT = "mysql+mysqldb"
MYSQL_HOST = "localhost"
MYSQL_PORT = "3306"
MYSQL_USER = "root"
MYSQL_PASSWORD = "980226"
MYSQL_DATABASE = "student_info"

constant.py

添加数据库配置常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding:utf-8 -*-
# Author: Zachary

from common.config import config

HOST = config.env.get("HOST")
PORT = config.env.get("PORT")

MYSQL_DIALECT = config.env.get("MYSQL_DIALECT")
MYSQL_HOST = config.env.get("MYSQL_HOST")
MYSQL_PORT = config.env.get("MYSQL_PORT")
MYSQL_USER = config.env.get("MYSQL_USER")
MYSQL_PASSWORD = config.env.get("MYSQL_PASSWORD")
MYSQL_DATABASE = config.env.get("MYSQL_DATABASE")

model/init.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

from common.constant import *


class Base(DeclarativeBase):
pass


# mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
engine = create_engine(
f"{MYSQL_DIALECT}://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4",
echo=True)

Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

管理员实体类定义

/model下创建一个admin.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- 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


class Admin(Base):
__tablename__ = "admin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)


class AdminModel(BaseModel):
username: str
password: str

管理员登录的 api 接口

为了实现每一个 api 都能拿到一个连接数据库的 session,在/model/__init__.py中实现一个 session 的获取方法

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
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

from common.constant import *


class Base(DeclarativeBase):
pass


# mysql+mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
engine = create_engine(
f"{MYSQL_DIALECT}://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4",
echo=True)

Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)


def get_db_session():
session = Session()
try:
yield session
finally:
session.close()

adminApi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi import Body, Depends
from fastapi.encoders import jsonable_encoder

from api import app
from common.result import Result, ResultModel
from model import Session, get_db_session
from model.admin import AdminModel
from service.adminService import AdminService


@app.post("/login", response_model=ResultModel)
async def login(admin: AdminModel = Body(...), db_session: Session = Depends(get_db_session)):
dbadmin = AdminService.login(admin, db_session)
return Result.success(jsonable_encoder(dbadmin))

adminService

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

from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.admin import Admin, AdminModel


class AdminService:
@staticmethod
def login(admin: AdminModel, db_session: Session) -> Admin:
query = select(Admin).where(Admin.username == admin.username)
result = db_session.execute(query).scalars().first()
if not result:
raise UserNotFoundException("用户不存在")
if result.password != admin.password:
raise PasswordNotMatchException("身份验证未通过")
return result

exception

创建自定义异常的exception的 package

然后创建一个customException.py

1
2
3
4
5
6
7
8
9
10
11
# -*- coding:utf-8 -*-
# Author: Zachary

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


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

exceptionHandler

/api包下面创建一个exceptionHandler.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
27
28
# -*- 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


@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))

/api/init.py

1
2
3
4
5
6
7
8
# -*- coding:utf-8 -*-
# Author: Zachary

from fastapi import FastAPI

app = FastAPI()

from api import adminApi, exceptionHandler

测试

postman 测试后端接口

正确登录:

密码错误:

账号错误:

前后端测试

跨域问题

发生了跨域 CORS 的问题

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

正确账号密码:

错误账号:

错误密码:

数据库密码

为了安全,数据库中的 password 不应该以明文显示,需要做个加密

这里需要使用到一个库 bcrypt

1
2
3
4
fastapi[all]
mysqlclient==2.1.1
SQLAlchemy==2.0.23
bcrypt==4.1.1

密码加密

通过hashed_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) 依据生成的盐值,生成 hash 后的密码值,结果是字节码

1
2
3
4
5
6
7
8
9
10
(venv) ➜  studentbackend python3
Python 3.10.11 (v3.10.11:7d4cc5aa85, Apr 4 2023, 19:05:19) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import bcrypt
>>> password = "admin"
>>> hashed_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
>>> print(hashed_pw)
b'$2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum'
>>> print(hashed_pw.decode('utf-8'))
$2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum

将密码对应的加密密码 $2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum 替换到数据库中的 admin 账号中(这一步操作其实应该是,在注册的时候将这个密码值写入数据库 de~)

密码验证

通过bcrypt.check(check_pw.encode(), hashed_pw)检验密码是否匹配

给 Admin 实体类定义一个密码检查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding:utf-8 -*-
# Author: Zachary
import bcrypt
from pydantic import BaseModel
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Admin(Base):
__tablename__ = "admin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)

def password_check(self, password):
return bcrypt.checkpw(password.encode(), self.password.encode())


class AdminModel(BaseModel):
username: str
password: str
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import select

from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.admin import Admin, AdminModel


class AdminService:
@staticmethod
def login(admin: AdminModel, db_session: Session) -> Admin:
query = select(Admin).where(Admin.username == admin.username)
result = db_session.execute(query).scalars().first()
if not result:
raise UserNotFoundException("用户不存在")
if result.password_check(admin.password) is False:
raise PasswordNotMatchException("身份验证未通过")
return result

前后端测试

密码错误:

密码正确:


JWT 权限校验

在退出登录之后,依旧可以进入到主页面;同样后续其他 api 接口也需要在登录之后才能调用,需要加上 token,使用 JWT 实现。

前端

首先从前端这块处理,除了 login 和后续会增加的 register 页面不需要登录才能访问,其他页面,都需要登录用户之后才能访问,否则自动跳转回 login 页面

请求头

给请求头带上 token,提供给后端进行验证

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
import { ElMessage } from "element-plus";
import router from "../router";
import axios from "axios";

const request = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 30000, // 后台接口超时时间设置
});

// request 拦截器
// 可以自请求发送前对请求做一些处理
request.interceptors.request.use(
(config) => {
const user = JSON.parse(localStorage.getItem("student-user"));
config.headers["Content-Type"] = "application/json;charset=utf-8";
if (user) {
config.headers.Authorization = "Bearer ${user.token}";
}

return config;
},
(error) => {
return Promise.reject(error);
}
);

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
(response) => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === "blob") {
return res;
}
// 兼容服务端返回的字符串数据
if (typeof res === "string") {
res = res ? JSON.parse(res) : res;
}
// 当权限验证不通过的时候给出提示
if (res.code === "401") {
ElMessage.error(res.msg);
router.push("/login");
}
return res;
},
(error) => {
console.log("err" + error);
return Promise.reject(error);
}
);

export default request;

路由

实现在没有 token 的情况下无法进入主页而跳转到 login 页面,在路由守卫中进行判断和处理

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
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: '/login',
name: 'Login',
component: () => import('@/views/Login.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

测试

不登录,无法进入 home 页面

登录后,才可以进入 home 页面

后端

后端这块需要实现一下 jwt,包括密码的校验和生成,以及 token 的生成和校验;那么之前的密码校验就可以舍去了,写一个统一的 auth 验证

安装依赖

这里需要两个依赖:pyjwtpasslib[bcrypt]

1
2
3
4
5
fastapi[all]
mysqlclient==2.1.1
SQLAlchemy==2.0.23
pyjwt
passlib[bcrypt]

AuthHandler

密码生成与校验

接下先来编写 密码的生成和校验,在项目目录的/common下创建一个auth.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.security import HTTPBearer
from passlib.context import CryptContext


class AuthHandler:
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
secret = "SECRET"

def get_password_hash(self, password):
return self.pwd_context.hash(password)

def verify_password(self, plain_password, hashed_password):
return self.pwd_context.verify(plain_password, hashed_password)

if __name__ == '__main__':
auth = AuthHandler()
print(auth.get_password_hash("admin"))
print(auth.verify_password("admin", '$2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum'))
print(auth.verify_password("123", '$2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum'))

其中的$2b$12$ttbSmj8jD5rW/c0XdkBZyOZI5F7GP.kMuBMWgE2.yyzreJCWdwAum是数据库中存储的 admin 的密码

运行一下

可以看到,之前使用 bcrypt 直接生成的 hash 密码,虽然与现在生成的不一样,但是依旧可以用于验证

修改一下 adminService 的验证逻辑,尝试一下登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.security import HTTPBearer
from passlib.context import CryptContext


class AuthHandler:
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
secret = "SECRET"

def get_password_hash(self, password):
return self.pwd_context.hash(password)

def verify_password(self, plain_password, hashed_password):
return self.pwd_context.verify(plain_password, hashed_password)


auth_handler = AuthHandler()
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 sqlalchemy import select

from common.auth import auth_handler
from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.admin import Admin, AdminModel


class AdminService:
@staticmethod
def login(admin: AdminModel, db_session: Session) -> Admin:
query = select(Admin).where(Admin.username == admin.username)
result = db_session.execute(query).scalars().first()
if not result:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(admin.password, result.password):
raise PasswordNotMatchException("身份验证未通过")
return result

token 的生成与校验

通过装饰器实现一个,登录验证

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
# -*- coding:utf-8 -*-
# Author: Zachary
from datetime import datetime, timedelta

import jwt
from fastapi import Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from passlib.context import CryptContext

from exception.customException import TokenException


class AuthHandler:
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
secret = "SECRET"

def get_password_hash(self, password):
return self.pwd_context.hash(password)

def verify_password(self, plain_password, hashed_password):
return self.pwd_context.verify(plain_password, hashed_password)

def encode_token(self, user_id):
payload = {
'exp': datetime.utcnow() + timedelta(days=7, minutes=0, seconds=0),
'iat': datetime.utcnow(),
'sub': user_id
}
return jwt.encode(
payload,
self.secret,
algorithm='HS256'
)

def decode_token(self, token):
try:
payload = jwt.decode(token, self.secret, algorithms=['HS256'])
return payload['sub']
except jwt.ExpiredSignatureError:
raise TokenException("token过期")
except jwt.InvalidTokenError:
raise TokenException("无效token")

def auth_required(self, auth: HTTPAuthorizationCredentials = Security(security)):
return self.decode_token(auth.credentials)


auth_handler = AuthHandler()

customException.py添加一个自定义异常

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

exceptionHandler.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
27
28
29
30
31
32
33
34
35
# -*- 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


@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))

这个先写到这,后面新增加 api 的时候进行一个验证


课程管理

接下来开发一个课程管理功能,用于管理和显示学生课程的

数据库

先设计一下课程数据表,命名为 course,字段(课程名称、课程编号、课程描述、课时、任课老师)

课程页面

在前端的项目目录下的/manage下创建一个Course.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
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-button type="primary" style="margin: 0 0 0 10px" plain
>查询</el-button
>
<el-button type="info" plain>重置</el-button>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-button type="primary" style="margin-bottom: 5px" plain
>新增</el-button
>
</div>
<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>编辑</el-button>
<el-button type="danger" size="small" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>

<div class="card">
<el-pagination background layout="prev, pager, next" :total="1000" />
</div>
</div>
</template>

<script setup>
import { reactive } from "vue";
import { Search } from "@element-plus/icons-vue";

const data = reactive({
name: "",
tableData: [
{
id: 1,
name: "大学英语",
number: "001",
description: "大学英语不想学-6学分",
periods: "36课时",
teacher: "张三",
},
{
id: 2,
name: "高等数学",
number: "002",
description: "高等数学好难学-4学分",
periods: "24课时",
teacher: "李四",
},
{
id: 3,
name: "必修物理",
number: "003",
description: "必修物理有难度-2学分",
periods: "24课时",
teacher: "王五",
},
{
id: 4,
name: "思想政治",
number: "004",
description: "思想政治必修课-4学分",
periods: "18课时",
teacher: "赵六",
},
{
id: 5,
name: "微机原理",
number: "005",
description: "微机原理很基础-3学分",
periods: "24课时",
teacher: "钱七",
},
{
id: 6,
name: "通信原理",
number: "006",
description: "通信原理很难懂-4学分",
periods: "24课时",
teacher: "孙八",
},
{
id: 7,
name: "离散数学",
number: "007",
description: "离散数学很离散-2学分",
periods: "18课时",
teacher: "周九",
},
{
id: 8,
name: "工程制图",
number: "008",
description: "工程制图好有趣-3学分",
periods: "24课时",
teacher: "吴十",
},
],
});
</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
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: "/login",
name: "Login",
component: () => import("@/views/Login.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
<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="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
alt=""
style="width: 40px; height: 40px"
/>
<span style="margin-left: 5px">管理员</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']"
>
<el-menu-item index="/home">
<el-icon>
<HomeFilled />
</el-icon>
<span>系统首页</span>
</el-menu-item>

<el-sub-menu index="2">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person">
<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");
};
</script>

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

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

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

课程查询 api

Course 实体类定义

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

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

from model import Base


class Course(Base):
__tablename__ = "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)
description: Mapped[str] = mapped_column(String(255), nullable=False)
periods: Mapped[str] = mapped_column(String(255), nullable=False)
teacher: Mapped[str] = mapped_column(String(255), nullable=False)

课程管理 Api 接口

courseApi

在项目目录/api下创建一个courseApi.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
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi import APIRouter, Query, Depends

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from service.courseService import CourseService

course_router = APIRouter(prefix="/course")


@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"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
course_list = CourseService.select_page(db_session)
result = Result.success(pageInfo.of(course_list))
return result


app.include_router(course_router)

需要在/api/__init__.py下增加 courseApi

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

courseService

在项目目录/service下创建一个courseService.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import desc, select

from model import Session
from model.course import Course


class CourseService:

@staticmethod
def select_page(db_session: Session):
query = select(Course).order_by(desc(Course.id))
result = db_session.execute(query).scalars().all()
return result

分页插件

在项目路径的/common下创建一个pageHelper.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder


class Page:
list: list
total: int
pageNum: int
pageSize: int

def __init__(self, list: list, total: int, pageNum: int, pageSize: int):
self.list = list
self.total = total
self.pageNum = pageNum
self.pageSize = pageSize


class PageHelper:
page: int
size: int
limit: int
offset: int

def __init__(self, page: int, size: int, limit: int, offset: int):
self.page = page
self.size = size
self.limit = limit
self.offset = offset

@classmethod
def startPage(cls, page: int, size: int):
limit = size
offset = size * (page - 1)
return cls(page, size, limit, offset)

def of(self, data):
data_list = [jsonable_encoder(dataitem) for dataitem in data[self.offset:self.offset + self.limit]]
data_total = len(data)
page = Page(data_list, data_total, self.page, self.size)
return jsonable_encoder(page)

测试

可以发现,请求是成功的,但就是没数据,因为数据库里面是空的,给数据库中插入数据

再次使用 postman 测试一下

增删查改实现

数据分页

前端这块需要调用分页查询的请求接口,并将结果显示出来

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
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-button type="primary" style="margin: 0 0 0 10px" plain
>查询</el-button
>
<el-button type="info" plain>重置</el-button>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-button type="primary" style="margin-bottom: 5px" plain
>新增</el-button
>
</div>
<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>编辑</el-button>
<el-button type="danger" size="small" plain>删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>

<div class="card">
<el-pagination
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";

const data = reactive({
name: "",
tableData: [],
total: 0,
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: 1,
pageSize: 5,
},
})
.then((res) => {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
});
};

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

虽然可以显示一部分数据,但是这个换页有问题,不显示后续跳转的页面按钮

需要绑定一下这两个值

同时还需要,在换了页面之后也重新刷新一下数据,触发 current-change

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
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
style="width: 260px"
v-model="data.name"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-button type="primary" style="margin: 0 0 0 10px" plain
>查询</el-button
>
<el-button type="info" plain>重置</el-button>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-button type="primary" style="margin-bottom: 5px" plain
>新增</el-button
>
</div>
<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>编辑</el-button>
<el-button type="danger" size="small" plain>删除</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";

const data = reactive({
name: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
},
})
.then((res) => {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
});
};

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

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

分页成功实现了

查询和重置

为了实现依据课程名称实现筛查的功能,需要传递一个name字段传给后端进行数据库层面的模糊查询

重置按钮要将查询的内容清空,同时显示回完整的数据

前端

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
<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-button type="info" plain style="margin: 0 0 0 10px" @click="reset"
>重置</el-button
>
</div>

<div class="card" style="margin-bottom: 10px">
<div>
<el-button type="primary" style="margin-bottom: 5px" plain
>新增</el-button
>
</div>
<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>编辑</el-button>
<el-button type="danger" size="small" plain>删除</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";

const data = reactive({
name: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
},
})
.then((res) => {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
});
};

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

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

const reset = () => {
data.name = "";
load();
};
</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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Query, Depends

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from service.courseService import CourseService

course_router = APIRouter(prefix="/course")


@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="Course name"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
course_list = CourseService.select_page(name, db_session)
result = Result.success(pageInfo.of(course_list))
return result


app.include_router(course_router)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import desc, select

from model import Session
from model.course import Course


class CourseService:

@staticmethod
def select_page(name, db_session: Session):
query = select(Course).order_by(desc(Course.id))
if name:
query = query.where(Course.name.like(f"%{name}%"))
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
<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-button type="primary" style="margin-bottom: 5px" plain
>新增</el-button
>
</div>
<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>编辑</el-button>
<el-button type="danger" size="small" plain>删除</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";

const data = reactive({
name: "",
number: "",
teacher: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
teacher: data.teacher,
},
})
.then((res) => {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
});
};

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

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

const reset = () => {
data.name = "";
data.number = "";
data.teacher = "";
load();
};
</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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Query, Depends

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.course import Course, CourseSearch
from service.courseService import CourseService

course_router = APIRouter(prefix="/course")


@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="Course name"),
number: Optional[str] = Query(None, description="Course number"),
teacher: Optional[str] = Query(None, description="Course teacher"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
course_search = CourseSearch(name=name, number=number, teacher=teacher)
course_list = CourseService.select_page(course_search, db_session)
result = Result.success(pageInfo.of(course_list))
return result


app.include_router(course_router)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import desc, select

from model import Session
from model.course import Course, CourseSearch


class CourseService:

@staticmethod
def select_page(course_search: CourseSearch, db_session: Session):
query = select(Course).order_by(desc(Course.id))
if course_search.name:
query = query.where(Course.name.like(f"%{course_search.name}%"))
if course_search.number:
query = query.where(Course.number.like(f"%{course_search.number}%"))
if course_search.teacher:
query = query.where(Course.teacher.like(f"%{course_search.teacher}%"))
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
# -*- 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


class Course(Base):
__tablename__ = "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)
description: Mapped[str] = mapped_column(String(255), nullable=False)
periods: Mapped[str] = mapped_column(String(255), nullable=False)
teacher: Mapped[str] = mapped_column(String(255), nullable=False)


class CourseSearch(BaseModel):
name: str | None
number: str | None
teacher: str | None

测试

新增

下面实现给课程表新增数据的功能,需要点击新增按钮后,出现弹窗

在官网找一个Dialog组件

前端

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<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-button
type="primary"
style="margin-bottom: 5px"
plain
@click="handleAdd"
>新增</el-button
>
</div>
<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>编辑</el-button>
<el-button type="danger" size="small" plain>删除</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>

<el-dialog width="35%" v-model="data.formVisible" title="课程信息">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 45px"
>
<el-form-item label="课程名称">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="课程编号">
<el-input v-model="data.form.number" autocomplete="off" />
</el-form-item>
<el-form-item label="课程描述">
<el-input v-model="data.form.description" autocomplete="off" />
</el-form-item>
<el-form-item label="课时">
<el-input v-model="data.form.periods" autocomplete="off" />
</el-form-item>
<el-form-item label="任课教师">
<el-input v-model="data.form.teacher" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false" plain>取消</el-button>
<el-button type="primary" @click="save" plain>保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
name: "",
number: "",
teacher: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
formVisible: false,
form: {},
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
teacher: data.teacher,
},
})
.then((res) => {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
});
};

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

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

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

const handleAdd = () => {
data.formVisible = true;
data.form = {};
};

const save = () => {
request
.post("/course/add", data.form)
.then((res) => {
if (res.code === "200") {
ElMessage.success("新增成功");
data.formVisible = false;
load();
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Query, Depends

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.course import Course, CourseSearch, CourseCreate
from service.courseService import CourseService

course_router = APIRouter(prefix="/course")


@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="Course name"),
number: Optional[str] = Query(None, description="Course number"),
teacher: Optional[str] = Query(None, description="Course teacher"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
course_search = CourseSearch(name=name, number=number, teacher=teacher)
course_list = CourseService.select_page(course_search, db_session)
result = Result.success(pageInfo.of(course_list))
return result


@course_router.post("/add", response_model=ResultModel)
async def add(course: CourseCreate, db_session: Session = Depends(get_db_session)):
CourseService.add_course(course, db_session)
result = Result.success()
return result


app.include_router(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
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import desc, select

from exception.customException import CourseExistException
from model import Session
from model.course import Course, CourseSearch, CourseCreate


class CourseService:

@staticmethod
def select_page(course_search: CourseSearch, db_session: Session):
query = select(Course).order_by(desc(Course.id))
if course_search.name:
query = query.where(Course.name.like(f"%{course_search.name}%"))
if course_search.number:
query = query.where(Course.number.like(f"%{course_search.number}%"))
if course_search.teacher:
query = query.where(Course.teacher.like(f"%{course_search.teacher}%"))
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def add_course(course: CourseCreate, db_session: Session):
query = select(Course).where(Course.name == course.name)
exist_course: Course = db_session.execute(query).scalars().all()
if exist_course:
raise CourseExistException("课程名已存在")
course = Course(**course.dict())
db_session.add(course)
db_session.commit()
return 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
# -*- 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


class Course(Base):
__tablename__ = "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)
description: Mapped[str] = mapped_column(String(255), nullable=False)
periods: Mapped[str] = mapped_column(String(255), nullable=False)
teacher: Mapped[str] = mapped_column(String(255), nullable=False)


class CourseSearch(BaseModel):
name: str | None
number: str | None
teacher: str | None


class CourseBase(BaseModel):
name: str
number: str
description: str
periods: str
teacher: str


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

class UserNotFoundException(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
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
# -*- 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


@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))

测试

编辑

点击数据中的编辑按钮可以对已有的课程数据,进行修改

前端

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
<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-button
type="primary"
style="margin-bottom: 5px"
plain
@click="handleAdd"
>新增</el-button
>
</div>
<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="handleEdit(scope.row)"
>编辑</el-button
>
<el-button type="danger" size="small" plain>删除</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>

<el-dialog width="35%" v-model="data.formVisible" title="课程信息">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 45px"
>
<el-form-item label="课程名称">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="课程编号">
<el-input v-model="data.form.number" autocomplete="off" />
</el-form-item>
<el-form-item label="课程描述">
<el-input v-model="data.form.description" autocomplete="off" />
</el-form-item>
<el-form-item label="课时">
<el-input v-model="data.form.periods" autocomplete="off" />
</el-form-item>
<el-form-item label="任课教师">
<el-input v-model="data.form.teacher" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false" plain>取消</el-button>
<el-button type="primary" @click="save" plain>保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
name: "",
number: "",
teacher: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
formVisible: false,
form: {},
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
teacher: data.teacher,
},
})
.then((res) => {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
});
};

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

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

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

const handleAdd = () => {
data.formVisible = true;
data.form = {};
};

const save = () => {
request
.request({
url: data.form.id ? "/course/update" : "/course/add",
method: data.form.id ? "put" : "post",
data: data.form,
})
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
load();
} else {
ElMessage.error(res.msg);
}
});
};

const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row));
data.formVisible = true;
};
</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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Query, Depends

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.course import Course, CourseSearch, CourseCreate, CourseUpdate
from service.courseService import CourseService

course_router = APIRouter(prefix="/course")


@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="Course name"),
number: Optional[str] = Query(None, description="Course number"),
teacher: Optional[str] = Query(None, description="Course teacher"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
course_search = CourseSearch(name=name, number=number, teacher=teacher)
course_list = CourseService.select_page(course_search, db_session)
result = Result.success(pageInfo.of(course_list))
return result


@course_router.post("/add", response_model=ResultModel)
async def add(course: CourseCreate, db_session: Session = Depends(get_db_session)):
CourseService.add_course(course, db_session)
result = Result.success()
return result


@course_router.put("/update", response_model=ResultModel)
async def update(course: CourseUpdate, db_session: Session = Depends(get_db_session)):
CourseService.update_by_id(course, db_session)
result = Result.success()
return result


app.include_router(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
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import desc, select

from common.utils import set_attrs
from exception.customException import CourseExistException, CourseNotExistException
from model import Session
from model.course import Course, CourseSearch, CourseCreate, CourseUpdate


class CourseService:

@staticmethod
def select_page(course_search: CourseSearch, db_session: Session):
query = select(Course).order_by(desc(Course.id))
if course_search.name:
query = query.where(Course.name.like(f"%{course_search.name}%"))
if course_search.number:
query = query.where(Course.number.like(f"%{course_search.number}%"))
if course_search.teacher:
query = query.where(Course.teacher.like(f"%{course_search.teacher}%"))
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def add_course(course: CourseCreate, db_session: Session):
query = select(Course).where(Course.name == course.name)
exist_course: Course = db_session.execute(query).scalars().all()
if exist_course:
raise CourseExistException("课程名已存在")
course = Course(**course.dict())
db_session.add(course)
db_session.commit()
return course

@staticmethod
def update_by_id(course: CourseUpdate, db_session: Session):
query = select(Course).where(Course.id == course.id)
exist_course: Course = db_session.execute(query).scalar()
if not exist_course:
raise CourseNotExistException("课程不存在")
set_attrs(exist_course, jsonable_encoder(course))
db_session.commit()
return exist_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
# -*- 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


class Course(Base):
__tablename__ = "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)
description: Mapped[str] = mapped_column(String(255), nullable=False)
periods: Mapped[str] = mapped_column(String(255), nullable=False)
teacher: Mapped[str] = mapped_column(String(255), nullable=False)


class CourseSearch(BaseModel):
name: str | None
number: str | None
teacher: str | None


class CourseBase(BaseModel):
name: str
number: str
description: str
periods: str
teacher: str


class CourseCreate(CourseBase):
...


class CourseUpdate(CourseBase):
id: int

实现给对象的属性更新的一个方法,在项目目录/common下,新建一个utils.py

1
2
3
4
5
6
7
8
9
# -*- coding:utf-8 -*-
# Author: Zachary


# 用于更新对象属性
def set_attrs(obj, data: dict):
if data:
for key, value in data.items():
setattr(obj, key, value)
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
# -*- coding:utf-8 -*-
# Author: Zachary

class UserNotFoundException(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

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
# -*- 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


@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))

测试

删除

点击已有课程数据中的删除按钮,实现删除数据表中对应数据

前端

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<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-button
type="primary"
style="margin-bottom: 5px"
plain
@click="handleAdd"
>新增</el-button
>
</div>
<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="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
type="danger"
size="small"
plain
@click="handleDelete(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>

<el-dialog width="35%" v-model="data.formVisible" title="课程信息">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 45px"
>
<el-form-item label="课程名称">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="课程编号">
<el-input v-model="data.form.number" autocomplete="off" />
</el-form-item>
<el-form-item label="课程描述">
<el-input v-model="data.form.description" autocomplete="off" />
</el-form-item>
<el-form-item label="课时">
<el-input v-model="data.form.periods" autocomplete="off" />
</el-form-item>
<el-form-item label="任课教师">
<el-input v-model="data.form.teacher" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false" plain>取消</el-button>
<el-button type="primary" @click="save" plain>保存</el-button>
</span>
</template>
</el-dialog>
</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,
formVisible: false,
form: {},
});

const load = () => {
request
.get("/course/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
name: data.name,
number: data.number,
teacher: data.teacher,
},
})
.then((res) => {
data.tableData = res.data?.list || [];
data.total = res.data?.total || 0;
});
};

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

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

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

const handleAdd = () => {
data.formVisible = true;
data.form = {};
};

const save = () => {
request
.request({
url: data.form.id ? "/course/update" : "/course/add",
method: data.form.id ? "put" : "post",
data: data.form,
})
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
load();
} else {
ElMessage.error(res.msg);
}
});
};

const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row));
data.formVisible = true;
};

const handleDelete = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/course/delete/" + id).then((res) => {
if (res.code === "200") {
ElMessage.success("删除成功");
load();
} else {
ElMessage.error(res.msg);
}
});
})
.catch((err) => {
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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Query, Depends

from api import app
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.course import Course, CourseSearch, CourseCreate, CourseUpdate
from service.courseService import CourseService

course_router = APIRouter(prefix="/course")


@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="Course name"),
number: Optional[str] = Query(None, description="Course number"),
teacher: Optional[str] = Query(None, description="Course teacher"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
course_search = CourseSearch(name=name, number=number, teacher=teacher)
course_list = CourseService.select_page(course_search, db_session)
result = Result.success(pageInfo.of(course_list))
return result


@course_router.post("/add", response_model=ResultModel)
async def add(course: CourseCreate, db_session: Session = Depends(get_db_session)):
CourseService.add_course(course, db_session)
result = Result.success()
return result


@course_router.put("/update", response_model=ResultModel)
async def update(course: CourseUpdate, db_session: Session = Depends(get_db_session)):
CourseService.update_by_id(course, db_session)
result = Result.success()
return result


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


app.include_router(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
55
56
57
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import desc, select

from common.utils import set_attrs
from exception.customException import CourseExistException, CourseNotExistException
from model import Session
from model.course import Course, CourseSearch, CourseCreate, CourseUpdate


class CourseService:

@staticmethod
def select_page(course_search: CourseSearch, db_session: Session):
query = select(Course).order_by(desc(Course.id))
if course_search.name:
query = query.where(Course.name.like(f"%{course_search.name}%"))
if course_search.number:
query = query.where(Course.number.like(f"%{course_search.number}%"))
if course_search.teacher:
query = query.where(Course.teacher.like(f"%{course_search.teacher}%"))
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def add_course(course: CourseCreate, db_session: Session):
query = select(Course).where(Course.name == course.name)
exist_course: Course = db_session.execute(query).scalars().all()
if exist_course:
raise CourseExistException("课程名已存在")
course = Course(**course.dict())
db_session.add(course)
db_session.commit()
return course

@staticmethod
def update_by_id(course: CourseUpdate, db_session: Session):
exist_course: Course = check_course_exist(course.id, db_session)
set_attrs(exist_course, jsonable_encoder(course))
db_session.commit()
return exist_course

@staticmethod
def delete_by_id(id: int, db_session: Session):
exist_course: Course = check_course_exist(id, db_session)
db_session.delete(exist_course)
db_session.commit()
return exist_course


def check_course_exist(course_id: int, db_session: Session):
query = select(Course).where(Course.id == course_id)
exist_course: Course = db_session.execute(query).scalar()
if not exist_course:
raise CourseNotExistException("课程不存在")
return exist_course

测试

先创建一个课程

然后删除

删除成功


API 鉴权

为了不允许,任何人都可以调用后端的 api 接口,现在给 api 接口加上权限校验,只有登录过的才能调用后端的 api 接口,之前在auth.py中实现了一个校验,通过依赖注入给 api 直接加上即可

依赖注入

比如给courseApi.py的所有接口加上 token 校验的依赖

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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import APIRouter, Query, Depends

from api import app
from common.auth import auth_handler
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.course import CourseSearch, CourseCreate, CourseUpdate
from service.courseService import CourseService

course_router = APIRouter(prefix="/course")


@course_router.get("/selectPage", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
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="Course name"),
number: Optional[str] = Query(None, description="Course number"),
teacher: Optional[str] = Query(None, description="Course teacher"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
course_search = CourseSearch(name=name, number=number, teacher=teacher)
course_list = CourseService.select_page(course_search, db_session)
result = Result.success(pageInfo.of(course_list))
return result


@course_router.post("/add", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def add(course: CourseCreate, db_session: Session = Depends(get_db_session)):
CourseService.add_course(course, db_session)
result = Result.success()
return result


@course_router.put("/update", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def update(course: CourseUpdate, db_session: Session = Depends(get_db_session)):
CourseService.update_by_id(course, db_session)
result = Result.success()
return result


@course_router.delete("/delete/{id}", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def delete(id: int, db_session: Session = Depends(get_db_session)):
CourseService.delete_by_id(id, db_session)
result = Result.success()
return result


app.include_router(course_router)

这样如果没有登录的情况下,直接用 postman 是无法调用接口的

登录 token

虽然现在前端页面上已经登录了,但由于请求头没有带上 token,所以无法获取数据

为了让浏览器拿到后端生成的这个 token,需要改变一下登录后的返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-
# Author: Zachary
from sqlalchemy import select

from common.auth import auth_handler
from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.admin import Admin, AdminModel, AdminLoginResponse


class AdminService:
@staticmethod
def login(admin: AdminModel, db_session: Session) -> Admin:
query = select(Admin).where(Admin.username == admin.username)
exist_admin: Admin = db_session.execute(query).scalars().first()
if not exist_admin:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(admin.password, exist_admin.password):
raise PasswordNotMatchException("身份验证未通过")
admin_login_response = AdminLoginResponse(id=exist_admin.id, username=exist_admin.username,
token=auth_handler.encode_token(exist_admin.id))
return admin_login_response
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
# -*- coding:utf-8 -*-
# Author: Zachary
import bcrypt
from pydantic import BaseModel
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Admin(Base):
__tablename__ = "admin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)

def password_check(self, password):
return bcrypt.checkpw(password.encode(), self.password.encode())


class AdminModel(BaseModel):
username: str
password: str


class AdminLoginResponse(BaseModel):
id: int
username: str
token: str

这样子在重新登录之后就可以获得后端返回的 token,前端 axios 会在发起请求之前,将 token 放到请求头中,再向后端请求 api 接口

登录后会返回 token:

能正确获取数据:

token 超时失效

为了让前端的 token 失效之后,自动回到 Login 页面,需要做些调整

当请求 api 接口的时候返回的状态值为 401 的时候,移除存储的 student-user,这样就会自动跳转回 login 页面重新进行登录

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
<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-button
type="primary"
style="margin-bottom: 5px"
plain
@click="handleAdd"
>新增</el-button
>
</div>
<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="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
type="danger"
size="small"
plain
@click="handleDelete(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>

<el-dialog width="35%" v-model="data.formVisible" title="课程信息">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 45px"
>
<el-form-item label="课程名称">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="课程编号">
<el-input v-model="data.form.number" autocomplete="off" />
</el-form-item>
<el-form-item label="课程描述">
<el-input v-model="data.form.description" autocomplete="off" />
</el-form-item>
<el-form-item label="课时">
<el-input v-model="data.form.periods" autocomplete="off" />
</el-form-item>
<el-form-item label="任课教师">
<el-input v-model="data.form.teacher" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false" plain>取消</el-button>
<el-button type="primary" @click="save" plain>保存</el-button>
</span>
</template>
</el-dialog>
</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";
import router from "@/router";

const data = reactive({
name: "",
number: "",
teacher: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
formVisible: false,
form: {},
});

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 handleAdd = () => {
data.formVisible = true;
data.form = {};
};

const save = () => {
request
.request({
url: data.form.id ? "/course/update" : "/course/add",
method: data.form.id ? "put" : "post",
data: data.form,
})
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
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);
});
};

const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row));
data.formVisible = true;
};

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

token 超时配置

为了更便捷地配置 token 的超时时间,设置一下超时时间常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding:utf-8 -*-
# Author: Zachary

from common.config import config

HOST = config.env.get("HOST")
PORT = config.env.get("PORT")

MYSQL_DIALECT = config.env.get("MYSQL_DIALECT")
MYSQL_HOST = config.env.get("MYSQL_HOST")
MYSQL_PORT = config.env.get("MYSQL_PORT")
MYSQL_USER = config.env.get("MYSQL_USER")
MYSQL_PASSWORD = config.env.get("MYSQL_PASSWORD")
MYSQL_DATABASE = config.env.get("MYSQL_DATABASE")

TOKEN_EXPIRE_DAYS = 7
TOKEN_EXPIRE_MINUTES = 0
TOKEN_EXPIRE_SECONDS = 0
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
# -*- coding:utf-8 -*-
# Author: Zachary
from datetime import datetime, timedelta

import jwt
from fastapi import Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from passlib.context import CryptContext

from common.constant import TOKEN_EXPIRE_DAYS, TOKEN_EXPIRE_MINUTES, TOKEN_EXPIRE_SECONDS
from exception.customException import TokenException


class AuthHandler:
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
secret = "SECRET"

def get_password_hash(self, password):
return self.pwd_context.hash(password)

def verify_password(self, plain_password, hashed_password):
return self.pwd_context.verify(plain_password, hashed_password)

def encode_token(self, user_id):
payload = {
'exp': datetime.utcnow() + timedelta(days=TOKEN_EXPIRE_DAYS, minutes=TOKEN_EXPIRE_MINUTES,
seconds=TOKEN_EXPIRE_SECONDS),
'iat': datetime.utcnow(),
'sub': user_id
}
return jwt.encode(
payload,
self.secret,
algorithm='HS256'
)

def decode_token(self, token):
try:
payload = jwt.decode(token, self.secret, algorithms=['HS256'])
return payload['sub']
except jwt.ExpiredSignatureError:
raise TokenException("token过期")
except jwt.InvalidTokenError:
raise TokenException("无效token")

def auth_required(self, auth: HTTPAuthorizationCredentials = Security(security)):
return self.decode_token(auth.credentials)


auth_handler = AuthHandler()

学生管理

数据库

先在数据库中创建一张学生信息表

Student 实体类定义

/model的包下创建一个student.py

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

from model import Base


class Student(Base):
__tablename__ = "student"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(String(255), nullable=False)
gender: Mapped[str] = mapped_column(String(255), nullable=False)
birthday: Mapped[str] = mapped_column(String(255), nullable=False)
avatar: Mapped[str] = mapped_column(String(255), nullable=False

学生登录

前端-登录页面

学生登录和 admin 的登录,我们复用同一个前端页面和后端接口,所以需要加一个role用以区分是 admin 登录了还是普通的 student 登录了;同时,不同用户进入页面之后。看到的内容应该是不同的。

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
<template>
<div>
<div class="login-container">
<div style="width: 420px" class="login-box">
<div class="title">学生信息管理系统 - 登录</div>
<el-form :model="data.form" ref="formRef" :rules="rules">
<el-form-item prop="username">
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="Avatar"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
show-password
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item prop="role">
<el-radio-group size="large" v-model="data.form.role">
<el-radio-button
style="margin-left: 70px"
label="管理员"
value="ADMIN"
/>
<el-radio-button
style="margin-left: 50px"
label="学生"
value="STUDENT"
/>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width: 100%;font-size: 18px"
plain
@click="login"
>登 录</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:35px;text-align: right;font-size: 15px">
还没有账号?请<a href="/register">注册</a>
</div>
</div>
</div>
</div>
</template>

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

const data = reactive({
form: { role: "ADMIN" },
});

const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const login = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.post("/login", data.form)
.then((res) => {
if (res.code === "200") {
localStorage.setItem("student-user", JSON.stringify(res.data));
ElMessage.success("登录成功");
router.push("/home");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
ElMessage.error(err.response?.data?.msg || err.message);
});
}
});
};
</script>

<style scoped>
.login-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.login-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</style>

后端-管理员/学生登录的 api 接口

管理员和学生的登录复用同一个 api 接口

所以需要修改一下之前的登录逻辑

Account

/model下创建一个account.py,接收前端传递来的账号、密码、角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel


class AccountLogin(BaseModel):
username: str
password: str
role: str


class AccountLoginResponse:
id: int
username: str
name: str
role: str
token: str

Student

给 student 表追加role字段

/model/student.py更新一下

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

from model import Base


class Student(Base):
__tablename__ = "student"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(String(255), nullable=False)
gender: Mapped[str] = mapped_column(String(255), nullable=False)
birthday: Mapped[str] = mapped_column(String(255), nullable=False)
avatar: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(255), nullable=False)

Admin

给 admin 表追加name字段和role字段

/model/admin.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
27
28
# -*- coding:utf-8 -*-
# Author: Zachary
import bcrypt
from pydantic import BaseModel
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Admin(Base):
__tablename__ = "admin"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(255), nullable=False)


class AdminModel(BaseModel):
username: str
password: str


class AdminLoginResponse(BaseModel):
id: int
username: str
token: str

Service

在项目目录/service下创建一个studentService.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

from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.account import AccountLogin, AccountLoginResponse
from model.student import Student


class StudentService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Student).where(Student.username == account.username)
exist_student: Student = db_session.execute(query).scalars().first()
if not exist_student:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_student.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_student))
account_login_response.token = auth_handler.encode_token(exist_student.id)
return account_login_response

然后修改adminService.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

from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException
from model import Session
from model.account import AccountLoginResponse, AccountLogin
from model.admin import Admin


class AdminService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Admin).where(Admin.username == account.username)
exist_admin: Admin = db_session.execute(query).scalars().first()
if not exist_admin:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_admin.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_admin))
account_login_response.token = auth_handler.encode_token(exist_admin.id)
return account_login_response

Role

创建一个枚举类 Role,用以区分学生和管理员

/common下创建一个Enum.py

1
2
3
4
5
6
7
8
# -*- coding:utf-8 -*-
# Author: Zachary
from enum import Enum


class Role(str, Enum):
ADMIN = "管理员"
STUDENT = "学生"

API

然后修改一下登录的 api

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

from api import app
from common.Enum import Role
from common.result import Result, ResultModel
from model import Session, get_db_session
from model.account import AccountLogin
from service.adminService import AdminService
from service.studentService import StudentService


@app.post("/login", response_model=ResultModel)
async def login(account: AccountLogin, db_session: Session = Depends(get_db_session)):
if Role.ADMIN.name.__eq__(account.role):
db_account = AdminService.login(account, db_session)
elif Role.STUDENT.name.__eq__(account.role):
db_account = StudentService.login(account, db_session)
else:
return Result.error("角色错误")
return Result.success(jsonable_encoder(db_account))

学生注册

前端-注册页面

写一下学生的注册页面,这里管理员就不参与注册了

在前端项目目录/views下创建一个Register.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
<template>
<div>
<div class="register-container">
<div style="width: 400px" class="register-box">
<div class="title">学生信息管理系统 - 注册</div>
<el-form :model="data.form" ref="formRef" :rules="rules">
<el-form-item prop="username">
<el-input
style="height: 40px;font-size: 18px"
prefix-icon="User"
v-model="data.form.username"
placeholder="请输入账号"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
show-password
style="height: 40px;font-size: 18px"
prefix-icon="Lock"
v-model="data.form.password"
placeholder="请输入密码"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width: 100%"
plain
@click="register"
>注 册</el-button
>
</el-form-item>
</el-form>
<div style="margin-top:30px;text-align: right">
已有账号?请直接<a href="/login">登录</a>
</div>
</div>
</div>
</div>
</template>

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

const data = reactive({
form: {},
});

const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const register = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.post("/register", data.form)
.then((res) => {
if (res.code === "200") {
ElMessage.success("注册成功");
router.push("/login");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
ElMessage.error(err.response?.data?.msg || err.message);
});
}
});
};
</script>

<style scoped>
.register-container {
min-height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image: url("@/assets/imgs/login_background.png");
background-size: cover;
}

.register-box {
background-color: rgb(255, 255, 255, 50%);
box-shadow: 0 0 10px rgba(84, 221, 245, 0.41);
padding: 30px;
}

.title {
font-weight: bold;
font-size: 30px;
text-align: center;
margin-bottom: 35px;
}
</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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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: "/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;

后端-注册的 api 接口

接下来实现一下,后端注册的逻辑

API

因为这个注册只是给学生使用的,在项目目录/api下新建一个studentApi.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi import Depends

from api import app
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.account import AccountRegister
from service.studentService import StudentService


@app.post("/register", response_model=ResultModel)
async def register(account: AccountRegister, db_session: Session = Depends(get_db_session)):
StudentService.register(account, db_session)
return Result.success()

添加一下 studentApi

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

account.py添加注册的请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-
# Author: Zachary
from pydantic import BaseModel


class AccountLogin(BaseModel):
username: str
password: str
role: str


class AccountLoginResponse:
id: int
username: str
name: str
role: str
token: str


class AccountRegister(BaseModel):
username: str
password: str

Service

实现一下注册的 service

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
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select

from common.Enum import Role
from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException, UserExistException
from model import Session
from model.account import AccountLogin, AccountLoginResponse, AccountRegister
from model.student import Student


class StudentService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Student).where(Student.username == account.username)
exist_student: Student = db_session.execute(query).scalars().first()
if not exist_student:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_student.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_student))
account_login_response.token = auth_handler.encode_token(exist_student.id)
return account_login_response

@staticmethod
def register(account: AccountRegister, db_session: Session):
query = select(Student).where(Student.username == account.username)
exist_student = db_session.execute(query).scalars().first()
if exist_student:
raise UserExistException("账号已存在")

new_student = Student()
account.password = auth_handler.get_password_hash(account.password)
set_attrs(new_student, jsonable_encoder(account))
if new_student.name is None:
new_student.name = account.username
new_student.role = Role.STUDENT.name

db_session.add(new_student)
db_session.commit()
return new_student

添加自定义异常

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
# -*- 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
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 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


@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))

登录/注册测试

现在测试一下学生登录和注册的逻辑

学生注册

学生登录

现在登录进来了,但是右上角的显示不太对,等下改改

管理员登录

学生管理

这块咱们做个设定,当管理员新增用户、替用户修改用户数据,密码必须重新设置为初始密码-123456

前端

复制粘贴一下之前的Course.vue,命名为Student.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.username"
placeholder="请输入要查询的学生学号"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.name"
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-button
type="primary"
style="margin-bottom: 5px"
plain
@click="handleAdd"
>新增</el-button
>
</div>
<div>
<el-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="学生学号" />
<el-table-column prop="name" label="学生姓名" />
<el-table-column prop="gender" label="性别" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="birthday" label="出生日期" />
<el-table-column prop="avatar" label="头像" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
type="danger"
size="small"
plain
@click="handleDelete(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>

<el-dialog width="35%" v-model="data.formVisible" title="学生信息">
<el-form
:model="data.form"
:rules="rules"
ref="formRef"
label-width="100px"
label-position="right"
style="padding-right: 45px"
>
<el-form-item label="学生学号" prop="username">
<el-input v-model="data.form.username" autocomplete="off" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="data.form.password" autocomplete="off" disabled />
</el-form-item>
<el-form-item label="学生姓名">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="data.form.gender">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="data.form.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model="data.form.birthday"
></el-date-picker>
</el-form-item>
<el-form-item label="头像">
<el-input v-model="data.form.avatar" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false" plain>取消</el-button>
<el-button type="primary" @click="save" plain>保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
username: "",
name: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
formVisible: false,
form: { password: "123456" },
});

const load = () => {
request
.get("/student/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
username: data.username,
name: data.name,
},
})
.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.username = "";
data.name = "";
load();
};

const rules = reactive({
username: [{ required: true, message: "请输入学生学号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const handleAdd = () => {
data.formVisible = true;
data.form = { password: "123456" };
};

const save = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.request({
url: data.form.id ? "/student/update" : "/student/add",
method: data.form.id ? "put" : "post",
data: data.form,
})
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
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);
});
}
});
};

const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row));
data.formVisible = true;
data.form.password = "123456";
};

const handleDelete = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/student/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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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: "/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
<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="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
alt=""
style="width: 40px; height: 40px"
/>
<span style="margin-left: 5px">管理员</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">
<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">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person">
<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");
};
</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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from fastapi import Depends, APIRouter, Query

from api import app
from common.auth import auth_handler
from common.pageHelper import PageHelper
from common.result import ResultModel, Result
from model import Session, get_db_session
from model.account import AccountRegister
from model.student import StudentSearch, StudentCreate, StudentUpdate
from service.studentService import StudentService


@app.post("/register", response_model=ResultModel)
async def register(account: AccountRegister, db_session: Session = Depends(get_db_session)):
StudentService.register(account, db_session)
return Result.success()


student_router = APIRouter(prefix="/student")


@student_router.get("/selectPage", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
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"),
username: Optional[str] = Query(None, description="Student username"),
name: Optional[str] = Query(None, description="Student name"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
student_search = StudentSearch(username=username, name=name)
student_list = StudentService.select_page(student_search, db_session)
return Result.success(pageInfo.of(student_list))


@student_router.post("/add", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def add(student: StudentCreate, db_session: Session = Depends(get_db_session)):
StudentService.add_student(student, db_session)
return Result.success()


@student_router.put("/update", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def update(student: StudentUpdate, db_session: Session = Depends(get_db_session)):
StudentService.update_by_id(student, db_session)
return Result.success()


@student_router.delete("/delete/{id}", response_model=ResultModel, dependencies=[Depends(auth_handler.auth_required)])
async def delete(id: int, db_session: Session = Depends(get_db_session)):
StudentService.delete_by_id(id, db_session)
return Result.success()


app.include_router(student_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
# -*- coding:utf-8 -*-
# Author: Zachary
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select, asc

from common.Enum import Role
from common.auth import auth_handler
from common.utils import set_attrs
from exception.customException import UserNotFoundException, PasswordNotMatchException, UserExistException
from model import Session
from model.account import AccountLogin, AccountLoginResponse, AccountRegister
from model.student import Student, StudentCreate, StudentUpdate


class StudentService:
@staticmethod
def login(account: AccountLogin, db_session: Session) -> AccountLoginResponse:
query = select(Student).where(Student.username == account.username)
exist_student: Student = db_session.execute(query).scalars().first()
if not exist_student:
raise UserNotFoundException("用户不存在")
if not auth_handler.verify_password(account.password, exist_student.password):
raise PasswordNotMatchException("身份验证未通过")
account_login_response = AccountLoginResponse()
set_attrs(account_login_response, jsonable_encoder(exist_student))
account_login_response.token = auth_handler.encode_token(exist_student.id)
return account_login_response

@staticmethod
def register(account: AccountRegister, db_session: Session):
query = select(Student).where(Student.username == account.username)
exist_student = db_session.execute(query).scalars().first()
if exist_student:
raise UserExistException("账号已存在")

new_student = Student()
account.password = auth_handler.get_password_hash(account.password)
set_attrs(new_student, jsonable_encoder(account))
if new_student.name is None:
new_student.name = account.username
new_student.role = Role.STUDENT.name

db_session.add(new_student)
db_session.commit()
return new_student

@staticmethod
def select_page(student_search: Student, db_session: Session):
query = select(Student).order_by(asc(Student.id))
if student_search.username:
query = query.where(Student.username.like(f"%{student_search.username}%"))
if student_search.name:
query = query.where(Student.name.like(f"%{student_search.name}%"))

result = db_session.execute(query).scalars().all()
return result

@staticmethod
def add_student(student: StudentCreate, db_session: Session):
query = select(Student).where(Student.username == student.username)
exist_student = db_session.execute(query).scalars().all()
if exist_student:
raise UserExistException("账号已存在")

student.password = auth_handler.get_password_hash(student.password)
student = Student(**student.dict())
if student.name is None:
student.name = student.username
student.role = Role.STUDENT.name
db_session.add(student)
db_session.commit()
return student

@staticmethod
def update_by_id(student: StudentUpdate, db_session: Session):
exist_student: Student = check_student_exist(student.id, db_session)
student.password = auth_handler.get_password_hash(student.password)
set_attrs(exist_student, jsonable_encoder(student))
db_session.commit()
return exist_student

@staticmethod
def delete_by_id(id: int, db_session: Session):
exist_student: Student = check_student_exist(id, db_session)
db_session.delete(exist_student)
db_session.commit()
return exist_student


def check_student_exist(student_id: int, db_session: Session):
query = select(Student).where(Student.id == student_id)
exist_student: Student = db_session.execute(query).scalar()
if not exist_student:
raise UserNotFoundException("账号不存在")
return exist_student

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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from pydantic import BaseModel, Field
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base


class Student(Base):
__tablename__ = "student"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
username: Mapped[str] = mapped_column(String(255), nullable=False)
password: Mapped[str] = mapped_column(String(255), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
phone: Mapped[str] = mapped_column(String(255), nullable=False)
gender: Mapped[str] = mapped_column(String(255), nullable=False)
birthday: Mapped[str] = mapped_column(String(255), nullable=False)
avatar: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[str] = mapped_column(String(255), nullable=False)


class StudentBase(BaseModel):
username: str
password: str = Field(..., min_length=1)
name: Optional[str] = None
role: Optional[str] = None


class StudentSearch(BaseModel):
username: str | None
name: str | None


class StudentCreate(StudentBase):
phone: Optional[str] = None
gender: Optional[str] = None
birthday: Optional[str] = Field(None)
avatar: Optional[str] = Field(None)


class StudentUpdate(StudentCreate):
id: int

测试

但是修改过后,密码会被强制重置为 123456

限制权限

现在不管普通的学生还是管理员都能看到管理页面 这不符合

需要加入一个权限v-if="user.role === 'ADMIN'"

顺便修改一下右上角和主页显示的用户名称

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
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="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
alt=""
style="width: 40px; height: 40px"
/>
<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" v-if="user.role === 'ADMIN'">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course">
<el-icon>
<Document />
</el-icon>
<span>课程信息</span>
</el-menu-item>
</el-sub-menu>

<el-menu-item index="/person">
<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
<template>
<div class="welcome-container">
<div class="custom-card bg-white shadow rounded p-4 mb-5">
<h2 class="text-center text-primary">欢迎来到本系统</h2>
<p class="text-center mt-3">
您好 <span :style="{ color: '#116ca9' }">{{ user.name }}</span
>,祝您使用愉快!
</p>
</div>
</div>
</template>

<script setup>
import request from "@/utils/request";

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

<style scoped>
.welcome-container {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 100px); /* 根据实际项目需要调整高度 */
}

.custom-card {
max-width: 400px;
border: none;
}
</style>

普通学生登录

管理员登录


个人页面

文件上传与下载

依赖包

需要安装一下werkzeug这个包

1
2
3
4
5
6
fastapi[all]
mysqlclient==2.1.1
SQLAlchemy==2.0.23
pyjwt
passlib[bcrypt]
werkzeug

fileApi

为了实现头像上传的功能创建一个新的fileApi.py

注意:

  • StreamingResponse 引得是from fastapi.responses import StreamingResponse
  • secure_filename 引得是from werkzeug.utils import secure_filename
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
# -*- coding:utf-8 -*-
# Author: Zachary
import mimetypes
import os
from datetime import datetime
from fastapi.encoders import jsonable_encoder
from werkzeug.utils import secure_filename
from fastapi.responses import StreamingResponse
from fastapi import APIRouter, UploadFile

from api import app
from common.constant import HOST, PORT
from common.profile import Profile
from common.result import ResultModel, Result
from exception.customException import FileNotFoundException

file_router = APIRouter(prefix="/files")


@file_router.post("/upload", response_model=ResultModel)
async def upload(file: UploadFile):
original_filename = secure_filename(file.filename)
timestamp = int(datetime.now().timestamp())
unique_filename = f"{timestamp}_{original_filename}"
file_save_path = Profile.get_files_path()

# 创建保存文件的完整路径
file_final_path = file_save_path.joinpath(unique_filename)

# 将文件保存到指定位置
with open(file_final_path, 'wb') as buffer_file:
content = await file.read()
buffer_file.write(content)

# 构建文件访问URL
url = f"http://{HOST}:{PORT}/files/download?filename={unique_filename}"
return Result.success(jsonable_encoder({"url": url}))


@file_router.get("/download")
async def download(filename: str):
file_save_path = Profile.get_files_path()
file_path = file_save_path.joinpath(filename)

if not file_path.exists():
raise FileNotFoundException("文件不存在")
# 用于触发下载文件的
# return FileResponse(file_path, media_type='image/png', filename=filename)

mime_type, _ = mimetypes.guess_type(file_path)

# 创建一个StreamingResponse,以便流式传输大文件,同时设置正确的MIME类型
response = StreamingResponse(
open(file_path, 'rb'),
media_type=mime_type,
)
# 不设置Content-Disposition,避免浏览器触发下载
return response


app.include_router(file_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

Profile

在项目目录/common下创建一个profile.py,用于获取项目目录路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding:utf-8 -*-
# Author: Zachary

from pathlib import Path


class Profile:
__file_path = None

@staticmethod
def get_files_path():
project_path = Path(__file__).parent.parent # 获取项目根目录
file_path = project_path.joinpath("files")
if not file_path.exists():
file_path.mkdir(parents=True)
Profile.__file_path = file_path
return file_path

自定义异常

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

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
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
# -*- 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


@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))

测试

使用 postman 测试一下

上传

下载

完善图片上传

修改一下Student.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.username"
placeholder="请输入要查询的学生学号"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.name"
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-button
type="primary"
style="margin-bottom: 5px"
plain
@click="handleAdd"
>新增</el-button
>
</div>
<div>
<el-table :data="data.tableData" stripe style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="学生学号" />
<el-table-column prop="name" label="学生姓名" />
<el-table-column prop="gender" label="性别" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="birthday" label="出生日期" />

<el-table-column prop="avatar" label="头像">
<template #default="scope">
<el-image
v-if="scope.row.avatar"
:src="scope.row.avatar"
:preview-src-list="[scope.row.avatar]"
style="width: 40px; height: 40px"
></el-image>
</template>
</el-table-column>

<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
>编辑</el-button
>
<el-button
type="danger"
size="small"
plain
@click="handleDelete(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>

<el-dialog width="35%" v-model="data.formVisible" title="学生信息">
<el-form
:model="data.form"
:rules="rules"
ref="formRef"
label-width="100px"
label-position="right"
style="padding-right: 45px"
>
<el-form-item label="学生学号" prop="username">
<el-input v-model="data.form.username" autocomplete="off" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="data.form.password" autocomplete="off" disabled />
</el-form-item>
<el-form-item label="学生姓名">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="data.form.gender">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="data.form.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model="data.form.birthday"
></el-date-picker>
</el-form-item>
<el-form-item label="头像">
<el-upload
action="http://localhost:9090/files/upload"
list-type="picture"
:on-success="handleImgUploadSuccess"
>
<el-button type="primary">上传头像 </el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.formVisible = false" plain>取消</el-button>
<el-button type="primary" @click="save" plain>保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>

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

const data = reactive({
username: "",
name: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 5,
formVisible: false,
form: { password: "123456" },
});

const load = () => {
request
.get("/student/selectPage", {
params: {
pageNum: data.pageNum,
pageSize: data.pageSize,
username: data.username,
name: data.name,
},
})
.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.username = "";
data.name = "";
load();
};

const rules = reactive({
username: [{ required: true, message: "请输入学生学号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});

const formRef = ref();

const handleAdd = () => {
data.formVisible = true;
data.form = { password: "123456" };
};

const save = () => {
formRef.value.validate((valid) => {
if (valid) {
request
.request({
url: data.form.id ? "/student/update" : "/student/add",
method: data.form.id ? "put" : "post",
data: data.form,
})
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
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);
});
}
});
};

const handleEdit = (row) => {
data.form = JSON.parse(JSON.stringify(row));
data.formVisible = true;
data.form.password = "123456";
};

const handleDelete = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/student/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);
});
};

const handleImgUploadSuccess = (res) => {
data.form.avatar = res.data.url;
};
</script>

个人资料

在项目目录/manager文件夹下面创建一个Person.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
<template>
<div>
<div class="card" style="width: 50%; padding: 40px">
<el-form
:model="data.form"
ref="formRef"
:rules="rules"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
action="http://localhost:9090/files/upload"
:show-file-list="false"
:on-success="handleImgUploadSuccess"
>
<img
v-if="data.form.avatar"
:src="data.form.avatar"
class="avatar"
/>
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="学生学号" prop="username">
<el-input v-model="data.form.username" autocomplete="off" disabled />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
show-password
v-model="data.form.password"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="学生姓名">
<el-input v-model="data.form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="data.form.gender">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="data.form.phone" autocomplete="off" />
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model="data.form.birthday"
></el-date-picker>
</el-form-item>
<el-form-item style="padding-left: 60%">
<el-button type="primary" @click="update">保存</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

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

const data = reactive({
form: JSON.parse(localStorage.getItem("student-user") || "{}"),
});

if (data.form) {
data.form.password = "";
}

const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [
{
required: true,
message: "修改个人资料必须重新输入密码",
trigger: "blur",
},
],
});

const update = () => {
request
.put("/student/update", data.form)
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
router.push("/login");
} else {
ElMessage.error(res.msg);
}
})
.catch((err) => {
ElMessage.error(err.response?.data?.msg || err.message);
});
};

const handleImgUploadSuccess = (res) => {
data.form.avatar = res.data;
};
</script>

<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}

.avatar-uploader .el-upload:hover {
border-color: #409eff;
}

.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
}

.avatar {
width: 100px;
height: 100px;
display: block;
}
</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
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
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: "/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
<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" v-if="user.role === 'ADMIN'">
<template #title>
<el-icon>
<Memo />
</el-icon>
<span>课程管理</span>
</template>
<el-menu-item index="/course">
<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>


学生选课功能

数据库

新增一张学生选课表

前端

学生选课页面,把 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))

测试

管理员删除

学生自己删除


学生成绩管理

成绩表设计

管理员课程打分

前端

添加课程打分弹窗

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
<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="addGrade(scope.row)"
v-if="data.user.role === 'ADMIN'"
>评分
</el-button>
<el-button
type="danger"
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>

<!-- 评分表单-->
<el-dialog v-model="data.formVisible" title="成绩信息" width="35%">
<el-form
:model="data.gradeForm"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="课程名称">
<el-input v-model="data.gradeForm.name" autocomplete="off" disabled />
</el-form-item>
<el-form-item label="分数">
<el-input v-model="data.gradeForm.score" autocomplete="off" />
</el-form-item>
<el-form-item label="评语">
<el-input
type="textarea"
v-model="data.gradeForm.comment"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</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") || "{}"),
gradeForm: {},
formVisible: false,
});

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);
});
};

// 打分函数,弹窗
const addGrade = (row) => {
// 弹窗
data.formVisible = true;
data.gradeForm.name = row.name;
data.gradeForm.studentId = row.studentId;
data.gradeForm.courseId = row.courseId;
data.gradeForm.score = "";
data.gradeForm.comment = "";
};

const save = () => {
request
.post("/grade/add", data.gradeForm)
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
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>

后端

依次创建上:gradeApi.pygradeService.pygrade.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- 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.grade import GradeCreate
from service.gradeService import GradeService

grade_router = APIRouter(prefix="/grade")


@grade_router.post("/add", response_model=ResultModel)
async def add(grade: GradeCreate, db_session: Session = Depends(get_db_session)):
GradeService.add_grade(grade, db_session)
return Result.success()


app.include_router(grade_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, gradeApi
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
# -*- 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 GradeExistException
from model import Session
from model.grade import GradeCreate, Grade


class GradeService:

@staticmethod
def add_grade(grade: GradeCreate, db_session: Session):
query = select(Grade).where(
and_(Grade.studentId == grade.studentId,
Grade.courseId == grade.courseId))
exist_grade: Grade = db_session.execute(query).scalar()
if exist_grade:
raise GradeExistException("课程已打分")
new_grade = Grade()
set_attrs(new_grade, jsonable_encoder(grade))
db_session.add(new_grade)
db_session.commit()
return new_grade

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 typing import Optional

from pydantic import BaseModel
from sqlalchemy import Integer, Double, String
from sqlalchemy.orm import Mapped, mapped_column

from model import Base
from model.course import Course
from model.student import Student


class Grade(Base):
__tablename__ = "grade"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, nullable=False)
score: Mapped[str] = mapped_column(Double, nullable=False)
comment: Mapped[str] = mapped_column(String(255), nullable=False)
feedback: Mapped[str] = mapped_column(String(255), nullable=False)



class GradeBase(BaseModel):
courseId: int
studentId: int
score: Optional[float] = None
comment: Optional[str] = None
feedback: Optional[str] = None


class GradeCreate(GradeBase):
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
43
44
45
46
47
48
49
50
51
# -*- 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


class GradeExistException(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
73
74
75
76
77
78
79
# -*- 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, GradeExistException


@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))


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

测试

管理员评分

显示课程分数

前端

复制StudentCourse.vue重命名为Grade.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.courseName"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.studentName"
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="course.name" label="课程名称" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column prop="score" label="成绩" />
<el-table-column prop="comment" label="教师评语" />
<el-table-column prop="feedback" label="学生评价" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="danger"
size="small"
plain
@click="del(scope.row.id)"
v-if="data.user.role === 'ADMIN'"
>删除
</el-button>
<el-button
type="primary"
size="small"
plain
@click="setFeedback(scope.row)"
v-if="data.user.role === 'STUDENT'"
>评价
</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>

<!-- 评分表单-->
<el-dialog v-model="data.formVisible" title="反馈信息" width="35%">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="反馈">
<el-input
type="textarea"
v-model="data.form.feedback"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</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({
form: {},
courseName: "",
studentName: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
formVisible: false,
});

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

request
.get("/grade/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.courseName = "";
data.studentName = "";
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);
});
};

// 打分
const setFeedback = (row) => {
// 弹窗
data.formVisible = true;
data.form = JSON.parse(JSON.stringify(row));
};

const save = () => {
request
.post("/grade/update", data.form)
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
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
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
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: "grade",
name: "Grade",
component: () => import("@/views/manager/Grade.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<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', '4']"
>
<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-sub-menu index="4">
<template #title>
<el-icon>
<Management />
</el-icon>
<span>成绩信息</span>
</template>
<el-menu-item index="/grade">
<el-icon>
<List />
</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
37
38
39
40
41
42
43
# -*- 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.grade import GradeCreate, GradeSearch, GradeUpdate
from service.gradeService import GradeService

grade_router = APIRouter(prefix="/grade")


@grade_router.post("/add", response_model=ResultModel)
async def add(grade: GradeCreate, db_session: Session = Depends(get_db_session)):
GradeService.add_grade(grade, db_session)
return Result.success()


@grade_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"),
studentName: Optional[str] = Query(None, description="Student name"),
courseName: Optional[str] = Query(None, description="Course name"),
studentId: Optional[str] = Query(None, description="Student id"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
grade_search: GradeSearch = GradeSearch(studentName=studentName, courseName=courseName, studentId=studentId)
grade_list = GradeService.select_page(grade_search, db_session)
return Result.success(pageInfo.of(grade_list))


@grade_router.put("/update", response_model=ResultModel)
async def update(grade: GradeUpdate, db_session: Session = Depends(get_db_session)):
GradeService.update_by_id(grade, db_session)
return Result.success()


app.include_router(grade_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
# -*- 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 GradeExistException
from model import Session
from model.course import Course
from model.grade import GradeCreate, Grade, GradeSearch, GradeUpdate
from model.student import Student


class GradeService:

@staticmethod
def add_grade(grade: GradeCreate, db_session: Session):
query = select(Grade).where(
and_(Grade.studentId == grade.studentId,
Grade.courseId == grade.courseId))
exist_grade: Grade = db_session.execute(query).scalar()
if exist_grade:
raise GradeExistException("课程已打分")
new_grade = Grade()
set_attrs(new_grade, jsonable_encoder(grade))
db_session.add(new_grade)
db_session.commit()
return new_grade

@staticmethod
def select_page(grade_search: GradeSearch, db_session: Session):
query = select(Grade, Student, Course).outerjoin(Grade.student).outerjoin(Grade.course).order_by(asc(Grade.id))
if grade_search.courseName:
query = query.where(Course.name.like(f"%{grade_search.courseName}%"))
if grade_search.studentName:
query = query.where(Student.name.like(f"%{grade_search.studentName}%"))
if grade_search.studentId:
query = query.where(Grade.studentId == grade_search.studentId)
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def update_by_id(grade: GradeUpdate, db_session: Session):
exist_grade: Grade = check_grade_exist(grade.id, db_session)
set_attrs(exist_grade, jsonable_encoder(grade))
db_session.commit()
return exist_grade


def check_grade_exist(grade_id: int, db_session: Session):
query = select(Grade).where(Grade.id == grade_id)
exist_grade: Grade = db_session.execute(query).scalar()
if not exist_grade:
raise GradeExistException("成绩记录不存在")
return exist_grade

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
# -*- coding:utf-8 -*-
# Author: Zachary
from typing import Optional

from pydantic import BaseModel
from sqlalchemy import Integer, Double, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from model import Base
from model.course import Course
from model.student import Student


class Grade(Base):
__tablename__ = "grade"
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
courseId: Mapped[int] = mapped_column("course_id", Integer, ForeignKey('course.id'), nullable=False)
studentId: Mapped[int] = mapped_column("student_id", Integer, ForeignKey('student.id'), nullable=False)
score: Mapped[str] = mapped_column(Double, nullable=False)
comment: Mapped[str] = mapped_column(String(255), nullable=False)
feedback: Mapped[str] = mapped_column(String(255), nullable=False)

student: Mapped[Student] = relationship(lazy=False, backref="grade")
course: Mapped[Course] = relationship(lazy=False, backref="grade")


class GradeBase(BaseModel):
courseId: int
studentId: int
score: Optional[float] = None
comment: Optional[str] = None
feedback: Optional[str] = None


class GradeCreate(GradeBase):
pass


class GradeSearch(BaseModel):
courseName: str | None
studentName: str | None
studentId: str | None


class GradeUpdate(GradeBase):
id: int

测试

管理员

课程分数编辑

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.courseName"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.studentName"
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="course.name" label="课程名称" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column prop="score" label="成绩" />
<el-table-column prop="comment" label="教师评语" />
<el-table-column prop="feedback" label="学生评价" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'ADMIN'"
>编辑
</el-button>
<el-button
type="danger"
size="small"
plain
@click="del(scope.row.id)"
v-if="data.user.role === 'ADMIN'"
>删除
</el-button>
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'STUDENT'"
>评价
</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>

<!-- 评分表单-->
<el-dialog v-model="data.formVisible" title="反馈信息" width="35%">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="分数" v-if="data.user.role === 'ADMIN'">
<el-input v-model="data.form.score" autocomplete="off" />
</el-form-item>
<el-form-item label="教师评语" v-if="data.user.role === 'ADMIN'">
<el-input
type="textarea"
v-model="data.form.comment"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="学生评价" v-if="data.user.role === 'STUDENT'">
<el-input
type="textarea"
v-model="data.form.feedback"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</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({
form: {},
courseName: "",
studentName: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
formVisible: false,
});

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

request
.get("/grade/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.courseName = "";
data.studentName = "";
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);
});
};

// 打分
const handleEdit = (row) => {
// 弹窗
data.formVisible = true;
data.form = JSON.parse(JSON.stringify(row));
};

const save = () => {
request
.post("/grade/update", data.form)
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
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
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
<template>
<div>
<div class="card" style="margin-bottom: 10px">
<el-input
@input="load"
style="width: 260px"
v-model="data.courseName"
placeholder="请输入要查询的课程名称"
:prefix-icon="Search"
/>
<el-input
@input="load"
style="margin-left:10px; width: 260px"
v-model="data.studentName"
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="course.name" label="课程名称" />
<el-table-column prop="student.name" label="学生名称" />
<el-table-column prop="score" label="成绩" />
<el-table-column prop="comment" label="教师评语" />
<el-table-column prop="feedback" label="学生评价" />
<el-table-column label="操作" align="center" width="180px">
<template #default="scope">
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'ADMIN'"
>编辑
</el-button>
<el-button
type="danger"
size="small"
plain
@click="del(scope.row.id)"
v-if="data.user.role === 'ADMIN'"
>删除
</el-button>
<el-button
type="primary"
size="small"
plain
@click="handleEdit(scope.row)"
v-if="data.user.role === 'STUDENT'"
>评价
</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>

<!-- 评分表单-->
<el-dialog v-model="data.formVisible" title="反馈信息" width="35%">
<el-form
:model="data.form"
label-width="100px"
label-position="right"
style="padding-right: 40px"
>
<el-form-item label="分数" v-if="data.user.role === 'ADMIN'">
<el-input v-model="data.form.score" autocomplete="off" />
</el-form-item>
<el-form-item label="教师评语" v-if="data.user.role === 'ADMIN'">
<el-input
type="textarea"
v-model="data.form.comment"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="学生评价" v-if="data.user.role === 'STUDENT'">
<el-input
type="textarea"
v-model="data.form.feedback"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="data.formVisible = false">取消</el-button>
<el-button type="primary" plain @click="save">保存</el-button>
</span>
</template>
</el-dialog>
</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({
form: {},
courseName: "",
studentName: "",
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
user: JSON.parse(localStorage.getItem("student-user") || "{}"),
formVisible: false,
});

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

request
.get("/grade/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.courseName = "";
data.studentName = "";
load();
};

const del = (id) => {
ElMessageBox.confirm("删除后内容将无法恢复,您确认删除嘛?", "删除确认", {
type: "warning",
})
.then((res) => {
request.delete("/grade/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);
});
};

// 打分
const handleEdit = (row) => {
// 弹窗
data.formVisible = true;
data.form = JSON.parse(JSON.stringify(row));
};

const save = () => {
request
.put("/grade/update", data.form)
.then((res) => {
if (res.code === "200") {
ElMessage.success("操作成功");
data.formVisible = false;
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
43
44
45
46
47
48
49
# -*- 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.grade import GradeCreate, GradeSearch, GradeUpdate
from service.gradeService import GradeService

grade_router = APIRouter(prefix="/grade")


@grade_router.post("/add", response_model=ResultModel)
async def add(grade: GradeCreate, db_session: Session = Depends(get_db_session)):
GradeService.add_grade(grade, db_session)
return Result.success()


@grade_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"),
studentName: Optional[str] = Query(None, description="Student name"),
courseName: Optional[str] = Query(None, description="Course name"),
studentId: Optional[str] = Query(None, description="Student id"),
db_session: Session = Depends(get_db_session)):
pageInfo = PageHelper.startPage(page, size)
grade_search: GradeSearch = GradeSearch(studentName=studentName, courseName=courseName, studentId=studentId)
grade_list = GradeService.select_page(grade_search, db_session)
return Result.success(pageInfo.of(grade_list))


@grade_router.put("/update", response_model=ResultModel)
async def update(grade: GradeUpdate, db_session: Session = Depends(get_db_session)):
GradeService.update_by_id(grade, db_session)
return Result.success()


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


app.include_router(grade_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
# -*- 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 GradeExistException
from model import Session
from model.course import Course
from model.grade import GradeCreate, Grade, GradeSearch, GradeUpdate
from model.student import Student


class GradeService:

@staticmethod
def add_grade(grade: GradeCreate, db_session: Session):
query = select(Grade).where(
and_(Grade.studentId == grade.studentId,
Grade.courseId == grade.courseId))
exist_grade: Grade = db_session.execute(query).scalar()
if exist_grade:
raise GradeExistException("课程已打分")
new_grade = Grade()
set_attrs(new_grade, jsonable_encoder(grade))
db_session.add(new_grade)
db_session.commit()
return new_grade

@staticmethod
def select_page(grade_search: GradeSearch, db_session: Session):
query = select(Grade, Student, Course).outerjoin(Grade.student).outerjoin(Grade.course).order_by(asc(Grade.id))
if grade_search.courseName:
query = query.where(Course.name.like(f"%{grade_search.courseName}%"))
if grade_search.studentName:
query = query.where(Student.name.like(f"%{grade_search.studentName}%"))
if grade_search.studentId:
query = query.where(Grade.studentId == grade_search.studentId)
result = db_session.execute(query).scalars().all()
return result

@staticmethod
def update_by_id(grade: GradeUpdate, db_session: Session):
exist_grade: Grade = check_grade_exist(grade.id, db_session)
set_attrs(exist_grade, jsonable_encoder(grade))
db_session.commit()
return exist_grade

@staticmethod
def delete_by_id(id: int, db_session: Session):
exist_grade: Grade = check_grade_exist(id, db_session)
db_session.delete(exist_grade)
db_session.commit()
return exist_grade


def check_grade_exist(grade_id: int, db_session: Session):
query = select(Grade).where(Grade.id == grade_id)
exist_grade: Grade = db_session.execute(query).scalar()
if not exist_grade:
raise GradeExistException("成绩记录不存在")
return exist_grade

测试

i

更新: 2024-05-26 21:19:26
原文: https://www.yuque.com/zacharyblock/cx2om6/ka7buim7w1uxo6pg