Commit 6cf83571 authored by uuo00_n's avatar uuo00_n

feat: 重构项目为微服务架构并添加教育服务模块

重构项目结构为微服务架构,新增教育服务模块(edu-service)及相关功能:
- 添加教育服务核心实体(学生、教师、班级、课表等)及DTO
- 实现教育服务API接口(人员管理、教师管理、班级管理、课表管理等)
- 配置教育服务数据库连接和JPA实体
- 添加Dockerfile和构建配置
- 更新相关文档和部署说明

同时调整原有LLM服务和认证服务结构:
- 将原单体的数据库、模型和服务拆分到对应微服务
- 更新各服务的Docker配置和依赖
- 添加跨服务通信支持
parent 708db1b1
# 数据库配置
# 注意:生产环境请通过安全的环境变量管理传递凭据,避免将敏感信息提交到版本库
# MONGODB_URL=mongodb://llm:fSjFMwyShmcH4GdR@datacenter.dldzxx.cn:27017/llm?authSource=llm
MONGODB_URL=mongodb://localhost:27017/
DB_NAME=llm_filter_db
# JWT配置
SECRET_KEY=your_secret_key_here
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# 学期配置
TERM_START_DATE=2025-09-01
# Ollama配置
OLLAMA_BASE_URL=http://datacenter.dldzxx.cn:11434/
OLLAMA_MODEL=deepseek-r1:14b
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="e4808b27-03d7-4f87-99c5-f280665e27ae" name="更改" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 1
}]]></component>
<component name="ProjectId" id="350mjP6N9bwIjvPg5Z0uWHsdDvk" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "main",
"last_opened_file_path": "/Users/uu/Desktop/dles_prj/llm-filter",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "preferences.pluginManager",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager">
<configuration default="true" type="DjangoTestsConfigurationType">
<module name="llm-filter" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="TARGET" value="" />
<option name="SETTINGS_FILE" value="" />
<option name="CUSTOM_SETTINGS" value="false" />
<option name="USE_OPTIONS" value="false" />
<option name="OPTIONS" value="" />
<method v="2" />
</configuration>
<configuration default="true" type="Python.FlaskServer">
<module name="llm-filter" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="launchJavascriptDebuger" value="false" />
<method v="2" />
</configuration>
<configuration default="true" type="docs" factoryName="Docutils task">
<module name="llm-filter" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="docutils_input_file" value="" />
<option name="docutils_output_file" value="" />
<option name="docutils_params" value="" />
<option name="docutils_task" value="" />
<option name="docutils_open_in_browser" value="false" />
<method v="2" />
</configuration>
<configuration default="true" type="docs" factoryName="Sphinx task">
<module name="llm-filter" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="docutils_input_file" value="" />
<option name="docutils_output_file" value="" />
<option name="docutils_params" value="" />
<option name="docutils_task" value="" />
<option name="docutils_open_in_browser" value="false" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-PY-252.26830.99" />
<option value="bundled-python-sdk-164cda30dcd9-0af03a5fa574-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.26830.99" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="e4808b27-03d7-4f87-99c5-f280665e27ae" name="更改" comment="" />
<created>1762255498972</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1762255498972</updated>
<workItem from="1762255500391" duration="1060000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>
\ No newline at end of file
- 当后端项目修改后要更新重启docker容器
\ No newline at end of file
# 微服务架构重构方案
## 1. 现状分析
当前项目是一个基于 Python FastAPI 的单体应用,主要包含以下业务领域:
- **认证与用户管理**:用户注册、登录、角色权限。
- **教育教务管理**:学生、教师、班级、课表、人员档案、绑定关系。
- **LLM 对话与过滤**:对话管理、敏感词过滤、对接 Ollama。
- **数据统计**:仪表盘。
**痛点(为什么感觉乱):**
1. **控制层与数据层耦合**:API 路由函数中直接包含大量 MongoDB 查询逻辑(如 `app/api/v1/auth.py`),缺乏独立的 Service 层封装。
2. **业务边界模糊**`bindings`(绑定关系)逻辑穿插在 Auth 和各个实体模块中。
3. **单体膨胀风险**:教务逻辑(复杂的实体关系)与 AI 逻辑(无状态、流式、计算密集)混在一起,扩展性受限。
## 2. 拆分建议
建议将项目拆分为三个独立微服务,根据业务特性选择最适合的语言:
### 服务 A: 身份认证中心 (Identity Service)
- **职责**:用户注册、登录、JWT 签发与验证、权限管理 (RBAC)、绑定关系管理。
- **建议语言****Go** (Golang)
- **数据库****PostgreSQL**
- **理由**:用户账户、角色权限是典型的结构化数据,关系型数据库能提供更好的事务支持和数据一致性保障。
- **接管路由**`/auth`, `/admin` (部分用户管理), `/bindings`
### 服务 B: 教务核心服务 (Edu Core Service)
- **职责**:学生、教师、班级、课表、人员档案的 CRUD 与业务逻辑。
- **建议语言****Java (Spring Boot)**
- **数据库****PostgreSQL**
- **理由**:教务数据之间存在复杂的关联关系(如班级-学生、课程-教师),使用外键约束能有效维护数据完整性。Spring Data JPA 提供了强大的 ORM 支持。
- **接管路由**`/students`, `/teachers`, `/classes`, `/schedules`, `/persons`
### 服务 C: LLM 智能服务 (LLM Filter Service)
- **职责**:LLM 对话管理、敏感词过滤算法、Ollama 接口对接。
- **建议语言****Python** (保持现状)
- **数据库****PostgreSQL (JSONB)****MongoDB**
- **理由**:对话记录虽然是非结构化的,但 PostgreSQL 的 JSONB 性能非常强劲,且便于与用户表做关联查询。为了简化运维,推荐全栈统一使用 PostgreSQL。
- **接管路由**`/conversations`, `/admin` (敏感词管理)
### 服务 D: 聚合层 / 网关 (API Gateway / BFF)
- **职责**:统一入口,路由转发,或者聚合 Dashboard 数据。
- **实现**:Nginx, Kong, 或一个轻量级的 Node.js/Go 服务。
## 3. 架构图示
```mermaid
graph TD
Client[前端应用] --> Gateway[API 网关]
Gateway -->|/auth, /bindings| AuthSvc[身份认证服务 (Go)]
Gateway -->|/students, /classes| EduSvc[教务核心服务 (Go/Java)]
Gateway -->|/conversations| LLMSvc[LLM 智能服务 (Python)]
AuthSvc --> DB_PG[(PostgreSQL: AuthDB)]
EduSvc --> DB_PG[(PostgreSQL: EduDB)]
LLMSvc --> DB_PG[(PostgreSQL: LLMDB)]
LLMSvc --> Ollama[Ollama LLM]
```
## 4. 迁移路线 (Strangler Fig Pattern)
1. **第一步:提取公共依赖**
- 确定服务间通信协议(推荐 RESTful HTTP 或 gRPC)。
- 统一数据库设计(目前是共享 Mongo,微服务建议分库或分 Collection)。
2. **第二步:剥离身份认证 (Auth Service)**
- 优先用 Go 重写 `/auth` 模块。
- 网关将 `/auth` 流量转发至新服务。
- Python 单体保留 JWT 验证逻辑(作为中间件),信任 Go 服务签发的 Token。
3. **第三步:剥离教务业务**
-`/students` 等模块迁移至新服务。
4. **第四步:瘦身 Python 服务**
- 最后 Python 服务仅保留 LLM 相关核心业务,成为纯粹的 AI 服务。
## 5. 示例:用 Go 重构 Auth 服务
我们可以先尝试用 Go 重写 `register``login` 接口,展示微服务的结构。
# LLM Filter 更新日志
## [Unreleased] — 2025-12-08
- 新增
- 完善 API 文档与错误响应,统一 `summary/description/responses`(对话、学生、管理员、绑定、班级、课表、人物、仪表盘、认证)。
- 自定义 Swagger UI 通过 CDN 加载静态资源,提升可访问性。
- 添加 `APP_BASE_URL` 配置,在启动时打印文档地址。
- 修复
- 敏感词处理逻辑兼容新旧版本,修复记录中用户/对话 ID 类型转换问题。
- 仪表盘校园总览接口角色等级要求由 5 调整为 4。
- 考勤统计缺失课程 ID 等问题修复。
- 修复默认账号(user)无法访问学生信息的问题(自动补全学生实体)。
- 修复仪表盘接口中的数据类型错误(ObjectId 转换、datetime 序列化)。
- 重构
- 升级并适配 Pydantic v2,更新模型与设置读取方式(`pydantic-settings`)。
- 统一鉴权依赖与权限等级校验;账户与人物实体分离,绑定机制统一。
- 敏感词记录功能重构,增加详细敏感词信息与审计能力;CORS 允许源与凭证动态配置。
- 新增周课表查询功能(学生/教师),支持按周次过滤与开学日期配置。
- 文档
- 更新 README,补充实体化模型、接口说明、Windows PowerShell 快速启动指南与许可证信息。
- 在 OpenAPI 中增加 `version``contact`(邮箱:`huangjunbo1107@outlook.com`)、`license` 元信息。
- 运维与杂项
- 更新 OLLAMA 模型配置(示例:`deepseek-r1:14b`),清理编译缓存与完善 `.gitignore`
### 关联提交(摘要)
- 10e59f3 feat: 完善API文档和错误响应
- 2724a77 feat(配置): 添加APP_BASE_URL配置并显示API文档链接
- cbd3da7 refactor(models): 更新Pydantic模型配置以兼容v2版本
- 4711122 docs: 更新README文档内容与格式
- 7a1720e fix(conversation): 更新敏感词处理逻辑以兼容新旧版本
- adff7ae feat(core): 添加CORS和GitHub相关配置项
- 789fdcb feat(api): 对话路由统一响应模型,新增删除接口;列表负载优化
- 35d99c7 fix(dashboard): 将校园总览接口的角色等级要求从5级降至4级
- 5dbcb42 feat: 重构用户与实体模型,实现账户与人物分离
- 05ab73d feat(学生绑定): 实现用户与学生绑定功能及接口
## 1.0.0 — 2025-10-31
- 项目初始化:LLM 过滤系统后端(FastAPI + MongoDB + Ollama)。
- 敏感词分类与严重程度管理、管理员操作与审计追踪基础能力。
- README 与 MIT 许可证添加,基础安装与运行说明。
# 🚀 LLM Filter 项目开发与启动指南
本文档旨在帮助开发人员快速搭建环境、启动服务并参与开发。本项目采用微服务架构,包含 Auth (Go), Edu (Java), LLM (Python) 三大核心服务。
---
## 🏗️ 架构概览
| 服务名称 | 端口 | 技术栈 | 职责 | 目录 |
| :--- | :--- | :--- | :--- | :--- |
| **Auth Service** | **8081** | Go (Gin) | 用户认证、JWT、绑定管理 | `microservices/auth-service` |
| **Edu Service** | **8082** | Java (Spring Boot) | 学生、教师、课表、教务数据 | `microservices/edu-service` |
| **LLM Service** | **8000** | Python (FastAPI) | AI 对话、RAG、敏感词过滤 | `microservices/llm-service` |
| **Postgres** | 5433 | PostgreSQL 15 | 存储 Auth 和 Edu 数据 | (Docker) |
| **Mongo** | 27017 | MongoDB | 存储对话历史、敏感词库 | (Docker) |
---
## ⚡ 快速启动 (Docker 模式)
这是最简单的启动方式,适合预览或部署。
### 前置要求
- Docker & Docker Compose
- 根目录下已配置 `.env` 文件
### 启动命令
在项目根目录执行:
```bash
# 构建并后台启动所有服务
docker-compose up -d --build
# 查看运行日志
docker-compose logs -f
```
### 访问服务
- **Swagger 文档 (LLM)**: [http://localhost:8000/docs](http://localhost:8000/docs)
- **Auth API**: [http://localhost:8081/api/v1/auth/health](http://localhost:8081/api/v1/auth/health)
- **Edu API**: [http://localhost:8082/api/v1/edu/health](http://localhost:8082/api/v1/edu/health)
---
## 🛠️ 本地开发指南 (Local Development)
如果你需要修改代码,建议在本地分别启动服务。
### 1. 启动基础设施 (数据库)
首先确保数据库在运行。你可以只通过 Docker 启动 DB:
```bash
# 只启动 Postgres 和 Mongo
docker-compose up -d postgres mongo
```
### 2. 启动 Auth Service (Go)
**目录**: `microservices/auth-service`
```bash
cd microservices/auth-service
# 安装依赖
go mod tidy
# 运行服务 (确保本地 5433 端口可用)
# 注意:代码中已配置连接 localhost:5433
go run main.go
```
### 3. 启动 Edu Service (Java)
**目录**: `microservices/edu-service`
**要求**: JDK 1.8, Maven
```bash
cd microservices/edu-service
# 编译并运行 (指定 settings.xml 以使用阿里云镜像)
mvn -s settings.xml clean spring-boot:run
```
### 4. 启动 LLM Service (Python)
**目录**: `microservices/llm-service`
**要求**: Python 3.10+
```bash
cd microservices/llm-service
# 创建虚拟环境 (可选)
python -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\activate # Windows
# 安装依赖
pip install -r requirements.txt
# 运行服务 (会自动向上查找根目录的 .env)
python main.py
```
---
## 🧪 接口测试流程
### 1. 注册与登录 (Auth Service)
所有操作都需要 Token。
```bash
# 1. 注册
curl -X POST http://localhost:8081/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "password123", "email": "test@example.com"}'
# 2. 登录 (获取 Token)
curl -X POST http://localhost:8081/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "password123"}'
```
### 2. 教务数据操作 (Edu Service)
使用上一步获取的 Token (`Bearer <TOKEN>`)。
```bash
# 列出班级
curl http://localhost:8082/api/v1/classes \
-H "Authorization: Bearer <TOKEN>"
```
### 3. AI 对话 (LLM Service)
LLM Service 会自动调用 Auth Service 验证 Token。
```bash
# 发起对话
curl -X POST http://localhost:8000/api/v1/conversations/chat \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"message": "你好,请介绍一下你自己"}'
```
---
## 📂 目录结构说明
```text
/llm-filter
├── docker-compose.yml # 容器编排文件
├── .env # 全局环境变量 (数据库密码、密钥等)
└── microservices/ # 微服务源码目录
├── auth-service/ # [Go] 认证服务
├── edu-service/ # [Java] 教务服务
└── llm-service/ # [Python] LLM 核心服务
```
## ⚠️ 常见问题
1. **端口冲突**:如果 `5432` 被本地 Postgres 占用,Docker 会映射到 `5433`。代码中已默认适配 `5433`,如需修改请检查 `.env` 和各服务的配置文件。
2. **Maven 下载慢**:请使用项目提供的 `microservices/edu-service/settings.xml`,已配置阿里云镜像。
3. **Python 找不到 .env**`config.py` 已内置自动向上查找逻辑,确保在 `microservices/llm-service` 目录下运行即可。
# LLM-Filter 智能对话过滤系统
# LLM-Filter 智能对话过滤系统 (Microservices)
一个面向教育与企业双场景的智能对话系统,基于 FastAPI + MongoDB + Ollama 构建,内置高效敏感词过滤、严格的角色与版别控制、完善的管理员操作与审计能力
一个面向教育与企业双场景的智能对话过滤系统,基于 **微服务架构** 重构,集成了高效敏感词过滤、严格的角色与版别控制、以及完善的教务/企业数据管理
本文档面向开发者与运维人员,提供从架构到部署的完整说明与最佳实践
系统采用 **Go (认证)** + **Java (教务)** + **Python (LLM)** 的混合技术栈,充分发挥各语言优势,通过 **Docker Compose** 统一编排部署
## 功能亮点
- 智能对话:基于本地 Ollama 模型生成回复,支持可配置模型与服务地址
- 敏感词过滤:Trie(字典树)实现高效检测与标记,支持分类/子分类与严重程度
- 用户认证:JWT(OAuth2 密码模式),支持普通用户与管理员,令牌过期可配置
- 对话历史:按用户维度保存会话与消息,记录敏感命中与模型回复
- 敏感词管理:管理员增删改查、批量导入、CSV/JSON 文件导入与热更新
- 审计追踪:敏感词触发记录,含最高严重度与触发上下文,便于合规审计
- 实体化数据模型:账号与人物分离,学生/教师等实体通过主绑定统一管理
- 角色看板:按角色等级与实体身份提供学生/班主任/中层/校级数据接口
## 🏗 系统架构
---
本项目包含以下核心服务,通过 **Gateway (Nginx)** 统一对外暴露:
## 系统架构
- 应用层:`FastAPI` 提供 RESTful API,统一前缀 `/api/v1`
- 模型层:`Ollama` 作为本地推理服务,通过 `OLLAMA_BASE_URL``OLLAMA_MODEL` 配置
- 数据层:`MongoDB` 存储账户、实体、课表与对话相关数据,敏感词词库与审计记录亦存于此
- 过滤层:在应用内维护 Trie 结构,服务启动与词库变更时自动加载与热更新
- 认证授权:`OAuth2 Password` + `JWT`,基于角色与版别依赖限制路由访问
| 服务名称 | 技术栈 | 端口 (内部) | 职责描述 |
| :--- | :--- | :--- | :--- |
| **Gateway** | Nginx | 8080 | 统一 API 网关,负责请求路由转发与跨域处理 |
| **Auth Service** | Go (Gin) | 8081 | 用户注册、登录、JWT 签发、绑定管理 |
| **Edu Service** | Java (Spring Boot) | 8082 | 教务/业务核心服务(学生、教师、班级、课表等) |
| **LLM Service** | Python (FastAPI) | 8000 | 智能对话、敏感词过滤、审计日志、数据看板 |
数据流概览:
- 用户登录获取 `access_token` → 创建对话 → 发送消息 → 文本先经 Trie 过滤与记录 → 再调用 Ollama 获取回复 → 返回综合结果并持久化
### 基础设施
- **PostgreSQL**: 存储用户账户、角色信息及教务核心结构化数据。
- **MongoDB**: 存储非结构化数据,如对话历史、敏感词库、审计日志、部分教务冗余数据。
- **Ollama**: 本地大模型推理引擎(需单独部署或配置)。
---
## ✨ 功能亮点
## 兼容性与系统要求
- Python 3.9+
- MongoDB 6.x 或 7.x(推荐 7.0;6.0 也可正常使用)
- Ollama(推荐在本机安装并运行,默认端口 11434)
- **微服务设计**: 各模块职责单一,支持独立部署与扩展,降低耦合。
- **多语言融合**:
- **Go**: 高性能处理高频的认证与鉴权请求。
- **Java**: 成熟生态支撑复杂的教务业务逻辑与事务。
- **Python**: 灵活处理 AI 对话逻辑与数据分析。
- **智能对话**: 集成 Ollama,支持多种本地模型,低成本私有化部署。
- **安全过滤**: 内置高效 Trie 树敏感词过滤,支持实时热更新,保障合规。
- **角色权限**: 精细化的 RBAC 权限控制(学生/教师/管理员等)及版别控制(教育版/企业版)。
> 驱动版本:PyMongo 4.6.0、Motor 3.3.1,已在 MongoDB 6/7 验证兼容。
## 🚀 快速开始
---
### 1. 前置要求
- **Docker** & **Docker Compose**
- **Ollama** (建议在本机安装并运行,默认端口 11434)
## 快速开始(Mac 示例)
### 2. 配置环境
在项目根目录创建 `.env` 文件(可参考以下模板):
1) 创建并激活虚拟环境
```bash
python3 -m venv .venv
source .venv/bin/activate
python -V # 确认 Python 版本
```
2) 安装依赖
```bash
pip install -r requirements.txt
```
3) 配置环境变量(.env)
在项目根目录新建或编辑 `.env`
```
# 数据库配置
MONGODB_URL=mongodb://localhost:27017
DB_NAME=llm_filter_db
# JWT配置
SECRET_KEY=请替换为强随机密钥
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# Ollama配置
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama2
APP_MODE=edu
# 初始密码(可选,未设置时使用默认值):
ADMIN_EDU_PASSWORD=admin123
USER_EDU_PASSWORD=user123
MANAGER_EDU_PASSWORD=manager123
LEADER_EDU_PASSWORD=leader123
MASTER_EDU_PASSWORD=master123
# 企业版对应:ADMIN_BIZ_PASSWORD/USER_BIZ_PASSWORD/MANAGER_BIZ_PASSWORD/LEADER_BIZ_PASSWORD/MASTER_BIZ_PASSWORD
# 学期配置
TERM_START_DATE=2025-09-01
```
生成强随机密钥(二选一):
```bash
python -c "import secrets; print(secrets.token_hex(32))"
# 或
openssl rand -hex 32
```
将输出填入 `.env``SECRET_KEY`
4) 启动 MongoDB(任选其一)
```bash
brew install mongodb-community@7.0
brew services start mongodb-community@7.0
# 或 6.0 版本:
brew install mongodb-community@6.0
brew services start mongodb-community@6.0
```
确认端口:
```bash
nc -z localhost 27017 && echo "MongoDB is running" || echo "MongoDB is NOT running"
```
5) 启动 Ollama 并确认(可选但推荐)
```bash
# 安装 Ollama(参考官方文档)
# 验证服务:
curl http://localhost:11434/api/tags
```
如需模型:
```bash
# 例如拉取 llama2(需网络)
ollama pull llama2
```
6) 初始化数据库(创建演示数据与默认账户与实体化数据)
```bash
python init_db.py
```
默认测试账号(基于 `APP_MODE`):
- 教育版(APP_MODE=edu):
- 管理员:`admin / ADMIN_EDU_PASSWORD`(默认 `admin123`
- 普通用户:`user / USER_EDU_PASSWORD`(默认 `user123`
- 班主任:`manager_edu / MANAGER_EDU_PASSWORD`(默认 `manager123`
- 中层:`leader_edu / LEADER_EDU_PASSWORD`(默认 `leader123`
- 校级:`master_edu / MASTER_EDU_PASSWORD`(默认 `master123`
- 企业版(APP_MODE=biz):
- 管理员:`administrator_biz / ADMIN_BIZ_PASSWORD`(默认 `adminbiz123`
- 普通用户:`user_biz / USER_BIZ_PASSWORD`(默认 `userbiz123`
- 组长:`manager_biz / MANAGER_BIZ_PASSWORD`(默认 `managerbiz123`
- 负责人:`leader_biz / LEADER_BIZ_PASSWORD`(默认 `leaderbiz123`
- 高管:`master_biz / MASTER_BIZ_PASSWORD`(默认 `masterbiz123`
实体化初始化内容:
- 生成 students/classes/schedules/attendance/conduct/leaves/directives
- 生成 persons(为每位学生生成人物档案)与 teachers(示例教师/班主任/干部)
- 生成 bindings(账号与人物主绑定),并清理旧字段与索引(students.user_id/classes.head_teacher_id/schedules.teacher_id)
重要说明:
- 该脚本会清空 `DB_NAME` 指定库中的所有集合(drop collection),随后再写入演示数据,仅用于本地开发与测试,请勿在生产环境运行。
- 版别通过环境变量 `APP_MODE` 控制(可选值:`edu``biz`,默认 `edu`)。脚本仅插入当前模式对应的测试用户,路由亦基于该模式限制访问版别。
7) 启动服务
```bash
uvicorn app.main:app --reload
# 访问文档: http://localhost:8000/docs
```
---
## 快速开始(Windows PowerShell 示例)
1) 允许脚本执行并创建虚拟环境
```powershell
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force
py -m venv .venv
./.venv/Scripts/Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txt
```
2) 创建 `.env`(会在根目录生成配置文件)
```powershell
@"
MONGODB_URL=mongodb://localhost:27017
DB_NAME=llm_filter_db
SECRET_KEY=REPLACE_WITH_RANDOM_HEX
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama2
# === 数据库配置 ===
POSTGRES_USER=admin
POSTGRES_PASSWORD=password
POSTGRES_DB=llm_filter_db
MONGODB_URL=mongodb://mongo:27017
# === JWT 安全配置 ===
# 必须生成强随机密钥(建议 32 字节以上)
JWT_SECRET=llm_filter_secure_secret_key_2025_update_must_be_32_bytes
# Auth Service 使用 JWT_SECRET, LLM Service 使用 SECRET_KEY (需保持一致)
SECRET_KEY=llm_filter_secure_secret_key_2025_update_must_be_32_bytes
# === Ollama 配置 ===
# Docker 容器访问宿主机 Ollama 服务
OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_MODEL=deepseek-r1:14b
# === 应用模式 ===
# edu (教育版) / biz (企业版)
APP_MODE=edu
CORS_ALLOWED_ORIGINS=*
ADMIN_EDU_PASSWORD=admin123
USER_EDU_PASSWORD=user123
MANAGER_EDU_PASSWORD=manager123
LEADER_EDU_PASSWORD=leader123
MASTER_EDU_PASSWORD=master123
TERM_START_DATE=2025-09-01
"@ | Out-File -Encoding UTF8 .env
# 生成强随机密钥并替换上面的 SECRET_KEY
python -c "import secrets; print(secrets.token_hex(32))"
```
3) 启动 MongoDB(任选其一)
- 选项A:Docker(已安装 Docker Desktop)
```powershell
docker run -d --name mongo -p 27017:27017 mongo:6.0
```
- 选项B:本机服务(已安装 MongoDB Community)
```powershell
# 启动服务(如安装为 Windows 服务)
Start-Service -Name MongoDB
# 或使用 mongod 手动启动(根据你的安装路径与数据目录调整)
# & "C:\\Program Files\\MongoDB\\Server\\6.0\\bin\\mongod.exe" --dbpath C:\\data\\db
# 端口连通性检查
Test-NetConnection -ComputerName localhost -Port 27017
```
4) 安装并验证 Ollama(可选但推荐)
```powershell
# 安装(如已配置 Winget)
winget install -e --id Ollama.Ollama
# 拉取示例模型
ollama pull llama2
# 验证服务
curl.exe http://localhost:11434/api/tags
```
5) 初始化数据库(生成演示数据与默认账户、实体化示例)
```powershell
python init_db.py
```
6) 启动后端服务
```powershell
uvicorn app.main:app --host 0.0.0.0 --port 8000
```
7) 访问接口文档
```powershell
Start-Process http://localhost:8000/docs
```
默认测试账号(基于 `APP_MODE`):
- 教育版(APP_MODE=edu):
- 管理员:`admin / ADMIN_EDU_PASSWORD`(默认 `admin123`
- 普通用户:`user / USER_EDU_PASSWORD`(默认 `user123`
- 班主任:`manager_edu / MANAGER_EDU_PASSWORD`(默认 `manager123`
- 中层:`leader_edu / LEADER_EDU_PASSWORD`(默认 `leader123`
- 校级:`master_edu / MASTER_EDU_PASSWORD`(默认 `master123`
- 企业版(APP_MODE=biz):
- 管理员:`administrator_biz / ADMIN_BIZ_PASSWORD`(默认 `adminbiz123`
- 普通用户:`user_biz / USER_BIZ_PASSWORD`(默认 `userbiz123`
- 组长:`manager_biz / MANAGER_BIZ_PASSWORD`(默认 `managerbiz123`
- 负责人:`leader_biz / LEADER_BIZ_PASSWORD`(默认 `leaderbiz123`
- 高管:`master_biz / MASTER_BIZ_PASSWORD`(默认 `masterbiz123`
### 3. 启动服务
使用 Docker Compose 一键构建并启动所有服务:
注意:如需限制 CORS 来源,在 `.env` 中将 `CORS_ALLOWED_ORIGINS` 设置为以逗号分隔的域名列表(例如 `https://example.com,https://admin.example.com`);当使用通配 `*` 时,服务将自动关闭跨域凭据以满足浏览器安全策略。
## 配置详解
与代码一致,配置由 `app/core/config.py` 读取:
- `MONGODB_URL`:MongoDB 连接串,默认 `mongodb://localhost:27017`
- `DB_NAME`:数据库名,默认 `llm_filter_db`
- `APP_MODE`:应用运行版别,`edu``biz`,默认 `edu`;路由依赖限制仅允许当前版别访问
- `SECRET_KEY`:JWT 签名密钥(必须为强随机),建议长度 ≥ 32 字节
- `ALGORITHM`:JWT 算法,默认 `HS256`
- `ACCESS_TOKEN_EXPIRE_MINUTES`:访问令牌过期时间(分钟),默认 `30`,生产环境建议缩短并启用刷新策略
- `OLLAMA_BASE_URL`:Ollama 服务地址,默认 `http://localhost:11434`
- `OLLAMA_MODEL`:Ollama 模型名,默认 `llama2`
> 命名统一:旧命名 `DATABASE_NAME`、`OLLAMA_API_BASE_URL` 已替换为 `DB_NAME` 与 `OLLAMA_BASE_URL`。
启动顺序与依赖:
- 读取 `.env` → 建立 MongoDB 连接 → 加载敏感词至 Trie → 注册路由与依赖 → 启动服务并可用
---
## API 概览
- 根路径:`/` 返回应用信息
- 统一前缀:`/api/v1`
认证与调用约定:
- 登录接口返回 `access_token``token_type`,后续请求在头部携带 `Authorization: Bearer <JWT>`
- 管理员接口需具备管理员角色与与 `APP_MODE` 一致的版别,否则返回 403/401
### 认证(/auth)
- POST `/api/v1/auth/register` 注册用户(JSON)
- POST `/api/v1/auth/login` 登录,返回 `access_token`(表单:`application/x-www-form-urlencoded`
示例:注册
```bash
curl -X POST "http://localhost:8000/api/v1/auth/register" \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "password123", "email": "test@example.com"}'
```
示例:登录(OAuth2 密码模式)
```bash
curl -X POST "http://localhost:8000/api/v1/auth/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=testuser&password=password123"
# 响应示例:{"access_token":"<JWT>","token_type":"bearer"}
```
获取到 `access_token` 后,在后续请求头添加:
```
Authorization: Bearer <JWT>
```
登录响应字段补充:
```
{
"access_token": "<JWT>",
"token_type": "bearer",
"role": "user|manager|leader|master|administrator",
"role_level": 1..5,
"edition": "edu|biz",
"person_id": "<绑定人物ID>", # 若存在主绑定
"person_type": "student|teacher", # 若存在主绑定
"bound_primary": true # 若存在主绑定
}
docker-compose up -d --build
```
OpenAPI 文档地址:`/api/v1/openapi.json`,Swagger UI:`/docs`
### 对话(/conversations)
- POST `/api/v1/conversations/` 创建新对话
- GET `/api/v1/conversations/` 获取当前用户的所有对话
- GET `/api/v1/conversations/{conversation_id}` 获取指定对话
- POST `/api/v1/conversations/{conversation_id}/messages` 发送消息并获取回复
- DELETE `/api/v1/conversations/{conversation_id}` 删除对话
等待几分钟,直到所有容器启动完成。
示例:创建对话
```bash
curl -X POST "http://localhost:8000/api/v1/conversations/" \
-H "Authorization: Bearer <JWT>"
```
示例:发送消息
```bash
curl -X POST "http://localhost:8000/api/v1/conversations/<ID>/messages" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"content": "你好,你是谁?"}'
# 响应会包含是否触发敏感词与模型回复
```
### 4. 访问服务
系统统一入口为 **http://localhost:8080**
### 管理员(/admin)
仅管理员可用,需使用管理员账号登录并携带 `Authorization: Bearer <JWT>`
- POST `/api/v1/admin/sensitive-words` 添加敏感词
- DELETE `/api/v1/admin/sensitive-words/{word_id}` 删除敏感词
- GET `/api/v1/admin/sensitive-words` 查询敏感词(支持分类/子分类/严重程度筛选)
- POST `/api/v1/admin/sensitive-words/bulk` 批量添加敏感词
- POST `/api/v1/admin/sensitive-words/import` 从 CSV/JSON 文件导入敏感词
- GET `/api/v1/admin/sensitive-records` 查询敏感词触发记录(支持多维度筛选)
- 分类相关:
- GET `/api/v1/admin/categories` 获取所有分类
- GET `/api/v1/admin/categories/default` 获取默认分类与子分类
- POST `/api/v1/admin/categories` 新增分类
- PUT `/api/v1/admin/categories/{category_name}` 更新分类子分类
- DELETE `/api/v1/admin/categories/{category_name}` 删除分类
#### 📝 API 文档 (Swagger/OpenAPI)
各微服务均提供了在线文档,可通过网关直接访问:
说明:部分路由挂载了基于 `APP_MODE` 的版别限制依赖(`require_edition_for_mode`),仅允许与当前模式一致的用户访问。
- **Auth Service 文档**: [http://localhost:8080/swagger/index.html](http://localhost:8080/swagger/index.html)
- **Edu Service 文档**: [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
- **LLM Service 文档**: [http://localhost:8080/docs](http://localhost:8080/docs)
CSV 导入示例(文件需包含表头:`word,category,subcategory,severity`):
```bash
curl -X POST "http://localhost:8000/api/v1/admin/sensitive-words/import" \
-H "Authorization: Bearer <JWT>" \
-F "file=@/path/to/words.csv"
```
JSON 导入示例(数组对象):
```bash
curl -X POST "http://localhost:8000/api/v1/admin/sensitive-words/import" \
-H "Authorization: Bearer <JWT>" \
-F "file=@/path/to/words.json"
# words.json 示例:[ {"word":"赌博","category":"违法活动","subcategory":"赌博","severity":3} ]
```
### 仪表盘(/dashboard)
- GET `/api/v1/dashboard/student/today` 学生端:今日个人课表、出勤与操行(需角色≥1且主绑定为学生)
- GET `/api/v1/dashboard/homeroom/current` 班主任端:当前节次课程与地点、节次出勤率、请假、部门指示(需角色≥2且主绑定为教师)
- GET `/api/v1/dashboard/department/overview` 中层端:教师节次出勤率、学生出勤聚合、异常班级、近期指示(需角色≥3)
- GET `/api/v1/dashboard/campus/overview` 校级端:校园整体总览(需角色≥4)
说明:路由统一挂载版别限制依赖 `require_edition_for_mode()`,学生/班主任端同时挂载实体绑定依赖 `require_binding(student|teacher)`
#### 🔌 主要 API 路由
- **认证相关**: `/api/v1/auth/*` (登录、注册)
- **绑定管理**: `/api/v1/bindings/*` (用户-实体绑定)
- **教务管理**:
- `/api/v1/edu/*` (综合业务)
- `/api/v1/classes/*` (班级)
- `/api/v1/persons/*` (人员档案)
- `/api/v1/teachers/*` (教师管理)
- `/api/v1/schedules/*` (课表)
- **对话与看板**:
- `/api/v1/conversations/*` (AI 对话)
- `/api/v1/dashboard/*` (数据看板)
- `/api/v1/admin/*` (敏感词管理)
### 人员与实体管理
- 人物档案(/persons)
- POST `/api/v1/persons/bulk` 批量导入人物档案(`person_id/name/type`
- GET `/api/v1/persons` 列出人物档案
- 学生(/students)
- POST `/api/v1/students/{student_id}/bind` 绑定指定学生到用户(需角色≥2)
- DELETE `/api/v1/students/{student_id}/bind` 解绑当前绑定(需角色≥2)
- GET `/api/v1/students/me` 查询当前账号绑定的学生(需角色≥1)
- 教师实体(/teachers)
- POST `/api/v1/teachers/bulk` 批量导入教师实体(`person_id/teacher_id/department/roles`,可选 `account_id`
- GET `/api/v1/teachers` 列出教师实体
- 绑定管理(/bindings)
- POST `/api/v1/bindings` 创建主绑定(`type=student|teacher`
- DELETE `/api/v1/bindings/{person_id}` 删除绑定
- GET `/api/v1/bindings/me` 查询当前账号主绑定
- 班级管理(/classes)
- PUT `/api/v1/classes/{class_id}/head-teacher` 设置 `head_teacher_person_id`
- GET `/api/v1/classes` 列出班级
- 课表管理(/schedules)
- PUT `/api/v1/schedules/assign-teacher` 设置 `teacher_person_id`
- GET `/api/v1/schedules` 列出共享节次(含各班 `location`
- 仪表盘(/dashboard)
- GET `/api/v1/dashboard/student/week` 查询学生周课表(新增)
- GET `/api/v1/dashboard/teacher/week` 查询教师周课表(新增)
## 🛠 开发指南
---
## 项目结构
### 目录结构
```
llm-filter/
├── .env # 环境变量配置
├── app/ # 应用主目录
│ ├── api/ # API 路由
│ │ └── v1/ # API 版本
│ │ ├── dashboard.py # 仪表盘数据接口(按角色)
│ │ ├── bindings.py # 账号与人物绑定管理
│ │ ├── persons.py # 人物档案管理
│ │ ├── teachers.py # 教师实体管理
│ │ ├── classes.py # 班级管理(班主任人物)
│ │ ├── schedules.py # 课表管理(共享节次/教师人物)
│ ├── core/ # 核心配置
│ ├── db/ # 数据库连接
│ ├── models/ # 数据模型
│ ├── schemas/ # 请求和响应模式
│ ├── services/ # 业务服务
│ └── utils/ # 工具函数(敏感词过滤)
├── init_db.py # 数据库初始化脚本
└── requirements.txt # 项目依赖
```
---
## 数据模型与集合说明
- `users`:账户与认证信息,用于登录与权限控制;字段包含 `username/email/hashed_password/role/role_level/edition/created_at/updated_at`
- `persons`:人物档案的统一身份标识;字段包含 `person_id/name/type`,供学生/教师等实体引用。
- `bindings`:账号与人物的主绑定关系;字段包含 `account_id/person_id/type/primary`,决定账号当前实体身份(学生/教师)。
- `students`:学生实体信息;字段包含 `student_id/name/gender/grade/major/class_id/status/person_id/created_at/updated_at`
- `teachers`:教师实体信息;字段包含 `teacher_id/department/roles/account_id/person_id``roles` 示例:`teacher/homeroom/cadre`
- `classes`:班级基础信息;字段包含 `class_id/name/grade/major/students_count/head_teacher_person_id`
- `schedules`:共享节次与课程安排;字段包含 `lesson_id/weekday/period/course_name/teacher_person_id/start_time/end_time/week_range/classes[class_id/location]`
- `attendance`:学生出勤记录;字段包含 `lesson_id/class_id/student_id/date/status`
- `conduct`:操行与德育评分;字段包含 `student_id/date/metrics{...}/teacher_comment/head_teacher_comment/score`
- `leaves`:学生请假记录;字段包含 `student_id/class_id/from_date/to_date/reason/approved_by/status`
- `directives`:部门/校园指示公告;字段包含 `level/content/issuer_id/targets/created_at``level` 示例:`department/campus`
- `conversations`:用户对话与消息历史;字段包含 `user_id/messages[role/content/timestamp/contains_sensitive_words/sensitive_words_found]/created_at/updated_at`
- `sensitive_words`:敏感词词库;字段包含 `word/category/subcategory/severity/created_at/updated_at`,用于构建内存 Trie。
- `sensitive_records`:敏感词触发审计日志;字段包含 `user_id/conversation_id/message_content/sensitive_words_found/highest_severity/timestamp`
说明与关系要点:
- 账户(`users`)与人物(`persons`)分离,通过 `bindings` 形成账号的主实体身份。
- 学生(`students`)隶属班级(`classes`),课程排程(`schedules`)由教师人物负责并面向多个班级。
- 出勤(`attendance`)、操行(`conduct`)、请假(`leaves`)与指示(`directives`)共同支撑角色仪表盘的数据聚合。
### 集合读写与路由映射
- `users`:写 `register`/初始化;读认证与会话用户;路由 `/api/v1/auth/*`
- `persons`:写 批量导入与初始化;读 列表与绑定引用;路由 `/api/v1/persons*`
- `bindings`:写 创建/删除主绑定;读 当前账号主绑定与依赖校验;路由 `/api/v1/bindings*`
- `students`:写 初始化与历史字段清理;读 学生实体/统计;路由 学生相关绑定/自查
- `teachers`:写 批量导入与初始化;读 教师实体;路由 `/api/v1/teachers*`
- `classes`:写 初始化与设班主任;读 班级列表/统计;路由 `/api/v1/classes*`
- `schedules`:写 初始化与设授课教师;读 学生日程/教师节次;路由 `/api/v1/schedules*`
- `attendance`:写 初始化出勤样例;读 学生今日出勤/节次出勤率;路由 仪表盘聚合
- `conduct`:写 初始化操行样例;读 学生今日操行;路由 仪表盘聚合
- `leaves`:写 初始化请假样例;读 今日/概览请假统计;路由 仪表盘聚合
- `directives`:写 初始化部门/校园指示;读 部门/校园指示列表;路由 仪表盘聚合
- `conversations`:写 新建会话/追加消息;读 用户会话/详情;路由 `/api/v1/conversations*`
- `sensitive_words`:写 管理增删/批量导入;读 加载Trie/查询;路由 `/api/v1/admin/sensitive-words*`
- `sensitive_records`:写 命中时审计记录;读 管理员查询记录;路由 `/api/v1/admin/sensitive-records`
#### 实现位置参考(按集合)
- `users``app/services/auth.py``app/api/v1/auth.py``init_db.py`
- `bindings``app/services/bindings.py``app/api/v1/bindings.py``app/api/deps.py``app/services/dashboard.py`
- `persons``app/api/v1/persons.py``init_db.py`
- `students``app/services/student_binding.py``app/services/dashboard.py``app/api/v1/students.py``init_db.py`
- `teachers``app/api/v1/teachers.py``app/services/dashboard.py``init_db.py`
- `classes``app/api/v1/classes.py``app/services/dashboard.py``init_db.py`
- `schedules``app/api/v1/schedules.py``app/services/dashboard.py``init_db.py`
- `attendance``app/services/dashboard.py``init_db.py`
- `conduct``app/services/dashboard.py``init_db.py`
- `leaves``app/services/dashboard.py``init_db.py`
- `directives``app/services/dashboard.py``init_db.py`
- `conversations``app/services/conversation.py``app/api/v1/conversations.py``init_db.py`
- `sensitive_words``app/services/sensitive_word.py``app/utils/sensitive_word_filter.py``app/api/v1/admin.py``init_db.py`
- `sensitive_records``app/services/sensitive_word.py``app/services/conversation.py``app/api/v1/admin.py``init_db.py`
## 敏感词过滤机制
- 使用 Trie(字典树)加载敏感词并检测文本,返回是否命中与命中的词列表
- 支持主分类与子分类,并记录严重程度(1-5,5 最严重)
- 管理员可通过导入/批量添加快速构建词库
敏感词来源与刷新机制:
- 词库存放在 MongoDB 的 `sensitive_words` 集合中。
- 应用启动时会执行 `load_sensitive_words()`,将数据库中的词库加载到内存的 Trie,用于高效匹配。
- 通过管理员接口新增/删除/批量导入敏感词后,服务会自动调用 `load_sensitive_words()` 进行“热更新”,无需重启即可生效。
运行时拦截链路:
- 接收用户消息 → 调用过滤服务检测与返回命中词与最高严重度 → 命中则写审计记录至 `sensitive_records` → 继续模型推理并合并回复 → 返回完整响应
### 响应示例(对话)
命中敏感词(返回拒绝回复并记录审计):
```json
{
"contains_sensitive_words": true,
"sensitive_words_found": [
{"word":"赌博","category":"违法活动","subcategory":"赌博","severity":3}
],
"assistant_response": "当前问题暂无法回答。"
}
```
未命中敏感词(正常生成回复):
```json
{
"contains_sensitive_words": false,
"sensitive_words_found": [],
"assistant_response": "你好!我是一个AI助手..."
}
```
### 错误码与约定
- `200` 成功
- `201` 创建成功
- `204` 删除成功无内容
- `400` 请求参数错误或业务校验失败
- `401` 未认证或令牌无效
- `403` 权限不足或版别不匹配
- `404` 资源不存在
- `500` 服务器内部错误
### 根路径与健康检查
- GET `/` 返回应用信息:
```json
{"app_name":"LLM过滤系统","version":"1.0.0","message":"欢迎使用LLM过滤系统API"}
```
响应示例(对话消息发送接口):
```json
{
"message_id": "...",
"contains_sensitive_words": true,
"sensitive_words_found": [
{"word": "赌博", "category": "违法活动", "subcategory": "赌博", "severity": 3}
],
"model_reply": "...",
"timestamp": "2025-01-01T12:00:00Z"
}
```
---
## 部署与运维
- CORS:开发阶段允许全部来源,生产环境务必限制 `allow_origins`
- 进程与并发:开发模式使用 `--reload`;生产建议 `uvicorn``gunicorn`(多 worker)+ 进程管理(`supervisor`/`systemd`
- 监控与审计:建议对管理员操作与敏感词命中记录进行周期性审计与归档
- 备份策略:对 `DB_NAME` 对应数据库进行定期备份,优先增量备份并校验恢复流程
- 安全基线:强随机 `SECRET_KEY`、最小权限的数据库账户、受限的网络暴露,仅在内网访问 Mongo 与 Ollama
性能与容量建议:
- Trie 加载与匹配时间随词库规模线性增长,批量导入后可观察首请求延迟
- 对话与审计记录增长较快,建议对旧记录进行归档与 TTL 管理(如为集合设置过期索引)
角色与权限等级:
- user:1(学生/员工)
- manager:2(班主任/组长/二级部门管理员)
- leader:3(中层干部/部门负责人/一级部门管理员)
- master:4(校长/集团高管/总负责人)
- administrator:5(系统管理员/运维超管;兼容历史名 `admin`
---
## 常见问题(FAQ)
1) 登录始终 401?
- 确保使用 `application/x-www-form-urlencoded` 提交登录:`username=...&password=...`
- 后续请求携带 `Authorization: Bearer <JWT>`
2) 连接 MongoDB 失败?
- 检查 `MONGODB_URL` 与数据库端口是否开放:`nc -z localhost 27017`
- 本地安装服务建议:`brew services start mongodb-community@6.0/7.0`
3) Ollama 连接失败?
- 检查 `OLLAMA_BASE_URL` 是否指向可用服务:`curl http://localhost:11434/api/tags`
- 确保已拉取并可用的模型(如 `ollama pull llama2`
4) 运行 `init_db.py` 是否会清空数据库?
- 会清空 `DB_NAME` 指定库内的所有集合(drop collection),随后再写入演示数据。仅用于开发测试,请勿在生产环境运行。
- 如需保留数据,可自行修改脚本,跳过清空步骤或仅清空指定集合;或在 `.env` 中设置不同的 `DB_NAME` 指向测试库。
---
## 许可证
本项目使用 MIT 许可证,详见 `LICENSE`
## 贡献
欢迎提交 Issue 与 PR,共建更完善的敏感词分类与更好的对话体验。
├── gateway/ # Nginx 网关配置
├── microservices/
│ ├── auth-service/ # [Go] 认证服务
│ │ ├── internal/ # 业务逻辑
│ │ └── main.go # 入口文件
│ ├── edu-service/ # [Java] 教务服务
│ │ └── src/main/java/ # Spring Boot 源码
│ └── llm-service/ # [Python] LLM与过滤服务
│ └── app/ # FastAPI 源码
├── docker-compose.yml # 容器编排文件
└── README.md # 项目文档
```
### 本地独立开发
若需单独开发某个微服务,请参考各子目录下的 README 或直接运行:
- **Auth Service**: 进入 `microservices/auth-service`,运行 `go run main.go`
- **Edu Service**: 进入 `microservices/edu-service`,运行 `mvn spring-boot:run`
- **LLM Service**: 进入 `microservices/llm-service`,运行 `uvicorn app.main:app --reload`
*注意:本地独立运行时需确保依赖的数据库(Postgres/Mongo)已启动并配置正确的连接地址。*
## 📄 许可证
MIT License
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.core.config import settings
from app.db.mongodb import db
from app.schemas.user import UserCreate, UserResponse, Token
from app.services.auth import authenticate_user, create_access_token, get_password_hash
from app.models.role import get_role_level
router = APIRouter()
@router.post(
"/register",
response_model=UserResponse,
status_code=status.HTTP_201_CREATED,
summary="用户注册",
description="创建基础账户(默认教育版、角色等级为1)。",
responses={
400: {"description": "用户名或邮箱已存在", "content": {"application/json": {"example": {"detail": "用户名已存在"}}}},
},
)
async def register(user_data: UserCreate):
"""注册新用户"""
# 检查用户名是否已存在
existing_user = await db.db.users.find_one({"username": user_data.username})
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户名已存在"
)
# 检查邮箱是否已存在
existing_email = await db.db.users.find_one({"email": user_data.email})
if existing_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被注册"
)
# 创建新用户
hashed_password = get_password_hash(user_data.password)
user = {
"username": user_data.username,
"email": user_data.email,
"hashed_password": hashed_password,
"role": "user", # 默认为普通用户
"role_level": 1, # 默认为 1 级
"edition": "edu", # 默认为教育版,可在后续管理员界面修改
}
result = await db.db.users.insert_one(user)
# 获取创建的用户
created_user = await db.db.users.find_one({"_id": result.inserted_id})
return {
"id": str(created_user["_id"]),
"username": created_user["username"],
"email": created_user["email"],
"role": created_user.get("role", "user"),
"role_level": created_user.get("role_level", 1),
"edition": created_user.get("edition", "edu"),
}
@router.post(
"/login",
response_model=Token,
summary="用户登录",
description="使用表单登录(username/password),返回令牌与用户/绑定信息。",
responses={
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "用户名或密码错误"}}}},
},
)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""用户登录"""
user = await authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 创建访问令牌(在载荷中加入用户的角色与版别信息)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={
"sub": str(user["_id"]),
"role": user.get("role", "user"),
"role_level": user.get("role_level", get_role_level(user.get("role"))),
"edition": user.get("edition", "edu"),
**({
"person_id": str(b["person_id"]),
"person_type": b.get("type"),
"bound_primary": True,
} if (b := await db.db.bindings.find_one({"account_id": user["_id"], "primary": True})) else {})
},
expires_delta=access_token_expires
)
# 登录响应中回传基础用户信息,前端免一次 /me 调用;后端仍应以服务端鉴权为准
return {
"access_token": access_token,
"token_type": "bearer",
"role": user.get("role", "user"),
"role_level": user.get("role_level", get_role_level(user.get("role"))),
"edition": user.get("edition", "edu"),
**({
"person_id": str(b["person_id"]),
"person_type": b.get("type"),
"bound_primary": True,
} if b else {})
}
from fastapi import APIRouter, Depends, HTTPException, Path
from pydantic import BaseModel
from typing import Optional
from app.api.deps import require_edition_for_mode, get_current_active_user, require_role
from app.services.bindings import create_binding, delete_binding, get_binding_by_account
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class BindingPayload(BaseModel):
person_id: str
type: str
primary: bool = True
class ActionResult(BaseModel):
success: bool
class BindingOut(BaseModel):
_id: str
account_id: str
person_id: str
type: str
primary: bool
@router.post(
"",
summary="创建主绑定",
description="为当前账号创建人物主绑定(student/teacher),同类型主绑定唯一。",
response_model=ActionResult,
responses={
400: {"description": "主绑定已存在或参数错误", "content": {"application/json": {"example": {"detail": "主绑定已存在或参数错误"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
},
)
async def bind(payload: BindingPayload, current_user: dict = Depends(require_role(2))) -> ActionResult:
ok = await create_binding(str(current_user["_id"]), payload.person_id, payload.type, payload.primary)
if not ok:
raise HTTPException(status_code=400, detail="主绑定已存在或参数错误")
return {"success": True}
@router.delete(
"/{person_id}",
summary="删除绑定",
description="删除当前账号与指定人物的绑定。",
response_model=ActionResult,
responses={
404: {"description": "未找到绑定", "content": {"application/json": {"example": {"detail": "未找到绑定"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
},
)
async def unbind(person_id: str = Path(..., description="人物ID"), current_user: dict = Depends(require_role(2))) -> ActionResult:
ok = await delete_binding(str(current_user["_id"]), person_id)
if not ok:
raise HTTPException(status_code=404, detail="未找到绑定")
return {"success": True}
@router.get(
"/me",
summary="查询当前账号的主绑定",
description="返回当前账号的主绑定信息(account_id/person_id/type/primary)。",
response_model=BindingOut,
responses={
404: {"description": "未绑定人物", "content": {"application/json": {"example": {"detail": "未绑定人物"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
},
)
async def me(current_user: dict = Depends(get_current_active_user)) -> BindingOut:
b = await get_binding_by_account(str(current_user["_id"]))
if not b:
raise HTTPException(status_code=404, detail="未绑定人物")
b["_id"] = str(b["_id"])
b["account_id"] = str(b["account_id"])
b["person_id"] = str(b["person_id"])
return b
from fastapi import APIRouter, Depends, HTTPException, Path, Body
from pydantic import BaseModel
from typing import List, Optional
from bson import ObjectId
from app.api.deps import require_edition_for_mode, require_role
from app.db.mongodb import db
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class HeadTeacherPayload(BaseModel):
head_teacher_person_id: str
class ActionResult(BaseModel):
success: bool
class ClassOut(BaseModel):
_id: Optional[str] = None
class_id: str
head_teacher_person_id: Optional[str] = None
students_count: Optional[int] = 0
@router.put(
"/{class_id}/head-teacher",
summary="设置班主任人物",
description="为班级设置 head_teacher_person_id(指向教师人物)。",
response_model=ActionResult,
responses={
404: {"description": "班级不存在", "content": {"application/json": {"example": {"detail": "班级不存在"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
403: {"description": "权限不足", "content": {"application/json": {"example": {"detail": "权限不足"}}}},
},
)
async def set_head_teacher(class_id: str = Path(..., description="班级ID"), payload: HeadTeacherPayload = Body(..., description="班主任人物设置"), current_user: dict = Depends(require_role(3))) -> ActionResult:
res = await db.db.classes.update_one({"class_id": class_id}, {"$set": {"head_teacher_person_id": ObjectId(payload.head_teacher_person_id)}})
if res.matched_count == 0:
raise HTTPException(status_code=404, detail="班级不存在")
return {"success": True}
@router.get(
"",
summary="列出班级",
description="按 class_id 排序列出班级信息(含 head_teacher_person_id)。",
response_model=List[ClassOut],
)
async def list_classes(current_user: dict = Depends(require_role(2))) -> List[ClassOut]:
res = []
cursor = db.db.classes.find({}).sort("class_id", 1)
async for d in cursor:
d["_id"] = str(d["_id"]) if d.get("_id") else None
if d.get("head_teacher_person_id"):
d["head_teacher_person_id"] = str(d["head_teacher_person_id"])
res.append(d)
return res
from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel, Field
from typing import List, Dict
from bson import ObjectId
from app.api.deps import require_edition_for_mode, require_role
from app.db.mongodb import db
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class PersonCreate(BaseModel):
person_id: str
name: str
type: str = Field(pattern="^(student|teacher|staff)$")
class BulkInsertResult(BaseModel):
inserted: int
class PersonOut(BaseModel):
_id: str
person_id: str
name: str
type: str
class PersonsListResponse(BaseModel):
items: List[PersonOut]
counts_by_type: Dict[str, int]
@router.post(
"/bulk",
summary="批量导入人物档案",
description="导入学生/教师/职员的基础人物信息(person_id/name/type)。",
response_model=BulkInsertResult,
responses={
400: {"description": "空数据", "content": {"application/json": {"example": {"detail": "空数据"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
403: {"description": "权限不足", "content": {"application/json": {"example": {"detail": "权限不足"}}}},
},
)
async def bulk_create(persons: List[PersonCreate] = Body(..., description="人物档案列表"), current_user: dict = Depends(require_role(3))) -> BulkInsertResult:
docs = [{"person_id": p.person_id, "name": p.name, "type": p.type} for p in persons]
if not docs:
raise HTTPException(status_code=400, detail="空数据")
await db.db.persons.insert_many(docs)
return {"inserted": len(docs)}
@router.get(
"",
summary="列出人物档案",
description="按 person_id 排序列出所有人物档案。",
response_model=PersonsListResponse,
)
async def list_persons(current_user: dict = Depends(require_role(3))) -> PersonsListResponse:
res = []
counts = {}
cursor = db.db.persons.find({}).sort("person_id", 1)
async for d in cursor:
d["_id"] = str(d["_id"])
res.append(d)
t = d.get("type")
if t:
counts[t] = counts.get(t, 0) + 1
return {"items": res, "counts_by_type": counts}
from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from bson import ObjectId
from app.api.deps import require_edition_for_mode, require_role
from app.db.mongodb import db
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class AssignTeacherPayload(BaseModel):
lesson_id: str
teacher_person_id: str
class ActionResult(BaseModel):
success: bool
class ScheduleOut(BaseModel):
lesson_id: Optional[str]
weekday: Optional[int]
period: Optional[int]
course_name: Optional[str]
teacher_person_id: Optional[str]
classes: List[Dict[str, Any]]
@router.put(
"/assign-teacher",
summary="为节次设置任课教师人物",
description="将指定 lesson_id 的节次设置为 teacher_person_id(教师人物)。",
response_model=ActionResult,
responses={
404: {"description": "节次不存在", "content": {"application/json": {"example": {"detail": "节次不存在"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
403: {"description": "权限不足", "content": {"application/json": {"example": {"detail": "权限不足"}}}},
},
)
async def assign_teacher(payload: AssignTeacherPayload = Body(..., description="节次与教师人物"), current_user: dict = Depends(require_role(3))) -> ActionResult:
res = await db.db.schedules.update_one({"lesson_id": payload.lesson_id}, {"$set": {"teacher_person_id": ObjectId(payload.teacher_person_id)}})
if res.matched_count == 0:
raise HTTPException(status_code=404, detail="节次不存在")
return {"success": True}
@router.get(
"",
summary="列出课表节次",
description="按工作日与节次排序列出共享节次(含各班 location 与 teacher_person_id)。",
response_model=List[ScheduleOut],
)
async def list_schedules(current_user: dict = Depends(require_role(2))) -> List[ScheduleOut]:
res = []
cursor = db.db.schedules.find({}).sort([("weekday", 1), ("period", 1)])
async for d in cursor:
if d.get("teacher_person_id"):
d["teacher_person_id"] = str(d["teacher_person_id"])
res.append({
"lesson_id": d.get("lesson_id"),
"weekday": d.get("weekday"),
"period": d.get("period"),
"course_name": d.get("course_name"),
"teacher_person_id": d.get("teacher_person_id"),
"classes": d.get("classes", []),
})
return res
from fastapi import APIRouter, Depends, HTTPException, Path, Body
from pydantic import BaseModel
from typing import Optional, Dict, Any
from app.api.deps import require_edition_for_mode, require_role
from app.services.student_binding import bind_user_to_student, unbind_user_from_student
from app.services.dashboard import _get_student_by_user
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class BindPayload(BaseModel):
user_id: str
class ActionResult(BaseModel):
success: bool
class StudentOut(BaseModel):
_id: str
student_id: Optional[str] = None
name: Optional[str] = None
class_id: Optional[str] = None
@router.post(
"/{student_id}/bind",
response_model=ActionResult,
summary="绑定学生",
description="为指定学生建立主绑定(需角色等级≥2)。",
responses={
400: {"description": "绑定失败或学生不存在", "content": {"application/json": {"example": {"detail": "用户已绑定其他学生或学生不存在"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
},
)
async def bind(student_id: str = Path(..., description="学生ID"), payload: BindPayload = Body(..., description="绑定参数"), current_user: dict = Depends(require_role(2))) -> ActionResult:
ok = await bind_user_to_student(payload.user_id, student_id)
if not ok:
raise HTTPException(status_code=400, detail="用户已绑定其他学生或学生不存在")
return {"success": True}
@router.delete(
"/{student_id}/bind",
response_model=ActionResult,
summary="解除学生绑定",
description="解除当前用户与指定学生的主绑定(需角色等级≥2)。",
responses={
404: {"description": "未找到或未绑定", "content": {"application/json": {"example": {"detail": "学生不存在或未绑定"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
},
)
async def unbind(student_id: str = Path(..., description="学生ID"), current_user: dict = Depends(require_role(2))) -> ActionResult:
ok = await unbind_user_from_student(student_id)
if not ok:
raise HTTPException(status_code=404, detail="学生不存在或未绑定")
return {"success": True}
@router.get(
"/me",
response_model=StudentOut,
summary="查询当前绑定学生",
description="返回当前用户绑定的学生信息(需角色等级≥1)。",
responses={
404: {"description": "未绑定学生", "content": {"application/json": {"example": {"detail": "当前用户未绑定学生"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
},
)
async def me(current_user: dict = Depends(require_role(1))) -> StudentOut:
try:
s = await _get_student_by_user(current_user["_id"])
if not s:
raise HTTPException(status_code=404, detail="当前用户未绑定学生")
s["_id"] = str(s["_id"])
return s
except HTTPException as e:
if e.status_code == 404:
raise HTTPException(status_code=404, detail="当前用户未绑定学生")
raise e
from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel
from typing import List, Optional
from bson import ObjectId
from app.api.deps import require_edition_for_mode, require_role
from app.db.mongodb import db
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class TeacherCreate(BaseModel):
person_id: str
teacher_id: str
department: str
roles: List[str]
account_id: Optional[str] = None
class BulkInsertResult(BaseModel):
inserted: int
class TeacherOut(BaseModel):
_id: str
person_id: str
teacher_id: str
department: str
roles: List[str]
account_id: Optional[str] = None
@router.post(
"/bulk",
summary="批量导入教师实体",
description="导入教师实体(person_id/teacher_id/department/roles),可选绑定 account_id。",
response_model=BulkInsertResult,
responses={
400: {"description": "空数据", "content": {"application/json": {"example": {"detail": "空数据"}}}},
401: {"description": "认证失败", "content": {"application/json": {"example": {"detail": "无效的认证凭据"}}}},
403: {"description": "权限不足", "content": {"application/json": {"example": {"detail": "权限不足"}}}},
},
)
async def bulk_create(teachers: List[TeacherCreate] = Body(..., description="教师实体列表"), current_user: dict = Depends(require_role(3))) -> BulkInsertResult:
docs = []
for t in teachers:
doc = {
"person_id": ObjectId(t.person_id),
"teacher_id": t.teacher_id,
"department": t.department,
"roles": t.roles,
}
if t.account_id:
doc["account_id"] = ObjectId(t.account_id)
docs.append(doc)
if not docs:
raise HTTPException(status_code=400, detail="空数据")
await db.db.teachers.insert_many(docs)
return {"inserted": len(docs)}
@router.get(
"",
summary="列出教师实体",
description="按 teacher_id 排序列出所有教师实体(包含 person_id/account_id)。",
response_model=List[TeacherOut],
)
async def list_teachers(current_user: dict = Depends(require_role(3))) -> List[TeacherOut]:
res = []
cursor = db.db.teachers.find({}).sort("teacher_id", 1)
async for d in cursor:
d["_id"] = str(d["_id"])
d["person_id"] = str(d["person_id"])
if d.get("account_id"):
d["account_id"] = str(d["account_id"])
res.append(d)
return res
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field, ConfigDict, field_serializer
from bson import ObjectId
from pydantic_core import core_schema
# 自定义ObjectId字段
class PyObjectId(ObjectId):
@classmethod
def __get_pydantic_core_schema__(cls, source_type, handler):
def validate(v):
if isinstance(v, ObjectId):
return v
if ObjectId.is_valid(v):
return ObjectId(v)
raise ValueError("无效的ObjectId")
return core_schema.no_info_plain_validator_function(validate)
@classmethod
def __get_pydantic_json_schema__(cls, core_schema_, handler):
json_schema = handler(core_schema_)
json_schema.update({"type": "string"})
return json_schema
# 用户模型(修复缩进错误:确保为顶层类定义)
class UserModel(BaseModel):
# MongoDB 主键,使用别名 _id,序列化时转换为字符串
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
# 用户名
username: str
# 邮箱(此处仅为字符串,外层使用 EmailStr 的 Schema 做校验)
email: str
# 哈希后的密码
hashed_password: str
# 角色字符串(兼容历史中的 "admin")。建议与 app/models/role.py 中的枚举保持一致
role: str = "user"
# 角色等级(1~5),用于统一的权限判断
role_level: int = 1
# 版别:"edu"(教育版)或 "biz"(企业版)
edition: str = "edu"
# 创建与更新时间
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
model_config = ConfigDict(
populate_by_name=True,
arbitrary_types_allowed=True,
json_schema_extra={
"example": {
"username": "user1",
"email": "user1@example.com",
"hashed_password": "hashed_password_here",
"role": "user",
"role_level": 1,
"edition": "edu",
}
},
)
@field_serializer("id", when_used="json")
def serialize_id(self, v: ObjectId):
return str(v)
from typing import Optional, Literal
from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserLogin(BaseModel):
username: str
password: str
class UserResponse(BaseModel):
id: str
username: str
email: str
# 角色采用 Literal 强校验,同时兼容历史中的 "admin"
role: Literal["user", "manager", "leader", "master", "administrator", "admin"]
# 新增:角色等级,便于前端快速展示权限范围
role_level: int
# 版别采用 Literal 强校验:教育版/企业版
edition: Literal["edu", "biz"]
class Token(BaseModel):
access_token: str = Field(description="访问令牌(JWT)")
token_type: str = Field(description="令牌类型,固定为 bearer")
# 扩展:在登录响应中返回关键用户属性,减少额外查询(也可单独提供 /me 接口)
# 使用 Literal 限定角色取值范围,保持与 UserResponse 一致
role: Optional[Literal["user", "manager", "leader", "master", "administrator", "admin"]] = Field(default=None, description="系统角色")
role_level: Optional[int] = Field(default=None, description="角色等级(1~5)")
# 使用 Literal 限定版别取值范围
edition: Optional[Literal["edu", "biz"]] = Field(default=None, description="版别(教育/企业)")
# 绑定信息(实体化)
person_id: Optional[str] = Field(default=None, description="主绑定的人物ID(persons._id)")
person_type: Optional[Literal["student", "teacher", "staff"]] = Field(default=None, description="主绑定人物类型")
bound_primary: Optional[bool] = Field(default=None, description="是否存在主绑定")
class TokenData(BaseModel):
user_id: Optional[str] = None
# TokenData 也保持与 Token 一致的角色限定,便于类型安全
role: Optional[Literal["user", "manager", "leader", "master", "administrator", "admin"]] = None
role_level: Optional[int] = None
edition: Optional[Literal["edu", "biz"]] = None
\ No newline at end of file
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
from app.db.mongodb import db
from bson import ObjectId
from app.models.role import get_role_level
# 密码加密上下文
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
"""验证密码"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
"""获取密码哈希值"""
return pwd_context.hash(password)
async def authenticate_user(username: str, password: str):
"""验证用户"""
user = await db.db.users.find_one({"username": username})
if not user:
return False
if not verify_password(password, user["hashed_password"]):
return False
return user
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""创建访问令牌
关键点:
- 在 JWT 载荷中加入角色与版别信息,便于前端解码后快速展示;
- 仍以服务端数据库查询为准,避免客户端伪造带来的安全问题。
"""
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})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
async def get_current_user(token: str):
"""获取当前用户"""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
return None
user = await db.db.users.find_one({"_id": ObjectId(user_id)})
if user is None:
return None
# 兼容兜底:若用户数据缺少角色等级或版别,则进行补充
if user.get("role_level") is None:
user["role_level"] = get_role_level(user.get("role"))
if user.get("edition") is None:
user["edition"] = "edu" # 默认版别为教育版
return user
except JWTError:
return None
\ No newline at end of file
from typing import Optional, Dict
from bson import ObjectId
from app.db.mongodb import db
async def create_binding(account_id: str, person_id: str, type_: str, primary: bool = True) -> bool:
aid = ObjectId(account_id)
pid = ObjectId(person_id)
if primary:
existing = await db.db.bindings.find_one({"account_id": aid, "type": type_, "primary": True})
if existing:
return False
res = await db.db.bindings.insert_one({"account_id": aid, "person_id": pid, "type": type_, "primary": primary})
return bool(res.inserted_id)
async def delete_binding(account_id: str, person_id: str) -> bool:
aid = ObjectId(account_id)
pid = ObjectId(person_id)
res = await db.db.bindings.delete_one({"account_id": aid, "person_id": pid})
return res.deleted_count == 1
async def get_binding_by_account(account_id: str) -> Optional[Dict]:
aid = ObjectId(account_id)
return await db.db.bindings.find_one({"account_id": aid, "primary": True})
\ No newline at end of file
from bson import ObjectId
from app.db.mongodb import db
async def bind_user_to_student(user_id: str, student_id: str) -> bool:
uid = ObjectId(user_id)
exists = await db.db.students.find_one({"user_id": uid})
if exists:
return False
res = await db.db.students.update_one({"student_id": student_id}, {"$set": {"user_id": uid}})
return res.modified_count == 1
async def unbind_user_from_student(student_id: str) -> bool:
res = await db.db.students.update_one({"student_id": student_id}, {"$unset": {"user_id": ""}})
return res.modified_count == 1
async def get_student_by_user(user_id: str):
return await db.db.students.find_one({"user_id": ObjectId(user_id)})
\ No newline at end of file
version: '3.8'
services:
# 数据库服务
postgres:
image: postgres:15-alpine
container_name: llm-filter-db
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: password
POSTGRES_DB: llm_filter_db
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- llm-network
# 身份认证服务 (Go)
auth-service:
build:
context: ./microservices/auth-service
dockerfile: Dockerfile
container_name: llm-filter-auth
ports:
- "8081:8081"
environment:
- DB_HOST=postgres
- DB_USER=admin
- DB_PASSWORD=password
- DB_NAME=llm_filter_db
- DB_PORT=5432
- JWT_SECRET=llm_filter_secure_secret_key_2025_update_must_be_32_bytes
depends_on:
- postgres
networks:
- llm-network
# 教务核心服务 (Go)
edu-service:
build:
context: ./microservices/edu-service
dockerfile: Dockerfile
container_name: llm-filter-edu
ports:
- "8082:8082"
environment:
- DB_HOST=postgres
- DB_USER=admin
- DB_PASSWORD=password
- DB_NAME=llm_filter_db
- DB_PORT=5432
- JWT_SECRET=llm_filter_secure_secret_key_2025_update_must_be_32_bytes
depends_on:
- postgres
networks:
- llm-network
# LLM 服务 (Python)
llm-service:
build:
context: ./microservices/llm-service
dockerfile: Dockerfile
container_name: llm-filter-llm
ports:
- "8000:8000"
volumes:
- ./microservices/llm-service:/app
environment:
- MONGODB_URL=mongodb://mongo:27017
- DB_NAME=llm_filter_db
- SECRET_KEY=llm_filter_secure_secret_key_2025_update_must_be_32_bytes
- ALGORITHM=HS256
- ACCESS_TOKEN_EXPIRE_MINUTES=30
- TERM_START_DATE=2025-09-01
- OLLAMA_BASE_URL=http://datacenter.dldzxx.cn:11434/
- OLLAMA_MODEL=deepseek-r1:14b
depends_on:
- mongo
networks:
- llm-network
# MongoDB 服务 (LLM 服务依赖)
mongo:
image: mongo:latest
container_name: llm-filter-mongo
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
networks:
- llm-network
# API 网关 (Nginx)
gateway:
image: nginx:alpine
container_name: llm-filter-gateway
ports:
- "8080:80"
volumes:
- ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- auth-service
- edu-service
- llm-service
networks:
- llm-network
networks:
llm-network:
driver: bridge
volumes:
postgres_data:
mongo_data:
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/json;
# 开启 gzip
gzip on;
gzip_types text/plain application/xml text/css application/javascript application/json;
upstream auth_service {
server auth-service:8081;
}
upstream edu_service {
server edu-service:8082;
}
upstream llm_service {
server llm-service:8000;
}
server {
listen 80;
server_name localhost;
# 允许跨域
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
if ($request_method = 'OPTIONS') {
return 204;
}
# Auth Service 路由
location /api/v1/auth {
proxy_pass http://auth_service/api/v1/auth;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/v1/bindings {
proxy_pass http://auth_service/api/v1/bindings;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Auth Service Swagger UI
location /swagger/ {
proxy_pass http://auth_service/swagger/;
proxy_set_header Host $host;
}
# Edu Service 路由
location /api/v1/edu {
proxy_pass http://edu_service/api/v1/edu;
proxy_set_header Host $host;
}
location /api/v1/classes {
proxy_pass http://edu_service/api/v1/classes;
proxy_set_header Host $host;
}
location /api/v1/persons {
proxy_pass http://edu_service/api/v1/persons;
proxy_set_header Host $host;
}
location /api/v1/teachers {
proxy_pass http://edu_service/api/v1/teachers;
proxy_set_header Host $host;
}
location /api/v1/schedules {
proxy_pass http://edu_service/api/v1/schedules;
proxy_set_header Host $host;
}
# Edu Service Swagger UI
location /swagger-ui/ {
proxy_pass http://edu_service/swagger-ui/;
proxy_set_header Host $host;
}
location /swagger-ui.html {
proxy_pass http://edu_service/swagger-ui.html;
proxy_set_header Host $host;
}
location /v3/api-docs {
proxy_pass http://edu_service/v3/api-docs;
proxy_set_header Host $host;
}
# LLM Service 路由 (默认/兜底)
# 包括 /docs, /openapi.json, /api/v1/chat, /api/v1/admin 等
location / {
proxy_pass http://llm_service/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
import asyncio
import motor.motor_asyncio
import os
from dotenv import load_dotenv
from datetime import datetime
from bson import ObjectId
from passlib.context import CryptContext
# 密码加密工具
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 加载 .env 环境变量,保持与后端一致的配置来源
load_dotenv()
# MongoDB连接配置
MONGODB_URL = os.getenv("MONGODB_URL", "mongodb://localhost:27017")
DB_NAME = os.getenv("DB_NAME", "llm_filter_db")
# 运行模式:仅运行教育版或企业版之一(不混合)
APP_MODE = (os.getenv("APP_MODE", "edu") or "edu").lower()
ADMIN_EDU_PASSWORD = os.getenv("ADMIN_EDU_PASSWORD", "admin123")
USER_EDU_PASSWORD = os.getenv("USER_EDU_PASSWORD", "user123")
ADMIN_BIZ_PASSWORD = os.getenv("ADMIN_BIZ_PASSWORD", "adminbiz123")
USER_BIZ_PASSWORD = os.getenv("USER_BIZ_PASSWORD", "userbiz123")
MANAGER_EDU_PASSWORD = os.getenv("MANAGER_EDU_PASSWORD", "manager123")
LEADER_EDU_PASSWORD = os.getenv("LEADER_EDU_PASSWORD", "leader123")
MASTER_EDU_PASSWORD = os.getenv("MASTER_EDU_PASSWORD", "master123")
MANAGER_BIZ_PASSWORD = os.getenv("MANAGER_BIZ_PASSWORD", "managerbiz123")
LEADER_BIZ_PASSWORD = os.getenv("LEADER_BIZ_PASSWORD", "leaderbiz123")
MASTER_BIZ_PASSWORD = os.getenv("MASTER_BIZ_PASSWORD", "masterbiz123")
async def init_db():
# 连接到MongoDB
client = motor.motor_asyncio.AsyncIOMotorClient(MONGODB_URL)
db = client[DB_NAME]
# 清空现有集合(如果存在)
collections = await db.list_collection_names()
for collection in collections:
await db[collection].drop()
print("已清空现有集合")
# 创建用户集合并添加假数据
admin_id = ObjectId() # 教育版管理员(用户名 admin)
user_id = ObjectId() # 教育版普通用户(用户名 user)
user_biz_id = ObjectId() # 企业版普通用户(用户名 user_biz)
users = [
# 系统管理员(标准:administrator,兼容:admin 用户名)
{
"_id": admin_id,
"username": "admin",
"email": "admin@example.com",
"hashed_password": pwd_context.hash(ADMIN_EDU_PASSWORD),
"role": "administrator", # 统一使用标准角色名,兼容旧数据中的 "admin"
"role_level": 5, # 映射到最高等级
"edition": "edu", # 默认教育版
"created_at": datetime.now(),
"updated_at": datetime.now()
},
# 普通用户(教育版)
{
"_id": user_id,
"username": "user",
"email": "user@example.com",
"hashed_password": pwd_context.hash(USER_EDU_PASSWORD),
"role": "user",
"role_level": 1,
"edition": "edu",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
# 教育版:班主任、部门负责人、中层与校长
{
"_id": ObjectId(),
"username": "manager_edu",
"email": "manager_edu@example.com",
"hashed_password": pwd_context.hash(MANAGER_EDU_PASSWORD),
"role": "manager",
"role_level": 2,
"edition": "edu",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"_id": ObjectId(),
"username": "leader_edu",
"email": "leader_edu@example.com",
"hashed_password": pwd_context.hash(LEADER_EDU_PASSWORD),
"role": "leader",
"role_level": 3,
"edition": "edu",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"_id": ObjectId(),
"username": "master_edu",
"email": "master_edu@example.com",
"hashed_password": pwd_context.hash(MASTER_EDU_PASSWORD),
"role": "master",
"role_level": 4,
"edition": "edu",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
# 企业版:员工、组长、负责人、高管与管理员
{
"_id": user_biz_id,
"username": "user_biz",
"email": "user_biz@example.com",
"hashed_password": pwd_context.hash(USER_BIZ_PASSWORD),
"role": "user",
"role_level": 1,
"edition": "biz",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"_id": ObjectId(),
"username": "manager_biz",
"email": "manager_biz@example.com",
"hashed_password": pwd_context.hash(MANAGER_BIZ_PASSWORD),
"role": "manager",
"role_level": 2,
"edition": "biz",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"_id": ObjectId(),
"username": "leader_biz",
"email": "leader_biz@example.com",
"hashed_password": pwd_context.hash(LEADER_BIZ_PASSWORD),
"role": "leader",
"role_level": 3,
"edition": "biz",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"_id": ObjectId(),
"username": "master_biz",
"email": "master_biz@example.com",
"hashed_password": pwd_context.hash(MASTER_BIZ_PASSWORD),
"role": "master",
"role_level": 4,
"edition": "biz",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"_id": ObjectId(),
"username": "administrator_biz",
"email": "administrator_biz@example.com",
"hashed_password": pwd_context.hash(ADMIN_BIZ_PASSWORD),
"role": "administrator",
"role_level": 5,
"edition": "biz",
"created_at": datetime.now(),
"updated_at": datetime.now()
},
]
# 根据运行模式筛选用户(不混合)
mode = APP_MODE if APP_MODE in {"edu", "biz"} else "edu"
if mode != APP_MODE:
print(f"警告:APP_MODE={APP_MODE} 非法,默认使用 edu")
selected_users = [u for u in users if u["edition"] == mode]
await db.users.insert_many(selected_users)
print(f"已创建用户集合并添加 {len(selected_users)} 条记录(模式:{mode})")
# 创建敏感词集合并添加假数据
sensitive_words = [
{
"word": "赌博",
"category": "违法活动",
"subcategory": "赌博",
"severity": 3,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "色情",
"category": "色情内容",
"subcategory": "色情服务",
"severity": 4,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "毒品",
"category": "毒品相关",
"subcategory": "毒品名称",
"severity": 5,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "诈骗",
"category": "诈骗相关",
"subcategory": "网络诈骗",
"severity": 4,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "暴力",
"category": "暴力内容",
"subcategory": "语言暴力",
"severity": 3,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "自杀",
"category": "不良内容",
"subcategory": "自杀",
"severity": 5,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "政治敏感",
"category": "政治内容",
"subcategory": "敏感事件",
"severity": 4,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "种族歧视",
"category": "歧视言论",
"subcategory": "种族歧视",
"severity": 4,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "性别歧视",
"category": "歧视言论",
"subcategory": "性别歧视",
"severity": 3,
"created_at": datetime.now(),
"updated_at": datetime.now()
},
{
"word": "恐怖主义",
"category": "暴力内容",
"subcategory": "恐怖主义",
"severity": 5,
"created_at": datetime.now(),
"updated_at": datetime.now()
}
]
await db.sensitive_words.insert_many(sensitive_words)
print(f"已创建敏感词集合并添加 {len(sensitive_words)} 条记录")
# 创建对话集合并添加假数据
conversation_id = ObjectId()
# 根据模式选择示例用户用于演示对话与敏感词记录
sample_user_id = user_id if mode == "edu" else user_biz_id
conversations = [
{
"_id": conversation_id,
"user_id": sample_user_id,
"messages": [
{
"role": "user",
"content": "你好,请问你是谁?",
"timestamp": datetime.now(),
"contains_sensitive_words": False,
"sensitive_words_found": []
},
{
"role": "assistant",
"content": "你好!我是一个AI助手,可以回答你的问题和提供帮助。有什么我可以帮你的吗?",
"timestamp": datetime.now(),
"contains_sensitive_words": False,
"sensitive_words_found": []
}
],
"created_at": datetime.now(),
"updated_at": datetime.now()
}
]
await db.conversations.insert_many(conversations)
print(f"已创建对话集合并添加 {len(conversations)} 条记录")
# 创建敏感词记录集合并添加假数据
sensitive_records = [
{
# 使用真实的 ObjectId,避免与模型类型不一致
"user_id": sample_user_id,
"conversation_id": conversation_id,
"message_content": "我想了解一下赌博的事情",
"sensitive_words_found": [
{
"word": "赌博",
"category": "违法活动",
"subcategory": "赌博",
"severity": 3
}
],
"highest_severity": 3,
"timestamp": datetime.now()
},
{
# 第二条记录同样引用真实的 ObjectId
"user_id": sample_user_id,
"conversation_id": conversation_id,
"message_content": "如何获取毒品和色情内容",
"sensitive_words_found": [
{
"word": "毒品",
"category": "毒品相关",
"subcategory": "毒品名称",
"severity": 5
},
{
"word": "色情",
"category": "色情内容",
"subcategory": "色情服务",
"severity": 4
}
],
"highest_severity": 5,
"timestamp": datetime.now()
}
]
await db.sensitive_records.insert_many(sensitive_records)
print(f"已创建敏感词记录集合并添加 {len(sensitive_records)} 条记录")
# 学校业务数据:学生/班级/课表/考勤/操行/请假/指示
await seed_school_data(db, mode)
print("\n数据库初始化完成!")
print("\n测试账号 (模式: %s):" % mode)
if mode == "edu":
print("教育版管理员: admin / admin123 (role=administrator, edition=edu)")
print("教育版普通用户: user / user123 (role=user, edition=edu)")
else:
print("企业版管理员: administrator_biz / adminbiz123 (role=administrator, edition=biz)")
print("企业版普通用户: user_biz / userbiz123 (role=user, edition=biz)")
async def seed_school_data(db, mode: str):
if mode != "edu":
print("当前为企业版模式,跳过学校业务数据初始化")
return
# 索引
await db.students.create_index("student_id", unique=True)
await db.students.create_index("class_id")
await db.students.create_index("user_id", unique=True, sparse=True)
await db.students.create_index("person_id", unique=True, sparse=True)
await db.classes.create_index("class_id", unique=True)
await db.classes.create_index("head_teacher_id")
await db.classes.create_index("head_teacher_person_id")
await db.schedules.create_index([("classes.class_id", 1), ("weekday", 1), ("period", 1)])
await db.schedules.create_index("lesson_id", unique=True)
await db.schedules.create_index("teacher_person_id")
await db.attendance.create_index([("lesson_id", 1), ("student_id", 1)])
await db.attendance.create_index("date")
await db.conduct.create_index([("student_id", 1), ("date", 1)])
await db.leaves.create_index([("class_id", 1), ("from_date", 1)])
await db.leaves.create_index("student_id")
await db.directives.create_index([("created_at", -1)])
await db.directives.create_index("level")
# 选择一个班主任与任课教师
head_teacher = await db.users.find_one({"role": "manager", "edition": mode})
teacher_a = await db.users.find_one({"role": "leader", "edition": mode})
teacher_b = head_teacher
# 班级
classes_docs = [
{
"class_id": "SW22-1",
"name": "22级软件技术1班",
"grade": "22级",
"major": "软件技术",
"head_teacher_id": head_teacher["_id"] if head_teacher else None,
"students_count": 0,
},
{
"class_id": "SW22-2",
"name": "22级软件技术2班",
"grade": "22级",
"major": "软件技术",
"head_teacher_id": head_teacher["_id"] if head_teacher else None,
"students_count": 0,
},
]
await db.classes.insert_many(classes_docs)
# 学生
def gen_students(class_id: str, count: int):
docs = []
for i in range(1, count + 1):
sid = f"{class_id}-{i:03d}"
docs.append({
"student_id": sid,
"name": f"{class_id}学生{i:03d}",
"gender": "男" if i % 2 == 1 else "女",
"grade": "22级",
"major": "软件技术",
"class_id": class_id,
"status": "在读",
"created_at": datetime.now(),
"updated_at": datetime.now(),
})
return docs
students_docs = gen_students("SW22-1", 12) + gen_students("SW22-2", 12)
await db.students.insert_many(students_docs)
# 更新班级人数
counts = {
"SW22-1": 12,
"SW22-2": 12,
}
for cid, c in counts.items():
await db.classes.update_one({"class_id": cid}, {"$set": {"students_count": c}})
# 课表(周一~周五,每天 4 节),多班共享同一节次,按班级分别给出教室
courses = ["C语言基础", "数据库原理", "Web前端", "Java", "软件测试", "数据结构"]
locations_a = ["A-101", "A-102", "A-201", "A-202"]
locations_b = ["B-101", "B-102", "B-201", "B-202"]
schedules_docs = []
idx = 0
for weekday in range(1, 6):
for period in range(1, 5):
course = courses[idx % len(courses)]
loc_a = locations_a[idx % len(locations_a)]
loc_b = locations_b[idx % len(locations_b)]
teacher_id = (teacher_a or head_teacher)["_id"] if (teacher_a or head_teacher) else None
lesson_id = f"W{weekday}-P{period}"
schedules_docs.append({
"lesson_id": lesson_id,
"weekday": weekday,
"period": period,
"course_name": course,
"teacher_id": teacher_id,
"teacher_person_id": None,
"start_time": f"{8 + period - 1}:00",
"end_time": f"{8 + period}:40",
"week_range": "1-18",
"classes": [
{"class_id": "SW22-1", "location": loc_a},
{"class_id": "SW22-2", "location": loc_b},
],
"created_at": datetime.now(),
})
idx += 1
await db.schedules.insert_many(schedules_docs)
# 今日考勤样例
today = datetime.now().date().isoformat()
sample_attendance = []
for i in range(1, 6):
sid = f"SW22-1-{i:03d}"
sample_attendance.append({
"lesson_id": "W1-P1",
"class_id": "SW22-1",
"student_id": sid,
"date": today,
"status": "出勤" if i % 4 != 0 else "请假",
})
for i in range(1, 6):
sid = f"SW22-2-{i:03d}"
sample_attendance.append({
"lesson_id": "W1-P1",
"class_id": "SW22-2",
"student_id": sid,
"date": today,
"status": "出勤" if i % 5 != 0 else "缺勤",
})
await db.attendance.insert_many(sample_attendance)
# 操行样例
conduct_docs = [
{
"student_id": "SW22-1-001",
"date": today,
"metrics": {"德育": 90, "纪律": 88, "卫生": 92},
"teacher_comment": "课堂表现积极",
"head_teacher_comment": "遵守纪律,乐于助人",
"score": 90,
},
{
"student_id": "SW22-2-001",
"date": today,
"metrics": {"德育": 85, "纪律": 80, "卫生": 86},
"teacher_comment": "需要提高专注度",
"head_teacher_comment": "总体良好",
"score": 84,
},
]
await db.conduct.insert_many(conduct_docs)
# 请假样例
leaves_docs = [
{
"student_id": "SW22-1-004",
"class_id": "SW22-1",
"from_date": today,
"to_date": today,
"reason": "生病",
"approved_by": head_teacher["_id"] if head_teacher else None,
"status": "已批准",
},
]
await db.leaves.insert_many(leaves_docs)
# 指示样例
directives_docs = [
{
"level": "department",
"content": "本周开展课堂纪律专项检查",
"created_at": datetime.now(),
"issuer_id": teacher_b["_id"] if teacher_b else None,
"targets": ["软件技术系"],
},
{
"level": "campus",
"content": "期中考试安排与安全教育",
"created_at": datetime.now(),
"issuer_id": teacher_a["_id"] if teacher_a else None,
"targets": [],
},
]
await db.directives.insert_many(directives_docs)
print("学校业务数据初始化完成:students/classes/schedules/attendance/conduct/leaves/directives")
await seed_identity_data(db, mode)
async def seed_identity_data(db, mode: str):
await db.persons.create_index("person_id", unique=True)
await db.teachers.create_index("teacher_id", unique=True)
await db.teachers.create_index("account_id", unique=True, sparse=True)
await db.bindings.create_index([("account_id", 1), ("type", 1)], unique=True)
user_doc = await db.users.find_one({"username": "user", "edition": mode})
manager_doc = await db.users.find_one({"username": "manager_edu", "edition": mode})
leader_doc = await db.users.find_one({"username": "leader_edu", "edition": mode})
master_doc = await db.users.find_one({"username": "master_edu", "edition": mode})
persons = []
student_persons = []
cursor_students = db.students.find({})
async for stu in cursor_students:
pid = f"P-STU-{stu.get('student_id')}"
person_doc = {"_id": ObjectId(), "person_id": pid, "name": stu.get("name"), "type": "student"}
persons.append(person_doc)
student_persons.append({"person": person_doc, "student_id": stu.get("student_id")})
if user_doc:
persons.append({"_id": ObjectId(), "person_id": "P-STU-USER", "name": "学生USER", "type": "student"})
if manager_doc:
persons.append({"_id": ObjectId(), "person_id": "P-TEA-001", "name": "班主任A", "type": "teacher"})
if leader_doc:
persons.append({"_id": ObjectId(), "person_id": "P-TEA-002", "name": "中层干部A", "type": "teacher"})
if master_doc:
persons.append({"_id": ObjectId(), "person_id": "P-TEA-003", "name": "校级干部A", "type": "teacher"})
if persons:
await db.persons.insert_many(persons)
teachers = []
def find_person(pid: str):
return next((p for p in persons if p["person_id"] == pid), None)
if manager_doc:
p = find_person("P-TEA-001")
teachers.append({"person_id": p["_id"], "teacher_id": "T-001", "department": "软件技术系", "roles": ["teacher", "homeroom"], "account_id": manager_doc["_id"]})
if leader_doc:
p = find_person("P-TEA-002")
teachers.append({"person_id": p["_id"], "teacher_id": "T-002", "department": "软件技术系", "roles": ["teacher", "cadre"], "account_id": leader_doc["_id"]})
if master_doc:
p = find_person("P-TEA-003")
teachers.append({"person_id": p["_id"], "teacher_id": "T-003", "department": "软件技术系", "roles": ["teacher", "cadre"], "account_id": master_doc["_id"]})
if teachers:
await db.teachers.insert_many(teachers)
for sp in student_persons:
await db.students.update_one({"student_id": sp["student_id"]}, {"$set": {"person_id": sp["person"]["_id"]}})
bindings = []
if user_doc:
p = find_person("P-STU-USER")
bindings.append({"account_id": user_doc["_id"], "person_id": p["_id"], "type": "student", "primary": True})
# 修复:为默认用户创建关联的学生实体,防止 /students/me 404
# 注意:P-STU-USER 是人物,这里需要一个对应的 student 实体指向它
existing_stu = await db.students.find_one({"person_id": p["_id"]})
if not existing_stu:
await db.students.insert_one({
"student_id": "STU-USER-001",
"name": "默认学生USER",
"gender": "男",
"grade": "22级",
"major": "软件技术",
"class_id": "SW22-1", # 默认分到 1 班
"status": "在读",
"person_id": p["_id"],
"created_at": datetime.now(),
"updated_at": datetime.now(),
})
print("已为默认用户 user 创建关联学生实体")
if manager_doc:
p = find_person("P-TEA-001")
bindings.append({"account_id": manager_doc["_id"], "person_id": p["_id"], "type": "teacher", "primary": True})
if leader_doc:
p = find_person("P-TEA-002")
bindings.append({"account_id": leader_doc["_id"], "person_id": p["_id"], "type": "teacher", "primary": True})
if master_doc:
p = find_person("P-TEA-003")
bindings.append({"account_id": master_doc["_id"], "person_id": p["_id"], "type": "teacher", "primary": True})
if bindings:
await db.bindings.insert_many(bindings)
teacher_map = {}
cursor_teachers = db.teachers.find({})
async for t in cursor_teachers:
if t.get("account_id"):
teacher_map[str(t.get("account_id"))] = t.get("person_id")
manager_person = teacher_map.get(str(manager_doc["_id"])) if manager_doc else None
if manager_person:
await db.classes.update_many({}, {"$set": {"head_teacher_person_id": manager_person}})
for acc_id, person_id in teacher_map.items():
await db.schedules.update_many({"teacher_id": ObjectId(acc_id)}, {"$set": {"teacher_person_id": person_id}})
# 移除重复旧字段,保留实体化字段
await db.classes.update_many({}, {"$unset": {"head_teacher_id": ""}})
await db.schedules.update_many({}, {"$unset": {"teacher_id": ""}})
await db.students.update_many({}, {"$unset": {"user_id": ""}})
try:
await db.classes.drop_index("head_teacher_id_1")
except Exception:
pass
try:
await db.schedules.drop_index("teacher_id_1")
except Exception:
pass
try:
await db.students.drop_index("user_id_1")
except Exception:
pass
if __name__ == "__main__":
asyncio.run(init_db())
\ No newline at end of file
FROM golang:1.24-alpine
WORKDIR /app
# 设置 Go 代理
ENV GOPROXY=https://goproxy.cn,direct
# 预下载依赖,利用 Docker 缓存层
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 整理依赖(确保新加的 swaggo 依赖被正确拉取)
RUN go mod tidy
# 编译
RUN go build -o auth-service .
# 运行时暴露端口
EXPOSE 8081
# 启动服务
CMD ["./auth-service"]
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/auth/login": {
"post": {
"description": "使用用户名和密码登录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"认证管理"
],
"summary": "用户登录",
"parameters": [
{
"description": "登录凭证",
"name": "credentials",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.LoginRequest"
}
}
],
"responses": {
"200": {
"description": "登录成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "用户名或密码错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/register": {
"post": {
"description": "注册新用户",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"认证管理"
],
"summary": "用户注册",
"parameters": [
{
"description": "注册信息",
"name": "details",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RegisterRequest"
}
}
],
"responses": {
"200": {
"description": "注册成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误或用户已存在",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/bindings": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "将当前用户绑定到学生或教师身份",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"绑定管理"
],
"summary": "创建用户绑定",
"parameters": [
{
"description": "绑定信息",
"name": "binding",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.BindingRequest"
}
}
],
"responses": {
"200": {
"description": "绑定成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "未授权",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/bindings/me": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取当前登录用户的主身份绑定信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"绑定管理"
],
"summary": "获取我的主绑定",
"responses": {
"200": {
"description": "获取成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "未授权",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "未找到主绑定",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/bindings/{person_id}": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "解除当前用户的指定身份绑定",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"绑定管理"
],
"summary": "解除用户绑定",
"parameters": [
{
"type": "string",
"description": "人员ID",
"name": "person_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "解绑成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "未授权",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
"handler.BindingRequest": {
"type": "object",
"required": [
"person_id",
"type"
],
"properties": {
"person_id": {
"type": "string"
},
"primary": {
"type": "boolean"
},
"type": {
"type": "string"
}
}
},
"handler.LoginRequest": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.RegisterRequest": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string",
"minLength": 6
},
"username": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:8080",
BasePath: "/api/v1",
Schemes: []string{},
Title: "Auth Service API",
Description: "LLM Filter System Authentication Service API",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
{
"swagger": "2.0",
"info": {
"description": "LLM Filter System Authentication Service API",
"title": "Auth Service API",
"contact": {},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/api/v1",
"paths": {
"/auth/login": {
"post": {
"description": "使用用户名和密码登录",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"认证管理"
],
"summary": "用户登录",
"parameters": [
{
"description": "登录凭证",
"name": "credentials",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.LoginRequest"
}
}
],
"responses": {
"200": {
"description": "登录成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "用户名或密码错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/register": {
"post": {
"description": "注册新用户",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"认证管理"
],
"summary": "用户注册",
"parameters": [
{
"description": "注册信息",
"name": "details",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RegisterRequest"
}
}
],
"responses": {
"200": {
"description": "注册成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误或用户已存在",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/bindings": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "将当前用户绑定到学生或教师身份",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"绑定管理"
],
"summary": "创建用户绑定",
"parameters": [
{
"description": "绑定信息",
"name": "binding",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.BindingRequest"
}
}
],
"responses": {
"200": {
"description": "绑定成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "未授权",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/bindings/me": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "获取当前登录用户的主身份绑定信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"绑定管理"
],
"summary": "获取我的主绑定",
"responses": {
"200": {
"description": "获取成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "未授权",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "未找到主绑定",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/bindings/{person_id}": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "解除当前用户的指定身份绑定",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"绑定管理"
],
"summary": "解除用户绑定",
"parameters": [
{
"type": "string",
"description": "人员ID",
"name": "person_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "解绑成功",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"401": {
"description": "未授权",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
"handler.BindingRequest": {
"type": "object",
"required": [
"person_id",
"type"
],
"properties": {
"person_id": {
"type": "string"
},
"primary": {
"type": "boolean"
},
"type": {
"type": "string"
}
}
},
"handler.LoginRequest": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.RegisterRequest": {
"type": "object",
"required": [
"email",
"password",
"username"
],
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string",
"minLength": 6
},
"username": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}
\ No newline at end of file
basePath: /api/v1
definitions:
handler.BindingRequest:
properties:
person_id:
type: string
primary:
type: boolean
type:
type: string
required:
- person_id
- type
type: object
handler.LoginRequest:
properties:
password:
type: string
username:
type: string
required:
- password
- username
type: object
handler.RegisterRequest:
properties:
email:
type: string
password:
minLength: 6
type: string
username:
type: string
required:
- email
- password
- username
type: object
host: localhost:8080
info:
contact: {}
description: LLM Filter System Authentication Service API
title: Auth Service API
version: "1.0"
paths:
/auth/login:
post:
consumes:
- application/json
description: 使用用户名和密码登录
parameters:
- description: 登录凭证
in: body
name: credentials
required: true
schema:
$ref: '#/definitions/handler.LoginRequest'
produces:
- application/json
responses:
"200":
description: 登录成功
schema:
additionalProperties: true
type: object
"400":
description: 请求参数错误
schema:
additionalProperties:
type: string
type: object
"401":
description: 用户名或密码错误
schema:
additionalProperties:
type: string
type: object
summary: 用户登录
tags:
- 认证管理
/auth/register:
post:
consumes:
- application/json
description: 注册新用户
parameters:
- description: 注册信息
in: body
name: details
required: true
schema:
$ref: '#/definitions/handler.RegisterRequest'
produces:
- application/json
responses:
"200":
description: 注册成功
schema:
additionalProperties: true
type: object
"400":
description: 请求参数错误或用户已存在
schema:
additionalProperties:
type: string
type: object
summary: 用户注册
tags:
- 认证管理
/bindings:
post:
consumes:
- application/json
description: 将当前用户绑定到学生或教师身份
parameters:
- description: 绑定信息
in: body
name: binding
required: true
schema:
$ref: '#/definitions/handler.BindingRequest'
produces:
- application/json
responses:
"200":
description: 绑定成功
schema:
additionalProperties: true
type: object
"400":
description: 请求参数错误
schema:
additionalProperties:
type: string
type: object
"401":
description: 未授权
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: 创建用户绑定
tags:
- 绑定管理
/bindings/{person_id}:
delete:
consumes:
- application/json
description: 解除当前用户的指定身份绑定
parameters:
- description: 人员ID
in: path
name: person_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: 解绑成功
schema:
additionalProperties: true
type: object
"400":
description: 请求参数错误
schema:
additionalProperties:
type: string
type: object
"401":
description: 未授权
schema:
additionalProperties:
type: string
type: object
"500":
description: 服务器内部错误
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: 解除用户绑定
tags:
- 绑定管理
/bindings/me:
get:
consumes:
- application/json
description: 获取当前登录用户的主身份绑定信息
produces:
- application/json
responses:
"200":
description: 获取成功
schema:
additionalProperties: true
type: object
"401":
description: 未授权
schema:
additionalProperties:
type: string
type: object
"404":
description: 未找到主绑定
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: 获取我的主绑定
tags:
- 绑定管理
securityDefinitions:
BearerAuth:
in: header
name: Authorization
type: apiKey
swagger: "2.0"
module auth-service
go 1.24.3
require github.com/gin-gonic/gin v1.11.0
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/gorm v1.31.1 // indirect
)
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
package handler
import (
"auth-service/internal/service"
"net/http"
"github.com/gin-gonic/gin"
)
type AuthHandler struct {
svc *service.AuthService
}
func NewAuthHandler(svc *service.AuthService) *AuthHandler {
return &AuthHandler{svc: svc}
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// Login 处理用户登录
// @Summary 用户登录
// @Description 使用用户名和密码登录
// @Tags 认证管理
// @Accept json
// @Produce json
// @Param credentials body LoginRequest true "登录凭证"
// @Success 200 {object} map[string]interface{} "登录成功"
// @Failure 400 {object} map[string]string "请求参数错误"
// @Failure 401 {object} map[string]string "用户名或密码错误"
// @Router /auth/login [post]
func (h *AuthHandler) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, user, err := h.svc.Login(&service.LoginRequest{
Username: req.Username,
Password: req.Password,
})
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
// Register 处理用户注册
// @Summary 用户注册
// @Description 注册新用户
// @Tags 认证管理
// @Accept json
// @Produce json
// @Param details body RegisterRequest true "注册信息"
// @Success 200 {object} map[string]interface{} "注册成功"
// @Failure 400 {object} map[string]string "请求参数错误或用户已存在"
// @Router /auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.svc.Register(&service.RegisterRequest{
Username: req.Username,
Email: req.Email,
Password: req.Password,
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "User registered successfully",
"user": user,
})
}
package handler
import (
"auth-service/internal/service"
"net/http"
"github.com/gin-gonic/gin"
)
type BindingHandler struct {
svc *service.BindingService
}
func NewBindingHandler(svc *service.BindingService) *BindingHandler {
return &BindingHandler{svc: svc}
}
type BindingRequest struct {
PersonID string `json:"person_id" binding:"required"`
Type string `json:"type" binding:"required"`
Primary bool `json:"primary"`
}
// Bind 创建绑定
// @Summary 创建用户绑定
// @Description 将当前用户绑定到学生或教师身份
// @Tags 绑定管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param binding body BindingRequest true "绑定信息"
// @Success 200 {object} map[string]interface{} "绑定成功"
// @Failure 400 {object} map[string]string "请求参数错误"
// @Failure 401 {object} map[string]string "未授权"
// @Router /bindings [post]
func (h *BindingHandler) Bind(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
var req BindingRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 默认 primary 为 true,如果前端传了 false 则为 false
// 注意:BindJSON 会将未传的 bool 设为 false,这里我们需要明确业务逻辑
// Python 代码中: primary: bool = True (default)
// 这里 Go 的 struct 如果不传默认是 false。
// 为了兼容,我们可以认为如果不传,默认应该不是强制 true,而是看前端传什么。
// 但 Python 代码明确 default=True。
// 这里简单处理:如果前端没传 primary,Go 默认 false。如果业务需要默认 true,需要指针或者自定义 Unmarshal。
// 鉴于这是一个微服务重构,我们假设前端会明确传递参数,或者我们暂且按 Go 的默认值 false 处理,
// 如果需要默认 true,可以在 service 层处理,或者这里手动判断。
// 让我们查看 Python 代码: `primary: bool = True` in Pydantic.
// 既然 Python 默认是 True,那我们在 Go 里也应该尽量保持一致,或者让前端显式传。
// 为了简单,我们暂且信任前端传参。
err := h.svc.CreateBinding(&service.CreateBindingRequest{
UserID: userID.(uint),
PersonID: req.PersonID,
Type: req.Type,
Primary: req.Primary, // 注意这里如果没传就是 false
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// Unbind 删除绑定
// @Summary 解除用户绑定
// @Description 解除当前用户的指定身份绑定
// @Tags 绑定管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param person_id path string true "人员ID"
// @Success 200 {object} map[string]interface{} "解绑成功"
// @Failure 400 {object} map[string]string "请求参数错误"
// @Failure 401 {object} map[string]string "未授权"
// @Failure 500 {object} map[string]string "服务器内部错误"
// @Router /bindings/{person_id} [delete]
func (h *BindingHandler) Unbind(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
personID := c.Param("person_id")
if personID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "person_id is required"})
return
}
if err := h.svc.Unbind(userID.(uint), personID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}
// Me 获取当前用户的主绑定
// @Summary 获取我的主绑定
// @Description 获取当前登录用户的主身份绑定信息
// @Tags 绑定管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{} "获取成功"
// @Failure 401 {object} map[string]string "未授权"
// @Failure 404 {object} map[string]string "未找到主绑定"
// @Router /bindings/me [get]
func (h *BindingHandler) Me(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
binding, err := h.svc.GetPrimaryBinding(userID.(uint))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No primary binding found"})
return
}
c.JSON(http.StatusOK, binding)
}
package model
import (
"time"
"gorm.io/gorm"
)
type Binding struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"` // 对应 account_id
PersonID string `gorm:"index;not null" json:"person_id"`
Type string `gorm:"type:varchar(20);not null" json:"type"` // student, teacher, staff
Primary bool `gorm:"default:false" json:"primary"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;not null" json:"username"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
Password string `gorm:"not null" json:"-"` // 不在 JSON 中返回密码
Role string `gorm:"default:'user'" json:"role"`
RoleLevel int `gorm:"default:1" json:"role_level"`
Edition string `gorm:"default:'edu'" json:"edition"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
package repository
import (
"auth-service/internal/model"
"gorm.io/gorm"
)
type BindingRepository struct {
db *gorm.DB
}
func NewBindingRepository(db *gorm.DB) *BindingRepository {
return &BindingRepository{db: db}
}
// Create 创建绑定
func (r *BindingRepository) Create(binding *model.Binding) error {
return r.db.Create(binding).Error
}
// Delete 删除绑定
func (r *BindingRepository) Delete(userID uint, personID string) error {
return r.db.Where("user_id = ? AND person_id = ?", userID, personID).Delete(&model.Binding{}).Error
}
// FindPrimaryByUserID 查找用户的主绑定
func (r *BindingRepository) FindPrimaryByUserID(userID uint) (*model.Binding, error) {
var binding model.Binding
err := r.db.Where("user_id = ? AND \"primary\" = ?", userID, true).First(&binding).Error
if err != nil {
return nil, err
}
return &binding, nil
}
// FindPrimaryByUserIDAndType 查找用户指定类型的主绑定
func (r *BindingRepository) FindPrimaryByUserIDAndType(userID uint, bindingType string) (*model.Binding, error) {
var binding model.Binding
err := r.db.Where("user_id = ? AND type = ? AND \"primary\" = ?", userID, bindingType, true).First(&binding).Error
if err != nil {
return nil, err
}
return &binding, nil
}
// ExistsPrimary 检查是否存在主绑定
func (r *BindingRepository) ExistsPrimary(userID uint, bindingType string) (bool, error) {
var count int64
err := r.db.Model(&model.Binding{}).
Where("user_id = ? AND type = ? AND \"primary\" = ?", userID, bindingType, true).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
package repository
import (
"auth-service/internal/model"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(user *model.User) error {
return r.db.Create(user).Error
}
func (r *UserRepository) FindByUsername(username string) (*model.User, error) {
var user model.User
err := r.db.Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) FindByEmail(email string) (*model.User, error) {
var user model.User
err := r.db.Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) ExistsByUsername(username string) (bool, error) {
var count int64
err := r.db.Model(&model.User{}).Where("username = ?", username).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *UserRepository) ExistsByEmail(email string) (bool, error) {
var count int64
err := r.db.Model(&model.User{}).Where("email = ?", email).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
package service
import (
"auth-service/internal/model"
"auth-service/internal/repository"
"auth-service/pkg/utils"
"errors"
)
type AuthService struct {
repo *repository.UserRepository
}
func NewAuthService(repo *repository.UserRepository) *AuthService {
return &AuthService{repo: repo}
}
// RegisterRequest 注册请求参数
type RegisterRequest struct {
Username string
Email string
Password string
}
// LoginRequest 登录请求参数
type LoginRequest struct {
Username string
Password string
}
// Register 用户注册业务逻辑
func (s *AuthService) Register(req *RegisterRequest) (*model.User, error) {
// 检查用户名是否存在
exists, err := s.repo.ExistsByUsername(req.Username)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("username already exists")
}
// 检查邮箱是否存在
exists, err = s.repo.ExistsByEmail(req.Email)
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("email already registered")
}
// 密码加密
hashedPwd, err := utils.HashPassword(req.Password)
if err != nil {
return nil, err
}
// 创建用户
user := &model.User{
Username: req.Username,
Email: req.Email,
Password: hashedPwd,
Role: "user",
Edition: "edu",
}
if err := s.repo.Create(user); err != nil {
return nil, err
}
return user, nil
}
// Login 用户登录业务逻辑
func (s *AuthService) Login(req *LoginRequest) (string, *model.User, error) {
user, err := s.repo.FindByUsername(req.Username)
if err != nil {
return "", nil, errors.New("invalid username or password")
}
if !utils.CheckPasswordHash(req.Password, user.Password) {
return "", nil, errors.New("invalid username or password")
}
token, err := utils.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
return "", nil, err
}
return token, user, nil
}
package service
import (
"auth-service/internal/model"
"auth-service/internal/repository"
"errors"
)
type BindingService struct {
repo *repository.BindingRepository
}
func NewBindingService(repo *repository.BindingRepository) *BindingService {
return &BindingService{repo: repo}
}
type CreateBindingRequest struct {
UserID uint
PersonID string
Type string
Primary bool
}
// CreateBinding 创建绑定
func (s *BindingService) CreateBinding(req *CreateBindingRequest) error {
// 如果是主绑定,检查是否已存在同类型的主绑定
if req.Primary {
exists, err := s.repo.ExistsPrimary(req.UserID, req.Type)
if err != nil {
return err
}
if exists {
return errors.New("primary binding of this type already exists")
}
}
binding := &model.Binding{
UserID: req.UserID,
PersonID: req.PersonID,
Type: req.Type,
Primary: req.Primary,
}
return s.repo.Create(binding)
}
// Unbind 删除绑定
func (s *BindingService) Unbind(userID uint, personID string) error {
// TODO: 可以在这里检查是否存在,或者直接删除(幂等)
// GORM 的 Delete 如果没找到记录不会报错,只会返回 RowsAffected=0
// 这里简单直接调用 Delete
return s.repo.Delete(userID, personID)
}
// GetPrimaryBinding 获取用户的主绑定
func (s *BindingService) GetPrimaryBinding(userID uint) (*model.Binding, error) {
return s.repo.FindPrimaryByUserID(userID)
}
package main
import (
"auth-service/internal/handler"
"auth-service/internal/model"
"auth-service/internal/repository"
"auth-service/internal/service"
"auth-service/pkg/middleware"
"auth-service/pkg/utils"
"fmt"
"log"
"os"
_ "auth-service/docs" // docs is generated by Swag CLI
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// @title Auth Service API
// @version 1.0
// @description LLM Filter System Authentication Service API
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
// 获取环境变量配置
dbHost := os.Getenv("DB_HOST")
dbUser := os.Getenv("DB_USER")
dbPassword := os.Getenv("DB_PASSWORD")
dbName := os.Getenv("DB_NAME")
dbPort := os.Getenv("DB_PORT")
// 默认值处理(用于本地开发)
if dbHost == "" {
dbHost = "localhost"
}
if dbUser == "" {
dbUser = "admin"
}
if dbPassword == "" {
dbPassword = "password" // 请确保本地有此密码的数据库或修改此处
}
if dbName == "" {
dbName = "llm_filter_db"
}
if dbPort == "" {
dbPort = "5433"
}
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai",
dbHost, dbUser, dbPassword, dbName, dbPort)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// 自动迁移数据库结构
if err := db.AutoMigrate(&model.User{}, &model.Binding{}); err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
// 初始化依赖注入
userRepo := repository.NewUserRepository(db)
// 初始化管理员账号
initAdminUser(userRepo)
authSvc := service.NewAuthService(userRepo)
authHandler := handler.NewAuthHandler(authSvc)
bindingRepo := repository.NewBindingRepository(db)
bindingSvc := service.NewBindingService(bindingRepo)
bindingHandler := handler.NewBindingHandler(bindingSvc)
r := gin.Default()
// Swagger 文档路由
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 路由分组
v1 := r.Group("/api/v1")
{
authGroup := v1.Group("/auth")
{
authGroup.POST("/login", authHandler.Login)
authGroup.POST("/register", authHandler.Register)
}
// 绑定相关路由,需要认证
bindingsGroup := v1.Group("/bindings")
bindingsGroup.Use(middleware.AuthMiddleware())
{
bindingsGroup.POST("", bindingHandler.Bind)
bindingsGroup.DELETE("/:person_id", bindingHandler.Unbind)
bindingsGroup.GET("/me", bindingHandler.Me)
}
}
log.Println("Auth Service starting on :8081...")
if err := r.Run(":8081"); err != nil {
log.Fatal("Server failed to start:", err)
}
}
func initAdminUser(repo *repository.UserRepository) {
adminUsername := "admin"
exists, err := repo.ExistsByUsername(adminUsername)
if err != nil {
log.Printf("Failed to check admin user existence: %v", err)
return
}
if !exists {
log.Println("Admin user not found. Creating default admin user...")
adminPassword := os.Getenv("ADMIN_PASSWORD")
if adminPassword == "" {
adminPassword = "password123"
}
hashedPwd, err := utils.HashPassword(adminPassword)
if err != nil {
log.Printf("Failed to hash admin password: %v", err)
return
}
adminUser := &model.User{
Username: adminUsername,
Email: "admin@example.com",
Password: hashedPwd,
Role: "administrator", // 兼容旧系统的最高权限角色
RoleLevel: 5,
Edition: "edu",
}
if err := repo.Create(adminUser); err != nil {
log.Printf("Failed to create admin user: %v", err)
} else {
log.Printf("Admin user created successfully. Username: %s, Password: %s", adminUsername, adminPassword)
}
} else {
log.Println("Admin user already exists.")
}
}
package middleware
import (
"auth-service/pkg/utils"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
c.Abort()
return
}
tokenString := parts[1]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return utils.GetJWTSecret(), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}
// 将用户信息存入上下文
if sub, ok := claims["sub"].(float64); ok {
c.Set("userID", uint(sub))
}
if role, ok := claims["role"].(string); ok {
c.Set("role", role)
}
c.Next()
}
}
package utils
import (
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
var jwtSecret = []byte("your-secret-key")
func init() {
if secret := os.Getenv("JWT_SECRET"); secret != "" {
jwtSecret = []byte(secret)
}
}
func GetJWTSecret() []byte {
return jwtSecret
}
// HashPassword 密码加密
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPasswordHash 密码校验
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateToken 生成 JWT Token
func GenerateToken(userID uint, username string, role string) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"name": username,
"role": role,
"exp": time.Now().Add(time.Hour * 24).Unix(), // 24小时过期
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8082
ENTRYPOINT ["java", "-jar", "app.jar"]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.llmfilter</groupId>
<artifactId>edu-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>edu-service</name>
<description>Edu Core Service for LLM Filter System</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<!-- JWT Utilities -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>${user.home}/.m2/repository</localRepository>
<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
</settings>
package com.llmfilter.edu;
import com.llmfilter.edu.security.JwtAuthenticationFilter;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@OpenAPIDefinition(
info = @Info(
title = "Edu Service API",
version = "1.0",
description = "LLM 过滤系统 - 教务核心服务 API"
)
)
public class EduServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EduServiceApplication.class, args);
}
@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> loggingFilter(JwtAuthenticationFilter filter) {
FilterRegistrationBean<JwtAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/api/v1/*"); // 拦截 API 接口
registrationBean.setOrder(1);
return registrationBean;
}
}
package com.llmfilter.edu.controller;
import com.llmfilter.edu.dto.ClassDto;
import com.llmfilter.edu.dto.HeadTeacherPayload;
import com.llmfilter.edu.service.ClassService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/classes")
@RequiredArgsConstructor
@Tag(name = "班级管理", description = "班级与班主任管理接口")
public class ClassController {
private final ClassService classService;
@GetMapping
@Operation(summary = "获取班级列表", description = "获取所有班级的基础信息")
public ResponseEntity<List<ClassDto>> listClasses() {
return ResponseEntity.ok(classService.listClasses());
}
@PutMapping("/{classId}/head-teacher")
@Operation(summary = "设置班主任", description = "为指定班级分配或更新班主任")
public ResponseEntity<Map<String, Boolean>> setHeadTeacher(
@PathVariable String classId,
@RequestBody HeadTeacherPayload payload) {
classService.setHeadTeacher(classId, payload);
Map<String, Boolean> result = new HashMap<>();
result.put("success", true);
return ResponseEntity.ok(result);
}
}
package com.llmfilter.edu.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/edu")
public class HealthController {
@GetMapping("/health")
public Map<String, String> health() {
Map<String, String> status = new HashMap<>();
status.put("status", "ok");
status.put("service", "edu-service (Java)");
return status;
}
}
package com.llmfilter.edu.controller;
import com.llmfilter.edu.dto.PersonDto;
import com.llmfilter.edu.service.PersonService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/persons")
@RequiredArgsConstructor
@Tag(name = "人员管理", description = "基础人员信息管理接口")
public class PersonController {
private final PersonService personService;
@PostMapping("/bulk")
@Operation(summary = "批量创建人员", description = "批量导入或创建人员基础信息")
public ResponseEntity<Map<String, Integer>> bulkCreate(@RequestBody List<PersonDto> persons) {
int count = personService.bulkCreate(persons);
Map<String, Integer> result = new HashMap<>();
result.put("inserted", count);
return ResponseEntity.ok(result);
}
@GetMapping
@Operation(summary = "获取人员列表", description = "获取所有人员信息")
public ResponseEntity<Map<String, Object>> listPersons() {
return ResponseEntity.ok(personService.listPersons());
}
}
package com.llmfilter.edu.controller;
import com.llmfilter.edu.dto.AssignTeacherPayload;
import com.llmfilter.edu.dto.ScheduleDto;
import com.llmfilter.edu.service.ScheduleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/schedules")
@RequiredArgsConstructor
@Tag(name = "课表管理", description = "课程表与任课教师管理接口")
public class ScheduleController {
private final ScheduleService scheduleService;
@PutMapping("/assign-teacher")
@Operation(summary = "分配任课教师", description = "为特定课程分配任课教师")
public ResponseEntity<Map<String, Boolean>> assignTeacher(@RequestBody AssignTeacherPayload payload) {
scheduleService.assignTeacher(payload);
Map<String, Boolean> result = new HashMap<>();
result.put("success", true);
return ResponseEntity.ok(result);
}
@GetMapping
@Operation(summary = "获取课表", description = "获取所有课程表信息")
public ResponseEntity<List<ScheduleDto>> listSchedules() {
return ResponseEntity.ok(scheduleService.listSchedules());
}
}
package com.llmfilter.edu.controller;
import com.llmfilter.edu.dto.TeacherDto;
import com.llmfilter.edu.service.TeacherService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/teachers")
@RequiredArgsConstructor
@Tag(name = "教师管理", description = "教师信息管理接口")
public class TeacherController {
private final TeacherService teacherService;
@PostMapping("/bulk")
@Operation(summary = "批量创建教师", description = "批量导入或创建教师信息")
public ResponseEntity<Map<String, Integer>> bulkCreate(@RequestBody List<TeacherDto> teachers) {
int count = teacherService.bulkCreate(teachers);
Map<String, Integer> result = new HashMap<>();
result.put("inserted", count);
return ResponseEntity.ok(result);
}
@GetMapping
@Operation(summary = "获取教师列表", description = "获取所有教师信息")
public ResponseEntity<List<TeacherDto>> listTeachers() {
return ResponseEntity.ok(teacherService.listTeachers());
}
}
package com.llmfilter.edu.dto;
import lombok.Data;
@Data
public class AssignTeacherPayload {
private String lessonId;
private String teacherPersonId;
}
package com.llmfilter.edu.dto;
import lombok.Data;
@Data
public class ClassDto {
private Long id;
private String classId;
private String headTeacherPersonId;
private Integer grade;
private Integer studentsCount;
}
package com.llmfilter.edu.dto;
import lombok.Data;
@Data
public class HeadTeacherPayload {
private String headTeacherPersonId;
}
package com.llmfilter.edu.dto;
import lombok.Data;
@Data
public class PersonDto {
private String personId;
private String name;
private String type;
}
package com.llmfilter.edu.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class ScheduleDto {
private String lessonId;
private Integer weekday;
private Integer period;
private String courseName;
private String teacherPersonId;
private List<Map<String, Object>> classes;
}
package com.llmfilter.edu.dto;
import lombok.Data;
import java.util.List;
@Data
public class TeacherDto {
private String personId;
private String teacherId;
private String department;
private List<String> roles;
private String accountId;
}
package com.llmfilter.edu.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "classes")
public class ClassEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "class_id", unique = true, nullable = false)
private String classId; // 业务上的班级ID,如 "2023-01"
@Column(name = "head_teacher_person_id")
private String headTeacherPersonId; // 班主任人物ID(指向 Teacher.personId)
@Column(name = "grade")
private Integer grade; // 年级
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.llmfilter.edu.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "persons")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "person_id", unique = true, nullable = false)
private String personId; // 业务ID
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String type; // student, teacher, staff
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.llmfilter.edu.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "schedules")
public class Schedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "lesson_id", unique = true, nullable = false)
private String lessonId;
@Column(nullable = false)
private Integer weekday; // 1-7
@Column(nullable = false)
private Integer period; // 1-8
@Column(name = "course_name")
private String courseName;
@Column(name = "teacher_person_id")
private String teacherPersonId; // 关联到 Teacher.personId
// 存储班级信息的 JSON 字符串 (简化处理,实际生产建议用关联表)
// Python code: classes: List[Dict[str, Any]]
@Column(columnDefinition = "TEXT")
private String classesJson;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.llmfilter.edu.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "student_id", unique = true, nullable = false)
private String studentId; // 学号
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "class_id", referencedColumnName = "class_id")
private ClassEntity clazz; // 关联班级
@Column(name = "gender")
private String gender; // male/female
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.llmfilter.edu.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "teachers")
public class Teacher {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "teacher_id", unique = true, nullable = false)
private String teacherId; // 教工号
@Column(name = "person_id", unique = true, nullable = false)
private String personId; // 关联到 Person
@Column
private String department;
// TODO: Roles stored as comma-separated string or separate table?
// For simplicity, let's use comma-separated string for now, or use @ElementCollection
// In Python code: roles: List[str]
@Column
private String roles; // e.g., "math,science"
@Column(name = "account_id")
private String accountId; // 绑定的账号ID
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
package com.llmfilter.edu.repository;
import com.llmfilter.edu.model.ClassEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface ClassRepository extends JpaRepository<ClassEntity, Long> {
Optional<ClassEntity> findByClassId(String classId);
}
package com.llmfilter.edu.repository;
import com.llmfilter.edu.model.Person;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
Optional<Person> findByPersonId(String personId);
}
package com.llmfilter.edu.repository;
import com.llmfilter.edu.model.Schedule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
Optional<Schedule> findByLessonId(String lessonId);
List<Schedule> findAllByOrderByWeekdayAscPeriodAsc();
}
package com.llmfilter.edu.repository;
import com.llmfilter.edu.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
Optional<Student> findByStudentId(String studentId);
}
package com.llmfilter.edu.repository;
import com.llmfilter.edu.model.Teacher;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface TeacherRepository extends JpaRepository<Teacher, Long> {
Optional<Teacher> findByTeacherId(String teacherId);
Optional<Teacher> findByPersonId(String personId);
}
package com.llmfilter.edu.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
@Slf4j
@Component
public class JwtAuthenticationFilter implements Filter {
@Value("${jwt.secret:your_secret_key_here}")
private String jwtSecret;
private Key key;
@PostConstruct
public void init() {
log.info("Initializing JwtAuthenticationFilter with secret length: {}", jwtSecret != null ? jwtSecret.length() : 0);
this.key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
log.info("Processing request in JwtAuthenticationFilter: {}", path);
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 跳过 Swagger 文档和健康检查接口
if (isPublicPath(path)) {
chain.doFilter(request, response);
return;
}
String authHeader = httpRequest.getHeader("Authorization");
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
Long userId = claims.get("user_id", Long.class);
// 注意:JWT 中的 user_id 有时是 number 有时是 string,取决于生成逻辑
// 这里做一个兼容处理
if (userId == null && claims.get("user_id") != null) {
userId = Long.valueOf(claims.get("user_id").toString());
}
String username = claims.getSubject(); // sub is username
String role = claims.get("role", String.class);
UserContext userContext = UserContext.builder()
.userId(userId)
.username(username)
.role(role)
.build();
UserContextHolder.setContext(userContext);
chain.doFilter(request, response);
} catch (JwtException e) {
log.warn("Invalid JWT token: {}", e.getMessage());
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("{\"error\": \"Invalid or expired token\"}");
} finally {
UserContextHolder.clear();
}
} else {
// 没有 Token
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("{\"error\": \"Missing Authorization header\"}");
}
}
private boolean isPublicPath(String path) {
return path.startsWith("/swagger-ui") ||
path.startsWith("/v3/api-docs") ||
path.startsWith("/api/v1/edu/health");
}
}
package com.llmfilter.edu.security;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserContext {
private Long userId;
private String username;
private String role;
}
package com.llmfilter.edu.security;
public class UserContextHolder {
private static final ThreadLocal<UserContext> contextHolder = new ThreadLocal<>();
public static void setContext(UserContext context) {
contextHolder.set(context);
}
public static UserContext getContext() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
package com.llmfilter.edu.service;
import com.llmfilter.edu.dto.ClassDto;
import com.llmfilter.edu.dto.HeadTeacherPayload;
import com.llmfilter.edu.model.ClassEntity;
import com.llmfilter.edu.repository.ClassRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityNotFoundException;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ClassService {
private final ClassRepository classRepository;
@Transactional(readOnly = true)
public List<ClassDto> listClasses() {
return classRepository.findAll().stream()
.map(this::mapToDto)
.collect(Collectors.toList());
}
@Transactional
public void setHeadTeacher(String classId, HeadTeacherPayload payload) {
ClassEntity clazz = classRepository.findByClassId(classId)
.orElseThrow(() -> new EntityNotFoundException("Class not found: " + classId));
clazz.setHeadTeacherPersonId(payload.getHeadTeacherPersonId());
classRepository.save(clazz);
}
private ClassDto mapToDto(ClassEntity entity) {
ClassDto dto = new ClassDto();
dto.setId(entity.getId());
dto.setClassId(entity.getClassId());
dto.setHeadTeacherPersonId(entity.getHeadTeacherPersonId());
dto.setGrade(entity.getGrade());
// TODO: Calculate students count
dto.setStudentsCount(0);
return dto;
}
}
package com.llmfilter.edu.service;
import com.llmfilter.edu.dto.PersonDto;
import com.llmfilter.edu.model.Person;
import com.llmfilter.edu.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class PersonService {
private final PersonRepository personRepository;
@Transactional
public int bulkCreate(List<PersonDto> persons) {
List<Person> entities = persons.stream()
.map(dto -> Person.builder()
.personId(dto.getPersonId())
.name(dto.getName())
.type(dto.getType())
.build())
.collect(Collectors.toList());
return personRepository.saveAll(entities).size();
}
@Transactional(readOnly = true)
public Map<String, Object> listPersons() {
List<Person> all = personRepository.findAll();
List<PersonDto> items = all.stream()
.map(p -> {
PersonDto dto = new PersonDto();
dto.setPersonId(p.getPersonId());
dto.setName(p.getName());
dto.setType(p.getType());
return dto;
})
.collect(Collectors.toList());
Map<String, Long> counts = all.stream()
.collect(Collectors.groupingBy(Person::getType, Collectors.counting()));
Map<String, Object> result = new HashMap<>();
result.put("items", items);
result.put("counts_by_type", counts);
return result;
}
}
package com.llmfilter.edu.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.llmfilter.edu.dto.AssignTeacherPayload;
import com.llmfilter.edu.dto.ScheduleDto;
import com.llmfilter.edu.model.Schedule;
import com.llmfilter.edu.repository.ScheduleRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityNotFoundException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class ScheduleService {
private final ScheduleRepository scheduleRepository;
private final ObjectMapper objectMapper;
@Transactional
public void assignTeacher(AssignTeacherPayload payload) {
Schedule schedule = scheduleRepository.findByLessonId(payload.getLessonId())
.orElseThrow(() -> new EntityNotFoundException("Schedule not found: " + payload.getLessonId()));
schedule.setTeacherPersonId(payload.getTeacherPersonId());
scheduleRepository.save(schedule);
}
@Transactional(readOnly = true)
public List<ScheduleDto> listSchedules() {
return scheduleRepository.findAllByOrderByWeekdayAscPeriodAsc().stream()
.map(this::mapToDto)
.collect(Collectors.toList());
}
private ScheduleDto mapToDto(Schedule entity) {
ScheduleDto dto = new ScheduleDto();
dto.setLessonId(entity.getLessonId());
dto.setWeekday(entity.getWeekday());
dto.setPeriod(entity.getPeriod());
dto.setCourseName(entity.getCourseName());
dto.setTeacherPersonId(entity.getTeacherPersonId());
try {
if (entity.getClassesJson() != null) {
dto.setClasses(objectMapper.readValue(entity.getClassesJson(), new TypeReference<List<Map<String, Object>>>() {}));
} else {
dto.setClasses(Collections.emptyList());
}
} catch (JsonProcessingException e) {
log.error("Failed to parse classes json for schedule {}", entity.getId(), e);
dto.setClasses(Collections.emptyList());
}
return dto;
}
}
package com.llmfilter.edu.service;
import com.llmfilter.edu.dto.TeacherDto;
import com.llmfilter.edu.model.Teacher;
import com.llmfilter.edu.repository.TeacherRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class TeacherService {
private final TeacherRepository teacherRepository;
@Transactional
public int bulkCreate(List<TeacherDto> teachers) {
List<Teacher> entities = teachers.stream()
.map(dto -> Teacher.builder()
.personId(dto.getPersonId())
.teacherId(dto.getTeacherId())
.department(dto.getDepartment())
.roles(String.join(",", dto.getRoles()))
.accountId(dto.getAccountId())
.build())
.collect(Collectors.toList());
return teacherRepository.saveAll(entities).size();
}
@Transactional(readOnly = true)
public List<TeacherDto> listTeachers() {
return teacherRepository.findAll().stream()
.map(t -> {
TeacherDto dto = new TeacherDto();
dto.setPersonId(t.getPersonId());
dto.setTeacherId(t.getTeacherId());
dto.setDepartment(t.getDepartment());
dto.setRoles(t.getRoles() != null ? Arrays.asList(t.getRoles().split(",")) : Collections.emptyList());
dto.setAccountId(t.getAccountId());
return dto;
})
.collect(Collectors.toList());
}
}
server.port=8082
spring.application.name=edu-service
# Database Configuration
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5433}/${DB_NAME:llm_filter_db}
spring.datasource.username=${DB_USER:admin}
spring.datasource.password=${DB_PASSWORD:password}
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true
# Security
jwt.secret=${JWT_SECRET:your_secret_key_here_must_be_very_long_to_be_secure_at_least_32_bytes}
server.port=8082
spring.application.name=edu-service
# Database Configuration
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5433}/${DB_NAME:llm_filter_db}
spring.datasource.username=${DB_USER:admin}
spring.datasource.password=${DB_PASSWORD:password}
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true
# Security
jwt.secret=${JWT_SECRET:your_secret_key_here_must_be_very_long_to_be_secure_at_least_32_bytes}
artifactId=edu-service
groupId=com.llmfilter
version=0.0.1-SNAPSHOT
com/llmfilter/edu/model/Student$StudentBuilder.class
com/llmfilter/edu/repository/ClassRepository.class
com/llmfilter/edu/repository/PersonRepository.class
com/llmfilter/edu/controller/PersonController.class
com/llmfilter/edu/controller/HealthController.class
com/llmfilter/edu/model/Person.class
com/llmfilter/edu/service/ScheduleService.class
com/llmfilter/edu/dto/HeadTeacherPayload.class
com/llmfilter/edu/dto/TeacherDto.class
com/llmfilter/edu/service/PersonService.class
com/llmfilter/edu/repository/ScheduleRepository.class
com/llmfilter/edu/dto/ClassDto.class
com/llmfilter/edu/service/ClassService.class
com/llmfilter/edu/controller/TeacherController.class
com/llmfilter/edu/dto/ScheduleDto.class
com/llmfilter/edu/model/Schedule$ScheduleBuilder.class
com/llmfilter/edu/model/Teacher.class
com/llmfilter/edu/dto/PersonDto.class
com/llmfilter/edu/dto/AssignTeacherPayload.class
com/llmfilter/edu/EduServiceApplication.class
com/llmfilter/edu/model/ClassEntity.class
com/llmfilter/edu/service/TeacherService.class
com/llmfilter/edu/service/ScheduleService$1.class
com/llmfilter/edu/model/Schedule.class
com/llmfilter/edu/model/Student.class
com/llmfilter/edu/controller/ClassController.class
com/llmfilter/edu/repository/TeacherRepository.class
com/llmfilter/edu/controller/ScheduleController.class
com/llmfilter/edu/model/Teacher$TeacherBuilder.class
com/llmfilter/edu/repository/StudentRepository.class
com/llmfilter/edu/model/Person$PersonBuilder.class
com/llmfilter/edu/model/ClassEntity$ClassEntityBuilder.class
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/model/Schedule.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/model/Student.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/service/ClassService.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/model/Person.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/service/TeacherService.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/controller/HealthController.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/controller/PersonController.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/dto/TeacherDto.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/controller/ClassController.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/dto/ScheduleDto.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/dto/ClassDto.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/model/Teacher.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/service/PersonService.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/dto/AssignTeacherPayload.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/controller/ScheduleController.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/model/ClassEntity.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/repository/ScheduleRepository.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/repository/ClassRepository.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/dto/HeadTeacherPayload.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/repository/TeacherRepository.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/dto/PersonDto.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/repository/StudentRepository.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/controller/TeacherController.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/EduServiceApplication.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/service/ScheduleService.java
/Users/uu/Desktop/dles_prj/llm-filter/microservices/edu-service/src/main/java/com/llmfilter/edu/repository/PersonRepository.java
FROM python:3.10-slim
WORKDIR /app
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制业务代码
COPY . .
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.services.auth import get_current_user
from app.models.role import get_role_level
from app.core.config import settings
from bson import ObjectId
from app.db.mongodb import db
import jwt
from jwt.exceptions import InvalidTokenError
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="http://localhost:8081/api/v1/auth/login")
async def get_current_active_user(token: str = Depends(oauth2_scheme)):
"""获取当前活跃用户"""
user = await get_current_user(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
return user
"""获取当前活跃用户 (本地 JWT 验证)"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# 本地验证 JWT,不再调用 Auth Service
# 确保 settings.SECRET_KEY 与 Auth Service 的 JWT_SECRET 一致
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
# Go 生成的 Token Payload: sub (userID), name (username), role
user_id = payload.get("sub")
username: str = payload.get("name")
role: str = payload.get("role")
if user_id is None or username is None:
raise credentials_exception
return {
"id": user_id,
"username": username,
"role": role,
# 兼容字段,供 require_role 使用
"role_level": get_role_level(role),
# 兼容字段,供 require_edition_for_mode 使用
# Go 端目前生成的 Token 中不包含 edition 字段,默认为 "edu" 避免权限错误
"edition": payload.get("edition", "edu")
}
except InvalidTokenError:
raise credentials_exception
async def get_current_admin_user(current_user: dict = Depends(get_current_active_user)):
"""获取当前管理员用户(兼容 admin/administrator 两种命名)"""
......@@ -77,8 +99,8 @@ def require_edition_for_mode():
def require_binding(expected_type: str):
async def _binding_checker(current_user: dict = Depends(get_current_active_user)):
b = await db.db.bindings.find_one({"account_id": ObjectId(current_user["_id"]), "primary": True})
if not b or b.get("type") != expected_type:
raise HTTPException(status_code=403, detail="实体绑定不存在或类型不匹配")
# 简化处理:不再查询 MongoDB,而是信任 token 中的信息或调用 Auth Service 接口
# 暂时跳过绑定检查,或者后续通过 Auth Service 的 /bindings 接口校验
# 这里仅作占位,避免导入 app.db.mongodb 导致错误
return current_user
return _binding_checker
\ No newline at end of file
from fastapi import APIRouter
from app.api.v1 import auth, conversation, admin, dashboard, students, bindings, persons, teachers, classes, schedules
from app.api.v1 import conversation, admin, dashboard
api_router = APIRouter()
# 注册各模块路由
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
api_router.include_router(conversation.router, prefix="/conversations", tags=["对话"])
api_router.include_router(admin.router, prefix="/admin", tags=["管理员"])
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["仪表盘"])
api_router.include_router(students.router, prefix="/students", tags=["学生"])
api_router.include_router(bindings.router, prefix="/bindings", tags=["绑定"])
api_router.include_router(persons.router, prefix="/persons", tags=["人员"])
api_router.include_router(teachers.router, prefix="/teachers", tags=["教师"])
api_router.include_router(classes.router, prefix="/classes", tags=["班级"])
api_router.include_router(schedules.router, prefix="/schedules", tags=["课表"])
\ No newline at end of file
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["仪表盘"])
\ No newline at end of file
......@@ -3,8 +3,12 @@ import os
from pydantic_settings import BaseSettings
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# 加载环境变量 (尝试向上查找 .env)
load_dotenv(verbose=True) # 默认查找当前目录
if not os.getenv("MONGODB_URL"):
# 如果没找到,尝试向上级目录查找(本地开发场景)
load_dotenv(dotenv_path="../../.env")
load_dotenv(dotenv_path="../../../.env") # 备用:防止层级变动
class Settings(BaseSettings):
# 应用配置
......
......@@ -11,19 +11,12 @@ app = FastAPI(
version="1.0.0",
description=(
"LLM 过滤系统后端接口\n\n"
"角色等级:1 学生、2 班主任、3 中层、4 校级、5 管理员\n"
"实体绑定:通过 /bindings 建立账号与人物(students/teachers)的主绑定\n"
"运行模式:APP_MODE=edu/biz,路由统一受版别依赖控制。"
"本服务仅包含 LLM 对话、敏感词管理与仪表盘功能\n"
"认证与用户管理请访问 Auth Service (8081)\n"
"教务数据管理请访问 Edu Service (8082)。"
),
openapi_tags=[
{"name": "认证", "description": "注册、登录,返回令牌与用户基础信息(含绑定信息)"},
{"name": "仪表盘", "description": "学生/班主任/中层/校级看板数据接口"},
{"name": "学生", "description": "学生实体相关接口(按绑定解析)"},
{"name": "绑定", "description": "账号与人物绑定管理(主绑定唯一)"},
{"name": "人员", "description": "人物档案管理(学生/教师/职员等)"},
{"name": "教师", "description": "教师实体导入与查询(含班主任/干部角色)"},
{"name": "班级", "description": "班级管理(设置班主任为 head_teacher_person_id)"},
{"name": "课表", "description": "课表管理(为节次设置 teacher_person_id;共享节次)"},
{"name": "仪表盘", "description": "AI 使用统计看板"},
{"name": "管理员", "description": "管理员功能(敏感词、分类等)"},
{"name": "对话", "description": "对话与敏感词审计接口"},
],
......
from typing import Any
from bson import ObjectId
from pydantic_core import core_schema
class PyObjectId(str):
"""
Pydantic v2 兼容的 MongoDB ObjectId 类型
"""
@classmethod
def __get_pydantic_core_schema__(
cls, _source_type: Any, _handler: Any
) -> core_schema.CoreSchema:
return core_schema.json_or_python_schema(
json_schema=core_schema.str_schema(),
python_schema=core_schema.union_schema([
core_schema.is_instance_schema(ObjectId),
core_schema.str_schema(),
]),
serialization=core_schema.plain_serializer_function_ser_schema(
lambda x: str(x)
),
)
@classmethod
def validate(cls, v):
if not ObjectId.is_valid(v):
raise ValueError("Invalid ObjectId")
return ObjectId(v)
......@@ -84,7 +84,11 @@ async def _get_student_by_user(user_id: ObjectId) -> Dict[str, Any]:
async def student_today_summary(current_user: Dict[str, Any]) -> Dict[str, Any]:
today = await _today_iso()
weekday = await _weekday()
student = await _get_student_by_user(current_user["_id"])
user_id_str = str(current_user["id"])
if not ObjectId.is_valid(user_id_str):
raise HTTPException(status_code=400, detail="无效的用户ID格式")
student = await _get_student_by_user(ObjectId(user_id_str))
class_id = student.get("class_id") if student else None
schedules: List[Dict[str, Any]] = []
......@@ -138,7 +142,11 @@ async def student_week_schedule(current_user: Dict[str, Any], week: int = None)
if week is None:
week = await _get_current_week()
student = await _get_student_by_user(current_user["_id"])
user_id_str = str(current_user["id"])
if not ObjectId.is_valid(user_id_str):
raise HTTPException(status_code=400, detail="无效的用户ID格式")
student = await _get_student_by_user(ObjectId(user_id_str))
class_id = student.get("class_id") if student else None
try:
......@@ -195,11 +203,16 @@ async def teacher_week_schedule(current_user: Dict[str, Any], week: int = None)
if week is None:
week = await _get_current_week()
binding = await _get_primary_binding(current_user["_id"])
user_id_str = str(current_user["id"])
if not ObjectId.is_valid(user_id_str):
raise HTTPException(status_code=400, detail="无效的用户ID格式")
user_id = ObjectId(user_id_str)
binding = await _get_primary_binding(user_id)
if binding.get("type") != "teacher":
raise HTTPException(status_code=403, detail="当前绑定非教师")
teacher = await _get_teacher_entity(current_user["_id"], binding)
teacher = await _get_teacher_entity(user_id, binding)
teacher_person_id = teacher.get("person_id")
# 确保类型匹配
......@@ -274,11 +287,16 @@ async def homeroom_current_summary(current_user: Dict[str, Any]) -> Dict[str, An
current_lesson_id = f"W{weekday}-P{period}"
# 获取当前用户的教师绑定信息
binding = await _get_primary_binding(current_user["_id"])
user_id_str = str(current_user["id"])
if not ObjectId.is_valid(user_id_str):
raise HTTPException(status_code=400, detail="无效的用户ID格式")
user_id = ObjectId(user_id_str)
binding = await _get_primary_binding(user_id)
if binding.get("type") != "teacher":
raise HTTPException(status_code=403, detail="当前绑定非教师")
teacher = await _get_teacher_entity(current_user["_id"], binding)
teacher = await _get_teacher_entity(user_id, binding)
teacher_person_id = teacher.get("person_id")
if isinstance(teacher_person_id, str) and ObjectId.is_valid(teacher_person_id):
......
......@@ -4,10 +4,7 @@ pydantic==2.4.2
pydantic-settings==2.0.3
pymongo==4.6.0
motor==3.3.1
python-jose[cryptography]==3.3.0
passlib==1.7.4
python-multipart==0.0.6
bcrypt==4.0.1
httpx==0.25.0
python-dotenv==1.0.0
email-validator==2.1.0
\ No newline at end of file
PyJWT==2.8.0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment