更新v1.4版本,修复了一些已知问题

This commit is contained in:
2026-04-28 03:16:17 +08:00
parent 76088b0dd4
commit 3aac2395a0
26 changed files with 342 additions and 151 deletions

View File

@@ -81,13 +81,13 @@ DEDUCTION_HOMEWORK_NOT_SUBMIT=2
DEDUCTION_HOMEWORK_LATE=1
# 缺勤扣分 - 学生无故缺勤时扣除的操行分
DEDUCTION_ATTENDANCE_ABSENT=5
DEDUCTION_ATTENDANCE_ABSENT=3
# 迟到扣分 - 学生迟到时扣除的操行分
DEDUCTION_ATTENDANCE_LATE=2
DEDUCTION_ATTENDANCE_LATE=1
# 请假扣分 - 学生请假时扣除的操行分(设为0表示不扣分
DEDUCTION_ATTENDANCE_LEAVE=1
# 请假扣分 - 学生请假时扣除的操行分设为0表示不扣分
DEDUCTION_ATTENDANCE_LEAVE=0
# ===========================================
# 劳动委员固定分值配置

View File

@@ -53,9 +53,9 @@ class Settings:
DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2"))
DEDUCTION_HOMEWORK_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1"))
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "5"))
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "2"))
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "1"))
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "3"))
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "1"))
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "0"))
LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "1"))
LABOR_POINTS_SUBTRACT: int = int(os.getenv("LABOR_POINTS_SUBTRACT", "-1"))

View File

@@ -92,7 +92,6 @@ class AuthMiddleware(BaseHTTPMiddleware):
logger.warning(f"[Auth] {path} - Redis Token不匹配, user_id={user_id}, stored={'' if stored_token else ''}")
return self._cors_response(request, 401, "令牌已失效,请重新登录")
# 将用户信息存储到request.state
# 将用户信息存储到request.state
request.state.user_id = payload.get("user_id")
request.state.username = payload.get("username")
@@ -142,20 +141,3 @@ class AuthMiddleware(BaseHTTPMiddleware):
},
headers=headers
)
async def get_current_user(request: Request) -> Dict[str, Any]:
"""获取当前登录用户信息"""
return {
"user_id": request.state.user_id,
"username": request.state.username,
"real_name": getattr(request.state, 'real_name', None) or request.state.username,
"user_type": request.state.user_type,
"student_id": request.state.student_id,
"role": request.state.role
}
async def get_current_user_id(request: Request) -> int:
"""获取当前用户ID"""
return request.state.user_id

View File

@@ -26,6 +26,7 @@ async def get_current_user(request: Request) -> Dict[str, Any]:
return {
"user_id": getattr(request.state, 'user_id', None),
"username": getattr(request.state, 'username', None),
"real_name": getattr(request.state, 'real_name', None),
"user_type": getattr(request.state, 'user_type', None),
"student_id": getattr(request.state, 'student_id', None),
"role": getattr(request.state, 'role', None)
@@ -124,19 +125,23 @@ class PermissionChecker:
async def check_can_revoke(user_id: int, record_id: int) -> bool:
"""
检查是否可以撤销扣分记录
班主任:可以撤销任何记录
班长:可以撤销任何记录
考勤委员:可以撤销自己的记录
其他:只能撤销自己的记录
班主任:可以撤销/反撤销任何记录
班长:可以撤销/反撤销任何记录
考勤委员:可以撤销自己创建的记录
其他角色:无撤销权限
"""
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
record = await execute_one(sql, (record_id,))
record = await execute_one(
"SELECT record_id, recorder_id FROM conduct_records WHERE record_id = %s",
(record_id,)
)
if not record:
return False
role = await PermissionChecker.get_user_role(user_id)
if role in ["班主任", "班长", "志愿委员"]:
if role in ["班主任", "班长"]:
return True
return record["recorder_id"] == user_id
if role == "考勤委员" and record.get("recorder_id") == user_id:
return True
return False
def require_auth(func: Callable):

View File

@@ -40,6 +40,21 @@ class ConductModel:
student_id, points_change, reason, recorder_id, recorder_name, related_type, related_id
))
@staticmethod
async def count_student_records(student_id: int, include_revoked: bool = False) -> int:
"""统计学生操行分记录总数"""
revoked_condition = "" if include_revoked else " AND is_revoked = 0"
sql = f"SELECT COUNT(*) as total FROM conduct_records WHERE student_id = %s{revoked_condition}"
result = await execute_one(sql, (student_id,))
return result["total"] if result else 0
@staticmethod
async def count_records_by_recorder(recorder_id: int) -> int:
"""统计记录人提交的操行分记录总数"""
sql = "SELECT COUNT(*) as total FROM conduct_records WHERE recorder_id = %s"
result = await execute_one(sql, (recorder_id,))
return result["total"] if result else 0
@staticmethod
async def get_student_records(
student_id: int,

View File

@@ -27,7 +27,8 @@ from schemas.admin import (
AddPointsRequest, RevokeRequest, AddAdminRequest,
AddStudentRequest, UpdateStudentRequest,
UpdateHomeworkStatusRequest, AddAttendanceRequest,
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest,
CreateAssignmentRequest
)
from utils.response import success_response, error_response
from utils.logger import get_logger
@@ -48,6 +49,8 @@ async def get_students(
):
"""获取所有学生列表(单班级)"""
user = await get_current_user(request)
if user["user_type"] != "admin":
return error_response(message="仅管理员可查看学生列表", code=403)
result = await AdminService.get_students(page=page, page_size=page_size, search=search)
return success_response(data=result)
@@ -138,6 +141,13 @@ async def update_student(request: Request, student_id: int, req: UpdateStudentRe
parent_phone=req.parent_phone
)
if result["success"]:
await LogService.write_operation_log(
operator_id=user["user_id"], operator_name=user["real_name"],
operator_role="班主任", operation_type="update_student",
target_type="student", target_id=student_id,
details=f"编辑学生ID: {student_id}",
ip=request.client.host
)
return success_response(message=result["message"])
else:
return error_response(message=result["message"])
@@ -153,6 +163,13 @@ async def delete_student(request: Request, student_id: int):
result = await AdminService.delete_student(student_id=student_id)
if result["success"]:
await LogService.write_operation_log(
operator_id=user["user_id"], operator_name=user["real_name"],
operator_role="班主任", operation_type="delete_student",
target_type="student", target_id=student_id,
details=f"删除学生ID: {student_id}",
ip=request.client.host
)
return success_response(message=result["message"])
else:
return error_response(message=result["message"])
@@ -171,6 +188,13 @@ async def reset_student_password(request: Request, student_id: int, req: ResetPa
new_password=req.new_password
)
if result["success"]:
await LogService.write_operation_log(
operator_id=user["user_id"], operator_name=user["real_name"],
operator_role="班主任", operation_type="reset_student_password",
target_type="student", target_id=student_id,
details=f"重置学生密码, 学生ID: {student_id}",
ip=request.client.host
)
return success_response(message=result["message"])
else:
return error_response(message=result["message"])
@@ -272,6 +296,8 @@ async def get_conduct_history(
"""获取操行分历史记录"""
try:
user = await get_current_user(request)
if user["user_type"] != "admin":
return error_response(message="仅管理员可查看历史记录", code=403)
result = await ConductService.get_history(
user_id=user["user_id"],
student_id=student_id,
@@ -316,23 +342,17 @@ async def get_submissions(request: Request, assignment_id: int):
@router.post("/homework/assignment")
async def create_assignment(
request: Request,
subject_id: int,
title: str,
description: Optional[str] = None,
deadline: str = None
):
async def create_assignment(request: Request, req: CreateAssignmentRequest):
"""发布作业(班主任)"""
user = await get_current_user(request)
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
if not is_teacher:
return error_response(message="仅班主任可发布作业", code=403)
result = await HomeworkService.create_assignment(
subject_id=subject_id,
title=title,
description=description,
deadline=deadline,
subject_id=req.subject_id,
title=req.title,
description=req.description,
deadline=req.deadline,
created_by=user["user_id"]
)
if result["success"]:
@@ -416,6 +436,9 @@ async def get_attendance_records(
):
"""获取考勤记录"""
user = await get_current_user(request)
role = await PermissionChecker.get_user_role(user["user_id"])
if role not in ["班主任", "考勤委员"]:
return error_response(message="无权查看考勤记录", code=403)
result = await AttendanceService.get_records(
user_id=user["user_id"],
date=date,

View File

@@ -91,7 +91,6 @@ class AddAttendanceRequest(BaseModel):
class UpdateAdminRequest(BaseModel):
"""更新管理员请求"""
user_id: int = Field(..., gt=0, description="用户ID")
real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名")
role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型")
@@ -109,4 +108,12 @@ class ResetPasswordRequest(BaseModel):
class UpdateStudentRequest(BaseModel):
"""更新学生请求"""
name: Optional[str] = Field(None, min_length=1, max_length=50, description="姓名")
parent_phone: Optional[str] = Field(None, max_length=11, pattern=r'^\d{0,11}$', description="家长手机号")
parent_phone: Optional[str] = Field(None, max_length=11, pattern=r'^\d{0,11}$', description="家长手机号")
class CreateAssignmentRequest(BaseModel):
"""创建作业请求"""
subject_id: int = Field(..., gt=0, description="科目ID")
title: str = Field(..., min_length=1, max_length=200, description="作业标题")
description: Optional[str] = Field(None, max_length=1000, description="作业描述")
deadline: str = Field(..., min_length=1, max_length=20, description="截止日期")

View File

@@ -296,9 +296,8 @@ class AdminService:
if not user:
return {"success": False, "message": "未找到对应的用户账号"}
password_hash = security.sha1_md5_password(new_password)
result = await UserModel.update_password(user['user_id'], password_hash)
# UserModel.update_password 内部会进行哈希,无需预先哈希
result = await UserModel.update_password(user['user_id'], new_password)
if result:
await execute_update(
"UPDATE users SET need_change_password = 1 WHERE user_id = %s",

View File

@@ -89,14 +89,17 @@ class AttendanceService:
points_change = -settings.DEDUCTION_ATTENDANCE_LATE
else:
points_change = -settings.DEDUCTION_ATTENDANCE_LEAVE
# 创建扣分记录
# 扣分为0时跳过如请假不扣分
if points_change == 0:
logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status} (不扣分)")
return {"success": True, "message": "考勤记录添加成功(不扣分)"}
student = await StudentModel.get_by_id(student_id)
if student:
# 获取操作人姓名
user = await UserModel.get_by_user_id(recorder_id)
recorder_name = user.get("real_name", "班主任") if user else "班主任"
# 使用中文状态
# 使用中文状态
status_text = ATTENDANCE_STATUS_MAP.get(status, status)
await ConductModel.create_record(
student_id=student_id,

View File

@@ -115,8 +115,10 @@ class ConductService:
details.append({"student_id": student_id, "error": str(e)})
fail_count += 1
message = "操作成功" if fail_count == 0 else f"{success_count}人成功,{fail_count}人失败"
return {
"success": fail_count == 0,
"message": message,
"success_count": success_count,
"fail_count": fail_count,
"details": details
@@ -278,7 +280,7 @@ class ConductService:
limit=page_size,
offset=offset
)
total = len(await ConductModel.get_student_records(student_id, limit=10000))
total = await ConductModel.count_student_records(student_id)
else:
# 查看自己提交的记录
records = await ConductModel.get_records_by_recorder(
@@ -286,7 +288,7 @@ class ConductService:
limit=page_size,
offset=offset
)
total = len(await ConductModel.get_records_by_recorder(user_id, limit=10000))
total = await ConductModel.count_records_by_recorder(user_id)
return {
"records": records,

View File

@@ -135,9 +135,8 @@ class ParentService:
offset=offset
)
# 获取总数
all_records = await ConductModel.get_student_records(user["student_id"], limit=10000)
total = len(all_records)
# 使用 COUNT 查询获取总数(避免获取全部记录)
total = await ConductModel.count_student_records(user["student_id"])
return {
"student_id": student["student_id"],