# 智慧教室管理系统 - Django 后端 Author: Zhenyu Yang Last updated: Mar 20, 2026 本文档基于 `backend/` 当前实现,描述 Classroom Manager Django 后端的实际架构、模块职责和开发注意事项。 ## 架构与技术栈 - **框架**:Django + Django REST Framework - **数据库**:MySQL - **认证**:SimpleJWT,自定义 `CookieJWTAuthentication` - **账号安全**:TOTP、WebAuthn、SM2 密码传输、微信小程序登录 - **文件处理**:本地上传目录;代码中预留 S3 兼容对象存储配置 - **Excel 导入**:`openpyxl` - **中间件扩展**:操作日志上下文、活动角色提取、请求时区切换 ## 项目结构 ```text backend/ ├── apps/ │ ├── accounts/ # 用户、登录、角色、TOTP、WebAuthn、微信绑定 │ ├── classrooms/ # 教室信息、公开查询、课表与空闲教室 │ ├── courses/ # 课程、节次、导入、调课、时令作息 │ ├── checkins/ # 课堂签到开关、签到场次、签到记录 │ ├── borrowings/ # 教室借用申请、多级审批、冲突校验 │ ├── repairs/ # 报修工单 │ ├── abuse/ # 违规举报 │ ├── logs/ # 操作日志查询与导出 │ └── signage/ # 门牌屏教室信息与课程日程 ├── classroom_manager/ │ ├── settings/ # base.py / dev.py / prod.py │ └── urls.py # 全局路由 ├── common/ # 通用响应、分页、系统配置、公共视图/中间件 ├── services/ # 微信、TOTP、WebAuthn、SM2、文件存储服务 └── manage.py ``` ## 配置、路由与认证 ### 配置 - 默认配置模块是 `classroom_manager.settings.base`;`manage.py`、`wsgi.py`、`asgi.py` 都直接指向它。 - `base.py` 负责大部分真实配置: - `INSTALLED_APPS` 注册了账户、教室、课程、签到、借用、报修、违规、日志、门牌和 `common`。 - 默认数据库是 MySQL,连接参数来自 `MYSQL_*` 环境变量。 - 默认语言是 `zh-hans`,时区是 `Asia/Shanghai`。 - REST framework 默认要求认证,默认分页类是 `common.pagination.DefaultPagination`,默认 `PAGE_SIZE=200`。 - 登录接口使用节流,速率由 `LOGIN_THROTTLE_RATE` 控制,默认 `5/minute`。 - CORS 允许携带 Cookie,来源由 `CORS_ALLOWED_ORIGINS` 控制。 - `dev.py` 仅覆盖 `DEBUG=True` 和本地 `ALLOWED_HOSTS`。 - `prod.py` 追加静态文件目录、媒体目录和 `CSRF_TRUSTED_ORIGINS`。 ### 路由 - Django 管理后台路径是 `/django-admin/`。 - API 统一前缀是 `/api/v1/`,主路由注册了以下资源: - `users` - `classrooms` - `borrow` - `repairs` - `abuse` - `logs` - `courses` - `checkin-sessions` - `checkin-records` - `checkin-configs` - 另外还定义了若干独立接口: - 认证相关:邮箱登录/注册、微信登录、登出、刷新 token、`/auth/me` - TOTP:`/auth/totp/setup|enable|disable` - WebAuthn:注册参数、注册提交、登录参数、登录提交 - 微信绑定:`/auth/wechat/bind|unbind` - 用户管理补充接口:邀请外教、修改角色、修改状态、Excel 导入 - 通用接口:`/upload`、`/config`、`/config/season` - 教室补充接口:公开列表、导入、日程、空闲教室 - 门牌接口:`/signage/classrooms/` 及其 `/schedule` - `apps.accounts.urls` 还暴露了 `/api/v1/accounts/sm2/public-key`,用于前端获取 SM2 公钥。 - 开发和简化部署场景下,Django 会直接通过 `/media/uploads/` 提供上传文件访问。 ### 认证与角色体系 - 默认认证类是: - `apps.accounts.authentication.CookieJWTAuthentication` - `rest_framework.authentication.SessionAuthentication` - Web 端邮箱登录和 WebAuthn 登录成功后,会同时返回 token 数据并设置这些 Cookie: - `token` - `refresh_token` - `logged_in` - `csrftoken` - 小程序微信登录不使用 Cookie,直接在响应体中返回 access/refresh token。 - `CookieJWTAuthentication` 优先读取 `Authorization` 头,其次读取 `token` Cookie;因此浏览器请求和脚本调用都兼容。 - 用户模型使用 `roles` JSON 列表保存全部角色,当前活动角色 `current_role` 不落库,而是放在 JWT claim 里,并可由请求头 `X-Current-Role` 覆盖。 - 登录 token 会携带 `roles`、`current_role` 和 `auth_version`。密码、角色或状态发生变化时,后端会递增 `auth_version`,旧 token 自动失效。 - 用户状态为 `inactive` 时会被认证层拒绝。 ### 响应格式现状 - `common.responses.success/error` 提供了 `{code, message, data}` 风格的统一返回。 - 但当前代码并不是所有接口都使用这套封装: - 许多自定义动作和函数视图使用统一封装。 - 多个 `ModelViewSet` 的默认 `list/retrieve/update` 仍直接返回 DRF 序列化结果或分页结果。 - 因此后端当前处于“统一封装与 DRF 原生返回并存”的状态,开发新接口时需要显式确认返回风格。 ## 主要业务模块 ### 账户与认证 `apps.accounts` - `User` 基于 `AbstractUser` 扩展,核心字段包括: - `user_code` - `full_name` - `wxid` - `roles` - `status` - `student_class` - `preferred_locale` - `totp_secret` - `is_totp_enabled` - `auth_version` - 支持的角色包括:`superadmin`、`secretary`、`assistant`、`counselor`、`chinese_teacher`、`foreign_teacher`、`student`。 - 邮箱登录接口支持“邮箱或工号/学号 + 密码”,并会尝试先做 SM2 解密,失败后回退到明文兼容逻辑。 - 支持: - 邮箱注册外教账号 - 微信小程序登录 - 微信绑定与解绑 - TOTP 双因素认证 - WebAuthn 安全密钥注册与登录 - 管理员邀请外教 - Excel 批量导入用户 - 用户语言偏好会在首次访问或登录时按 `Accept-Language` 规范化到 `zh-CN` 或 `en-US`。 ### 教室与门牌 `apps.classrooms` / `apps.signage` - `Classroom` 保存教学楼、教室号、容量、设备、状态、维护时间和备注,`building + room_number` 唯一。 - 教室列表支持按 `building`、`capacity_min`、`status` 过滤。 - 权限规则: - 创建教室:仅 `superadmin` - 更新教室:`superadmin` 或 `secretary` - 删除教室:仅 `superadmin` - Excel 导入教室:`superadmin`、`secretary`、`assistant` - `free` 接口会同时排除两类冲突: - 手动借用申请(`pending/approved` 且 `source_type=manual`) - 课程表展开后的课程占用 - `schedule` 接口会把“手动借用 + 课程排课”合并为时间块;未登录访问时不返回占用原因。 - `PublicClassroomViewSet` 允许匿名查看公开教室,但只暴露 `id/building/room_number/status`。 - 门牌侧使用 `SignageDevice` 与 `Classroom` 一对一关联。门牌日程接口只返回课程系统生成的日程,不合并手动借用。 ### 课程与时令作息 `apps.courses` - 课程数据拆成两层: - `Course`:课程基础信息、教师、班级、起止日期 - `CourseSession`:具体星期、教室、节次列表、周次列表 - 节次时间不是固定写死在模型里,而是通过 `SeasonConfigService` 按 `SystemConfig.season` 动态解析: - `winter` 和 `summer` 两套时间表 - 使用 Django cache 缓存当前季节和时段配置 - `CourseViewSet` 是只读接口,支持按 `teacher_id` 和 `class_name` 查询。 - 课程导入支持 Excel 文件,且要求传入 `semester_start_date`。 - 调课入口在两个层面: - `CourseViewSet.reschedule_session` 直接调整 `CourseSession` 的周次/教室/节次 - `BorrowApplicationViewSet.reschedule` 处理 `source_type=course` 的借用改期 - 课程冲突检测由 `get_conflicting_classrooms()` / `has_course_conflict()` 完成,借用和空闲教室查询都会复用这套逻辑。 ### 课堂签到 `apps.checkins` - 签到能力由三张表支撑: - `CourseCheckinSetting`:某门课程是否开启签到 - `CheckinSession`:某节课在某一天的签到场次 - `CheckinRecord`:学生签到记录 - 开启签到后,后端会按课程节次和日期范围批量生成 `CheckinSession`,避免重复创建。 - `CheckinSessionViewSet` 在读取列表时会根据当前时间自动刷新场次状态: - 开课前 30 分钟到下课前:`open` - 下课后:`closed` - 已提供的业务动作包括: - 开启/关闭课程签到 - 查询每日课程签到 - 手动开启签到场次 - 获取点名册 - 切换单个学生签到状态 - 学生查看自己的当日签到课表 - 教师/管理员手动签到 - 签到管理权限属于课程教师或 `superadmin/secretary/assistant`。 ### 借用审批 `apps.borrowings` - `BorrowApplication` 除了基础借用字段,还包含: - `source_type` - `activity_type` - `review_level` - `reviewer_role` - `is_urgent` - `auto_generated` - 列表权限不是简单按“是否管理员”区分,而是按当前活动角色分流: - 教师默认只看自己的人工借用 - 辅导员默认看一审学生活动 - 教学秘书默认看一审教学活动 - 助理默认看二审申请 - 学生只能看自己的申请 - 超管默认可看全部 - 创建借用申请时,后端会校验: - 开始结束时间是否合法 - 申请时间是否符合角色限制 - 学生活动/教学活动是否和申请人角色匹配 - 申请人数是否超过教室容量 - 是否与已有手动借用或课程排课冲突 - 审批是两级流程: - 一审由 `counselor` 或 `secretary` 负责,取决于 `activity_type` - 一审通过后自动流转到二审 `assistant` - `assistant` 和 `superadmin` 可以审批自己的申请 - `superadmin` 可以直接最终通过 - 申请人可取消自己的申请,但距离开始时间不足 6 小时时禁止取消。 ### 报修 `apps.repairs` - `RepairTicket` 保存报修人、指派人、教室、描述、图片、状态和备注。 - 普通报修创建时会检查同教室是否已有 `open/processing` 工单,存在则拒绝新建。 - 紧急报修接口不会做这层重复校验,响应里会附带 `duty_phone`。 - `assistant`、`superadmin`、`counselor` 可以更新工单状态并自动成为 `assignee`。 - 报修人本人可通过 `/confirm` 将工单标记为 `confirmed`。 ### 违规举报 `apps.abuse` - `AbuseReport` 记录举报人、教室、描述、图片、状态和备注。 - 非 `superadmin` 只能查看自己的举报记录。 - 只有 `superadmin` 可以更新举报状态和处理备注。 ### 操作日志 `apps.logs` - `OperationLogContextMiddleware` 使用 `ContextVar` 记录当前请求,便于服务层或装饰器写日志。 - `log_operation()` 会记录用户、活动角色、动作、模块、目标对象、IP、UA 和附加 JSON 数据。 - 目前借用、报修、签到等关键动作都已有显式日志写入。 - 日志接口支持按时间、用户、角色、模块、动作过滤,并支持导出 CSV。 - 需要注意:`IsLogAdmin` 仍保留对历史字段 `user.role` 的判断,但当前 `User` 模型已无该字段;因此实际可稳定访问日志接口的通常是 Django `is_superuser` 或 `is_staff` 账号。 ### 通用能力 `common` / `services` - `SystemConfig` 当前只管理季节作息 `season`,并通过 `/api/v1/config` 与 `/api/v1/config/season` 暴露。 - `RequestTimezoneMiddleware` 会读取请求头 `X-Timezone`,动态激活当前请求时区。 - `upload_file` 接口当前实现直接把文件写入 `LOCAL_UPLOAD_DIR`,并返回 `LOCAL_UPLOAD_URL_PREFIX` 下的 URL。 - `services/file_storage.py` 已实现本地/S3 双后端文件存储服务,但当前上传接口尚未接入这个服务类,实际运行仍以本地文件系统为准。 - 微信登录依赖 `WECHAT_APP_ID` / `WECHAT_APP_SECRET` 调用 `jscode2session`。 - WebAuthn 依赖 `WEBAUTHN_RP_ID`、`WEBAUTHN_RP_NAME`、`WEBAUTHN_ORIGIN`。 ## 开发与运维注意事项 - **默认 settings 不是 dev/prod 自动切换**:直接执行 `python manage.py ...` 默认使用 `classroom_manager.settings.base`;如果需要显式切到其他配置,必须手动设置 `DJANGO_SETTINGS_MODULE`。 - **分页参数**:分页接口支持 `page_size`,也兼容 `size`,最大 200。 - **登录限流**:登录节流键会把客户端 IP 和账号标识一起哈希,避免单纯按 IP 限流过粗。 - **活动角色优先级**:权限判断大量依赖 `get_current_role(request)`,扩展角色时要同时检查 JWT claim、中间件和业务判断。 - **时令缓存**:修改季节时会同步清理课程时间表缓存;调试排课问题时要注意缓存影响。 - **上传目录**:默认上传目录是 `/data/uploads`,而不是旧文档中的 `/var/www/...`。 - **响应风格不统一**:编写前端调用或新接口时,不要假设所有 API 都返回 `{code,message,data}`。 - **日志权限存在历史兼容代码**:如果后续要把日志接口真正接入 `roles/current_role` 体系,需要先修正 `apps.logs.permissions.IsLogAdmin`。