diff --git a/backend/models/student.py b/backend/models/student.py index 26fd61a..4f320b1 100644 --- a/backend/models/student.py +++ b/backend/models/student.py @@ -56,14 +56,18 @@ class StudentModel: @staticmethod async def get_dormitory_list() -> List[str]: """获取所有不重复的宿舍号列表""" - sql = """ - SELECT DISTINCT dormitory_number - FROM students - WHERE status = 1 AND dormitory_number IS NOT NULL AND dormitory_number != '' - ORDER BY dormitory_number - """ - rows = await execute_query(sql) - return [row["dormitory_number"] for row in rows] + try: + sql = """ + SELECT DISTINCT dormitory_number + FROM students + WHERE status = 1 AND dormitory_number IS NOT NULL AND dormitory_number != '' + ORDER BY dormitory_number + """ + rows = await execute_query(sql) + return [row["dormitory_number"] for row in rows] + except Exception as e: + logger.warning(f"dormitory_number 列不存在,返回空列表: {e}") + return [] @staticmethod async def create( @@ -74,17 +78,25 @@ class StudentModel: initial_points: int = 60 ) -> int: """创建学生(初始操行分默认60分)""" - sql = """ - INSERT INTO students (student_no, name, parent_phone, dormitory_number, total_points) - VALUES (%s, %s, %s, %s, %s) - """ - return await execute_insert(sql, (student_no, name, parent_phone, dormitory_number, initial_points)) + if dormitory_number is not None: + sql = """ + INSERT INTO students (student_no, name, parent_phone, dormitory_number, total_points) + VALUES (%s, %s, %s, %s, %s) + """ + return await execute_insert(sql, (student_no, name, parent_phone, dormitory_number, initial_points)) + else: + sql = """ + INSERT INTO students (student_no, name, parent_phone, total_points) + VALUES (%s, %s, %s, %s) + """ + return await execute_insert(sql, (student_no, name, parent_phone, initial_points)) @staticmethod async def update(student_id: int, name: str = None, parent_phone: str = None, dormitory_number: str = None, status: int = None) -> bool: """更新学生信息""" updates = [] params = [] + has_dormitory = False if name is not None: updates.append("name = %s") @@ -95,6 +107,7 @@ class StudentModel: if dormitory_number is not None: updates.append("dormitory_number = %s") params.append(dormitory_number) + has_dormitory = True if status is not None: updates.append("status = %s") params.append(status) @@ -104,8 +117,30 @@ class StudentModel: params.append(student_id) sql = f"UPDATE students SET {', '.join(updates)} WHERE student_id = %s" - result = await execute_update(sql, tuple(params)) - return result > 0 + try: + result = await execute_update(sql, tuple(params)) + return result > 0 + except Exception as e: + if has_dormitory: + logger.warning(f"dormitory_number 列不存在,尝试不含该字段重试: {e}") + retry_updates = [] + retry_params = [] + if name is not None: + retry_updates.append("name = %s") + retry_params.append(name) + if parent_phone is not None: + retry_updates.append("parent_phone = %s") + retry_params.append(parent_phone) + if status is not None: + retry_updates.append("status = %s") + retry_params.append(status) + if not retry_updates: + return True + retry_params.append(student_id) + sql = f"UPDATE students SET {', '.join(retry_updates)} WHERE student_id = %s" + result = await execute_update(sql, tuple(retry_params)) + return result > 0 + raise @staticmethod async def delete(student_id: int) -> bool: @@ -117,7 +152,7 @@ class StudentModel: @staticmethod async def update_total_points(student_id: int, points_change: int) -> bool: """更新学生总分""" - sql = "UPDATE students SET total_points = total_points + %s, points_updated_at = CURRENT_TIMESTAMP WHERE student_id = %s" + sql = "UPDATE students SET total_points = total_points + %s WHERE student_id = %s" result = await execute_update(sql, (points_change, student_id)) return result > 0 @@ -125,10 +160,10 @@ class StudentModel: async def get_ranking(limit: int = 50) -> List[Dict[str, Any]]: """获取学生排行(单班级)""" sql = """ - SELECT student_id, student_no, name, total_points, points_updated_at + SELECT student_id, student_no, name, total_points FROM students WHERE status = 1 - ORDER BY total_points DESC, points_updated_at ASC + ORDER BY total_points DESC, student_id ASC LIMIT %s """ results = await execute_query(sql, (limit,)) diff --git a/backend/routes/upgrade.py b/backend/routes/upgrade.py index 1e3b8aa..44c94a9 100644 --- a/backend/routes/upgrade.py +++ b/backend/routes/upgrade.py @@ -37,6 +37,64 @@ ALL_VERSIONS = { '2.3': 'v2.3.sql', } +# 版本特征标记(按优先级从高到低) +VERSION_MARKERS = [ + ('2.0', 'students', 'dormitory_number'), + ('1.8', 'conduct_records', 'related_type'), + ('1.7', 'subjects', 'sort_order'), +] + + +async def _detect_current_version() -> str: + """检测当前数据库版本,优先从 system_settings 读取,否则通过列特征推断""" + # 1. 尝试从 system_settings 读取 db_version + try: + row = await execute_query( + "SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'" + ) + if row: + return row[0]['setting_value'] + except Exception as e: + logger.warning(f"查询 system_settings 表失败,将通过列特征推断版本: {e}") + + # 2. 通过列特征推断版本 + inferred_version = '1.0' + for version, table, column in VERSION_MARKERS: + try: + result = await execute_query( + "SELECT COUNT(*) as cnt FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s", + (table, column) + ) + if result and result[0]['cnt'] > 0: + inferred_version = version + break + except Exception as e: + logger.warning(f"检查列特征失败 ({table}.{column}): {e}") + + logger.info(f"通过列特征推断数据库版本为: {inferred_version}") + + # 3. 确保 system_settings 表存在并写入推断版本 + try: + await execute_update( + "CREATE TABLE IF NOT EXISTS `system_settings` (" + "`setting_key` VARCHAR(50) PRIMARY KEY," + "`setting_value` VARCHAR(255) NOT NULL," + "`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ) + await execute_update( + "INSERT INTO system_settings (setting_key, setting_value) VALUES ('db_version', %s) " + "ON DUPLICATE KEY UPDATE setting_value = %s", + (inferred_version, inferred_version) + ) + logger.info(f"已将推断版本 {inferred_version} 写入 system_settings") + except Exception as e: + logger.error(f"写入推断版本失败: {e}") + + return inferred_version + + @router.get("/check") async def check_upgrade(request: Request): """检查数据库版本是否需要升级""" @@ -51,16 +109,8 @@ async def check_upgrade(request: Request): if not is_teacher: return error_response(message="仅班主任可执行升级操作", code=403) - # 检测当前数据库版本 - current_version = '0.0.0' - try: - row = await execute_query( - "SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'" - ) - if row: - current_version = row[0]['setting_value'] - except Exception: - pass # 表不存在时使用默认值 + # 检测当前数据库版本(支持自动推断) + current_version = await _detect_current_version() # 读取目标版本(从 VERSION 文件) version_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '..', 'VERSION') diff --git a/backend/services/admin_service.py b/backend/services/admin_service.py index 6c5e3fc..29f6df5 100644 --- a/backend/services/admin_service.py +++ b/backend/services/admin_service.py @@ -33,25 +33,45 @@ class AdminService: """获取所有学生列表""" offset = (page - 1) * page_size - sql = """ - SELECT student_id, student_no, name, total_points, parent_phone, dormitory_number, status - FROM students - WHERE status = 1 - """ - params = [] - - if search: - sql += " AND (student_no LIKE %s OR name LIKE %s)" - params.extend([f"%{search}%", f"%{search}%"]) - - if dormitory_number: - sql += " AND dormitory_number = %s" - params.append(dormitory_number) - - sql += " ORDER BY student_no LIMIT %s OFFSET %s" - params.extend([page_size, offset]) - - students = await execute_query(sql, tuple(params)) + try: + sql = """ + SELECT student_id, student_no, name, total_points, parent_phone, dormitory_number, status + FROM students + WHERE status = 1 + """ + params = [] + + if search: + sql += " AND (student_no LIKE %s OR name LIKE %s)" + params.extend([f"%{search}%", f"%{search}%"]) + + if dormitory_number: + sql += " AND dormitory_number = %s" + params.append(dormitory_number) + + sql += " ORDER BY student_no LIMIT %s OFFSET %s" + params.extend([page_size, offset]) + + students = await execute_query(sql, tuple(params)) + has_dormitory = True + except Exception as e: + logger.warning(f"dormitory_number 列不存在,使用不含该字段的查询: {e}") + sql = """ + SELECT student_id, student_no, name, total_points, parent_phone, status + FROM students + WHERE status = 1 + """ + params = [] + + if search: + sql += " AND (student_no LIKE %s OR name LIKE %s)" + params.extend([f"%{search}%", f"%{search}%"]) + + sql += " ORDER BY student_no LIMIT %s OFFSET %s" + params.extend([page_size, offset]) + + students = await execute_query(sql, tuple(params)) + has_dormitory = False # 获取总数 count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1" @@ -59,7 +79,7 @@ class AdminService: if search: count_sql += " AND (student_no LIKE %s OR name LIKE %s)" count_params.extend([f"%{search}%", f"%{search}%"]) - if dormitory_number: + if dormitory_number and has_dormitory: count_sql += " AND dormitory_number = %s" count_params.append(dormitory_number) if count_params: