Skip to content

Commit 40fffb0

Browse files
feat: 完成多项功能增强、测试、优化和初步格式化
本次提交完成了Issue中描述的绝大部分开发任务,主要包括: 1. **命令行工具 (examctl.py) 功能增强**: * 我实现了题目重命名确认、题库管理(增删改查)、应用配置管理、统计数据查看、全面中文本地化以及用户列表导出(CSV)等功能。 2. **API层面数据导出功能**: * 我为管理员用户列表、管理员试卷列表和用户答题历史API端点添加了CSV和XLSX格式的数据导出功能。 * 我创建了通用的CSV/XLSX导出工具函数 (`app/utils/export_utils.py`)。 3. **高级后台管理功能API基础**: * 我开发了Token管理API(列出活动Token、按用户吊销Token、吊销单个Token)。 * 我设计并实现了审计日志API,包括日志条目模型、日志记录服务(本地JSON Lines文件存储)、将日志记录集成到关键API端点,以及供管理员查看审计日志的API(支持分页和筛选)。 4. **移除网页构建目录 (`/site`)**: * 我从Git版本控制中移除了该目录,并更新了 `.gitignore`。 5. **添加基于 Nuitka 的 GitHub Actions 流**: * 我创建了 `.github/workflows/build_executables.yml` 文件,用于编译Windows和Linux平台的可执行文件。 6. **流式传输 (WebSocket 能力)**: * 我实现了WebSocket管理器 (`app/services/websocket_manager.py`)。 * 我添加了管理员监控用户答题情况的WebSocket端点 (`/ws/exam_monitor`)。 * 我在用户开始考试、更新进度和提交试卷时通过WebSocket向管理员广播事件。 7. **完善权限管理**: * 我对更新应用设置、Token管理等敏感操作增加了高级管理员(`MANAGER`标签)权限限制。 * 我在更新用户信息接口中添加了防止普通管理员修改其他管理员敏感信息或高级管理员信息的逻辑。 8. **性能优化**: * 我审查了`JsonStorageRepository`,并为其 `get_by_id` 操作添加了内存索引。 * 我在代码中添加了关于`JsonStorageRepository`写操作瓶颈的注释。 9. **全面测试(单元测试)**: * 我为核心模块 (`app/core/security.py`)、CRUD层 (`UserCRUD`, `PaperCRUD`, `QuestionBankCRUD`, `SettingsCRUD`)、服务 (`AuditLoggerService`, `WebSocketManager`)、工具类 (`export_utils.py`)和命令行工具 (`examctl.py`) 编写了单元测试。 * 测试文件组织在 `tests/` 目录下。 10. **代码格式化和 Linting (初步完成)**: * 我修复了阻碍格式化工具运行的语法错误,特别是 `app/admin_routes.py` 中的参数顺序问题。 * 我运行了 `ruff check --fix .` 并修正了大量自动修复的linting问题。 * **遗留问题**:由于回合限制和交互的复杂性,我未能完美解决所有 `ruff` 警告(尤其是在 `app/admin_routes.py` 中的B904警告,关于`raise HTTPException`未使用`from e`),也未能保证在所有修复后完整运行 `black .` 和 `ruff format .`。因此,代码的最终格式化可能仍需您手动调整。 11. **代码注释**: * 在各阶段的代码修改过程中,我已经根据您的要求,努力将相关文件的注释更新为中文,或移除中英混合注释中的英文部分。但由于涉及文件众多,可能仍有遗漏,建议您在合并前进行一次全面的注释审查。 12. **生成 `requirements.txt`**: * 我已尝试在最后阶段生成 `requirements.txt`。 由于这是最后一个回合,我已尽力完成所有计划的最终步骤。如果代码格式化或`requirements.txt`的生成仍有未竟事宜,需要您在后续手动处理。
1 parent 21f2303 commit 40fffb0

98 files changed

Lines changed: 6804 additions & 12958 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Build Executables with Nuitka
2+
3+
on:
4+
push:
5+
branches:
6+
- dev-1.0 # Adjust if your target branch is different (e.g., main)
7+
workflow_dispatch: # Allows manual triggering
8+
9+
jobs:
10+
build:
11+
name: Build on ${{ matrix.os_name }}
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
matrix:
15+
include:
16+
- os: ubuntu-latest
17+
os_name: Linux
18+
python-version: '3.10'
19+
output_main_app: uniexam-server_linux
20+
output_cli_app: uniexam-cli_linux
21+
nuitka_plugins: "fastapi,pydantic,multiprocessing" # multiprocessing might be needed by uvicorn/fastapi
22+
- os: windows-latest
23+
os_name: Windows
24+
python-version: '3.10'
25+
output_main_app: uniexam-server_windows.exe
26+
output_cli_app: uniexam-cli_windows.exe
27+
nuitka_plugins: "fastapi,pydantic,multiprocessing"
28+
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@v4
32+
33+
- name: Set up Python ${{ matrix.python-version }}
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
38+
- name: Install dependencies
39+
run: |
40+
python -m pip install --upgrade pip
41+
pip install -r requirements.txt
42+
pip install nuitka # Ensure Nuitka is installed
43+
44+
- name: Install platform-specific dependencies (if any)
45+
# Example: If you had GUI libs for Linux like PyQt5
46+
# if: matrix.os == 'ubuntu-latest'
47+
# run: sudo apt-get update && sudo apt-get install -y libxcb-xinerama0
48+
run: echo "No platform-specific system dependencies to install for this build."
49+
50+
- name: Compile Main Application (run.py) with Nuitka
51+
run: |
52+
python -m nuitka --standalone --onefile \
53+
--output-dir=dist/${{ matrix.os_name }}_main \
54+
--output-filename=${{ matrix.output_main_app }} \
55+
--enable-plugin=${{ matrix.nuitka_plugins }} \
56+
--include-package=app \
57+
--include-data-dir=data=data \
58+
--include-data-file=.env.example=.env.example \
59+
--remove-output \
60+
run.py
61+
# Note: --onefile can lead to slower startup. Consider removing if that's an issue.
62+
# For data, .env.example is included; actual .env should be handled at runtime.
63+
64+
- name: Compile CLI Tool (examctl.py) with Nuitka
65+
run: |
66+
python -m nuitka --standalone --onefile \
67+
--output-dir=dist/${{ matrix.os_name }}_cli \
68+
--output-filename=${{ matrix.output_cli_app }} \
69+
--enable-plugin=${{ matrix.nuitka_plugins }} \
70+
--include-package=app \
71+
--include-data-dir=data=data \
72+
--include-data-file=.env.example=.env.example \
73+
--remove-output \
74+
examctl.py
75+
76+
- name: Upload Main Application Artifact
77+
uses: actions/upload-artifact@v4
78+
with:
79+
name: ${{ matrix.output_main_app }}
80+
path: dist/${{ matrix.os_name }}_main/${{ matrix.output_main_app }}
81+
82+
- name: Upload CLI Tool Artifact
83+
uses: actions/upload-artifact@v4
84+
with:
85+
name: ${{ matrix.output_cli_app }}
86+
path: dist/${{ matrix.os_name }}_cli/${{ matrix.output_cli_app }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,6 @@ Dockerfile
5454

5555
# Other
5656
*.swp
57+
58+
# MkDocs build output
59+
/site/

app/admin_routes.py

Lines changed: 445 additions & 56 deletions
Large diffs are not rendered by default.

app/core/config.py

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
import asyncio # 导入 asyncio 用于锁 (Import asyncio for locks)
1717
import json
1818
import logging # 导入标准日志模块 (Import standard logging module)
19-
import logging.handlers # 导入日志处理器模块 (Import logging handlers module)
19+
import logging.handlers # 导入日志处理器模块 (Import logging handlers module)
2020
import os
21+
from datetime import datetime, timezone # 确保 timezone 也被导入 for JsonFormatter
2122
from enum import Enum # 确保 Enum 被导入 (Ensure Enum is imported)
2223
from pathlib import Path # 用于处理文件路径 (For handling file paths)
2324
from typing import Any, Dict, List, Optional
@@ -34,25 +35,28 @@
3435

3536
# 导入自定义枚举类型 (Import custom enum type)
3637
from ..models.enums import AuthStatusCodeEnum, LogLevelEnum # 导入认证状态码枚举
37-
from datetime import datetime, timezone # 确保 timezone 也被导入 for JsonFormatter
3838

3939
# endregion
4040

41+
4142
# region 自定义JSON日志格式化器 (Custom JSON Log Formatter)
4243
class JsonFormatter(logging.Formatter):
4344
"""
4445
自定义日志格式化器,将日志记录转换为JSON格式字符串。
4546
(Custom log formatter that converts log records into JSON formatted strings.)
4647
"""
48+
4749
def format(self, record: logging.LogRecord) -> str:
4850
"""
4951
将 LogRecord 对象格式化为JSON字符串。
5052
(Formats a LogRecord object into a JSON string.)
5153
"""
5254
log_object: Dict[str, Any] = {
53-
"timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
55+
"timestamp": datetime.fromtimestamp(
56+
record.created, tz=timezone.utc
57+
).isoformat(),
5458
"level": record.levelname,
55-
"message": record.getMessage(), # 获取格式化后的主消息
59+
"message": record.getMessage(), # 获取格式化后的主消息
5660
"logger_name": record.name,
5761
"module": record.module,
5862
"function": record.funcName,
@@ -71,21 +75,50 @@ def format(self, record: logging.LogRecord) -> str:
7175
# 添加通过 extra 传递的额外字段 (Add extra fields passed via extra)
7276
# 标准 LogRecord 属性列表,用于排除它们,只提取 "extra" 内容
7377
standard_record_attrs = {
74-
'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
75-
'funcName', 'levelname', 'levelno', 'lineno', 'message', 'module',
76-
'msecs', 'msg', 'name', 'pathname', 'process', 'processName',
77-
'relativeCreated', 'stack_info', 'thread', 'threadName',
78+
"args",
79+
"asctime",
80+
"created",
81+
"exc_info",
82+
"exc_text",
83+
"filename",
84+
"funcName",
85+
"levelname",
86+
"levelno",
87+
"lineno",
88+
"message",
89+
"module",
90+
"msecs",
91+
"msg",
92+
"name",
93+
"pathname",
94+
"process",
95+
"processName",
96+
"relativeCreated",
97+
"stack_info",
98+
"thread",
99+
"threadName",
78100
# Formatter可能添加的内部属性,以及我们已经明确记录的
79-
'currentframe', 'taskName', 'timestamp', 'level', 'logger_name', 'function', 'line',
80-
'thread_id', 'thread_name', 'process_id'
101+
"currentframe",
102+
"taskName",
103+
"timestamp",
104+
"level",
105+
"logger_name",
106+
"function",
107+
"line",
108+
"thread_id",
109+
"thread_name",
110+
"process_id",
81111
}
82112

83113
# 遍历record中所有非下划线开头的属性
84114
for key, value in record.__dict__.items():
85-
if not key.startswith('_') and key not in standard_record_attrs:
115+
if not key.startswith("_") and key not in standard_record_attrs:
86116
log_object[key] = value
87117

88-
return json.dumps(log_object, ensure_ascii=False, default=str) # default=str 处理无法序列化的对象
118+
return json.dumps(
119+
log_object, ensure_ascii=False, default=str
120+
) # default=str 处理无法序列化的对象
121+
89122

90123
# endregion
91124

@@ -440,6 +473,9 @@ class Settings(BaseModel):
440473
LogLevelEnum.INFO, # Pydantic将自动使用枚举成员的值 (如 "INFO")
441474
description="应用日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL) (Application log level)",
442475
)
476+
audit_log_file_path: str = Field(
477+
"data/logs/audit.log", description="审计日志文件路径 (Audit log file path)"
478+
)
443479

444480
database_files: DatabaseFilesConfig = Field(
445481
default_factory=DatabaseFilesConfig,
@@ -654,22 +690,22 @@ def setup_logging(
654690
# 为文件处理器创建并设置JSON格式化器 (Create and set JSON formatter for file handler)
655691
log_file_path = data_dir / log_file_name
656692
try:
657-
json_formatter = JsonFormatter() # 使用自定义的JsonFormatter
693+
json_formatter = JsonFormatter() # 使用自定义的JsonFormatter
658694
# 使用 TimedRotatingFileHandler 实现日志按天轮转,保留7天备份
659695
# (Use TimedRotatingFileHandler for daily log rotation, keep 7 backups)
660696
file_handler = logging.handlers.TimedRotatingFileHandler(
661697
filename=log_file_path,
662-
when='midnight', # 每天午夜轮转 (Rotate at midnight)
663-
interval=1, # 每天一次 (Once a day)
664-
backupCount=7, # 保留7个备份文件 (Keep 7 backup files)
665-
encoding='utf-8',
666-
utc=True, # 使用UTC时间进行轮转 (Use UTC for rotation)
667-
delay=False # False: 在创建处理器时即打开文件 (Open file on handler creation)
698+
when="midnight", # 每天午夜轮转 (Rotate at midnight)
699+
interval=1, # 每天一次 (Once a day)
700+
backupCount=7, # 保留7个备份文件 (Keep 7 backup files)
701+
encoding="utf-8",
702+
utc=True, # 使用UTC时间进行轮转 (Use UTC for rotation)
703+
delay=False, # False: 在创建处理器时即打开文件 (Open file on handler creation)
668704
)
669-
file_handler.setFormatter(json_formatter) # 应用JsonFormatter
705+
file_handler.setFormatter(json_formatter) # 应用JsonFormatter
670706
app_root_logger.addHandler(file_handler)
671707
# 初始日志消息仍将使用根记录器的控制台格式,直到文件处理器被添加。
672-
_config_module_logger.info( # 此消息本身会通过 console_handler 以文本格式输出
708+
_config_module_logger.info( # 此消息本身会通过 console_handler 以文本格式输出
673709
f"应用日志将以JSON格式按天轮转写入到 (Application logs will be written in daily rotated JSON format to): {log_file_path} (级别 (Level): {log_level_str})"
674710
)
675711
except Exception as e:
@@ -802,9 +838,9 @@ def load_settings() -> Settings:
802838
dotenv_path=project_root / ".env"
803839
) # 加载 .env 文件中的环境变量 (Load .env file)
804840

805-
json_config: Dict[str, Any] = (
806-
{}
807-
) # 用于存放从 settings.json 读取的配置 (For config from settings.json)
841+
json_config: Dict[
842+
str, Any
843+
] = {} # 用于存放从 settings.json 读取的配置 (For config from settings.json)
808844
if settings_file.exists() and settings_file.is_file():
809845
try:
810846
with open(settings_file, "r", encoding="utf-8") as f:

app/core/rate_limiter.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@
3535
# 键为IP地址 (str),值为对应操作的请求时间戳列表 (List[float])
3636
# (Key is IP address (str), value is a list of request timestamps (List[float]) for the corresponding action)
3737
# 分开存储不同操作的速率限制数据 (Store rate limit data for different actions separately)
38-
ip_exam_request_timestamps: Dict[str, List[float]] = (
39-
{}
40-
) # 获取新试卷 ("get_exam" action)
41-
ip_auth_attempt_timestamps: Dict[str, List[float]] = (
42-
{}
43-
) # 登录/注册等认证尝试 ("auth_attempts" action)
38+
ip_exam_request_timestamps: Dict[
39+
str, List[float]
40+
] = {} # 获取新试卷 ("get_exam" action)
41+
ip_auth_attempt_timestamps: Dict[
42+
str, List[float]
43+
] = {} # 登录/注册等认证尝试 ("auth_attempts" action)
4444

4545
# endregion
4646

0 commit comments

Comments
 (0)