From 63fa1620814a280394f114df8fd174ca28e3a42e Mon Sep 17 00:00:00 2001
From: zxiaosi <2692159310@qq.com>
Date: Fri, 1 Jul 2022 15:08:28 +0800
Subject: [PATCH 1/3] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96FastAPI?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 95 +-
backend/Dockerfile | 24 -
backend/README.md | 194 -
backend/apis/__init__.py | 5 +-
backend/apis/api_router.py | 32 -
backend/apis/common/__init__.py | 5 -
backend/apis/common/course.py | 65 -
backend/apis/common/dashboard.py | 43 -
backend/apis/common/department.py | 76 -
backend/apis/common/elective.py | 98 -
backend/apis/common/login.py | 77 -
backend/apis/common/major.py | 66 -
backend/apis/common/redis_check.py | 23 -
backend/apis/common/student.py | 72 -
backend/apis/common/taught.py | 72 -
backend/apis/common/teacher.py | 67 -
backend/apis/common/upload.py | 40 -
backend/apis/debug.py | 18 +
backend/apis/deps.py | 55 -
backend/core/__init__.py | 6 +-
backend/core/config.py | 42 +-
backend/core/logger.py | 37 -
backend/core/security.py | 64 -
backend/crud/__init__.py | 14 -
backend/crud/admin.py | 51 -
backend/crud/base.py | 124 -
backend/crud/course.py | 15 -
backend/crud/department.py | 15 -
backend/crud/elective.py | 53 -
backend/crud/major.py | 15 -
backend/crud/student.py | 61 -
backend/crud/taught.py | 32 -
backend/crud/teacher.py | 52 -
backend/db/__init__.py | 8 -
backend/db/data.py | 120 -
backend/db/init_db.py | 53 -
backend/db/redis.py | 58 -
backend/db/session.py | 29 -
backend/main.py | 38 +-
backend/models/__init__.py | 13 +-
backend/models/admin.py | 23 -
backend/models/base.py | 31 -
backend/models/course.py | 25 -
backend/models/department.py | 25 -
backend/models/elective.py | 20 -
backend/models/major.py | 25 -
backend/models/student.py | 35 -
backend/models/taught.py | 18 -
backend/models/teacher.py | 38 -
backend/register/__init__.py | 10 -
backend/register/cors.py | 21 -
backend/register/exception.py | 102 -
backend/register/middleware.py | 32 -
backend/register/mount.py | 16 -
backend/register/router.py | 25 -
backend/requirement.txt | 18 +
backend/requirements.txt | 37 -
backend/schemas/__init__.py | 17 +-
backend/schemas/admin.py | 35 -
backend/schemas/common.py | 18 -
backend/schemas/course.py | 32 -
backend/schemas/department.py | 34 -
backend/schemas/elective.py | 37 -
backend/schemas/login.py | 12 -
backend/schemas/major.py | 36 -
backend/schemas/result.py | 30 -
backend/schemas/student.py | 39 -
backend/schemas/taught.py | 34 -
backend/schemas/teacher.py | 41 -
backend/schemas/todo.py | 17 -
backend/schemas/token.py | 19 -
backend/sql_app.db | Bin 94208 -> 0 bytes
backend/static/author.jpg | Bin 45621 -> 0 bytes
backend/test_main.http | 11 +
backend/utils/__init__.py | 10 +-
backend/utils/check_enum.py | 23 -
backend/utils/create_dir.py | 20 -
backend/utils/custom_exc.py | 52 -
backend/utils/ip_address.py | 32 -
backend/utils/obj_dict.py | 16 -
backend/utils/permission_assign.py | 42 -
backend/utils/resp_code.py | 79 -
frontend/.gitignore | 28 -
frontend/.vscode/extensions.json | 3 -
frontend/README.md | 133 -
frontend/env.d.ts | 1 -
frontend/index.html | 15 -
frontend/package-lock.json | 3256 -----------------
frontend/package.json | 27 -
frontend/src/App.vue | 18 -
frontend/src/api/index.ts | 96 -
frontend/src/api/login.ts | 30 -
frontend/src/api/model.ts | 72 -
frontend/src/assets/css/color-dark.css | 28 -
frontend/src/assets/css/icon.css | 9 -
frontend/src/assets/css/main.css | 116 -
frontend/src/assets/img/img.jpg | Bin 45621 -> 0 bytes
frontend/src/assets/js/global.ts | 15 -
frontend/src/assets/logo.svg | 1 -
frontend/src/components/BaseTable/index.ts | 13 -
frontend/src/components/BaseTable/index.vue | 390 --
frontend/src/components/Breadcrumb/index.vue | 33 -
frontend/src/components/LoadingBar/index.vue | 56 -
frontend/src/components/Pagination/index.vue | 45 -
frontend/src/layout/Header/index.vue | 182 -
frontend/src/layout/Home/index.vue | 46 -
frontend/src/layout/Sidebar/index.vue | 116 -
frontend/src/layout/Tags/index.vue | 193 -
frontend/src/main.ts | 19 -
frontend/src/request/auth.ts | 28 -
frontend/src/request/http.ts | 135 -
frontend/src/request/index.ts | 20 -
frontend/src/request/statusCode.ts | 41 -
frontend/src/router/index.ts | 146 -
frontend/src/stores/data.ts | 25 -
frontend/src/stores/index.ts | 57 -
frontend/src/types/index.ts | 35 -
frontend/src/types/table.ts | 115 -
frontend/src/utils/clickRecover.ts | 11 -
frontend/src/utils/handleArray.ts | 32 -
frontend/src/utils/handleInject.ts | 17 -
frontend/src/utils/handleTime.ts | 8 -
frontend/src/utils/spanMethod.ts | 66 -
frontend/src/views/dashboard/index.ts | 27 -
frontend/src/views/dashboard/index.vue | 430 ---
frontend/src/views/error/403/index.vue | 55 -
frontend/src/views/error/404/index.vue | 55 -
frontend/src/views/login/index.ts | 4 -
frontend/src/views/login/index.vue | 158 -
frontend/src/views/messages/index.ts | 8 -
frontend/src/views/messages/index.vue | 166 -
frontend/src/views/settings/course/index.ts | 7 -
frontend/src/views/settings/course/index.vue | 144 -
.../src/views/settings/department/index.ts | 7 -
.../src/views/settings/department/index.vue | 132 -
frontend/src/views/settings/elective/index.ts | 10 -
.../src/views/settings/elective/index.vue | 120 -
frontend/src/views/settings/index.vue | 3 -
frontend/src/views/settings/major/index.ts | 9 -
frontend/src/views/settings/major/index.vue | 160 -
.../src/views/settings/myElective/index.ts | 11 -
.../src/views/settings/myElective/index.vue | 54 -
frontend/src/views/settings/myTaught/index.ts | 13 -
.../src/views/settings/myTaught/index.vue | 134 -
frontend/src/views/settings/student/index.ts | 20 -
frontend/src/views/settings/student/index.vue | 204 --
frontend/src/views/settings/taught/index.ts | 10 -
frontend/src/views/settings/taught/index.vue | 114 -
frontend/src/views/settings/teacher/index.ts | 34 -
frontend/src/views/settings/teacher/index.vue | 242 --
frontend/src/views/user/index.ts | 35 -
frontend/src/views/user/index.vue | 134 -
frontend/tsconfig.json | 21 -
frontend/tsconfig.vite-config.json | 8 -
frontend/vite.config.ts | 27 -
redis&&nginx.md | 89 -
156 files changed, 82 insertions(+), 11454 deletions(-)
delete mode 100644 backend/Dockerfile
delete mode 100644 backend/README.md
delete mode 100644 backend/apis/api_router.py
delete mode 100644 backend/apis/common/__init__.py
delete mode 100644 backend/apis/common/course.py
delete mode 100644 backend/apis/common/dashboard.py
delete mode 100644 backend/apis/common/department.py
delete mode 100644 backend/apis/common/elective.py
delete mode 100644 backend/apis/common/login.py
delete mode 100644 backend/apis/common/major.py
delete mode 100644 backend/apis/common/redis_check.py
delete mode 100644 backend/apis/common/student.py
delete mode 100644 backend/apis/common/taught.py
delete mode 100644 backend/apis/common/teacher.py
delete mode 100644 backend/apis/common/upload.py
create mode 100644 backend/apis/debug.py
delete mode 100644 backend/apis/deps.py
delete mode 100644 backend/core/logger.py
delete mode 100644 backend/core/security.py
delete mode 100644 backend/crud/__init__.py
delete mode 100644 backend/crud/admin.py
delete mode 100644 backend/crud/base.py
delete mode 100644 backend/crud/course.py
delete mode 100644 backend/crud/department.py
delete mode 100644 backend/crud/elective.py
delete mode 100644 backend/crud/major.py
delete mode 100644 backend/crud/student.py
delete mode 100644 backend/crud/taught.py
delete mode 100644 backend/crud/teacher.py
delete mode 100644 backend/db/__init__.py
delete mode 100644 backend/db/data.py
delete mode 100644 backend/db/init_db.py
delete mode 100644 backend/db/redis.py
delete mode 100644 backend/db/session.py
delete mode 100644 backend/models/admin.py
delete mode 100644 backend/models/base.py
delete mode 100644 backend/models/course.py
delete mode 100644 backend/models/department.py
delete mode 100644 backend/models/elective.py
delete mode 100644 backend/models/major.py
delete mode 100644 backend/models/student.py
delete mode 100644 backend/models/taught.py
delete mode 100644 backend/models/teacher.py
delete mode 100644 backend/register/__init__.py
delete mode 100644 backend/register/cors.py
delete mode 100644 backend/register/exception.py
delete mode 100644 backend/register/middleware.py
delete mode 100644 backend/register/mount.py
delete mode 100644 backend/register/router.py
create mode 100644 backend/requirement.txt
delete mode 100644 backend/requirements.txt
delete mode 100644 backend/schemas/admin.py
delete mode 100644 backend/schemas/common.py
delete mode 100644 backend/schemas/course.py
delete mode 100644 backend/schemas/department.py
delete mode 100644 backend/schemas/elective.py
delete mode 100644 backend/schemas/login.py
delete mode 100644 backend/schemas/major.py
delete mode 100644 backend/schemas/result.py
delete mode 100644 backend/schemas/student.py
delete mode 100644 backend/schemas/taught.py
delete mode 100644 backend/schemas/teacher.py
delete mode 100644 backend/schemas/todo.py
delete mode 100644 backend/schemas/token.py
delete mode 100644 backend/sql_app.db
delete mode 100644 backend/static/author.jpg
create mode 100644 backend/test_main.http
delete mode 100644 backend/utils/check_enum.py
delete mode 100644 backend/utils/create_dir.py
delete mode 100644 backend/utils/custom_exc.py
delete mode 100644 backend/utils/ip_address.py
delete mode 100644 backend/utils/obj_dict.py
delete mode 100644 backend/utils/permission_assign.py
delete mode 100644 backend/utils/resp_code.py
delete mode 100644 frontend/.gitignore
delete mode 100644 frontend/.vscode/extensions.json
delete mode 100644 frontend/README.md
delete mode 100644 frontend/env.d.ts
delete mode 100644 frontend/index.html
delete mode 100644 frontend/package-lock.json
delete mode 100644 frontend/package.json
delete mode 100644 frontend/src/App.vue
delete mode 100644 frontend/src/api/index.ts
delete mode 100644 frontend/src/api/login.ts
delete mode 100644 frontend/src/api/model.ts
delete mode 100644 frontend/src/assets/css/color-dark.css
delete mode 100644 frontend/src/assets/css/icon.css
delete mode 100644 frontend/src/assets/css/main.css
delete mode 100644 frontend/src/assets/img/img.jpg
delete mode 100644 frontend/src/assets/js/global.ts
delete mode 100644 frontend/src/assets/logo.svg
delete mode 100644 frontend/src/components/BaseTable/index.ts
delete mode 100644 frontend/src/components/BaseTable/index.vue
delete mode 100644 frontend/src/components/Breadcrumb/index.vue
delete mode 100644 frontend/src/components/LoadingBar/index.vue
delete mode 100644 frontend/src/components/Pagination/index.vue
delete mode 100644 frontend/src/layout/Header/index.vue
delete mode 100644 frontend/src/layout/Home/index.vue
delete mode 100644 frontend/src/layout/Sidebar/index.vue
delete mode 100644 frontend/src/layout/Tags/index.vue
delete mode 100644 frontend/src/main.ts
delete mode 100644 frontend/src/request/auth.ts
delete mode 100644 frontend/src/request/http.ts
delete mode 100644 frontend/src/request/index.ts
delete mode 100644 frontend/src/request/statusCode.ts
delete mode 100644 frontend/src/router/index.ts
delete mode 100644 frontend/src/stores/data.ts
delete mode 100644 frontend/src/stores/index.ts
delete mode 100644 frontend/src/types/index.ts
delete mode 100644 frontend/src/types/table.ts
delete mode 100644 frontend/src/utils/clickRecover.ts
delete mode 100644 frontend/src/utils/handleArray.ts
delete mode 100644 frontend/src/utils/handleInject.ts
delete mode 100644 frontend/src/utils/handleTime.ts
delete mode 100644 frontend/src/utils/spanMethod.ts
delete mode 100644 frontend/src/views/dashboard/index.ts
delete mode 100644 frontend/src/views/dashboard/index.vue
delete mode 100644 frontend/src/views/error/403/index.vue
delete mode 100644 frontend/src/views/error/404/index.vue
delete mode 100644 frontend/src/views/login/index.ts
delete mode 100644 frontend/src/views/login/index.vue
delete mode 100644 frontend/src/views/messages/index.ts
delete mode 100644 frontend/src/views/messages/index.vue
delete mode 100644 frontend/src/views/settings/course/index.ts
delete mode 100644 frontend/src/views/settings/course/index.vue
delete mode 100644 frontend/src/views/settings/department/index.ts
delete mode 100644 frontend/src/views/settings/department/index.vue
delete mode 100644 frontend/src/views/settings/elective/index.ts
delete mode 100644 frontend/src/views/settings/elective/index.vue
delete mode 100644 frontend/src/views/settings/index.vue
delete mode 100644 frontend/src/views/settings/major/index.ts
delete mode 100644 frontend/src/views/settings/major/index.vue
delete mode 100644 frontend/src/views/settings/myElective/index.ts
delete mode 100644 frontend/src/views/settings/myElective/index.vue
delete mode 100644 frontend/src/views/settings/myTaught/index.ts
delete mode 100644 frontend/src/views/settings/myTaught/index.vue
delete mode 100644 frontend/src/views/settings/student/index.ts
delete mode 100644 frontend/src/views/settings/student/index.vue
delete mode 100644 frontend/src/views/settings/taught/index.ts
delete mode 100644 frontend/src/views/settings/taught/index.vue
delete mode 100644 frontend/src/views/settings/teacher/index.ts
delete mode 100644 frontend/src/views/settings/teacher/index.vue
delete mode 100644 frontend/src/views/user/index.ts
delete mode 100644 frontend/src/views/user/index.vue
delete mode 100644 frontend/tsconfig.json
delete mode 100644 frontend/tsconfig.vite-config.json
delete mode 100644 frontend/vite.config.ts
delete mode 100644 redis&&nginx.md
diff --git a/README.md b/README.md
index 3713112..5beab7e 100644
--- a/README.md
+++ b/README.md
@@ -1,90 +1,17 @@
-# 学生选课系统
+## Vue3+FastAPI 小 demo
-## 预览
+### frontend
-+ [Vue3++TS+ElementPlus+Vite](http://8.136.82.204:8001/)
-+ [FastAPI接口预览](http://8.136.82.204:8000/)
-+ 🎉🎉🎉感谢 [**wendingming**](https://gitee.com/wendingming) 整理的 [项目部署的准备工作](https://gitee.com/zxiaosi/fast-api/issues/I4V6WV)
-+ 🎉🎉🎉感谢 [**dreamrise**](https://gitee.com/dreamrise) 整理的 [运行配置介绍](https://gitee.com/zxiaosi/fast-api/issues/I56HPN)
+- 框架:[Vue3.js](https://staging-cn.vuejs.org/)
+- UI 库:[Naive UI](https://www.naiveui.com/zh-CN/os-theme) _待定_
-## 安装
+### backend
-+ **后端安装**:[FastAPI](https://gitee.com/zxiaosi/fast-api/tree/master/backend#安装)(代码参考[CharmCode](https://www.charmcode.cn/category/FastAPI?page=1))
-+ **前端安装**:[Vue3+Ts](https://gitee.com/zxiaosi/fast-api/tree/master/frontend#安装) (代码参考[Vue-Manage-System](https://github.com/lin-xin/vue-manage-system))
+- 框架:[FastAPI](https://fastapi.tiangolo.com/zh/)
+- ORM:[Tortoise ORM ](https://tortoise.github.io/) _待定_
+- Redis:[aioredis](https://aioredis.readthedocs.io/en/latest/)
-## 版本
+### deploy
-+ `1.0` 测试数据的增删改查已完成
-+ `1.1` 院系表的增删改查已完成(见`信息表格`)
-+ `1.2` 首页仪表盘信息的优化
-+ `1.3` 院系表的增删改查初步完成
-+ `1.4` 整理代码
-+ `1.5` 添加了教师表
-+ `1.6` 添加了学生表、课程表、选课表
-+ `1.7` 重构前端代码
-+ `1.8` 封装组件,取出冗余代码
-+ `1.9` 自定义表格组件
-+ `2.0` 部署项目
-+ `2.1` 重构FastAPI
-+ `2.2` 配置nginx以及SSL证书(域名未备案,ssl证书未生效)
-+ `2.3` 添加Redis
-+ `2.4` 加入TS
-+ `2.5` 支持PostgreSQL,实现图片上传
-+ `2.6` 前端文件分离(vue与ts),后端实现权限管理
-+ `2.7` 简单实现权限管理
-+ `2.8` 调整数据库结构&&简单实现学生选课
-+ `2.9` 简单实现教师讲授课程
-
->TODO:优化代码
-
-## 开启服务
-
-1. 后端
-
- + 进入到 `backend` 项目下
- + 找到 `main.py` 右键运行(建议用Pycharm启动)
-
- >接口文档:http://127.0.0.1:8000/docs
-
-2. 前端
-
- + 进到 `frontend` 目录下
- + `npm run dev` 运行项目(建议用Vscode)
-
- >服务接口:http://localhost:3000/
-
-3. 效果
-
-+ 登录界面
-
- + `用户名`:`admin`
-
- + `密码`:`123`
-
- + 如图
-
- 
-
-+ 首页(假数据)
-
- 
-
-+ 数据的`增`
-
- 
-
-+ 数据的`删`
-
- 
-
-+ 数据的`改`
-
- 
-
-+ 搜索数据
-
- 
-
-+ 多选删除
-
- 
+- nginx
+- docker
diff --git a/backend/Dockerfile b/backend/Dockerfile
deleted file mode 100644
index 9d18d6d..0000000
--- a/backend/Dockerfile
+++ /dev/null
@@ -1,24 +0,0 @@
-# Python版本
-FROM python:3.9
-
-# 工作路径
-WORKDIR /code
-
-# 方便下面使用缓存加载
-COPY ./requirements.txt /code/requirements.txt
-
-# 使用缓存下载安装包
-#RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
-
-# 使用缓存下载安装包(镜像)
-RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --default-timeout=1000 --no-cache-dir --upgrade -r /code/requirements.txt
-
-# docker部署(https://fastapi.tiangolo.com/zh/deployment/docker/)
-# .表示同级目录下
-COPY . /code/
-
-# 启动命令
-#CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
-
-# main.py启动命令
-CMD ["python", "main.py"]
\ No newline at end of file
diff --git a/backend/README.md b/backend/README.md
deleted file mode 100644
index 74be6c0..0000000
--- a/backend/README.md
+++ /dev/null
@@ -1,194 +0,0 @@
-# FastAPI
-
-## 版本迭代
-
-+ `V1.0` FastAPI学习
-+ `V2.0` 搭建FastAPI脚手架
-+ `V2.1` 创建所需的表
-+ `V2.2` 已成功调试Mysql、Sqlite, 未调试Postgresql
-+ `V2.3` 初始化表数据(调试)
-+ `V2.4` 优化创建表问题
-+ `V2.5` 初始化所有表数据
-+ `V2.6` 封装日志模块
-+ `V2.7` 封装多进程日志模块(线程锁)
-+ `V2.8` 优化了目录结构
-+ `V2.9` 优化代码&&调试了user表的增删查接口
-+ `V3.0` 添加了防止跨域请求代码&&调试了接口
-+ `V3.1` 去除了测试表的自增
-+ `V3.2` 添加了department表的接口
-+ `V3.3` 添加了major表的接口
-+ `V3.4` 更新了major表接口的部分代码
-+ `V3.5` 更换了日志模块(loguru)&&添加了后端数据验证
-+ `V3.5` 添加了teacher表的接口
-+ `V3.6` 添加了student表的接口
-+ `V3.7` 添加了course表和selectCourse表的接口
-+ `V3.8` 更新了所需的包
-+ `V3.9` 修改了查询单个信息的数据
-+ `V4.0` 删除了获取所有数据以及获取单个数据的方法
-+ `V4.1` 修改了校验规则
-+ `V4.1.1` 删除了整型和浮点型的正则校验规则
-+ `v4.2` 尝试部署中。。。
-+ `v4.3` 部署成功,修复了部分bug
-+ `v4.4` 测试token
-+ `v4.5` 调试token成功(admin, 123)
-+ `v4.6` 重构FastAPI
-+ `v4.7` 添加redis
-+ `v4.8` 重构后端
-+ `v4.9` 支持PostgreSQL,以及图片上传
-+ `v5.0` 后台实现权限管理模块
-+ `v5.1` 整理接口,简单实现权限管理模块
-+ `v5.2` 调整数据库结构
-
-## 安装
-
-1. 配置Python3.9(及以上)的虚拟环境
-
-2. 安装运行所需的包
-
- ```python
- # 默认装了Mysql与ProgreSQL
- pip install requirements.txt
-
- # 或者
- pip install fastapi
- pip install uvicorn[fastapi]
- pip install loguru
- pip install SQLAlchemy
- pip install aioredis
- pip install python-jose
- pip install passlib
- pip install bcrypt
- pip install python-multipart
- pip install orjson
-
- # 使用Sqlite(异步), 请安装下面包
- pip install aiosqlite
-
- # 使用MySQL(异步), 请安装下面包
- pip install asyncmy
-
- # 使用ProgreSQL(异步), 请安装下面包
- pip install asyncpg
- ```
-
-
-3. 启动服务
-
- + 进入到 `backend` 项目下
- + 找到 `main.py` 右键运行
- + `core/config` 配置文件(默认数据库是sqlite)
-
- > 接口文档:http://127.0.0.1:8000/docs
-
-## 项目截图
-
-+ 成功运行的图片
-
- 
-
-+ 接口图
-
- 
-
-## 项目目录(待整理)
-
-```sh
-|-- backend
-
- |-- api # 接口
- |-- admin
- |-- __init__.py # 管理员接口
- |-- course.py # 课程表接口
- |-- department.py # 院系表接口
- |-- major.py # 专业表接口
- |-- index.py # 管理员首页
- |-- elective.py # 选课表接口
- |-- student.py # 学生表接口
- |-- teacher.py # 教师表接口
- |-- common
- |-- __init__.py # 共用接口
- |-- login.py # 登录接口
- |-- redis_check.py # 检查redis是否连接成功
- |-- upload.py # 图片上传
- |-- __init__.py
- |-- deps.py # 依赖项
- |-- api_router.py # admin接口汇总
-
- |-- core
- |-- __init__.py # 核心内容
- |-- config.py # 配置文件
- |-- security.py # 安全配置
-
- |-- crud
- |-- __init__.py # 数据库的增删改查操作
- |-- base.py # 封装数据库增删改查方法
- |-- course.py # 课程表
- |-- department.py # 院系表
- |-- major.py # 专业表
- |-- elective.py # 选课表
- |-- teacher.py # 教师表
- |-- student.py # 学生表
-
- |-- db
- |-- __init__.py # 初始数据库以及表数据
- |-- data.py # 所有数据
- |-- init_data.py # 两种初始化表数据的方式
- |-- init_db.py # 创建和删除base中的表
- |-- session.py # 创建数据库连接会话
- |-- redis.py # 注册Redis
-
- |-- logs # 日志模块(自动生成)
- |-- 2021-10-06_23-46-45.log
- |-- 2021-10-06_23-46-47.log
- |-- 2021-10-06_23-46-49.log
-
- |-- models
- |-- __init__.py # ORM模型映射
- |-- base.py # 自动生成 表名
- |-- index.py # 管理员表
- |-- course.py # 课程表
- |-- department.py # 院系表
- |-- major.py # 专业表
- |-- elective.py # 选课表
- |-- student.py # 学生表
- |-- teacher.py # 教师表
-
- |-- register
- |-- __init__.py # 注册中心
- |-- cors.py # 注册跨域请求
- |-- exception.py # 注册全局异常
- |-- middleware.py # 注册请求响应拦截
- |-- router.py # 注册路由
-
- |-- schemas
- |-- __init__.py # 数据模型
- |-- admin.py # 管理员表模型
- |-- common.py # 公用表模型
- |-- course.py # 课程表模型
- |-- department.py # 院系表模型
- |-- login.py # 登录模型
- |-- major.py # 专业表模型
- |-- result.py # 返回数据模型
- |-- elective.py # 选课表模型
- |-- student.py # 学生表模型
- |-- teacher.py # 教师表模型
- |-- todo.py # 待办模型
- |-- token.py # token模型
-
- |-- utils # 工具
- |-- __init__.py # 抛出工具类
- |-- create_dir.py # 创建文件夹类(位置勿动)
- |-- custon_exc.py # 自定义异常
- |-- ip_address.py # 根据ip获取位置
- |-- logger.py # 日志模块
- |-- permission_assign.py # 权限管理
- |-- resp_code.py # 状态码
-
- |-- __init__.py
- |-- main.py # 主程序
- |-- Dockerfile # Dockerfile文件
- |-- README.md # Readme文件
- |-- requirements.txt # 所需的包
- |-- sql_app.db # sqlite数据库
-```
-
diff --git a/backend/apis/__init__.py b/backend/apis/__init__.py
index fdb36b7..9803b20 100644
--- a/backend/apis/__init__.py
+++ b/backend/apis/__init__.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_
-# @Time : 2021/10/15 19:57
# @Author : zxiaosi
-# @desc : 所有接口
-from .api_router import app_router
+# @Time : 2022/7/1 14:41
+# @desc :
diff --git a/backend/apis/api_router.py b/backend/apis/api_router.py
deleted file mode 100644
index b7ce22f..0000000
--- a/backend/apis/api_router.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/10/15 20:01
-# @Author : zxiaosi
-# @desc : 接口汇总
-from fastapi import APIRouter
-
-from apis.common import upload, student, teacher, department, major, course, taught, elective
-
-app_router = APIRouter()
-
-# include_in_schema=False 隐藏属性
-# deprecated=True 弃用属性
-
-# 上传图片
-app_router.include_router(upload.router, tags=["Upload"])
-
-# common
-app_router.include_router(elective.router, prefix="/elective", tags=["elective"])
-app_router.include_router(department.router, prefix="/department", tags=["Department"])
-app_router.include_router(major.router, prefix="/major", tags=["major"])
-app_router.include_router(course.router, prefix="/course", tags=["course"])
-app_router.include_router(teacher.router, prefix="/teacher", tags=["teacher"])
-app_router.include_router(student.router, prefix="/student", tags=["student"])
-app_router.include_router(taught.router, prefix="/taught", tags=["taught"])
-
-
-# Admin
-
-# teacher
-
-# student
diff --git a/backend/apis/common/__init__.py b/backend/apis/common/__init__.py
deleted file mode 100644
index 0f878d9..0000000
--- a/backend/apis/common/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/2/24 9:43
-# @Author : zxiaosi
-# @desc : 共用接口
diff --git a/backend/apis/common/course.py b/backend/apis/common/course.py
deleted file mode 100644
index 845e1ec..0000000
--- a/backend/apis/common/course.py
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/17 16:42
-# @Author : zxiaosi
-# @desc : 课程表接口
-from typing import List
-from fastapi import APIRouter, Depends, Security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from apis.deps import get_db, get_current_user
-from schemas import CourseCreate, CourseUpdate, CourseOut as Course, Result, ResultPlus
-from crud import course
-from utils import resp_200, IdNotExist
-
-router = APIRouter()
-
-
-@router.get("/{id}", response_model=Result[Course], summary='根据 id 查询课程信息')
-async def read_course(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=[])):
- _course = await course.get(db, id)
- if not _course:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的课程.")
- return resp_200(data=_course, msg=f"查询到了 id 为 {id} 的课程.")
-
-
-@router.get("/", response_model=ResultPlus[Course], summary='根据页码 pageIndex 和每页个数 pageSize 查询所有课程')
-async def read_courses(pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- """ 查询所有院系 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await course.get_number(db)
- _courses = await course.get_multi(db, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _courses}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个课程信息.")
-
-
-@router.post("/", response_model=Result, summary='添加课程信息')
-async def create_course(course_in: CourseCreate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- await course.create(db, obj_in=course_in)
- return resp_200(msg=f"添加了 id 为 {course_in.id} 的课程信息.")
-
-
-@router.put("/{id}", response_model=Result, summary='通过 id 更新课程信息')
-async def update_course(id: int, course_in: CourseUpdate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- rowcount = await course.update(db, id=id, obj_in=course_in)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的课程.")
- return resp_200(msg=f"更新了 id 为 {id} 的课程信息.")
-
-
-@router.delete("/{id}", response_model=Result, summary='通过 id 删除课程信息')
-async def delete_course(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=["admin"])):
- rowcount = await course.remove(db, id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的课程.")
- return resp_200(msg=f"删除了 id 为 {id} 的课程信息.")
-
-
-@router.post("/del/", response_model=Result, summary='同时删除多个课程信息')
-async def delete_courses(idList: List, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- rowcount = await course.remove_multi(db, id_list=idList)
- if not rowcount:
- raise IdNotExist(err_desc="系统中不存在列表中的id.")
- return resp_200(msg='成功删除多个课程信息.')
diff --git a/backend/apis/common/dashboard.py b/backend/apis/common/dashboard.py
deleted file mode 100644
index 69b7915..0000000
--- a/backend/apis/common/dashboard.py
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/17 15:15
-# @Author : zxiaosi
-# @desc : 首页
-import json
-from fastapi import APIRouter, Depends
-from fastapi.responses import ORJSONResponse
-
-from db import MyRedis
-from apis.deps import get_redis
-from schemas import Todo, TodoId
-from utils import resp_200
-
-router = APIRouter()
-
-
-@router.get("/dashboard", response_class=ORJSONResponse, summary='访问量 && 待办数 && 请求数 && 待办事项')
-async def get_visit_todo_request(redis: MyRedis = Depends(get_redis)):
- """ 查询首页数据(访问量 && 待办事项 && 请求数 && 待办事项) """
- visit_num = await redis.get('visit_num')
- todo_num = await redis.llen('todo_list')
- request_num = await redis.get('request_num')
- todo_list = await redis.list_loads('todo_list', 6)
- data = {'visit_num': visit_num, 'todo_num': todo_num, 'request_num': request_num, 'todo_list': todo_list}
- return resp_200(data=data, msg='查询了访问量 && 待办事项 && 请求数 && 待办事项')
-
-
-@router.post("/todo/add", summary='添加待办')
-async def add_todo(todo_in: Todo, redis: MyRedis = Depends(get_redis)):
- """ 添加待办 """
- text = {'title': todo_in.title, 'status': todo_in.status}
- await redis.cus_lpush('todo_list', text)
- return resp_200(msg='添加待办成功!')
-
-
-@router.post("/todo/update", response_class=ORJSONResponse, summary='根据索引更新待办')
-async def update_todo(todo_in: TodoId, redis: MyRedis = Depends(get_redis)):
- """ 更新待办 """
- obj = await redis.get_list_by_index('todo_list', todo_in.id)
- obj["status"] = bool(1 - obj["status"]) # noqa 取反
- await redis.lset('todo_list', todo_in.id, json.dumps(obj))
- return resp_200(msg='更新待办成功!')
diff --git a/backend/apis/common/department.py b/backend/apis/common/department.py
deleted file mode 100644
index 8bf2251..0000000
--- a/backend/apis/common/department.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/10/28 19:18
-# @Author : zxiaosi
-# @desc : 院系表接口
-from typing import List
-
-from fastapi import APIRouter, Depends, Security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from apis.deps import get_db, get_current_user
-from schemas import DepartmentUpdate, DepartmentCreate, DepartmentOut as Department, Result, ResultPlus
-from crud import department
-from utils import resp_200, IdNotExist
-
-router = APIRouter()
-
-
-@router.get("/{id}", response_model=Result[Department], summary='根据 id 查询院系信息')
-async def read_department(id: int, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- _department = await department.get(db, id)
- if not _department:
- raise IdNotExist(f"系统中不存在 id 为 {id} 的院系.")
- return resp_200(data=_department, msg=f"查询到了 id 为 {id} 的院系.")
-
-
-@router.get("/", response_model=ResultPlus[Department], summary='根据页码 pageIndex 和每页个数 pageSize 查询所有院系')
-async def read_departments(pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- """ 查询所有院系 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await department.get_number(db)
- _departments = await department.get_multi(db, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _departments}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个院系信息.")
-
-
-@router.post("/", response_model=Result, summary='添加院系信息')
-async def create_department(department_in: DepartmentCreate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- await department.create(db, obj_in=department_in)
- return resp_200(msg=f"添加了 id 为 {department_in.id} 的院系信息.")
-
-
-@router.put("/{id}", response_model=Result, summary='通过 id 更新院系信息')
-async def update_department(id: int, department_in: DepartmentUpdate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- rowcount = await department.update(db, id=id, obj_in=department_in)
- if not rowcount: # 每次更新, 当前数据的更新时间会变, 只要id存在, 就会一直返回1
- raise IdNotExist(f"系统中不存在 id 为 {id} 的院系.")
- return resp_200(msg=f"更新了 id 为 {id} 的院系信息.")
-
-
-@router.delete("/{id}", response_model=Result, summary='通过 id 删除院系信息')
-async def delete_department(id: int, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- rowcount = await department.remove(db, id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的院系.")
- return resp_200(msg=f'成功删除 id 为 {id} 的院系信息.')
-
-
-@router.post("/del/", response_model=Result, summary='同时删除多个院系信息')
-async def delete_departments(idList: List, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- rowcount = await department.remove_multi(db, id_list=idList)
- if not rowcount:
- raise IdNotExist(err_desc="系统中不存在列表中的id.")
- return resp_200(msg='成功删除多个院系信息.')
-
-
-@router.get("/sort/{name}", summary='根据字段排序')
-async def get_select_courses(name: str, pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db)):
- """ 查询所有院系 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await department.get_number(db)
- _departments = await department.sort(db, name, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _departments}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个院系信息.")
diff --git a/backend/apis/common/elective.py b/backend/apis/common/elective.py
deleted file mode 100644
index 4e72e44..0000000
--- a/backend/apis/common/elective.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/18 10:25
-# @Author : zxiaosi
-# @desc : 选课表接口
-from typing import List
-from fastapi import APIRouter, Depends, Security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from apis.deps import get_db, get_current_user
-from models import Student
-from schemas import ElectiveCreate, ElectiveUpdate, ElectiveOut as Elective, Result, ResultPlus
-from crud import elective
-from utils import resp_200, IdNotExist
-
-router = APIRouter()
-
-
-@router.get("/{id}", response_model=Result[Elective], summary='根据 id 查询选课信息')
-async def read_elective(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=[])):
- _elective = await elective.get(db, id)
- if not _elective:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的选课.")
- return resp_200(data=_elective, msg=f"查询到了 id 为 {id} 的选课.")
-
-
-@router.get("/", response_model=ResultPlus[Elective], summary='根据页码 pageIndex 和每页个数 pageSize 查询所有选课')
-async def read_electives(pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- """ 查询所有院系 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await elective.get_number(db)
- _electives = await elective.get_multi(db, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _electives}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个选课信息.")
-
-
-@router.post("/", response_model=Result, summary='添加选课信息')
-async def create_elective(elective_in: ElectiveCreate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- await elective.create(db, obj_in=elective_in)
- return resp_200(msg='添加了选课信息.')
-
-
-@router.put("/{id}", response_model=Result, summary='通过 id 更新选课信息')
-async def update_elective(id: int, elective_in: ElectiveUpdate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- rowcount = await elective.update(db, id=id, obj_in=elective_in)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的选课.")
- return resp_200(msg=f"更新了 id 为 {id} 的选课信息.")
-
-
-@router.delete("/{id}", response_model=Result, summary='通过 id 删除选课信息')
-async def delete_elective(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=[])):
- rowcount = await elective.remove(db, id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的选课.")
- return resp_200(msg=f"删除了 id 为 {id} 的选课信息.")
-
-
-@router.post("/del/", response_model=Result, summary='同时删除多个选课信息')
-async def delete_electives(idList: List, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- rowcount = await elective.remove_multi(db, id_list=idList)
- if not rowcount:
- raise IdNotExist(err_desc="系统中不存在列表中的id.")
- return resp_200(msg='成功删除多个选课信息.')
-
-
-@router.post("/add/{courseId}", response_model=Result, summary='添加选课信息')
-async def create_elective_by_course_id(courseId: int, db: AsyncSession = Depends(get_db),
- user: Student = Security(get_current_user, scopes=[])):
- obj = await elective.is_exist(db, courseId=courseId, studentId=user.id)
- if obj:
- data, msg = 0, '数据已存在.'
- else:
- data, msg = 1, '添加了选课信息.'
- await elective.create(db, obj_in={'grade': 0, 'studentId': user.id, 'courseId': courseId})
- return resp_200(data=data, msg=msg)
-
-
-@router.post("/del/{courseId}", response_model=Result, summary='通过其他字段删除选课信息')
-async def del_elective_by_filed(courseId: int, db: AsyncSession = Depends(get_db),
- user: Student = Security(get_current_user, scopes=[])):
- obj = await elective.is_exist(db, courseId=courseId, studentId=user.id)
- if obj:
- data, msg = 1, '数据存在, 退课成功'
- rowcount = await elective.remove(db, id=obj.id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的选课.")
- else:
- data, msg = 0, '数据不存在, 退课失败!'
- return resp_200(data=data, msg=msg)
-
-
-@router.get("/detail/", response_model=Result, summary='获取学生选课信息详情')
-async def get_course_detail(db: AsyncSession = Depends(get_db), user: Student = Security(get_current_user, scopes=[])):
- data = await elective.get_course(db, id=user.id)
- return resp_200(data=data, msg='获取学生选课信息详情.')
diff --git a/backend/apis/common/login.py b/backend/apis/common/login.py
deleted file mode 100644
index 729219c..0000000
--- a/backend/apis/common/login.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/12/28 19:16
-# @Author : zxiaosi
-# @desc : 登录
-import json
-from datetime import timedelta
-
-from sqlalchemy.ext.asyncio import AsyncSession
-from fastapi.encoders import jsonable_encoder
-from fastapi import APIRouter, Depends, Request, Security
-from fastapi.security import OAuth2PasswordRequestForm
-
-from core import settings, create_access_token
-from db import MyRedis
-from models import Admin, Teacher, Student
-from schemas import Result, Token, AdminOut, TeacherOut, StudentOut
-from apis.deps import get_redis, get_db, get_current_user
-from utils import resp_200, SetRedis, ErrorUser, by_ip_get_address
-from utils.permission_assign import by_scopes_get_crud
-
-router = APIRouter()
-
-
-# 用户登录推荐看一下这个库: https://fastapi-users.github.io/fastapi-users/
-# 这里的登录借助的是OAuth2,存储用的JWT,与redis存储的token已经没有关系 (前端请求要发送 表单请求)
-# OAuth2中token过期时间与 设置的时间 以及 服务的开启关闭 有关, 时间到期或者服务关闭token过期
-@router.post("/login", response_model=Token, summary="docs接口文档登录 && 登录接口")
-async def login_access_token(
- request: Request,
- db: AsyncSession = Depends(get_db),
- form_data: OAuth2PasswordRequestForm = Depends()
-):
- """ 兼容OAuth2的令牌登录,为接口文档的请求获取访问令牌 """
- crud_obj = by_scopes_get_crud(form_data.scopes) # 权限分配
- _user = await crud_obj.authenticate(db, username=form_data.username, password=form_data.password)
- if not _user:
- raise ErrorUser()
-
- address = by_ip_get_address(request.client.host) # 根据ip获取地址
- await crud_obj.update(db, id=_user.id, obj_in={'address': address})
-
- access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
- token = create_access_token({"sub": str(_user.id), "scopes": form_data.scopes}, access_token_expires)
-
- try:
- await request.app.state.redis.incr('visit_num') # 用户访问量 自增1
- await request.app.state.redis.set(token, json.dumps(jsonable_encoder(_user)), access_token_expires)
- except Exception as e:
- raise SetRedis(f'Redis存储 token 失败!-- {e}')
-
- # 这里'access_token'和'token_type'一定要写,否则get_current_user依赖拿不到token
- # 可添加字段(先修改schemas/token里面的Token返回模型)
- return {"access_token": token, "token_type": "bearer"}
-
-
-@router.get("/admin/index", response_model=Result[AdminOut], summary="获取当前管理员")
-def get_current_admin(current_user: Admin = Security(get_current_user, scopes=["admin"])):
- return resp_200(data=jsonable_encoder(current_user), msg='获取当前管理员信息!')
-
-
-@router.get("/teacher/index", response_model=Result[TeacherOut], summary="获取当前教师")
-def get_current_teacher(current_user: Teacher = Security(get_current_user, scopes=["teacher"])):
- return resp_200(data=jsonable_encoder(current_user), msg='获取当前教师信息!')
-
-
-@router.get("/student/index", response_model=Result[StudentOut], summary="获取当前学生")
-def get_current_student(current_user: Student = Security(get_current_user, scopes=["student"])):
- return resp_200(data=jsonable_encoder(current_user), msg='获取当前学生信息!')
-
-
-@router.post("/logout", response_model=Result, summary="退出登录(已隐藏)", include_in_schema=False)
-async def logout_token(request: Request, redis: MyRedis = Depends(get_redis)):
- if 'authorization' in request.headers.keys():
- token = request.headers.get('authorization')[7:] # 去除token前面的 Bearer
- await redis.delete(token)
- return resp_200(msg='退出登录')
diff --git a/backend/apis/common/major.py b/backend/apis/common/major.py
deleted file mode 100644
index 0ed3ee4..0000000
--- a/backend/apis/common/major.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/1 21:17
-# @Author : zxiaosi
-# @desc : 专业表接口
-from typing import List
-
-from fastapi import APIRouter, Depends, Security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from apis.deps import get_db, get_current_user
-from schemas import MajorCreate, MajorUpdate, MajorOut as Major, Result, ResultPlus
-from crud import major
-from utils import resp_200, IdNotExist
-
-router = APIRouter()
-
-
-@router.get("/{id}", response_model=Result[Major], summary='根据 id 查询专业信息')
-async def read_major(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=[])):
- _major = await major.get(db, id)
- if not _major:
- raise IdNotExist(f"系统中不存在 id 为 {id} 的专业.")
- return resp_200(data=_major, msg=f"查询到了 id 为 {id} 的专业.")
-
-
-@router.get("/", response_model=ResultPlus[Major], summary='根据页码 pageIndex 和每页个数 pageSize 查询所有专业')
-async def read_majors(pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- """ 查询所有专业 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await major.get_number(db)
- _majors = await major.get_multi(db, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _majors}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个专业信息.")
-
-
-@router.post("/", response_model=Result, summary='添加专业信息')
-async def create_major(major_in: MajorCreate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- await major.create(db, obj_in=major_in)
- return resp_200(msg=f"添加了 id 为 {major_in.id} 的专业信息.")
-
-
-@router.put("/{id}", response_model=Result, summary='通过 id 更新专业信息')
-async def update_major(id: int, major_in: MajorUpdate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- rowcount = await major.update(db, id=id, obj_in=major_in)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的专业.")
- return resp_200(msg=f"更新了 id 为 {id} 的专业信息.")
-
-
-@router.delete("/{id}", response_model=Result, summary='通过 id 删除专业信息')
-async def delete_major(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=["admin"])):
- rowcount = await major.remove(db, id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的专业.")
- return resp_200(msg=f"删除了 id 为 {id} 的专业信息.")
-
-
-@router.post("/del/", response_model=Result, summary='同时删除多个专业信息')
-async def delete_majors(idList: List, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=["admin"])):
- rowcount = await major.remove_multi(db, id_list=idList)
- if not rowcount:
- raise IdNotExist(err_desc="系统中不存在列表中的id.")
- return resp_200(msg='同时删除多个专业信息.')
diff --git a/backend/apis/common/redis_check.py b/backend/apis/common/redis_check.py
deleted file mode 100644
index 86ab789..0000000
--- a/backend/apis/common/redis_check.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/5 13:55
-# @Author : zxiaosi
-# @desc : 检查redis
-from fastapi import APIRouter, Depends
-
-from db import MyRedis
-from apis.deps import get_redis
-
-router = APIRouter()
-
-
-@router.get("/health-check")
-async def health_check(redis: MyRedis = Depends(get_redis)):
- try:
- # key:test:keys => key/test/keys
- value = await redis.get('request_num')
- msg = "开启Redis成功!!!"
- except Exception as e: # noqa: E722
- value = 0
- msg = f"对不起,不能打开Redis!!! {e}"
- return {"request": f"总计请求次数 {value}", "msg": msg}
diff --git a/backend/apis/common/student.py b/backend/apis/common/student.py
deleted file mode 100644
index 23c781d..0000000
--- a/backend/apis/common/student.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/17 11:12
-# @Author : zxiaosi
-# @desc : 学生表接口
-from typing import List
-from fastapi import APIRouter, Depends, Security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from apis.deps import get_db, get_current_user
-from schemas import StudentCreate, StudentUpdate, StudentOut as Student, Result, ResultPlus
-from crud import student
-from utils import resp_200, IdNotExist
-
-router = APIRouter()
-
-
-@router.get("/{id}", response_model=Result[Student], summary='根据 id 查询学生信息')
-async def read_student(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=[])):
- _student = await student.get(db, id)
- if not _student:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的学生.")
- return resp_200(data=_student, msg=f"查询到了 id 为 {id} 的学生.")
-
-
-@router.get("/", response_model=ResultPlus[Student], summary='根据页码 pageIndex 和每页个数 pageSize 查询所有学生')
-async def read_students(pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- """ 查询所有院系 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await student.get_number(db)
- _students = await student.get_multi(db, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _students}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个学生信息.")
-
-
-@router.post("/", response_model=Result, summary='添加学生信息')
-async def create_student(student_in: StudentCreate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- await student.create(db, obj_in=student_in)
- return resp_200(msg=f"添加了 id 为 {student_in.id} 的学生信息.")
-
-
-@router.put("/{id}", response_model=Result, summary='通过 id 更新学生信息')
-async def update_student(id: int, student_in: StudentUpdate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await student.update(db, id=id, obj_in=student_in)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的学生.")
- return resp_200(msg=f"更新了 id 为 {id} 的学生信息.")
-
-
-@router.delete("/{id}", response_model=Result, summary='通过 id 删除学生信息')
-async def delete_student(id: int, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await student.remove(db, id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的学生.")
- return resp_200(msg=f"删除了 id 为 {id} 的学生信息.")
-
-
-@router.post("/del/", response_model=Result, summary='同时删除多个学生信息')
-async def delete_students(idList: List, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await student.remove_multi(db, id_list=idList)
- if not rowcount:
- raise IdNotExist(err_desc="系统中不存在列表中的id.")
- return resp_200(msg='成功删除多个学生信息.')
-
-
-# @router.get("/detail/{id}", summary='得到详细信息')
-# async def get_detail(id: int, db: AsyncSession = Depends(get_db)):
-# _data = await student.get_detail(db, id)
-# return _data
diff --git a/backend/apis/common/taught.py b/backend/apis/common/taught.py
deleted file mode 100644
index 28f6059..0000000
--- a/backend/apis/common/taught.py
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/5/10 21:08
-# @Author : zxiaosi
-# @desc : 讲授表接口
-from typing import List
-from fastapi import APIRouter, Depends, Security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from apis.deps import get_db, get_current_user
-from models import Teacher
-from schemas import TaughtCreate, TaughtUpdate, TaughtOut as Taught, Result, ResultPlus
-from crud import taught
-from utils import resp_200, IdNotExist
-
-router = APIRouter()
-
-
-@router.get("/{id}", response_model=Result[Taught], summary='根据 id 查询讲授信息')
-async def read_taught(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=[])):
- _taught = await taught.get(db, id)
- if not _taught:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的讲授.")
- return resp_200(data=_taught, msg=f"查询到了 id 为 {id} 的讲授.")
-
-
-@router.get("/", response_model=ResultPlus[Taught], summary='根据页码 pageIndex 和每页个数 pageSize 查询所有讲授')
-async def read_taughts(pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- """ 查询所有院系 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await taught.get_number(db)
- _taughts = await taught.get_multi(db, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _taughts}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个讲授信息.")
-
-
-@router.post("/", response_model=Result, summary='添加讲授信息')
-async def create_taught(taught_in: TaughtCreate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- await taught.create(db, obj_in=taught_in)
- return resp_200(msg="添加了讲授信息.")
-
-
-@router.put("/{id}", response_model=Result, summary='通过 id 更新讲授信息')
-async def update_taught(id: int, taught_in: TaughtUpdate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await taught.update(db, id=id, obj_in=taught_in)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的讲授.")
- return resp_200(msg=f"更新了 id 为 {id} 的讲授信息.")
-
-
-@router.delete("/{id}", response_model=Result, summary='通过 id 删除讲授信息')
-async def delete_taught(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=['admin'])):
- rowcount = await taught.remove(db, id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的讲授.")
- return resp_200(msg=f"删除了 id 为 {id} 的讲授信息.")
-
-
-@router.post("/del/", response_model=Result, summary='同时删除多个讲授信息')
-async def delete_taughts(idList: List, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await taught.remove_multi(db, id_list=idList)
- if not rowcount:
- raise IdNotExist(err_desc="系统中不存在列表中的id.")
- return resp_200(msg='成功删除多个讲授信息.')
-
-
-@router.get("/detail/", response_model=Result, summary='获取教师讲授信息详情')
-async def get_course_detail(db: AsyncSession = Depends(get_db), user: Teacher = Security(get_current_user, scopes=[])):
- data = await taught.get_course(db, id=user.id)
- return resp_200(data=data, msg='获取教师讲授信息详情.')
diff --git a/backend/apis/common/teacher.py b/backend/apis/common/teacher.py
deleted file mode 100644
index 29cc25a..0000000
--- a/backend/apis/common/teacher.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/16 9:48
-# @Author : zxiaosi
-# @desc : 教师表接口
-from typing import List
-
-from fastapi import APIRouter, Depends, Security
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from apis.deps import get_db, get_current_user
-from schemas import TeacherCreate, TeacherUpdate, TeacherOut as Teacher, Result, ResultPlus
-from crud import teacher
-from utils import resp_200, IdNotExist
-
-router = APIRouter()
-
-
-@router.get("/{id}", response_model=Result[Teacher], summary='根据 id 查询教师信息')
-async def read_teacher(id: int, db: AsyncSession = Depends(get_db), user=Security(get_current_user, scopes=[])):
- _teacher = await teacher.get(db, id)
- if not _teacher:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的教师.")
- return resp_200(data=_teacher, msg=f"查询到了 id 为 {id} 的教师.")
-
-
-@router.get("/", response_model=ResultPlus[Teacher], summary='根据页码 pageIndex 和每页个数 pageSize 查询所有教师')
-async def read_teachers(pageIndex: int = 1, pageSize: int = 10, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=[])):
- """ 查询所有教师 (pageIndex = -1 && pageSize = -1 表示查询所有) """
- _count = await teacher.get_number(db)
- _teachers = await teacher.get_multi(db, pageIndex, pageSize)
- return resp_200(data={"count": _count, "list": _teachers}, msg=f"查询了第 {pageIndex} 页中的 {pageSize} 个教师信息.")
-
-
-@router.post("/", response_model=Result, summary='添加教师信息')
-async def create_teacher(teacher_in: TeacherCreate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- await teacher.create(db, obj_in=teacher_in)
- return resp_200(msg=f"添加了 id 为 {teacher_in.id} 的教师信息.")
-
-
-@router.put("/{id}", response_model=Result, summary='通过 id 更新教师信息')
-async def update_teacher(id: int, teacher_in: TeacherUpdate, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await teacher.update(db, id=id, obj_in=teacher_in)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的教师.")
- return resp_200(msg=f"更新了 id 为 {id} 的教师信息.")
-
-
-@router.delete("/{id}", response_model=Result, summary='通过 id 删除教师信息')
-async def delete_teacher(id: int, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await teacher.remove(db, id)
- if not rowcount:
- raise IdNotExist(err_desc=f"系统中不存在 id 为 {id} 的教师.")
- return resp_200(msg=f"删除了 id 为 {id} 的教师信息.")
-
-
-@router.post("/del/", response_model=Result, summary='同时删除多个教师信息')
-async def delete_teachers(idList: List, db: AsyncSession = Depends(get_db),
- user=Security(get_current_user, scopes=['admin'])):
- rowcount = await teacher.remove_multi(db, id_list=idList)
- if not rowcount:
- raise IdNotExist(err_desc="系统中不存在列表中的id.")
- return resp_200(msg='成功删除多个教师信息.')
diff --git a/backend/apis/common/upload.py b/backend/apis/common/upload.py
deleted file mode 100644
index 191a372..0000000
--- a/backend/apis/common/upload.py
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/3/4 11:42
-# @Author : zxiaosi
-# @desc : 上传图片 https://fastapi.tiangolo.com/zh/tutorial/request-files/?h=up#uploadfile
-import shutil
-from pathlib import Path
-from tempfile import NamedTemporaryFile
-from fastapi import APIRouter, Depends, File, UploadFile
-from sqlalchemy.orm import Session
-
-import crud
-from core import settings
-from core.logger import logger
-from apis.deps import get_db, get_current_user
-from models import Admin
-from utils import resp_200
-from utils.create_dir import create_dir
-
-router = APIRouter()
-
-
-@router.post("/upload/file/", summary="上传头像")
-async def upload_image(
- file: UploadFile = File(...),
- db: Session = Depends(get_db),
- current_user: Admin = Depends(get_current_user)
-):
- static_path = create_dir(settings.STATIC_DIR)
- logger.info(f"用户 {current_user.name} 正在上传图片 {file.filename}.")
- try:
- suffix = Path(file.filename).suffix
- with NamedTemporaryFile(delete=False, suffix=suffix, dir=static_path) as tmp:
- shutil.copyfileobj(file.file, tmp)
- tmp_file_name = Path(tmp.name).name
- finally:
- file.file.close()
- user = crud.admin.update(db, db_obj=current_user,
- obj_in={'image': f"{settings.BASE_URL}/{settings.STATIC_DIR}/{tmp_file_name}"})
- return resp_200(data=user, msg='上传成功!')
diff --git a/backend/apis/debug.py b/backend/apis/debug.py
new file mode 100644
index 0000000..0871067
--- /dev/null
+++ b/backend/apis/debug.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+# _*_ coding: utf-8 _*_
+# @Author : zxiaosi
+# @Time : 2022/7/1 14:46
+# @desc : 调试接口
+from fastapi import APIRouter
+
+router = APIRouter()
+
+
+@router.get("/")
+async def root():
+ return {"message": "Hello World"}
+
+
+@router.get("/hello/{name}")
+async def say_hello(name: str):
+ return {"message": f"Hello {name}"}
diff --git a/backend/apis/deps.py b/backend/apis/deps.py
deleted file mode 100644
index 0c74d5a..0000000
--- a/backend/apis/deps.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/10/15 20:10
-# @Author : zxiaosi
-# @desc : 依赖项
-from typing import AsyncGenerator
-
-from fastapi import Depends, Security, HTTPException
-from fastapi.security import SecurityScopes, OAuth2PasswordBearer
-from sqlalchemy.ext.asyncio import AsyncSession
-from starlette.requests import Request
-
-from core import check_jwt_token, settings
-from db import async_session, MyRedis
-from schemas import TokenData
-from utils import UserNotExist, PermissionNotEnough
-from utils.permission_assign import by_scopes_get_crud, handle_oauth2_scopes
-
-get_token = OAuth2PasswordBearer(tokenUrl=f"{settings.API_PREFIX}/login", scopes=handle_oauth2_scopes())
-
-
-async def get_db() -> AsyncGenerator[AsyncSession, None]:
- """ sql连接会话 """
- async with async_session() as session:
- yield session
-
-
-async def get_redis(request: Request) -> MyRedis:
- """ redis连接对象 """
- return await request.app.state.redis
-
-
-async def get_current_user(
- security_scopes: SecurityScopes,
- db: AsyncSession = Depends(get_db),
- token: str = Depends(get_token)
-):
- """ 得到当前用户(docs接口文档) """
- payload = await check_jwt_token(token) # 检验token是否过期
- token_scopes = payload.get("scopes", []) # 得不到值,返回[]
- token_data = TokenData(scopes=token_scopes, sub=payload.get("sub")) # token存储的用户权限
- crud_obj = by_scopes_get_crud(token_scopes) # 验证用户是否存在
- user = await crud_obj.get(db, id=payload.get("sub"))
- if not user:
- raise UserNotExist()
- for scope in security_scopes.scopes: # 勾选的用户权限
- if scope not in token_data.scopes:
- raise PermissionNotEnough('权限不足,拒绝访问')
- return user
-
-# def get_current_active_user(current_user: Admin = Security(get_current_user, scopes=["admin"])):
-# """ 得到当前登录用户 """
-# if not admin.is_active_def(current_user):
-# raise HTTPException(status_code=400, detail="用户未登录!!!")
-# return current_user
diff --git a/backend/core/__init__.py b/backend/core/__init__.py
index d32b14e..9803b20 100644
--- a/backend/core/__init__.py
+++ b/backend/core/__init__.py
@@ -1,7 +1,5 @@
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_
-# @Time : 2021/10/18 19:35
# @Author : zxiaosi
-# @desc : 核心文件
-from .config import settings
-from .security import create_access_token, verify_password, get_password_hash, check_jwt_token
+# @Time : 2022/7/1 14:41
+# @desc :
diff --git a/backend/core/config.py b/backend/core/config.py
index 251fcb8..785de3e 100644
--- a/backend/core/config.py
+++ b/backend/core/config.py
@@ -1,50 +1,16 @@
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 17:02
# @Author : zxiaosi
+# @Time : 2022/7/1 14:47
# @desc : 配置文件
-import secrets
-from typing import Union, List
-from pydantic import BaseSettings, AnyHttpUrl
-
-project_desc = """
- 🎉 管理员接口汇总 🎉
- ✨ 账号: admin ✨
- ✨ 密码: 123456 ✨
- ✨ 权限(scopes): admin ✨
-"""
+from pydantic import BaseSettings
class Settings(BaseSettings):
- PROJECT_DESC: str = project_desc # 描述
- PROJECT_VERSION: Union[int, str] = 5.0 # 版本
- BASE_URL: AnyHttpUrl = "http://127.0.0.1:8000" # 开发环境
+ PROJECT_DESC: str = "接口文档" # 描述
+ PROJECT_VERSION: int | str = 1.0 # 版本
API_PREFIX: str = "/api" # 接口前缀
- STATIC_DIR: str = 'static' # 静态文件目录
- GLOBAL_ENCODING: str = 'utf-8' # 全局编码
- CORS_ORIGINS: List[AnyHttpUrl] = ["http://localhost:3000", "http://8.136.82.204:8001"] # 跨域请求
-
- SECRET_KEY: str = secrets.token_urlsafe(32) # 密钥(每次重启服务密钥都会改变, token解密失败导致过期, 可设置为常量)
- ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 1 # token过期时间: 60 minutes * 24 hours * 1 days = 1 days
-
- REDIS_URI: str = "redis://:123456@localhost:6379/1" # redis
- # DATABASE_URI: str = "sqlite+aiosqlite:///./sql_app.db?check_same_thread=False" # Sqlite(异步)
- DATABASE_URI: str = "mysql+asyncmy://root:123456@localhost:3306/elective_system?charset=utf8" # MySQL(异步)
- # DATABASE_URI: str = "postgresql+asyncpg://postgres:123456@localhost:5432/postgres" # PostgreSQL(异步)
- DATABASE_ECHO: bool = False # 是否打印数据库日志 (可看到创建表、表数据增删改查的信息)
-
- LOGGER_DIR: str = "logs" # 日志文件夹名
- LOGGER_NAME: str = '{time:YYYY-MM-DD_HH-mm-ss}.log' # 日志文件名 (时间格式)
- LOGGER_LEVEL: str = 'DEBUG' # 日志等级: ['DEBUG' | 'INFO']
- LOGGER_ROTATION: str = "12:00" # 日志分片: 按 时间段/文件大小 切分日志. 例如 ["500 MB" | "12:00" | "1 week"]
- LOGGER_RETENTION: str = "7 days" # 日志保留的时间: 超出将删除最早的日志. 例如 ["1 days"]
-
- # 权限数据表 (格式务必为 {'名称', '描述'})
- PERMISSION_DATA: List[dict] = [{'admin': '管理员权限'}, {'teacher': '教师权限'}, {'student': '学生权限'}]
-
- class Config:
- case_sensitive = True # 区分大小写
settings = Settings()
diff --git a/backend/core/logger.py b/backend/core/logger.py
deleted file mode 100644
index 773c64e..0000000
--- a/backend/core/logger.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/8 10:44
-# @Author : zxiaosi
-# @desc : 日志
-import os
-
-from loguru import logger
-
-from core import settings
-from utils import create_dir
-
-
-# 创建日志文件名
-def logger_file() -> str:
- """ 创建日志文件名 """
- log_path = create_dir(settings.LOGGER_DIR)
-
- """ 保留日志文件夹下最大个数(本地调试用)
- 本地调式需要多次重启, 日志轮转片不会生效 """
- file_list = os.listdir(log_path)
- if len(file_list) > 3:
- os.remove(os.path.join(log_path, file_list[0]))
-
- # 日志输出路径
- return os.path.join(log_path, settings.LOGGER_NAME)
-
-
-# 详见: https://loguru.readthedocs.io/en/stable/overview.html#features
-logger.add(
- logger_file(),
- encoding=settings.GLOBAL_ENCODING,
- level=settings.LOGGER_LEVEL,
- rotation=settings.LOGGER_ROTATION,
- retention=settings.LOGGER_RETENTION,
- enqueue=True
-)
diff --git a/backend/core/security.py b/backend/core/security.py
deleted file mode 100644
index ac998a4..0000000
--- a/backend/core/security.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/10/19 19:49
-# @Author : zxiaosi
-# @desc : 安全配置 https://fastapi.tiangolo.com/zh/advanced/security/oauth2-scopes/#global-view
-from typing import Any, Union, Optional
-from datetime import datetime, timedelta
-from fastapi import Header
-from jose import jwt
-from passlib.context import CryptContext
-
-from core import settings
-from utils import AccessTokenFail
-
-ALGORITHM = "HS256" # 加密算法
-
-pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # 加密密码
-
-
-def get_password_hash(password: str) -> str:
- """ 加密明文密码 """
- return pwd_context.hash(password)
-
-
-def verify_password(password: str, hashed_password: str) -> bool:
- """ 验证明文密码 与 加密后的密码 是否一致 """
- return pwd_context.verify(password, hashed_password)
-
-
-def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
- """
- 生成token
-
- :param data: 存储数据
- :param expires_delta: 有效时间
- :return: 加密后的token
- """
- to_encode = data.copy()
- if expires_delta:
- expire = datetime.utcnow() + expires_delta
- else:
- expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
- to_encode.update({"exp": expire}) # eg: {'sub': '1', scopes: ['items'] 'exp': '123'}
- encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
- return encoded_jwt
-
-
-# https://www.cnblogs.com/CharmCode/p/14191112.html?ivk_sa=1024320u
-async def check_jwt_token(token: Optional[str] = Header(...)) -> Union[str, Any]:
- """ 解密token """
- try:
- payload = jwt.decode(token=token, key=settings.SECRET_KEY, algorithms=[ALGORITHM])
- return payload
- except Exception as e: # jwt.JWTError, jwt.ExpiredSignatureError, AttributeError
- raise AccessTokenFail(f'token已过期! -- {e}')
-
-
-if __name__ == '__main__':
- # 对 '123456' 加密后得到的值不相同
- print(get_password_hash('123456'))
-
- # 但 加密前 和 加密后 验证是一致的
- print(verify_password('123456', '$2b$12$I5lfn4eO8M0oH4yYQWjSQ.t4VJz9cGKXA.ht6syIG6tAXmbnQywqa')) # True
- print(verify_password('123456', '$2b$12$h58wHhABGgNSRfQCqYFod.0mycfuLZIWQmtvKgP9s0VyYs78In6b.')) # True
diff --git a/backend/crud/__init__.py b/backend/crud/__init__.py
deleted file mode 100644
index cfdf617..0000000
--- a/backend/crud/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/10/14 16:59
-# @Author : zxiaosi
-# @desc : 数据库的增删改查操作(dao层)
-from .base import ModelType, CRUDBase
-from .department import department
-from .major import major
-from .teacher import teacher
-from .student import student
-from .course import course
-from .elective import elective
-from .taught import taught
-from .admin import admin
diff --git a/backend/crud/admin.py b/backend/crud/admin.py
deleted file mode 100644
index 4e7bbf7..0000000
--- a/backend/crud/admin.py
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/17 11:05
-# @Author : zxiaosi
-# @desc : 操作管理员表
-from typing import Union, Dict, Any
-from sqlalchemy import insert, update
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from core import get_password_hash
-from crud import CRUDBase
-from models import Admin
-from schemas import AdminCreate, AdminUpdate
-
-
-class CRUDAdmin(CRUDBase[Admin, AdminCreate, AdminUpdate]):
- async def create(self, db: AsyncSession, obj_in: AdminCreate) -> int:
- """ 添加管理员信息 """
- setattr(obj_in, 'id', int(obj_in.id)) # postgresql 字段类型限制
- obj_in_data = {}
- for k, v in obj_in.dict().items(): # 排除空值
- if v:
- if k == 'password':
- obj_in_data['hashed_password'] = get_password_hash(obj_in.password)
- else:
- obj_in_data[k] = v
- sql = insert(self.model).values(obj_in_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- async def update(self, db: AsyncSession, id: int, obj_in: Union[AdminUpdate, Dict[str, Any]]) -> int:
- """ 更新管理员信息 """
- if isinstance(obj_in, dict): # 判断对象是否为字典类型(更新部分字段)
- teacher_data = obj_in
- else:
- teacher_data = obj_in.dict(exclude_unset=True)
- obj_data = {}
- for k, v in teacher_data.items(): # 排除空值
- if v:
- if k == 'password':
- obj_data['hashed_password'] = get_password_hash(obj_in.password)
- else:
- obj_data[k] = v
- sql = update(self.model).where(self.model.id == id).values(obj_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
-
-admin = CRUDAdmin(Admin)
diff --git a/backend/crud/base.py b/backend/crud/base.py
deleted file mode 100644
index ea37dbf..0000000
--- a/backend/crud/base.py
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/10/14 17:00
-# @Author : zxiaosi
-# @desc : 封装数据库增删改查方法
-from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
-from pydantic import BaseModel
-from sqlalchemy import func, distinct, select, insert, update, desc, delete
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from core import verify_password
-from models import Base
-from utils import obj_as_dict, list_obj_as_dict
-
-ModelType = TypeVar("ModelType", bound=Base)
-CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
-UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
-
-
-# db.scalar(sql) 返回的是标量(原始数据)
-# db.execute(sql) 返回的是元组 ()
-# db.scalars(sql).all() [, , ]
-# db.execute(sql).fetchall() [(,), (,), (,)]
-class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
- """ CRUD 增 查 改 删 """
-
- def __init__(self, model: Type[ModelType]):
- self.model = model
-
- async def get(self, db: AsyncSession, id: Any) -> Optional[ModelType]:
- """ 通过 id 获取对象 """
- sql = select(self.model).where(self.model.id == id)
- result = await db.scalar(sql)
- # print(obj_as_dict(await db.scalar(sql)))
- await db.close() # 释放会话
- return result
-
- async def get_multi(self, db: AsyncSession, pageIndex: int = 1, pageSize: int = 10) -> List[ModelType]:
- """ 获取第 pageIndex 页的 pageSize 数据 """
- if pageIndex == -1 and pageSize == -1:
- sql = select(self.model).order_by(desc(self.model.id))
- else:
- sql = select(self.model).offset((pageIndex - 1) * pageSize).limit(pageSize).order_by(desc(self.model.id))
- result = await db.scalars(sql)
- # print(list_obj_as_dict(await db.scalars(sql)))
- await db.close() # 释放会话
- return result.all()
-
- async def get_number(self, db: AsyncSession) -> int:
- """ 获取表的总条数 """
- sql = select(func.count(distinct(self.model.id)))
- result = await db.scalar(sql)
- await db.close() # 释放会话
- return result
-
- async def create(self, db: AsyncSession, obj_in: CreateSchemaType) -> int:
- """ 添加对象 """
- if self.model.__tablename__ not in ['taught', 'elective']:
- setattr(obj_in, 'id', int(obj_in.id)) # postgresql 字段类型限制
- sql = insert(self.model).values(obj_in.dict())
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- async def update(self, db: AsyncSession, id: int, obj_in: Union[UpdateSchemaType, Dict[str, Any]]) -> int:
- """ 通过 id 更新对象 """
- if isinstance(obj_in, dict): # 判断对象是否为字典类型(更新部分字段)
- obj_data = obj_in
- else:
- obj_data = obj_in.dict()
- sql = update(self.model).where(self.model.id == id).values(obj_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- async def remove(self, db: AsyncSession, id: int) -> int:
- """ 通过 id 删除对象 """
- sql = delete(self.model).where(self.model.id == id)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- async def remove_multi(self, db: AsyncSession, id_list: list):
- """ 同时删除多个对象 """
- id_list = [int(i) for i in id_list] # postgresql 字段类型限制
- sql = delete(self.model).where(self.model.id.in_(id_list))
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- async def get_multi_relation(self, db: AsyncSession):
- """ 获取关系字段 """
- sql = select(self.model.id, self.model.name).order_by(desc(self.model.id)).distinct()
- result = await db.execute(sql)
- await db.close()
- return result.fetchall()
-
- async def get_by_name(self, db: AsyncSession, name: str) -> Optional[ModelType]:
- """ 通过名字得到用户 """
- sql = select(self.model).where(self.model.name == name)
- result = await db.scalar(sql)
- await db.close() # 释放会话
- return result
-
- async def authenticate(self, db: AsyncSession, username: str, password: str) -> Optional[ModelType]:
- """ 验证用户 """
- user = await self.get_by_name(db, name=username)
- if not user:
- return None
- if not verify_password(password, user.hashed_password):
- return None
- return user
-
- async def sort(self, db: AsyncSession, name: str, pageIndex: int = 1, pageSize: int = 10) -> List[ModelType]:
- """ 验证用户 """
- filed_name = self.model.__table__.c[name]
- if pageIndex == -1 and pageSize == -1:
- sql = select(self.model).order_by(desc(filed_name))
- else:
- sql = select(self.model).offset((pageIndex - 1) * pageSize).limit(pageSize).order_by(desc(filed_name))
- result = await db.scalars(sql)
- # print(list_obj_as_dict(await db.scalars(sql)))
- await db.close() # 释放会话
- return result.all()
diff --git a/backend/crud/course.py b/backend/crud/course.py
deleted file mode 100644
index 3f02f4c..0000000
--- a/backend/crud/course.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/17 16:29
-# @Author : zxiaosi
-# @desc : 操作课程表
-from crud.base import CRUDBase
-from models import Course
-from schemas import CourseCreate, CourseUpdate
-
-
-class CRUDCourse(CRUDBase[Course, CourseCreate, CourseUpdate]):
- pass
-
-
-course = CRUDCourse(Course)
diff --git a/backend/crud/department.py b/backend/crud/department.py
deleted file mode 100644
index 8ec4aea..0000000
--- a/backend/crud/department.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/10/28 19:01
-# @Author : zxiaosi
-# @desc : 操作院系表
-from crud import CRUDBase
-from models import Department
-from schemas import DepartmentCreate, DepartmentUpdate
-
-
-class CRUDDepartment(CRUDBase[Department, DepartmentCreate, DepartmentUpdate]):
- pass
-
-
-department = CRUDDepartment(Department)
diff --git a/backend/crud/elective.py b/backend/crud/elective.py
deleted file mode 100644
index 2af6690..0000000
--- a/backend/crud/elective.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/18 10:09
-# @Author : zxiaosi
-# @desc : 操作选课表
-from typing import Any, Union
-
-from sqlalchemy import select, insert
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from crud.base import CRUDBase
-from models import Elective, Taught, Student, Teacher, Course
-from schemas import ElectiveCreate, ElectiveUpdate
-from utils import obj_as_dict, list_obj_as_dict
-
-
-class CRUDElective(CRUDBase[Elective, ElectiveCreate, ElectiveUpdate]):
- async def get_course(self, db: AsyncSession, id: Any) -> Any:
- """ 得到课程详情 """
- fields = [self.model.courseId, Course.name.label('courseName'),
- Teacher.id.label('teacherId'), Teacher.name.label('teacherName'),
- self.model.grade, self.model.create_time]
- sql = select(*fields).where(self.model.studentId == int(id)) \
- .join(Course, self.model.courseId == Course.id) \
- .join(Taught, self.model.courseId == Taught.courseId, isouter=True) \
- .join(Teacher, Taught.teacherId == Teacher.id, isouter=True)
- print(id, sql)
- result = await db.execute(sql)
- # print(list_obj_as_dict(result.all()))
- await db.close() # 释放会话
- return result.fetchall()
-
- async def is_exist(self, db: AsyncSession, studentId: int, courseId: int) -> int:
- """ 判断数据是否已经存在 """
- sql1 = select(self.model) \
- .where(self.model.studentId == studentId) \
- .where(self.model.courseId == courseId)
- result = await db.scalar(sql1)
- return result
-
- async def create(self, db: AsyncSession, obj_in: Union[Elective]) -> int:
- """ 添加对象 """
- if isinstance(obj_in, dict): # 判断对象是否为字典类型(更新部分字段)
- obj_data = obj_in
- else:
- obj_data = obj_in.dict()
- sql = insert(self.model).values(obj_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
-
-elective = CRUDElective(Elective)
diff --git a/backend/crud/major.py b/backend/crud/major.py
deleted file mode 100644
index 3403143..0000000
--- a/backend/crud/major.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/1 21:03
-# @Author : zxiaosi
-# @desc : 操作专业表
-from crud import CRUDBase
-from models import Major
-from schemas import MajorCreate, MajorUpdate
-
-
-class CRUDMajor(CRUDBase[Major, MajorCreate, MajorUpdate]):
- pass
-
-
-major = CRUDMajor(Major)
diff --git a/backend/crud/student.py b/backend/crud/student.py
deleted file mode 100644
index 74baeaa..0000000
--- a/backend/crud/student.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/17 11:05
-# @Author : zxiaosi
-# @desc : 操作学生表
-from typing import Union, Dict, Any
-
-from sqlalchemy import update, insert, select
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from core import get_password_hash
-from crud import CRUDBase
-from models import Student, Major
-from schemas import StudentCreate, StudentUpdate
-from utils import obj_as_dict
-
-
-class CRUDStudent(CRUDBase[Student, StudentCreate, StudentUpdate]):
- async def create(self, db: AsyncSession, obj_in: StudentCreate) -> int:
- """ 添加学生信息 """
- setattr(obj_in, 'id', int(obj_in.id)) # postgresql 字段类型限制
- obj_in_data = {}
- for k, v in obj_in.dict().items(): # 排除空值
- if v:
- if k == 'password':
- obj_in_data['hashed_password'] = get_password_hash(obj_in.password)
- else:
- obj_in_data[k] = v
- sql = insert(self.model).values(obj_in_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- async def update(self, db: AsyncSession, id: int, obj_in: Union[StudentUpdate, Dict[str, Any]]) -> int:
- """ 更新学生信息 """
- if isinstance(obj_in, dict): # 判断对象是否为字典类型(更新部分字段)
- teacher_data = obj_in
- else:
- teacher_data = obj_in.dict(exclude_unset=True)
- obj_data = {}
- for k, v in teacher_data.items(): # 排除空值
- if v:
- if k == 'password':
- obj_data['hashed_password'] = get_password_hash(obj_in.password)
- else:
- obj_data[k] = v
- sql = update(self.model).where(self.model.id == id).values(obj_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- # async def get_detail(self, db: AsyncSession, id: int):
- # sql = select(self.model, Major.name.label("major_name")) \
- # .join(Major, self.model.majorId == Major.id) \
- # .where(self.model.id == id)
- # result = await db.execute(sql)
- # await db.close() # 释放会话
- # return result.fetchone()
-
-
-student = CRUDStudent(Student)
diff --git a/backend/crud/taught.py b/backend/crud/taught.py
deleted file mode 100644
index 99f560a..0000000
--- a/backend/crud/taught.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/5/10 21:00
-# @Author : zxiaosi
-# @desc : 操作讲授表
-from typing import Any
-
-from sqlalchemy import select
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from crud import CRUDBase
-from models import Taught, Course, Student, Elective
-from schemas import TaughtCreate, TaughtUpdate
-
-
-class CRUDTaught(CRUDBase[Taught, TaughtCreate, TaughtUpdate]):
- async def get_course(self, db: AsyncSession, id: Any) -> Any:
- """ 得到课程详情 """
- fields = [self.model.courseId, Course.name.label('courseName'),
- Elective.studentId, Student.name.label('studentName'),
- Elective.id, Elective.grade, Elective.update_time]
- sql = select(*fields).where(self.model.teacherId == int(id)) \
- .join(Elective, self.model.courseId == Elective.courseId, isouter=True) \
- .join(Course, self.model.courseId == Course.id) \
- .join(Student, Elective.studentId == Student.id)
- result = await db.execute(sql)
- # print(list_obj_as_dict(result.all()))
- await db.close() # 释放会话
- return result.fetchall()
-
-
-taught = CRUDTaught(Taught)
diff --git a/backend/crud/teacher.py b/backend/crud/teacher.py
deleted file mode 100644
index 9352fb2..0000000
--- a/backend/crud/teacher.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/11/15 19:53
-# @Author : zxiaosi
-# @desc : 操作教师表
-from typing import Union, Dict, Any
-
-from sqlalchemy import insert, update
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from core import get_password_hash
-from crud.base import CRUDBase
-from models import Teacher
-from schemas import TeacherCreate, TeacherUpdate
-
-
-class CRUDTeacher(CRUDBase[Teacher, TeacherCreate, TeacherUpdate]):
- async def create(self, db: AsyncSession, obj_in: TeacherCreate) -> int:
- """ 添加教师信息 """
- setattr(obj_in, 'id', int(obj_in.id)) # postgresql 字段类型限制
- obj_in_data = {}
- for k, v in obj_in.dict().items(): # 排除空值
- if v:
- if k == 'password':
- obj_in_data['hashed_password'] = get_password_hash(obj_in.password)
- else:
- obj_in_data[k] = v
- sql = insert(self.model).values(obj_in_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
- async def update(self, db: AsyncSession, id: int, obj_in: Union[TeacherUpdate, Dict[str, Any]]) -> int:
- """ 更新教师信息 """
- if isinstance(obj_in, dict): # 判断对象是否为字典类型(更新部分字段)
- teacher_data = obj_in
- else:
- teacher_data = obj_in.dict(exclude_unset=True)
- obj_data = {}
- for k, v in teacher_data.items(): # 排除空值
- if v:
- if k == 'password':
- obj_data['hashed_password'] = get_password_hash(obj_in.password)
- else:
- obj_data[k] = v
- sql = update(self.model).where(self.model.id == id).values(obj_data)
- result = await db.execute(sql)
- await db.commit()
- return result.rowcount
-
-
-teacher = CRUDTeacher(Teacher)
diff --git a/backend/db/__init__.py b/backend/db/__init__.py
deleted file mode 100644
index 485a4b0..0000000
--- a/backend/db/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 17:14
-# @Author : zxiaosi
-# @desc : 初始数据库以及表数据
-from .session import engine, async_session
-from .redis import MyRedis, init_redis_pool
-from .init_db import init_db, drop_db, init_data
diff --git a/backend/db/data.py b/backend/db/data.py
deleted file mode 100644
index 6743991..0000000
--- a/backend/db/data.py
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/25 23:00
-# @Author : zxiaosi
-# @desc : 假数据
-""" 免责声明: 数据来自网络,如有侵权,请联系删除!!! """
-from datetime import date
-
-departmentData = [
- {'id': 1001, 'name': '计算机工程学院', 'chairman': '张强', 'phone': '13312312312'},
- {'id': 1002, 'name': '医学院', 'chairman': '马建国', 'phone': '13312312312'},
-]
-
-majorData = [
- # 计算机工程学院
- {'id': 100101, 'name': '计算机科学与技术', 'assistant': '钱伟', 'phone': '13312312312', 'departmentId': 1001},
- {'id': 100102, 'name': '物联网工程', 'assistant': '吴晓红', 'phone': '13312312312', 'departmentId': 1001},
- {'id': 100103, 'name': '软件工程技术', 'assistant': '柯刚', 'phone': '13312312312', 'departmentId': 1001},
- # 医学院
- {'id': 100201, 'name': '中药学专业', 'assistant': '赵露', 'phone': '13312312312', 'departmentId': 1002},
- {'id': 100202, 'name': '护理学专业', 'assistant': '朱壮', 'phone': '13312312312', 'departmentId': 1002},
- {'id': 100203, 'name': '针灸推拿学专业', 'assistant': '李勇', 'phone': '13312312312', 'departmentId': 1002},
-]
-
-studentData = [
- # Sqlite: 'birthday': date(1999, 9, 9)
- # Mysql: 'birthday': '1999-09-09'
-
- # 计算机科学与技术
- {'id': 1810010101, 'name': '王芳', 'sex': '1', 'birthday': date(1999, 9, 9), 'majorId': 100101},
- {'id': 1810010102, 'name': '孙悟空', 'sex': '0', 'birthday': date(1999, 5, 23), 'majorId': 100101},
- {'id': 1810010103, 'name': '猪悟能', 'sex': '0', 'birthday': date(1998, 2, 12), 'majorId': 100101},
- {'id': 1810010104, 'name': '唐三葬', 'sex': '0', 'birthday': date(2000, 1, 7), 'majorId': 100101},
- {'id': 1810010105, 'name': '张悟本能', 'sex': '0', 'birthday': date(2000, 3, 29), 'majorId': 100101},
- # 物联网工程
- {'id': 1810010201, 'name': '宋清江', 'sex': '1', 'birthday': date(2000, 2, 10), 'majorId': 100102},
- {'id': 1810010202, 'name': '甄无用', 'sex': '0', 'birthday': date(1999, 6, 21), 'majorId': 100102},
- {'id': 1810010203, 'name': '关不胜', 'sex': '0', 'birthday': date(1999, 7, 19), 'majorId': 100102},
- {'id': 1810010204, 'name': '秦复明', 'sex': '1', 'birthday': date(2001, 1, 26), 'majorId': 100102},
- {'id': 1810010205, 'name': '花繁荣', 'sex': '1', 'birthday': date(2000, 5, 27), 'majorId': 100102},
- # 软件工程技术
- {'id': 1810010301, 'name': '柴进门', 'sex': '0', 'birthday': date(1999, 4, 26), 'majorId': 100103},
- {'id': 1810010302, 'name': '李天应', 'sex': '1', 'birthday': date(1999, 6, 4), 'majorId': 100103},
- {'id': 1810010303, 'name': '董不平', 'sex': '0', 'birthday': date(1999, 9, 30), 'majorId': 100103},
- {'id': 1810010304, 'name': '徐宁静', 'sex': '1', 'birthday': date(2000, 8, 20), 'majorId': 100103},
- {'id': 1810010305, 'name': '索超越', 'sex': '0', 'birthday': date(1999, 7, 20), 'majorId': 100103},
- # 中药学专业
- {'id': 1810020101, 'name': '刘唐氏', 'sex': '1', 'birthday': date(1999, 1, 17), 'majorId': 100201},
- {'id': 1810020102, 'name': '李逵悟', 'sex': '0', 'birthday': date(1999, 5, 13), 'majorId': 100201},
- {'id': 1810020103, 'name': '阮小三', 'sex': '0', 'birthday': date(1999, 9, 16), 'majorId': 100201},
- {'id': 1810020104, 'name': '石秀气', 'sex': '1', 'birthday': date(2000, 1, 18), 'majorId': 100201},
- {'id': 1810020105, 'name': '谢宝庆', 'sex': '0', 'birthday': date(2000, 2, 25), 'majorId': 100201},
- # 护理学专业
- {'id': 1810020201, 'name': '燕青青', 'sex': '1', 'birthday': date(2000, 7, 29), 'majorId': 100202},
- {'id': 1810020202, 'name': '朱文武', 'sex': '0', 'birthday': date(1999, 2, 12), 'majorId': 100202},
- {'id': 1810020203, 'name': '鲍旭日', 'sex': '0', 'birthday': date(1999, 8, 25), 'majorId': 100202},
- {'id': 1810020204, 'name': '孔明亮', 'sex': '1', 'birthday': date(1999, 3, 12), 'majorId': 100202},
- {'id': 1810020205, 'name': '童威猛', 'sex': '0', 'birthday': date(1999, 4, 16), 'majorId': 100202},
- # 针灸推拿学专业
- {'id': 1810020301, 'name': '朱富贵', 'sex': '0', 'birthday': date(1998, 2, 15), 'majorId': 100203},
- {'id': 1810020302, 'name': '孙大嫂', 'sex': '1', 'birthday': date(1999, 3, 27), 'majorId': 100203},
- {'id': 1810020303, 'name': '王小二', 'sex': '0', 'birthday': date(1999, 6, 28), 'majorId': 100203},
- {'id': 1810020304, 'name': '白胜利', 'sex': '1', 'birthday': date(1999, 8, 10), 'majorId': 100203},
- {'id': 1810020305, 'name': '段金柱', 'sex': '0', 'birthday': date(1999, 2, 12), 'majorId': 100203},
-]
-
-teacherData = [
- # 计算机工程学院
- {'id': 180101, 'name': '陈江川', 'sex': '0', 'birthday': date(1988, 9, 9), 'education': '1', 'title': '3',
- 'departmentId': 1001},
- {'id': 180102, 'name': '李小平', 'sex': '0', 'birthday': date(1993, 8, 17), 'education': '2', 'title': '2',
- 'departmentId': 1001},
- {'id': 180103, 'name': '赵希明', 'sex': '1', 'birthday': date(1988, 3, 28), 'education': '3', 'title': '4',
- 'departmentId': 1001},
- {'id': 180104, 'name': '张红', 'sex': '1', 'birthday': date(1995, 6, 15), 'education': '1', 'title': '1',
- 'departmentId': 1001},
- # 医学院
- {'id': 180201, 'name': '吴小龚', 'sex': '1', 'birthday': date(1988, 4, 20), 'education': '2', 'title': '2',
- 'departmentId': 1002},
- {'id': 180202, 'name': '张进明', 'sex': '0', 'birthday': date(1989, 10, 29), 'education': '1', 'title': '1',
- 'departmentId': 1002},
- {'id': 180203, 'name': '李历宁', 'sex': '0', 'birthday': date(1996, 3, 19), 'education': '1', 'title': '3',
- 'departmentId': 1002},
-]
-
-courseData = [
- {'id': 1101, 'name': '计算机导论', 'credit': 2, 'period': 16},
- {'id': 1102, 'name': '计算机网络', 'credit': 2, 'period': 16},
- {'id': 1103, 'name': 'Java', 'credit': 4, 'period': 16},
- {'id': 1104, 'name': 'C语言', 'credit': 4, 'period': 16},
- {'id': 1201, 'name': '基础化学', 'credit': 2, 'period': 16},
- {'id': 1202, 'name': '生理学', 'credit': 3, 'period': 16},
- {'id': 1203, 'name': '中医药基础', 'credit': 4, 'period': 64},
- {'id': 1301, 'name': '有机化学实验', 'credit': 2, 'period': 32},
- {'id': 1302, 'name': '方剂学', 'credit': 4, 'period': 64},
- {'id': 1303, 'name': '药用化学', 'credit': 2, 'period': 32},
- {'id': 1401, 'name': '马克思主义基本原理概论', 'credit': 2, 'period': 32},
- {'id': 1402, 'name': '大学生心理健康教育', 'credit': 2, 'period': 32},
- {'id': 1403, 'name': '职业生涯与发展规划', 'credit': 1, 'period': 16},
- {'id': 1404, 'name': '形式与政策', 'credit': 1, 'period': 16},
-]
-
-taughtData = [
- {'teacherId': 180101, 'courseId': 1101},
- {'teacherId': 180101, 'courseId': 1102},
- {'teacherId': 180102, 'courseId': 1103},
- {'teacherId': 180102, 'courseId': 1104},
-]
-
-electiveData = [
- {'studentId': 1810010101, 'courseId': 1101},
- {'studentId': 1810010101, 'courseId': 1102},
- {'studentId': 1810010102, 'courseId': 1102},
- {'studentId': 1810010201, 'courseId': 1103},
- {'studentId': 1810010202, 'courseId': 1104},
- {'studentId': 1810020301, 'courseId': 1201},
- {'studentId': 1810020302, 'courseId': 1202},
-]
-
-adminData = [{'name': 'admin'}]
diff --git a/backend/db/init_db.py b/backend/db/init_db.py
deleted file mode 100644
index b537e87..0000000
--- a/backend/db/init_db.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/25 22:08
-# @Author : zxiaosi
-# @desc : 创建与删除所有表, 初始化数据
-from core.logger import logger
-from db import engine
-from db.data import *
-from models import Base, Department, Major, Student, Teacher, Admin, Course, Taught, Elective
-
-
-async def init_db():
- """ 创建 models/__init__ 下的所有表 """
- try:
- await drop_db() # 删除所有的表
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.create_all)
- logger.info("创建表成功!!!")
- except Exception as e:
- logger.error(f"创建表失败!!! -- 错误信息如下:\n{e}")
- finally:
- await engine.dispose()
-
-
-async def drop_db():
- """ 删除 models/__init__ 下的所有表 """
- try:
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.drop_all)
- logger.info("删除表成功!!!")
- except Exception as e:
- logger.error(f"删除表失败!!! -- 错误信息如下:\n{e}")
- finally:
- await engine.dispose()
-
-
-async def init_data():
- """ 初始化表数据 """
- try:
- async with engine.begin() as conn:
- await conn.execute(Department.__table__.insert(), [department for department in departmentData])
- await conn.execute(Major.__table__.insert(), [major for major in majorData])
- await conn.execute(Student.__table__.insert(), [student for student in studentData])
- await conn.execute(Teacher.__table__.insert(), [teacher for teacher in teacherData])
- await conn.execute(Admin.__table__.insert(), [admin for admin in adminData])
- await conn.execute(Course.__table__.insert(), [course for course in courseData])
- await conn.execute(Taught.__table__.insert(), [selectCourse for selectCourse in taughtData])
- await conn.execute(Elective.__table__.insert(), [selectCourse for selectCourse in electiveData])
- logger.info(f"成功初始化表数据!!!")
- except Exception as e:
- logger.error(f"初始化表数据失败!!! -- 错误信息如下:\n{e}")
- finally:
- await engine.dispose()
diff --git a/backend/db/redis.py b/backend/db/redis.py
deleted file mode 100644
index 019f776..0000000
--- a/backend/db/redis.py
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/2/24 10:09
-# @Author : zxiaosi
-# @desc : redis
-import json
-from typing import Union
-from aioredis import Redis
-
-from core import settings
-
-
-class MyRedis(Redis):
- """ 继承Redis,并添加自己的方法 """
-
- # 写 __init__ 的话就取消下面注释
- # def __init__(self, connection_pool):
- # # print(connection_pool)
- # super(RedisPlus, self).__init__(connection_pool=connection_pool)
-
- async def list_loads(self, key: str, num: int = -1) -> list:
- """
- 将列表字符串转为对象
-
- :param key: 列表的key
- :param num: 最大长度(默认值 0-全部)
- :return: 列表对象
- """
- todo_list = await self.lrange(key, 0, (num - 1) if num > -1 else num)
- return [json.loads(todo) for todo in todo_list]
-
- async def cus_lpush(self, key: str, value: Union[str, list, dict]):
- """
- 向列表右侧插入数据
-
- :param key: 列表的key
- :param value: 插入的值
- """
- text = json.dumps(value)
- await self.lpush(key, text)
-
- async def get_list_by_index(self, key: str, id: int) -> object:
- """
- 根据索引得到列表值
-
- :param key: 列表的值
- :param id: 索引值
- :return:
- """
- value = await self.lindex(key, id)
- return json.loads(value)
-
-
-# 参考: https://github.com/grillazz/fastapi-redis/tree/main/app
-async def init_redis_pool() -> MyRedis:
- """ 连接redis """
- redis = await MyRedis.from_url(url=settings.REDIS_URI, encoding=settings.GLOBAL_ENCODING, decode_responses=True)
- return redis
diff --git a/backend/db/session.py b/backend/db/session.py
deleted file mode 100644
index 955484d..0000000
--- a/backend/db/session.py
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 17:27
-# @Author : zxiaosi
-# @desc : 数据库会话
-# https://www.osgeo.cn/sqlalchemy/orm/extensions/asyncio.html?highlight=async#asynchronous-i-o-asyncio
-from asyncio import current_task
-
-from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_scoped_session
-from sqlalchemy.orm import sessionmaker
-
-from core import settings
-
-# 创建表引擎
-engine = create_async_engine(
- url=settings.DATABASE_URI, # 数据库uri
- echo=settings.DATABASE_ECHO, # 是否打印日志
- # pool_size=10, # 队列池个数
- # max_overflow=20, # 队列池最大溢出个数
-)
-
-# 操作表会话
-async_session_factory = sessionmaker(
- bind=engine,
- class_=AsyncSession,
- expire_on_commit=False # 防止提交后属性过期
-)
-
-async_session = async_scoped_session(async_session_factory, scopefunc=current_task)
diff --git a/backend/main.py b/backend/main.py
index 07e59bb..8a58b2e 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,47 +1,29 @@
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_
-# @Time : 2022/4/17 17:08
# @Author : zxiaosi
+# @Time : 2022/7/1 14:33
# @desc : 主函数
import uvicorn
from fastapi import FastAPI
+from loguru import logger
-from core import settings
-from core.logger import logger
-from db import init_db, init_data, init_redis_pool
-from register import register_mount, register_exception, register_cors, register_middleware, register_router
+from apis import debug
+from core.config import settings
-app = FastAPI(description=settings.PROJECT_DESC, version=settings.PROJECT_VERSION)
+app = FastAPI(title=settings.PROJECT_DESC, version=settings.PROJECT_VERSION)
-
-def create_app():
- """ 注册中心 """
- register_mount(app) # 挂载静态文件
-
- register_exception(app) # 注册捕获全局异常
-
- register_router(app) # 注册路由
-
- register_middleware(app) # 注册请求响应拦截
-
- register_cors(app) # 注册跨域请求
-
- logger.info("日志初始化成功!!!") # 初始化日志
+app.include_router(debug.router)
@app.on_event("startup")
-async def startup():
- create_app() # 加载注册中心
- # await init_db() # 初始化表
- # await init_data() # 初始化数据
- app.state.redis = await init_redis_pool() # redis
+async def startup_event():
+ logger.success('🎉🎉🎉 项目启动 🎉🎉🎉')
@app.on_event("shutdown")
-async def shutdown():
- await app.state.redis.close() # 关闭 redis
+async def shutdown_event():
+ logger.success('项目关闭')
if __name__ == '__main__':
uvicorn.run(app='main:app', host="127.0.0.1", port=8000)
- # uvicorn.run(app='main:app', host="127.0.0.1", port=8000, debug=True, reload=True)
diff --git a/backend/models/__init__.py b/backend/models/__init__.py
index a5b212a..3e50d3b 100644
--- a/backend/models/__init__.py
+++ b/backend/models/__init__.py
@@ -1,14 +1,5 @@
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 17:12
# @Author : zxiaosi
-# @desc : ORM模型对象(创建/删除表的顺序, 慎动)
-from .base import Base
-from .department import Department
-from .major import Major
-from .teacher import Teacher
-from .student import Student
-from .admin import Admin
-from .course import Course
-from .taught import Taught
-from .elective import Elective
+# @Time : 2022/7/1 14:40
+# @desc :
diff --git a/backend/models/admin.py b/backend/models/admin.py
deleted file mode 100644
index 3182ef9..0000000
--- a/backend/models/admin.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 22:24
-# @Author : zxiaosi
-# @desc : 管理员表
-from sqlalchemy import Column, Integer, String, text
-
-from core import settings, get_password_hash
-from models import Base
-
-
-class Admin(Base):
- """ 管理员表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='编号')
-
- name = Column(String(10), nullable=False, index=True, comment='姓名')
-
- address = Column(String(20), server_default='广东省广州市', comment='上次登录地址')
-
- image = Column(String(60), server_default=f'{settings.BASE_URL}/{settings.STATIC_DIR}/author.jpg', comment='头像')
-
- hashed_password = Column(String(60), server_default=get_password_hash('123456'), comment='密码')
diff --git a/backend/models/base.py b/backend/models/base.py
deleted file mode 100644
index bb508c5..0000000
--- a/backend/models/base.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 17:14
-# @Author : zxiaosi
-# @desc : ORM父类
-from sqlalchemy import Column, DateTime, func
-from sqlalchemy.ext.declarative import as_declarative, declared_attr
-
-
-# id 也能写在这里, 不过为了方便看字段, 就在每个表内创建了
-# https://www.osgeo.cn/sqlalchemy/orm/mapping_api.html#sqlalchemy.orm.as_declarative
-@as_declarative()
-class Base:
- """ 基本表 """
-
- __name__: str # 表名
- __table_args__ = {"mysql_charset": "utf8"} # 设置表的字符集
- # __mapper_args__ = {"eager_defaults": True} # 防止 insert 插入后不刷新
-
- # 将类名小写并转化为表名 __tablename__
- @declared_attr
- def __tablename__(cls) -> str:
- return cls.__name__.lower()
-
- @declared_attr
- def create_time(cls): # 创建时间
- return Column(DateTime(timezone=True), server_default=func.now(), comment='创建时间')
-
- @declared_attr
- def update_time(cls): # 更新时间
- return Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), comment='更新时间')
diff --git a/backend/models/course.py b/backend/models/course.py
deleted file mode 100644
index fb04476..0000000
--- a/backend/models/course.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 22:24
-# @Author : zxiaosi
-# @desc : 课程表
-from sqlalchemy import Column, String, Float, SmallInteger, Integer
-from sqlalchemy.orm import relationship
-
-from models import Base
-
-
-class Course(Base):
- """ 课程表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='编号')
-
- name = Column(String(20), unique=True, nullable=False, comment='名字')
-
- credit = Column(Float, default=0, server_default='0', comment='学分')
-
- period = Column(SmallInteger, server_default='0', comment='课时')
-
- taught = relationship('Taught') # 不是字段, 可以通过 course ORM对象引用 taught 表的类集合
-
- elective = relationship('Elective') # 不是字段, 可以通过 elective ORM对象引用 taught 表的类集合
diff --git a/backend/models/department.py b/backend/models/department.py
deleted file mode 100644
index a9df470..0000000
--- a/backend/models/department.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 22:23
-# @Author : zxiaosi
-# @desc : 院系表
-from sqlalchemy import Column, Integer, String
-from sqlalchemy.orm import relationship
-
-from models import Base
-
-
-class Department(Base):
- """ 院系表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='编号')
-
- name = Column(String(20), unique=True, nullable=False, index=True, comment='名字')
-
- chairman = Column(String(10), nullable=False, comment='主任姓名')
-
- phone = Column(String(11), comment='主任手机号')
-
- majors = relationship('Major') # 不是字段, 可以通过 department ORM对象引用 major 表的类集合
-
- teachers = relationship('Teacher') # 不是字段, 可以通过 department ORM对象引用 teacher 表的类集合
diff --git a/backend/models/elective.py b/backend/models/elective.py
deleted file mode 100644
index 293e0c6..0000000
--- a/backend/models/elective.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 22:24
-# @Author : zxiaosi
-# @desc : 选课表
-from sqlalchemy import Column, ForeignKey, Integer
-
-from models import Base
-
-
-class Elective(Base):
- """ 选课表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='编号')
-
- grade = Column(Integer, default=0, server_default='0', comment='成绩')
-
- studentId = Column(Integer, ForeignKey('student.id', ondelete='CASCADE'), nullable=False, comment='学号')
-
- courseId = Column(Integer, ForeignKey('course.id', ondelete='CASCADE'), nullable=False, comment='课程编号')
diff --git a/backend/models/major.py b/backend/models/major.py
deleted file mode 100644
index 13484f2..0000000
--- a/backend/models/major.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 22:23
-# @Author : zxiaosi
-# @desc : 专业表
-from sqlalchemy import Column, Integer, String, ForeignKey
-from sqlalchemy.orm import relationship
-
-from models import Base
-
-
-class Major(Base):
- """ 专业表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='编号')
-
- name = Column(String(20), unique=True, nullable=False, comment='名字')
-
- assistant = Column(String(10), nullable=False, comment='辅导员姓名')
-
- phone = Column(String(11), comment='辅导员手机号')
-
- departmentId = Column(Integer, ForeignKey('department.id', ondelete='CASCADE'), nullable=False, comment='院系编号')
-
- students = relationship('Student') # 不是字段, 可以通过 major ORM对象引用 student 表的类集合
diff --git a/backend/models/student.py b/backend/models/student.py
deleted file mode 100644
index 0ad8b8d..0000000
--- a/backend/models/student.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 13:48
-# @Author : zxiaosi
-# @desc : 学生表
-from datetime import date
-
-from sqlalchemy import Column, Integer, String, Date, text, ForeignKey
-from sqlalchemy.orm import relationship
-
-from core import settings, get_password_hash
-from models import Base
-from utils import check_or_enum
-
-
-class Student(Base):
- """ 学生表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='学号')
-
- name = Column(String(10), unique=False, nullable=False, comment='姓名')
-
- sex = check_or_enum(name='sex', enumList=['0', '1'], comment='性别: 0->男, 1->女')
-
- birthday = Column(Date, default=date(2012, 1, 1), comment='生日')
-
- address = Column(String(20), server_default=text("'广东省广州市'"), comment='地址')
-
- image = Column(String(60), server_default=f'{settings.BASE_URL}/{settings.STATIC_DIR}/author.jpg', comment='头像')
-
- hashed_password = Column(String(60), server_default=get_password_hash('123456'), comment='密码')
-
- majorId = Column(Integer, ForeignKey('major.id', ondelete='CASCADE'), nullable=False, comment='专业编号')
-
- elective = relationship('Elective') # 不是字段, 可以通过 student ORM对象引用 elective 表的类集合
diff --git a/backend/models/taught.py b/backend/models/taught.py
deleted file mode 100644
index 3ead50e..0000000
--- a/backend/models/taught.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 22:24
-# @Author : zxiaosi
-# @desc : 讲授表
-from sqlalchemy import Column, ForeignKey, Integer
-
-from models import Base
-
-
-class Taught(Base):
- """ 讲授表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='编号')
-
- teacherId = Column(Integer, ForeignKey('teacher.id', ondelete='CASCADE'), comment='职工号')
-
- courseId = Column(Integer, ForeignKey('course.id', ondelete='CASCADE'), comment='课程编号')
diff --git a/backend/models/teacher.py b/backend/models/teacher.py
deleted file mode 100644
index de4e3e9..0000000
--- a/backend/models/teacher.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/19 22:24
-# @Author : zxiaosi
-# @desc : 教师表
-from datetime import date
-from sqlalchemy import Column, String, Integer, Date, ForeignKey
-from sqlalchemy.orm import relationship
-
-from core import settings, get_password_hash
-from models import Base
-from utils import check_or_enum
-
-
-class Teacher(Base):
- """ 教师表 """
-
- id = Column(Integer, primary_key=True, autoincrement=True, index=True, comment='职工号')
-
- name = Column(String(10), unique=True, nullable=False, comment='姓名')
-
- sex = check_or_enum(name='sex', enumList=['0', '1'], comment='性别: 0->男, 1->女')
-
- birthday = Column(Date, default=date(2012, 1, 1), comment='生日')
-
- education = check_or_enum(name='education', enumList=['1', '2', '3'], comment='学历: 1->学士, 2->硕士, 3->博士')
-
- title = check_or_enum(name='title', enumList=['1', '2', '3', '4'], comment='职称: 1->助教, 2->讲师, 3->副教授, 4->教授')
-
- hashed_password = Column(String(60), server_default=get_password_hash('123456'), comment='密码')
-
- address = Column(String(20), server_default='广东省广州市', comment='上次登录地点')
-
- image = Column(String(60), server_default=f'{settings.BASE_URL}/{settings.STATIC_DIR}/author.jpg', comment='头像')
-
- departmentId = Column(Integer, ForeignKey('department.id', ondelete='CASCADE'), nullable=False, doc='院系编号')
-
- taught = relationship('Taught') # 不是字段, 可以通过 teacher ORM对象引用 taught 表的类集合
diff --git a/backend/register/__init__.py b/backend/register/__init__.py
deleted file mode 100644
index 42780d2..0000000
--- a/backend/register/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/8 14:53
-# @Author : zxiaosi
-# @desc : 注册中心
-from .cors import register_cors
-from .exception import register_exception
-from .router import register_router
-from .middleware import register_middleware
-from .mount import register_mount
diff --git a/backend/register/cors.py b/backend/register/cors.py
deleted file mode 100644
index a9a6da9..0000000
--- a/backend/register/cors.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/9 16:48
-# @Author : zxiaosi
-# @desc : 跨域请求
-from fastapi import FastAPI
-from fastapi.middleware.cors import CORSMiddleware
-
-from core import settings
-
-
-def register_cors(app: FastAPI):
- """ 跨域请求 -- https://fastapi.tiangolo.com/zh/tutorial/cors/ """
-
- app.add_middleware(
- CORSMiddleware,
- allow_origins=[str(origin) for origin in settings.CORS_ORIGINS],
- allow_credentials=True,
- allow_methods=("GET", "POST", "PUT", "DELETE"),
- allow_headers=("*", "authentication"),
- )
diff --git a/backend/register/exception.py b/backend/register/exception.py
deleted file mode 100644
index 07ff876..0000000
--- a/backend/register/exception.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/8 15:48
-# @Author : zxiaosi
-# @desc : 全局异常
-import traceback
-from fastapi import FastAPI
-from fastapi.exceptions import RequestValidationError
-from pydantic import ValidationError
-from sqlalchemy.exc import IntegrityError, ProgrammingError
-from sqlalchemy.orm.exc import UnmappedInstanceError
-from starlette.requests import Request
-
-from core.logger import logger
-from utils.custom_exc import IpError, ErrorUser, UserNotExist, SetRedis, AccessTokenFail, IdNotExist, \
- PermissionNotEnough
-from utils.resp_code import resp_400, resp_401, resp_403, resp_422, resp_500
-
-
-def register_exception(app: FastAPI):
- """ 全局异常捕获 -- https://www.charmcode.cn/article/2020-07-19_fastapi_exception """
-
- @app.exception_handler(IpError)
- async def ip_error_handler(request: Request, exc: IpError):
- """ ip错误(自定义异常) """
- logger.warning(f"{exc.err_desc}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_400(msg=exc.err_desc)
-
- @app.exception_handler(ErrorUser)
- async def error_user_handler(request: Request, exc: ErrorUser):
- """ 错误的用户名或密码(自定义异常) """
- logger.warning(f"{exc.err_desc}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_400(msg=exc.err_desc)
-
- @app.exception_handler(UserNotExist)
- async def user_not_exist_handler(request: Request, exc: UserNotExist):
- """ 用户不存在(自定义异常) """
- logger.warning(f"{exc.err_desc}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_400(msg=exc.err_desc)
-
- @app.exception_handler(IdNotExist)
- async def id_not_exist_handler(request: Request, exc: IdNotExist):
- """ 查询id不存在(自定义异常) """
- logger.warning(f"{exc.err_desc}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_400(msg=exc.err_desc)
-
- @app.exception_handler(SetRedis)
- async def set_redis_handler(request: Request, exc: SetRedis):
- """ Redis存储失败(自定义异常) """
- logger.warning(f"{exc.err_desc}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_400(msg=exc.err_desc)
-
- @app.exception_handler(AccessTokenFail)
- async def access_token_fail_handler(request: Request, exc: AccessTokenFail):
- """ 访问令牌失败(自定义异常) """
- logger.warning(f"{exc.err_desc}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_401(msg=exc.err_desc)
-
- @app.exception_handler(PermissionNotEnough)
- async def permission_not_enough_handler(request: Request, exc: AccessTokenFail):
- """ 权限不足,拒绝访问(自定义异常) """
- logger.warning(f"{exc.err_desc}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_403(msg=exc.err_desc)
-
- @app.exception_handler(IntegrityError)
- async def integrity_error_handler(request: Request, exc: IntegrityError):
- """ 添加/更新的数据与数据库中数据冲突 """
- text = f"添加/更新的数据与数据库中数据冲突!"
- logger.warning(f"{text}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}\nerror:{exc.orig}")
- return resp_400(msg=text)
-
- @app.exception_handler(ProgrammingError)
- async def programming_error_handle(request: Request, exc: ProgrammingError):
- """ 请求参数丢失 """
- logger.error(f"请求参数丢失\nURL:{request.method}-{request.url}\nHeaders:{request.headers}\nerror:{exc}")
- return resp_400(msg='请求参数丢失!(实际请求参数错误)')
-
- @app.exception_handler(RequestValidationError)
- async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
- """ 请求参数验证异常 """
- logger.error(f"请求参数格式错误\nURL:{request.method}-{request.url}\nHeaders:{request.headers}\nerror:{exc.errors()}")
- return resp_422(msg=exc.errors())
-
- @app.exception_handler(ValidationError)
- async def inner_validation_exception_handler(request: Request, exc: ValidationError):
- """ 内部参数验证异常 """
- logger.error(f"内部参数验证错误\nURL:{request.method}-{request.url}\nHeaders:{request.headers}\nerror:{exc.errors()}")
- return resp_500(msg=exc.errors())
-
- @app.exception_handler(UnmappedInstanceError)
- async def un_mapped_instance_error_handler(request: Request, exc: UnmappedInstanceError):
- """ 删除数据的id在数据库中不存在 """
- id = request.path_params.get("id")
- text = f"不存在编号为 {id} 的数据, 删除失败!"
- logger.warning(f"{text}\nURL:{request.method}-{request.url}\nHeaders:{request.headers}")
- return resp_400(msg=text)
-
- @app.exception_handler(Exception)
- async def all_exception_handler(request: Request, exc: Exception):
- """ 捕获全局异常 """
- logger.error(f"全局异常\n{request.method}URL:{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")
- return resp_500(msg="服务器内部错误")
diff --git a/backend/register/middleware.py b/backend/register/middleware.py
deleted file mode 100644
index 0591d6c..0000000
--- a/backend/register/middleware.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/6 14:29
-# @Author : zxiaosi
-# @desc : 中间件
-from fastapi import FastAPI, Request
-from sqlalchemy.exc import OperationalError
-from aioredis.exceptions import ConnectionError
-
-from core.logger import logger
-from utils import resp_500
-
-
-# 权限验证 https://www.cnblogs.com/mazhiyong/p/13433214.html
-# 得到真实ip https://stackoverflow.com/questions/60098005/fastapi-starlette-get-client-real-ip
-# nginx 解决跨域请求 https://segmentfault.com/a/1190000019227927
-
-def register_middleware(app: FastAPI):
- """ 请求拦截与响应拦截 -- https://fastapi.tiangolo.com/tutorial/middleware/ """
-
- @app.middleware("http")
- async def intercept(request: Request, call_next):
- logger.info(f"访问记录:IP:{request.client.host}-method:{request.method}-url:{request.url}")
- try:
- await request.app.state.redis.incr('request_num') # redis 请求数量 (自增 1)
- return await call_next(request) # 返回请求(跳过token)
- except ConnectionError as e:
- logger.error(f'redis连接失败!-- {e}')
- return resp_500(msg=f'redis连接失败!')
- except OperationalError as e:
- logger.error(f'数据库连接失败!-- {e}')
- return resp_500(msg=f'数据库连接失败!')
diff --git a/backend/register/mount.py b/backend/register/mount.py
deleted file mode 100644
index 616dd8a..0000000
--- a/backend/register/mount.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/3/4 16:04
-# @Author : zxiaosi
-# @desc : 挂载静态文件
-from fastapi import FastAPI
-from fastapi.staticfiles import StaticFiles
-
-from core import settings
-
-
-def register_mount(app: FastAPI):
- """ 挂载静态文件 -- https://fastapi.tiangolo.com/zh/tutorial/static-files/ """
-
- # 第一个参数为url路径参数, 第二参数为静态文件目录的路径, 第三个参数是FastAPI内部使用的名字
- app.mount(f"/{settings.STATIC_DIR}", StaticFiles(directory=settings.STATIC_DIR), name=settings.STATIC_DIR)
diff --git a/backend/register/router.py b/backend/register/router.py
deleted file mode 100644
index 66709a7..0000000
--- a/backend/register/router.py
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/8 23:06
-# @Author : zxiaosi
-# @desc : 注册路由
-from fastapi import FastAPI, Security
-
-from core import settings
-from apis import app_router
-from apis.deps import get_current_user
-from apis.common import redis_check, login, dashboard
-
-
-def register_router(app: FastAPI):
- """ 注册路由 """
-
- app.include_router(redis_check.router, prefix=settings.API_PREFIX, tags=["Redis"]) # Redis(不需要权限)
-
- app.include_router(login.router, prefix=settings.API_PREFIX, tags=["Login"]) # Login(权限在每个接口上)
-
- app.include_router(dashboard.router, prefix=settings.API_PREFIX, tags=["Dashboard"],
- dependencies=[Security(get_current_user, scopes=[])]) # Dashboard(不需要权限,但需要登录)
-
- # 权限(权限在每个接口上)
- app.include_router(app_router, prefix=settings.API_PREFIX)
diff --git a/backend/requirement.txt b/backend/requirement.txt
new file mode 100644
index 0000000..106a53c
--- /dev/null
+++ b/backend/requirement.txt
@@ -0,0 +1,18 @@
+anyio==3.6.1
+click==8.1.3
+colorama==0.4.5
+fastapi==0.78.0
+h11==0.13.0
+httptools==0.4.0
+idna==3.3
+loguru==0.6.0
+pydantic==1.9.1
+python-dotenv==0.20.0
+PyYAML==6.0
+sniffio==1.2.0
+starlette==0.19.1
+typing-extensions==4.2.0
+uvicorn==0.18.1
+watchfiles==0.15.0
+websockets==10.3
+win32-setctime==1.1.0
diff --git a/backend/requirements.txt b/backend/requirements.txt
deleted file mode 100644
index 8b6beb2..0000000
--- a/backend/requirements.txt
+++ /dev/null
@@ -1,37 +0,0 @@
-aioredis==2.0.1
-aiosqlite==0.17.0
-anyio==3.5.0
-asgiref==3.5.0
-async-timeout==4.0.2
-asyncmy==0.2.5
-asyncpg==0.25.0
-bcrypt==3.2.2
-cffi==1.15.0
-click==8.1.2
-colorama==0.4.4
-ecdsa==0.17.0
-fastapi==0.75.2
-greenlet==1.1.2
-h11==0.13.0
-httptools==0.4.0
-idna==3.3
-loguru==0.6.0
-orjson==3.6.8
-passlib==1.7.4
-pyasn1==0.4.8
-pycparser==2.21
-pydantic==1.9.0
-python-dotenv==0.20.0
-python-jose==3.3.0
-python-multipart==0.0.5
-PyYAML==6.0
-rsa==4.8
-six==1.16.0
-sniffio==1.2.0
-SQLAlchemy==1.4.36
-starlette==0.17.1
-typing_extensions==4.2.0
-uvicorn==0.17.6
-watchgod==0.8.2
-websockets==10.3
-win32-setctime==1.1.0
diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py
index da3f104..44bf1b4 100644
--- a/backend/schemas/__init__.py
+++ b/backend/schemas/__init__.py
@@ -1,18 +1,5 @@
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 9:40
# @Author : zxiaosi
-# @desc : 数据模型(类似于TS中的interface)
-from .common import GMT
-from .department import DepartmentCreate, DepartmentUpdate, DepartmentOut
-from .major import MajorCreate, MajorUpdate, MajorOut
-from .teacher import TeacherCreate, TeacherUpdate, TeacherOut
-from .student import StudentCreate, StudentUpdate, StudentOut
-from .course import CourseCreate, CourseUpdate, CourseOut
-from .taught import TaughtCreate, TaughtUpdate, TaughtOut
-from .elective import ElectiveCreate, ElectiveUpdate, ElectiveOut
-from .admin import AdminCreate, AdminUpdate, AdminOut
-from .token import Token, TokenData
-from .result import SchemasType, Result, ResultPlus
-from .todo import TodoId, Todo
-from .login import Login
+# @Time : 2022/7/1 14:43
+# @desc :
diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py
deleted file mode 100644
index 05effc0..0000000
--- a/backend/schemas/admin.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 10:01
-# @Author : zxiaosi
-# @desc : 管理员表的模型
-from typing import Optional
-
-from pydantic import BaseModel, Field
-
-from schemas import GMT
-
-
-class AdminIn(BaseModel):
- """ 共享模型字段 """
- name: str = Field(..., max_length=10, example='姓名')
- image: str = Field(..., example='头像')
- address: str = Field(..., example='地址')
-
-
-class AdminUpdate(AdminIn):
- """ 更新数据的字段验证 """
- password: Optional[str] = Field(max_length=60, example='管理员密码') # 前端返回可不带该字段
-
-
-class AdminCreate(AdminUpdate):
- """ 添加数据时的字段验证 """
- id: int = Field(..., example='自增编号')
-
-
-class AdminOut(AdminIn, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='自增编号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(个人理解: 放行,不验证)
diff --git a/backend/schemas/common.py b/backend/schemas/common.py
deleted file mode 100644
index a35c86f..0000000
--- a/backend/schemas/common.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/2/22 20:06
-# @Author : zxiaosi
-# @desc : 常用模型
-from datetime import datetime
-
-from pydantic import BaseModel, validator
-
-
-class GMT(BaseModel):
- """ 时间字段处理 """
- create_time: datetime
- update_time: datetime
-
- @validator("create_time", "update_time")
- def format_time(cls, value: datetime) -> str:
- return value.strftime('%Y-%m-%d %H:%M:%S') # 格式化时间
diff --git a/backend/schemas/course.py b/backend/schemas/course.py
deleted file mode 100644
index 4609833..0000000
--- a/backend/schemas/course.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 10:01
-# @Author : zxiaosi
-# @desc : 课程表模型
-from pydantic import BaseModel, Field
-from schemas import GMT
-
-
-class CourseIn(BaseModel):
- """ 共享模型字段 """
- name: str = Field(min_length=1, max_length=20, example='课程名字')
- credit: float = Field(..., example='学分')
- period: int = Field(..., example='课时')
-
-
-class CourseCreate(CourseIn):
- """ 添加数据时的字段验证 """
- id: str = Field(regex=r'^[1-9][0-9]{3}$', min_length=4, max_length=4, example='课程编号:1101')
-
-
-class CourseUpdate(CourseIn):
- """ 更新数据的字段验证 """
- pass
-
-
-class CourseOut(CourseCreate, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='编号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(结果为字典类型)
diff --git a/backend/schemas/department.py b/backend/schemas/department.py
deleted file mode 100644
index 15bcff4..0000000
--- a/backend/schemas/department.py
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 9:59
-# @Author : zxiaosi
-# @desc : 院系表模型
-from typing import Optional
-from pydantic import BaseModel, Field
-
-from schemas import GMT
-
-
-class DepartmentIn(BaseModel):
- """ 共享模型字段 """
- name: str = Field(min_length=1, max_length=20, example='名字')
- chairman: str = Field(min_length=1, max_length=10, example='主任名')
- phone: Optional[str] = Field(regex=r'(^\s{0}$)|^(0\d{2,3}-\d{7,8})|(1[34578]\d{9})$', example='主任手机号')
-
-
-class DepartmentCreate(DepartmentIn):
- """ 添加数据时的字段验证 """
- id: str = Field(regex=r'^[1-9][0-9]{3}$', example='编号')
-
-
-class DepartmentUpdate(DepartmentIn):
- """ 更新数据的字段验证 """
- pass
-
-
-class DepartmentOut(DepartmentIn, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='编号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(结果为字典类型)
diff --git a/backend/schemas/elective.py b/backend/schemas/elective.py
deleted file mode 100644
index f599146..0000000
--- a/backend/schemas/elective.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 10:01
-# @Author : zxiaosi
-# @desc : 选课表模型
-from typing import Optional
-
-from pydantic import BaseModel, Field
-
-from schemas import GMT
-
-
-class ElectiveIn(BaseModel):
- """ 共享模型字段 """
- grade: Optional[str] = Field(regex=r'(^\s{0}$)|(^100)|(^([0]{0})([1-9]?)([0-9]))$', max_length=3, example='成绩')
- studentId: str = Field(regex=r'^[1-9][0-9]{9}$', min_length=10, max_length=10, example='学号:1810020401')
- courseId: str = Field(regex=r'^[1-9][0-9]{3}$', min_length=4, max_length=4, example='课程编号:1101')
-
-
-class ElectiveCreate(ElectiveIn):
- """ 添加数据时的字段验证 """
- pass
-
-
-class ElectiveUpdate(ElectiveIn):
- """ 更新数据的字段验证 """
- pass
-
-
-class ElectiveOut(ElectiveIn, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='自增编号')
- studentId: int = Field(..., example='学号')
- courseId: int = Field(..., example='课程号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(结果为字典类型)
diff --git a/backend/schemas/login.py b/backend/schemas/login.py
deleted file mode 100644
index 42e192f..0000000
--- a/backend/schemas/login.py
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/2/25 11:23
-# @Author : zxiaosi
-# @desc : 登录模型
-from pydantic import BaseModel
-
-
-class Login(BaseModel):
- """ 登录模型 """
- username: str
- password: str
diff --git a/backend/schemas/major.py b/backend/schemas/major.py
deleted file mode 100644
index 691377d..0000000
--- a/backend/schemas/major.py
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 10:00
-# @Author : zxiaosi
-# @desc : 专业表模型
-from typing import Optional
-from pydantic import BaseModel, Field
-
-from schemas import GMT
-
-
-class MajorIn(BaseModel):
- """ 共享模型字段 """
- name: str = Field(max_length=20, example='名字')
- assistant: str = Field(min_length=1, max_length=10, example='辅导员名')
- phone: Optional[str] = Field(regex=r'(^\s{0}$)|^(0\d{2,3}-\d{7,8})|(1[34578]\d{9})$', example='辅导员手机号')
- departmentId: str = Field(regex=r'^10[0-9]{2}$', min_length=4, max_length=4, example='院系编号')
-
-
-class MajorCreate(MajorIn):
- """ 添加数据时的字段验证 """
- id: str = Field(regex=r'^10[0-9]{4}$', min_length=6, max_length=6, example='编号')
-
-
-class MajorUpdate(MajorIn):
- """ 更新数据的字段验证 """
- pass
-
-
-class MajorOut(MajorIn, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='编号')
- departmentId: int = Field(..., example='院系编号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(结果为字典类型)
diff --git a/backend/schemas/result.py b/backend/schemas/result.py
deleted file mode 100644
index 1a6169a..0000000
--- a/backend/schemas/result.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/1/7 11:24
-# @Author : zxiaosi
-# @desc : 返回数据验证
-from typing import TypeVar, Generic, Optional, Union, List, Dict, Any
-from pydantic import BaseModel
-from pydantic.generics import GenericModel
-
-SchemasType = TypeVar("SchemasType", bound=BaseModel)
-
-
-class Result(GenericModel, Generic[SchemasType]):
- """ 普通结果验证 """
- code: int
- data: Union[SchemasType, str, list, dict, bool]
- msg: Optional[str]
-
-
-class ListModel(GenericModel, Generic[SchemasType]):
- """ 自定义结果 --> data 数据 """
- list: List[Union[SchemasType, Dict[str, Any]]]
- count: int
-
-
-class ResultPlus(GenericModel, Generic[SchemasType]):
- """ 列表结果验证 """
- code: int
- data: ListModel[SchemasType]
- msg: Optional[str]
diff --git a/backend/schemas/student.py b/backend/schemas/student.py
deleted file mode 100644
index a98990e..0000000
--- a/backend/schemas/student.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 10:00
-# @Author : zxiaosi
-# @desc : 学生表模型
-from datetime import date
-from typing import Optional, Literal
-from pydantic import BaseModel, Field
-
-from schemas import GMT
-
-
-class StudentIn(BaseModel):
- """ 共享模型字段 """
- name: str = Field(min_length=1, max_length=10, example='姓名')
- sex: Literal['0', '1'] = Field(..., example='性别:0->男, 1->女')
- birthday: date = Field(..., example='生日: 1998-7-2')
- image: str = Field(..., example='头像')
- address: str = Field(..., example='地址')
- majorId: str = Field(regex=r'^10\d{4}$', min_length=6, max_length=6, example='专业编号')
-
-
-class StudentUpdate(StudentIn):
- """ 更新数据的字段验证 """
- password: Optional[str] = Field(max_length=60, example='密码') # 前端返回可不带该字段
-
-
-class StudentCreate(StudentUpdate):
- """ 添加数据时的字段验证 """
- id: str = Field(regex=r'^[1-9][0-9]{9}$', min_length=10, max_length=10, example='学号:1810020401')
-
-
-class StudentOut(StudentIn, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='学号:1810020401')
- majorId: int = Field(..., example='专业编号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(结果为字典类型)
diff --git a/backend/schemas/taught.py b/backend/schemas/taught.py
deleted file mode 100644
index 6e4160d..0000000
--- a/backend/schemas/taught.py
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 10:01
-# @Author : zxiaosi
-# @desc : 讲授表模型
-from pydantic import BaseModel, Field
-
-from schemas import GMT
-
-
-class TaughtIn(BaseModel):
- """ 共享模型字段 """
- teacherId: str = Field(regex=r'^[1-9][0-9]{5}$', min_length=6, max_length=6, example='职工号:180404')
- courseId: str = Field(regex=r'^[1-9][0-9]{3}$', min_length=4, max_length=4, example='课程编号:1101')
-
-
-class TaughtCreate(TaughtIn):
- """ 添加数据时的字段验证 """
- pass
-
-
-class TaughtUpdate(TaughtIn):
- """ 更新数据的字段验证 """
- pass
-
-
-class TaughtOut(TaughtIn, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='自增编号')
- teacherId: int = Field(..., example='职工号')
- courseId: int = Field(..., example='课程号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(结果为字典类型)
diff --git a/backend/schemas/teacher.py b/backend/schemas/teacher.py
deleted file mode 100644
index 6d0f6c1..0000000
--- a/backend/schemas/teacher.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/9/22 10:01
-# @Author : zxiaosi
-# @desc : 教师表模型
-from datetime import date
-from typing import Optional, Literal
-from pydantic import BaseModel, Field
-
-from schemas import GMT
-
-
-class TeacherIn(BaseModel):
- """ 共享模型字段 """
- name: str = Field(min_length=1, max_length=10, example='姓名')
- sex: Literal['0', '1'] = Field(..., example='性别: 0->男, 1->女')
- birthday: date = Field(..., example='生日: 1998-7-2')
- education: Literal['1', '2', '3'] = Field(..., example='学历:1->学士, 2->硕士, 3->博士')
- title: Literal['1', '2', '3', '4'] = Field(..., example='职称:1->助教, 2->讲师, 3->副教授, 4->教授')
- image: str = Field(example='头像')
- address: str = Field(example='地址')
- departmentId: str = Field(regex=r'^10[0-9]{2}$', min_length=4, max_length=4, example='院系编号')
-
-
-class TeacherUpdate(TeacherIn):
- """ 更新数据的字段验证 """
- password: Optional[str] = Field(max_length=60, example='密码') # 前端返回可不带该字段
-
-
-class TeacherCreate(TeacherUpdate):
- """ 添加数据时的字段验证 """
- id: str = Field(regex=r'^[1-9][0-9]{5}$', min_length=6, max_length=6, example='职工号:180404')
-
-
-class TeacherOut(TeacherIn, GMT):
- """ 查询数据的字段验证 """
- id: int = Field(..., example='职工号:180404')
- departmentId: int = Field(..., example='院系编号')
-
- class Config:
- orm_mode = True # 是否使用orm模型(结果为字典类型)
diff --git a/backend/schemas/todo.py b/backend/schemas/todo.py
deleted file mode 100644
index f6c8768..0000000
--- a/backend/schemas/todo.py
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2022/2/24 11:20
-# @Author : zxiaosi
-# @desc : 待办模型
-from typing import Optional
-
-from pydantic import BaseModel
-
-
-class TodoId(BaseModel):
- id: Optional[int]
-
-
-class Todo(BaseModel):
- title: Optional[str]
- status: Optional[bool] = False
diff --git a/backend/schemas/token.py b/backend/schemas/token.py
deleted file mode 100644
index 060952e..0000000
--- a/backend/schemas/token.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python3
-# _*_ coding: utf-8 _*_
-# @Time : 2021/12/24 14:48
-# @Author : zxiaosi
-# @desc : token
-from typing import Optional, List
-
-from pydantic import BaseModel
-
-
-class Token(BaseModel):
- """ token """
- access_token: str
- token_type: str
-
-
-class TokenData(BaseModel):
- sub: Optional[str] = None
- scopes: List[str] = []
diff --git a/backend/sql_app.db b/backend/sql_app.db
deleted file mode 100644
index bb03a95a97f3e080f4dab8572f413ba76c7fc2eb..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 94208
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
diff --git a/backend/static/author.jpg b/backend/static/author.jpg
deleted file mode 100644
index b5901c1f7ad0d5e58f2a1953ae7a462dde7ff4df..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 45621
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
%T9BqKorZ`Uu^Asnewn+Y5%Lv2{S-j^
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