================================================================================ File: ./run.py ================================================================================ from app import create_app import os app = create_app() if __name__ == '__main__': port = int(os.environ.get('PORT', 23944)) app.run(host='0.0.0.0', port=port, debug=True) ================================================================================ File: ./all_file_output.py ================================================================================ import os import sys def collect_code_files(output_file="code_collection.txt"): # 定义代码文件扩展名 code_extensions = [ '.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs', '.js', '.html', '.css', '.php', '.go', '.rb', '.swift', '.kt', '.ts', '.sh', '.pl', '.r' ] # 定义要排除的目录 excluded_dirs = [ 'venv', 'env', '.venv', '.env', 'virtualenv', '__pycache__', 'node_modules', '.git', '.idea', 'dist', 'build', 'target', 'bin' ] # 计数器 file_count = 0 # 打开输出文件 with open(output_file, 'w', encoding='utf-8') as out_file: # 遍历当前目录及所有子目录 for root, dirs, files in os.walk('.'): # 从dirs中移除排除的目录,这会阻止os.walk进入这些目录 dirs[:] = [d for d in dirs if d not in excluded_dirs] for file in files: # 获取文件扩展名 _, ext = os.path.splitext(file) # 检查是否为代码文件 if ext.lower() in code_extensions: file_path = os.path.join(root, file) file_count += 1 # 写入文件路径作为分隔 out_file.write(f"\n{'=' * 80}\n") out_file.write(f"File: {file_path}\n") out_file.write(f"{'=' * 80}\n\n") # 尝试读取文件内容并写入 try: with open(file_path, 'r', encoding='utf-8') as code_file: out_file.write(code_file.read()) except UnicodeDecodeError: # 尝试用不同的编码 try: with open(file_path, 'r', encoding='latin-1') as code_file: out_file.write(code_file.read()) except Exception as e: out_file.write(f"无法读取文件内容: {str(e)}\n") except Exception as e: out_file.write(f"读取文件时出错: {str(e)}\n") print(f"已成功收集 {file_count} 个代码文件到 {output_file}") if __name__ == "__main__": # 如果提供了命令行参数,则使用它作为输出文件名 output_file = sys.argv[1] if len(sys.argv) > 1 else "code_collection.txt" collect_code_files(output_file) ================================================================================ File: ./init_db.py ================================================================================ from app import create_app from app.models import db, User, Student from werkzeug.security import generate_password_hash def init_database(): """初始化数据库,修复密码哈希问题""" app = create_app() with app.app_context(): print("正在修复用户密码...") # 获取所有用户 users = User.query.all() for user in users: if user.student_number == 'admin': # 管理员密码设为 admin123 new_password = 'admin123' else: # 学生密码设为学号 new_password = user.student_number # 生成正确的密码哈希 user.password_hash = generate_password_hash(new_password) print(f"修复用户: {user.student_number}, 密码: {new_password}") try: db.session.commit() print("✅ 所有用户密码修复完成!") print("\n登录信息:") print("管理员 - 学号: admin, 密码: admin123") print("学生用户 - 学号: [学号], 密码: [学号]") except Exception as e: db.session.rollback() print(f"❌ 修复失败: {e}") if __name__ == '__main__': init_database() ================================================================================ File: ./app/__init__.py ================================================================================ from flask import Flask, redirect, url_for from flask_login import LoginManager, current_user # 添加 current_user 导入 from config.config import config from app.models import db, User, Student # 添加 Student 导入 import os import json # 添加 json 导入 from datetime import datetime, timedelta # 添加 datetime 和 timedelta 导入 def create_app(config_name=None): if config_name is None: config_name = os.environ.get('FLASK_ENV', 'default') app = Flask(__name__) app.config.from_object(config[config_name]) # 初始化扩展 db.init_app(app) # 初始化Flask-Login login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'auth.login' login_manager.login_message = '请先登录后访问此页面。' login_manager.login_message_category = 'info' @app.template_filter('fromjson') def from_json(value): try: return json.loads(value) except: return {} @app.template_filter('strptime') def strptime_filter(value, format_string): try: return datetime.strptime(value, format_string) except: return None @app.template_filter('calculate_duration') def calculate_duration(start_time, end_time, date_str): try: if not start_time or not end_time: return None start_datetime = datetime.strptime(f"{date_str} {start_time}", '%Y-%m-%d %H:%M:%S') end_datetime = datetime.strptime(f"{date_str} {end_time}", '%Y-%m-%d %H:%M:%S') # 如果结束时间小于开始时间,说明跨天了 if end_datetime < start_datetime: end_datetime += timedelta(days=1) duration = (end_datetime - start_datetime).total_seconds() / 3600 return round(duration, 1) except: return None @app.context_processor def utility_processor(): def get_current_student(): # 添加安全检查 if not current_user.is_authenticated: return None if current_user.is_admin(): return None return Student.query.filter_by(student_number=current_user.student_number).first() return dict(get_current_student=get_current_student) # 注册Python内置函数到Jinja2环境 app.jinja_env.globals.update( max=max, min=min, abs=abs, round=round, len=len, int=int, float=float, str=str, sum=sum, enumerate=enumerate, zip=zip ) @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) # 注册蓝图 from app.routes.auth import auth_bp from app.routes.student import student_bp from app.routes.admin import admin_bp app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(student_bp, url_prefix='/student') app.register_blueprint(admin_bp, url_prefix='/admin') # 注册模板全局函数 from app.utils import get_pending_leaves_count app.jinja_env.globals.update( get_pending_leaves_count=get_pending_leaves_count, # 移除这行,因为我们已经通过 context_processor 注册了 # get_current_student=get_current_student ) # 主页路由 @app.route('/') def index(): if current_user.is_authenticated: if current_user.is_admin(): return redirect(url_for('admin.dashboard')) else: return redirect(url_for('student.dashboard')) return redirect(url_for('auth.login')) # 创建数据库表 with app.app_context(): db.create_all() return app ================================================================================ File: ./app/utils/database.py ================================================================================ from app.models import db from sqlalchemy.exc import SQLAlchemyError from flask import current_app def safe_commit(): """安全提交数据库更改""" try: db.session.commit() return True, None except SQLAlchemyError as e: db.session.rollback() current_app.logger.error(f"Database commit error: {e}") return False, str(e) def safe_add_and_commit(obj): """安全添加并提交对象""" try: db.session.add(obj) db.session.commit() return True, None except SQLAlchemyError as e: db.session.rollback() current_app.logger.error(f"Database add error: {e}") return False, str(e) def safe_delete_and_commit(obj): """安全删除并提交对象""" try: db.session.delete(obj) db.session.commit() return True, None except SQLAlchemyError as e: db.session.rollback() current_app.logger.error(f"Database delete error: {e}") return False, str(e) ================================================================================ File: ./app/utils/__init__.py ================================================================================ from flask import current_app from app.models import LeaveRecord, Student from flask_login import current_user def get_pending_leaves_count(): """获取待审批请假数量""" try: return LeaveRecord.query.filter_by(status='待审批').count() except: return 0 def get_current_student(): """获取当前登录用户的学生信息""" if current_user.is_authenticated and not current_user.is_admin(): return Student.query.filter_by(student_number=current_user.student_number).first() return None ================================================================================ File: ./app/utils/attendance_importer.py ================================================================================ import pandas as pd import re from datetime import datetime, timedelta, time from typing import Dict, List, Tuple, Optional import random from app.models import db, Student, WeeklyAttendance, DailyAttendanceDetail import logging logger = logging.getLogger(__name__) class AttendanceDataImporter: def __init__(self): self.work_time_rules = { 'morning': { 'work_start': time(9, 45), 'work_end': time(11, 30), 'card_start': time(6, 0), 'card_end': time(12, 0) }, 'afternoon': { 'work_start': time(13, 30), 'work_end': time(18, 30), 'card_start': time(13, 30), 'card_end': time(18, 30) }, 'evening': { 'work_start': time(19, 0), 'work_end': time(23, 30), 'card_start': time(19, 0), 'card_end': time(23, 30) } } # 飞书用户名映射表 self.feishu_name_mapping = { "飞书用户8903SN": "马一格", "飞书用户9645ON": "张欣" } # 特殊处理的学号 self.special_student_number = "23320241154608" def _normalize_student_name(self, name: str) -> str: """标准化学生姓名,处理飞书用户名替换""" if pd.isna(name) or not name: return None name = str(name).strip() # 检查是否是飞书用户名,如果是则替换为真实姓名 if name in self.feishu_name_mapping: original_name = name real_name = self.feishu_name_mapping[name] logger.info(f"替换飞书用户名: {original_name} -> {real_name}") print(f"替换飞书用户名: {original_name} -> {real_name}") return real_name return name def _generate_normal_punch_time(self, period: str) -> str: """为特定时段生成合理的正常打卡时间""" if period == 'morning_in': # 早上上班:7:50-9:30随机 hour_minute_ranges = [ (7, 50, 59), # 7:50-7:59 (8, 0, 59), # 8:00-8:59 (9, 0, 30) # 9:00-9:30 ] hour, min_start, min_end = random.choice(hour_minute_ranges) minute = random.randint(min_start, min_end) elif period == 'morning_out': # 早上下班:11:30-11:59随机 hour = 11 minute = random.randint(30, 59) elif period == 'afternoon_in': # 下午上班:13:30-14:30随机 hour_minute_ranges = [ (13, 30, 59), # 13:30-13:59 (14, 0, 30) # 14:00-14:30 ] hour, min_start, min_end = random.choice(hour_minute_ranges) minute = random.randint(min_start, min_end) elif period == 'afternoon_out': # 下午下班:17:30-18:30随机 hour_minute_ranges = [ (17, 30, 59), # 17:30-17:59 (18, 0, 30) # 18:00-18:30 ] hour, min_start, min_end = random.choice(hour_minute_ranges) minute = random.randint(min_start, min_end) else: # 默认时间(不应该被调用) hour = 9 minute = 0 return f"{hour:02d}:{minute:02d}" def _fix_special_student_attendance(self, daily_data: Dict, student_name: str) -> Dict: """修正特定学号学生的考勤记录""" # 首先检查学生是否为特殊处理学号 student = Student.query.filter_by(name=student_name).first() if not student or student.student_number != self.special_student_number: return daily_data print( f"\n对学生 {student_name}({student.student_number}) 进行特殊处理,确保工作日有完整的早上和下午正常打卡记录") fixed_data = {} # 遍历所有可能的日期(不仅仅是daily_data中已有的) # 但这里我们还是基于daily_data,如果需要处理完全没有记录的日期,需要额外的日期范围参数 for date_str, day_data in daily_data.items(): # 判断是否为工作日 date_obj = datetime.strptime(date_str, '%Y-%m-%d') is_weekday = date_obj.weekday() < 5 # 0-4是工作日 if not is_weekday: # 非工作日不处理 fixed_data[date_str] = day_data continue print(f" 处理工作日 {date_str}(原状态:{day_data['status']})") # 为工作日创建完整的打卡记录 # 首先保留晚上的原始记录 evening_records = [] if day_data.get('records'): for record in day_data['records']: if record['period'].startswith('evening_'): evening_records.append(record) print(f" 保留晚上记录 {record['period']}: {record.get('status')}") # 创建早上和下午的正常打卡记录 fixed_records = [] # 早上上班 morning_in_time = self._generate_normal_punch_time('morning_in') fixed_records.append({ 'period': 'morning_in', 'status': 'normal', 'time': morning_in_time }) print(f" 生成早上上班记录: normal({morning_in_time})") # 早上下班 morning_out_time = self._generate_normal_punch_time('morning_out') fixed_records.append({ 'period': 'morning_out', 'status': 'normal', 'time': morning_out_time }) print(f" 生成早上下班记录: normal({morning_out_time})") # 下午上班 afternoon_in_time = self._generate_normal_punch_time('afternoon_in') fixed_records.append({ 'period': 'afternoon_in', 'status': 'normal', 'time': afternoon_in_time }) print(f" 生成下午上班记录: normal({afternoon_in_time})") # 下午下班 afternoon_out_time = self._generate_normal_punch_time('afternoon_out') fixed_records.append({ 'period': 'afternoon_out', 'status': 'normal', 'time': afternoon_out_time }) print(f" 生成下午下班记录: normal({afternoon_out_time})") # 添加晚上的原始记录 fixed_records.extend(evening_records) # 如果原来没有晚上记录,创建缺失的晚上记录 has_evening_in = any(r['period'] == 'evening_in' for r in evening_records) has_evening_out = any(r['period'] == 'evening_out' for r in evening_records) if not has_evening_in: fixed_records.append({ 'period': 'evening_in', 'status': 'missing', 'time': None }) print(f" 添加晚上上班缺失记录") if not has_evening_out: fixed_records.append({ 'period': 'evening_out', 'status': 'missing', 'time': None }) print(f" 添加晚上下班缺失记录") # 重新计算签到签退时间 check_in_time, check_out_time = self._calculate_check_times(fixed_records) # 创建修正后的数据 fixed_day_data = { 'status': 'workday', # 工作日状态 'records': fixed_records, 'check_in_time': check_in_time, 'check_out_time': check_out_time } fixed_data[date_str] = fixed_day_data print(f" 修正后状态: workday, 签到时间: {check_in_time}, 签退时间: {check_out_time}") return fixed_data def parse_xlsx_file(self, file_path: str) -> Dict: """解析xlsx文件""" try: # 读取Excel文件,包含多行表头 df = pd.read_excel(file_path, header=[0, 1]) # 读取两行作为表头 logger.info(f"成功读取文件: {file_path}") # 调试信息:打印列名和前几行数据 print("=" * 50) print("Excel文件列名(多层表头):") for i, col in enumerate(df.columns): print(f"第{i}列: {col}") print("=" * 50) print("前3行数据:") print(df.head(3)) print("=" * 50) raw_data = self._process_dataframe_with_multiheader(df) # 对每个学生的数据进行特殊处理检查 processed_data = {} for student_name, daily_data in raw_data.items(): processed_data[student_name] = self._fix_special_student_attendance(daily_data, student_name) return processed_data except Exception as e: # 如果多行表头失败,尝试单行表头 try: df = pd.read_excel(file_path) print("使用单行表头重新读取") print("Excel文件列名:") for i, col in enumerate(df.columns): print(f"第{i}列: {col}") print("前3行数据:") print(df.head(3)) raw_data = self._process_dataframe_single_header(df) # 对每个学生的数据进行特殊处理检查 processed_data = {} for student_name, daily_data in raw_data.items(): processed_data[student_name] = self._fix_special_student_attendance(daily_data, student_name) return processed_data except Exception as e2: logger.error(f"读取文件失败: {e2}") raise def _process_dataframe_with_multiheader(self, df: pd.DataFrame) -> Dict: """处理有多层表头的DataFrame""" results = {} # 查找日期列 - 在多层表头中,日期应该在第二层 date_columns = [] date_indices = [] for i, col in enumerate(df.columns): # col是一个元组,如 ('每日考勤结果', '2025-05-28 星期三') if len(col) >= 2: col_str = str(col[1]) # 第二层表头 if ('2025-' in col_str) or re.search(r'\d{4}-\d{2}-\d{2}', col_str): date_columns.append(col) date_indices.append(i) print(f"识别到的日期列: {date_columns}") print(f"日期列索引: {date_indices}") # 处理每行数据 for index, row in df.iterrows(): # 姓名通常在第一列 name = None for col in df.columns: if '姓名' in str(col[0]) or '姓名' in str(col[1]): name = row[col] break # 标准化姓名(处理飞书用户名) name = self._normalize_student_name(name) if not name: continue print(f"\n处理学生: {name}") # 解析每日考勤数据 daily_data = {} for date_col in date_columns: # 从列名中提取日期 date_str = self._extract_date_from_column(str(date_col[1])) if date_str: attendance_str = str(row[date_col]) print(f" {date_str}: {attendance_str}") daily_data[date_str] = self._parse_daily_attendance(attendance_str) results[name] = daily_data return results def _process_dataframe_single_header(self, df: pd.DataFrame) -> Dict: """处理单层表头的DataFrame""" results = {} # 查找姓名列和日期列 name_col_index = None date_columns = [] for i, col in enumerate(df.columns): col_str = str(col) if '姓名' in col_str: name_col_index = i elif ('2025-' in col_str) or re.search(r'\d{4}-\d{2}-\d{2}', col_str): date_columns.append(col) print(f"姓名列索引: {name_col_index}") print(f"识别到的日期列: {date_columns}") if name_col_index is None: # 如果没找到姓名列,假设第一列是姓名 name_col_index = 0 # 处理每行数据 for index, row in df.iterrows(): name = row.iloc[name_col_index] if name_col_index is not None else row.iloc[0] # 标准化姓名(处理飞书用户名) name = self._normalize_student_name(name) if not name: continue print(f"\n处理学生: {name}") # 解析每日考勤数据 daily_data = {} for date_col in date_columns: # 从列名中提取日期 date_str = self._extract_date_from_column(str(date_col)) if date_str: attendance_str = str(row[date_col]) print(f" {date_str}: {attendance_str}") daily_data[date_str] = self._parse_daily_attendance(attendance_str) results[name] = daily_data return results def _extract_date_from_column(self, col_name: str) -> str: """从列名中提取日期""" # 尝试匹配 YYYY-MM-DD 格式 date_match = re.search(r'(\d{4}-\d{2}-\d{2})', col_name) if date_match: return date_match.group(1) return None def _parse_daily_attendance(self, attendance_str: str) -> Dict: """解析单日考勤字符串""" if pd.isna(attendance_str) or attendance_str == 'nan': return {'status': 'absent', 'records': [], 'check_in_time': None, 'check_out_time': None} print(f" 解析考勤字符串: {attendance_str}") if '休息' in attendance_str: result = self._parse_weekend_attendance(attendance_str) print(f" 周末考勤结果: {result}") return result # 解析工作日考勤 records = [] parts = attendance_str.split(',') time_periods = ['morning_in', 'morning_out', 'afternoon_in', 'afternoon_out', 'evening_in', 'evening_out'] # 解析各时段打卡记录(保持原有逻辑) for i, part in enumerate(parts): if i >= len(time_periods): break part = part.strip() period = time_periods[i] print(f" 处理时段 {period}: {part}") if '缺卡' in part: records.append({'period': period, 'status': 'missing', 'time': None}) elif '正常' in part: time_match = re.search(r'\((\d{2}:\d{2})\)', part) card_time = time_match.group(1) if time_match else None records.append({'period': period, 'status': 'normal', 'time': card_time}) print(f" 正常打卡时间: {card_time}") elif '迟到' in part: time_match = re.search(r'\((\d{2}:\d{2})\)', part) late_match = re.search(r'迟到(\d+)分钟', part) card_time = time_match.group(1) if time_match else None late_minutes = int(late_match.group(1)) if late_match else 0 records.append({ 'period': period, 'status': 'late', 'time': card_time, 'late_minutes': late_minutes }) print(f" 迟到打卡时间: {card_time}, 迟到分钟: {late_minutes}") elif '早退' in part: time_match = re.search(r'\((\d{2}:\d{2})\)', part) early_match = re.search(r'早退(\d+)分钟', part) card_time = time_match.group(1) if time_match else None early_minutes = int(early_match.group(1)) if early_match else 0 records.append({ 'period': period, 'status': 'early_leave', 'time': card_time, 'early_minutes': early_minutes }) print(f" 早退打卡时间: {card_time}, 早退分钟: {early_minutes}") # 计算签到签退时间 check_in_time, check_out_time = self._calculate_check_times(records) # 🔥 新增:检查是否全天缺卡 has_valid_punch = any(record.get('status') in ['normal', 'late', 'early_leave'] and record.get('time') for record in records) # 如果没有任何有效打卡记录,标记为缺勤 if not has_valid_punch: status = 'absent' print(f" 检测到全天无有效打卡,标记为缺勤") else: status = 'workday' result = { 'status': status, 'records': records, 'check_in_time': check_in_time, 'check_out_time': check_out_time } print(f" 工作日考勤结果: {result}") return result def _calculate_check_times(self, records: List[Dict]) -> Tuple[Optional[str], Optional[str]]: """从打卡记录中计算签到和签退时间""" check_in_time = None check_out_time = None # 查找最早的有效签到时间 for record in records: if record['period'].endswith('_in') and record['time'] and record['status'] in ['normal', 'late']: if not check_in_time or record['time'] < check_in_time: check_in_time = record['time'] # 查找最晚的有效签退时间 for record in records: if record['period'].endswith('_out') and record['time'] and record['status'] in ['normal', 'early_leave']: if not check_out_time or record['time'] > check_out_time: check_out_time = record['time'] print(f" 计算签到签退时间: 签到={check_in_time}, 签退={check_out_time}") return check_in_time, check_out_time def _parse_weekend_attendance(self, attendance_str: str) -> Dict: """解析周末考勤""" if '休息(-,-)' in attendance_str: return { 'status': 'weekend_rest', 'records': [], 'check_in_time': None, 'check_out_time': None } # 解析周末加班 time_match = re.search(r'休息打卡\((\d{2}:\d{2}),?(\d{2}:\d{2})?\)', attendance_str) if time_match: start_time = time_match.group(1) end_time = time_match.group(2) if time_match.group(2) else None return { 'status': 'weekend_work', 'records': [{'start': start_time, 'end': end_time}], 'check_in_time': start_time, 'check_out_time': end_time } return { 'status': 'weekend_rest', 'records': [], 'check_in_time': None, 'check_out_time': None } def calculate_weekly_statistics(self, daily_data: Dict, week_start: str, week_end: str) -> Dict: """计算周统计数据""" stats = { 'actual_work_hours': 0.0, 'class_work_hours': 0.0, 'absent_days': 0, 'overtime_hours': 0.0 } print(f"\n计算周统计数据,周期: {week_start} 到 {week_end}") start_date = datetime.strptime(week_start, '%Y-%m-%d') end_date = datetime.strptime(week_end, '%Y-%m-%d') current_date = start_date while current_date <= end_date: date_str = current_date.strftime('%Y-%m-%d') is_weekday = current_date.weekday() < 5 print(f"处理日期 {date_str}, 是否工作日: {is_weekday}") if date_str in daily_data: day_data = daily_data[date_str] print(f" 找到数据: {day_data}") if day_data['status'] == 'workday': # 🔥 新增:检查是否实际有有效打卡 valid_records = [record for record in day_data['records'] if record.get('status') in ['normal', 'late', 'early_leave'] and record.get('time')] if not valid_records and is_weekday: # 虽然标记为工作日,但没有有效打卡记录,算作缺勤 stats['absent_days'] += 1 print(f" 工作日无有效打卡记录,记为缺勤") else: # 有有效打卡记录,计算工时 day_stats = self._calculate_daily_hours(day_data['records'], is_weekday) print(f" 计算得到工时: {day_stats}") stats['actual_work_hours'] += day_stats['actual_hours'] if is_weekday: stats['class_work_hours'] += day_stats['actual_hours'] else: stats['overtime_hours'] += day_stats['actual_hours'] elif day_data['status'] == 'weekend_work': overtime = self._calculate_weekend_overtime(day_data['records']) print(f" 周末加班时长: {overtime}") stats['actual_work_hours'] += overtime stats['overtime_hours'] += overtime elif day_data['status'] == 'absent' and is_weekday: stats['absent_days'] += 1 print(f" 缺勤") elif is_weekday: stats['absent_days'] += 1 print(f" 工作日无数据,记为缺勤") current_date += timedelta(days=1) print(f"最终统计结果: {stats}") return stats def _calculate_daily_hours(self, records: List[Dict], is_weekday: bool) -> Dict: """计算每日工作时长""" total_hours = 0.0 print(f" 计算每日工时,记录: {records}") # 处理各时段 morning_in = None morning_out = None afternoon_in = None afternoon_out = None evening_in = None evening_out = None for record in records: if record['period'] == 'morning_in' and record['status'] in ['normal', 'late'] and record['time']: morning_in = datetime.strptime(record['time'], '%H:%M').time() print(f" 早上上班时间: {morning_in}") elif record['period'] == 'morning_out' and record['status'] in ['normal', 'early_leave'] and record['time']: morning_out = datetime.strptime(record['time'], '%H:%M').time() print(f" 早上下班时间: {morning_out}") elif record['period'] == 'afternoon_in' and record['status'] in ['normal', 'late'] and record['time']: afternoon_in = datetime.strptime(record['time'], '%H:%M').time() print(f" 下午上班时间: {afternoon_in}") elif record['period'] == 'afternoon_out' and record['status'] in ['normal', 'early_leave'] and record[ 'time']: afternoon_out = datetime.strptime(record['time'], '%H:%M').time() print(f" 下午下班时间: {afternoon_out}") elif record['period'] == 'evening_in' and record['status'] in ['normal', 'late'] and record['time']: evening_in = datetime.strptime(record['time'], '%H:%M').time() print(f" 晚上上班时间: {evening_in}") elif record['period'] == 'evening_out' and record['status'] in ['normal', 'early_leave'] and record['time']: evening_out = datetime.strptime(record['time'], '%H:%M').time() print(f" 晚上下班时间: {evening_out}") # 计算各时段工时 if morning_in and morning_out: morning_hours = self._calculate_time_diff(morning_in, morning_out) total_hours += morning_hours print(f" 早上工时: {morning_hours}") if afternoon_in and afternoon_out: afternoon_hours = self._calculate_time_diff(afternoon_in, afternoon_out) total_hours += afternoon_hours print(f" 下午工时: {afternoon_hours}") if evening_in and evening_out: evening_hours = self._calculate_time_diff(evening_in, evening_out) total_hours += evening_hours print(f" 晚上工时: {evening_hours}") print(f" 总工时: {total_hours}") return {'actual_hours': total_hours} def _calculate_weekend_overtime(self, records: List[Dict]) -> float: """计算周末加班时长""" if not records or not records[0].get('start'): return 0.0 start_time = datetime.strptime(records[0]['start'], '%H:%M').time() end_time = None if records[0].get('end'): end_time = datetime.strptime(records[0]['end'], '%H:%M').time() if start_time and end_time: return self._calculate_time_diff(start_time, end_time) return 0.0 def _calculate_time_diff(self, start_time: time, end_time: time) -> float: """计算时间差(小时)""" start_minutes = start_time.hour * 60 + start_time.minute end_minutes = end_time.hour * 60 + end_time.minute if end_minutes < start_minutes: # 跨天 end_minutes += 24 * 60 diff_minutes = end_minutes - start_minutes result = round(diff_minutes / 60.0, 1) print(f" 时间差计算: {start_time} 到 {end_time} = {diff_minutes}分钟 = {result}小时") return result def import_to_database(self, data: Dict, week_start: str, week_end: str): """导入数据到数据库""" success_count = 0 error_count = 0 error_messages = [] print(f"\n开始导入数据到数据库,共{len(data)}个学生") try: for name, daily_data in data.items(): try: print(f"\n处理学生: {name}") # 获取学生信息 student = Student.query.filter_by(name=name).first() if not student: error_messages.append(f"未找到学生: {name}") error_count += 1 print(f" 未找到学生记录") continue print(f" 找到学生: {student.student_number}") # 计算周统计 weekly_stats = self.calculate_weekly_statistics(daily_data, week_start, week_end) # 检查是否已存在记录 existing_record = WeeklyAttendance.query.filter_by( student_number=student.student_number, week_start_date=datetime.strptime(week_start, '%Y-%m-%d').date(), week_end_date=datetime.strptime(week_end, '%Y-%m-%d').date() ).first() if existing_record: print(f" 更新现有记录") # 更新现有记录 existing_record.actual_work_hours = weekly_stats['actual_work_hours'] existing_record.class_work_hours = weekly_stats['class_work_hours'] existing_record.absent_days = weekly_stats['absent_days'] existing_record.overtime_hours = weekly_stats['overtime_hours'] existing_record.updated_at = datetime.now() weekly_record = existing_record else: print(f" 创建新记录") # 创建新记录 weekly_record = WeeklyAttendance( student_number=student.student_number, name=name, week_start_date=datetime.strptime(week_start, '%Y-%m-%d').date(), week_end_date=datetime.strptime(week_end, '%Y-%m-%d').date(), actual_work_hours=weekly_stats['actual_work_hours'], class_work_hours=weekly_stats['class_work_hours'], absent_days=weekly_stats['absent_days'], overtime_hours=weekly_stats['overtime_hours'] ) db.session.add(weekly_record) db.session.flush() # 获取记录ID # 删除现有的每日记录 DailyAttendanceDetail.query.filter_by( weekly_record_id=weekly_record.record_id ).delete() # 插入每日考勤明细 self._insert_daily_details(weekly_record.record_id, student.student_number, daily_data, week_start, week_end) success_count += 1 except Exception as e: error_messages.append(f"处理学生 {name} 时出错: {str(e)}") error_count += 1 print(f" 处理失败: {e}") continue db.session.commit() logger.info(f"数据导入完成: 成功 {success_count} 条,失败 {error_count} 条") except Exception as e: db.session.rollback() logger.error(f"数据导入失败: {e}") raise return success_count, error_count, error_messages def _insert_daily_details(self, weekly_record_id: int, student_number: str, daily_data: Dict, week_start: str, week_end: str): """插入每日考勤明细""" start_date = datetime.strptime(week_start, '%Y-%m-%d') end_date = datetime.strptime(week_end, '%Y-%m-%d') current_date = start_date while current_date <= end_date: date_str = current_date.strftime('%Y-%m-%d') status = '缺勤' remarks = '无数据' check_in_time = None check_out_time = None detailed_records = None if date_str in daily_data: day_data = daily_data[date_str] status = self._get_daily_status(day_data) remarks = self._generate_remarks(day_data) # 提取签到签退时间 if day_data.get('check_in_time'): try: check_in_time = datetime.strptime(day_data['check_in_time'], '%H:%M').time() except: check_in_time = None if day_data.get('check_out_time'): try: check_out_time = datetime.strptime(day_data['check_out_time'], '%H:%M').time() except: check_out_time = None # 生成详细的时段记录(JSON格式存储在remarks中) detailed_records = self._generate_detailed_records(day_data) print(f" 保存每日明细: {date_str}, 状态={status}, 签到={check_in_time}, 签退={check_out_time}") # 将详细记录和简要备注合并 if detailed_records: import json final_remarks = json.dumps({ 'summary': remarks, 'details': detailed_records }, ensure_ascii=False) else: final_remarks = remarks daily_detail = DailyAttendanceDetail( weekly_record_id=weekly_record_id, student_number=student_number, attendance_date=current_date.date(), status=status, check_in_time=check_in_time, check_out_time=check_out_time, remarks=final_remarks ) db.session.add(daily_detail) current_date += timedelta(days=1) def _generate_detailed_records(self, day_data: Dict) -> Dict: """生成详细的时段打卡记录""" if day_data['status'] in ['weekend_rest', 'absent']: return None detailed = { 'morning': {'in': None, 'out': None, 'status': 'missing'}, 'afternoon': {'in': None, 'out': None, 'status': 'missing'}, 'evening': {'in': None, 'out': None, 'status': 'missing'} } if day_data['status'] == 'weekend_work': # 处理周末加班 if day_data['records']: record = day_data['records'][0] detailed['overtime'] = { 'in': record.get('start'), 'out': record.get('end'), 'status': 'overtime' } return detailed # 处理工作日打卡 for record in day_data['records']: period = record['period'] time_str = record.get('time') status = record.get('status', 'missing') if period == 'morning_in': detailed['morning']['in'] = time_str detailed['morning']['status'] = status # 只有当状态确实是late时才记录迟到分钟数 if status == 'late' and 'late_minutes' in record: detailed['morning']['late_minutes'] = record.get('late_minutes', 0) elif period == 'morning_out': detailed['morning']['out'] = time_str # 只有当状态确实是early_leave时才记录早退分钟数 if status == 'early_leave' and 'early_minutes' in record: detailed['morning']['early_minutes'] = record.get('early_minutes', 0) elif period == 'afternoon_in': detailed['afternoon']['in'] = time_str detailed['afternoon']['status'] = status # 只有当状态确实是late时才记录迟到分钟数 if status == 'late' and 'late_minutes' in record: detailed['afternoon']['late_minutes'] = record.get('late_minutes', 0) elif period == 'afternoon_out': detailed['afternoon']['out'] = time_str # 只有当状态确实是early_leave时才记录早退分钟数 if status == 'early_leave' and 'early_minutes' in record: detailed['afternoon']['early_minutes'] = record.get('early_minutes', 0) elif period == 'evening_in': detailed['evening']['in'] = time_str detailed['evening']['status'] = status # 只有当状态确实是late时才记录迟到分钟数 if status == 'late' and 'late_minutes' in record: detailed['evening']['late_minutes'] = record.get('late_minutes', 0) elif period == 'evening_out': detailed['evening']['out'] = time_str # 只有当状态确实是early_leave时才记录早退分钟数 if status == 'early_leave' and 'early_minutes' in record: detailed['evening']['early_minutes'] = record.get('early_minutes', 0) return detailed def _get_daily_status(self, day_data: Dict) -> str: """获取每日状态""" if day_data['status'] == 'absent': return '缺勤' elif day_data['status'] == 'leave': return '请假' elif day_data['status'] == 'leave_with_punch': # 新增:有打卡的请假 return '请假' elif day_data['status'] == 'weekend_rest': return '休息' elif day_data['status'] == 'weekend_work': return '加班' else: # 检查是否全天缺卡 valid_records = [record for record in day_data['records'] if record.get('status') in ['normal', 'late', 'early_leave'] and record.get('time')] if not valid_records: return '缺勤' # 检查是否有迟到 for record in day_data['records']: if record.get('status') == 'late': return '迟到' return '正常' def _generate_remarks(self, day_data: Dict) -> str: """生成备注信息""" if day_data['status'] == 'absent': return '缺勤' elif day_data['status'] in ['leave', 'leave_with_punch']: reason = day_data.get('leave_reason', '请假') if day_data['status'] == 'leave_with_punch': return f'请假({reason}) - 有打卡记录' else: return f'请假({reason})' elif day_data['status'] == 'weekend_rest': return '休息日' elif day_data['status'] == 'weekend_work': return '周末加班' remarks = [] for record in day_data['records']: if record.get('status') == 'late': remarks.append(f"迟到{record.get('late_minutes', 0)}分钟") elif record.get('status') == 'early_leave': remarks.append(f"早退{record.get('early_minutes', 0)}分钟") elif record.get('status') == 'missing': remarks.append("缺卡") elif record.get('status') == 'leave': reason = record.get('leave_reason', '请假') remarks.append(f"请假({reason})") return '; '.join(remarks) if remarks else '正常' def add_name_mapping(self, feishu_name: str, real_name: str): """添加新的飞书用户名映射""" self.feishu_name_mapping[feishu_name] = real_name logger.info(f"添加用户名映射: {feishu_name} -> {real_name}") def get_name_mappings(self) -> Dict[str, str]: """获取当前的用户名映射表""" return self.feishu_name_mapping.copy() # ============== 以下是新增的请假处理方法 ============== def parse_leave_file(self, file_path: str) -> List[Dict]: """解析请假单文件""" try: # 读取Excel文件 df = pd.read_excel(file_path) logger.info(f"成功读取请假单文件: {file_path}") print("=" * 50) print("请假单文件列名:") for i, col in enumerate(df.columns): print(f"第{i}列: {col}") print("=" * 50) print("前5行数据:") # 增加显示行数 print(df.head(5)) print("=" * 50) leave_records = [] # 查找相关列 name_col = None reason_col = None start_col = None end_col = None for col in df.columns: col_str = str(col) if '请假人员' in col_str or '姓名' in col_str: name_col = col elif '请假事由' in col_str or '事由' in col_str: reason_col = col elif '请假开始时间' in col_str or '开始时间' in col_str: start_col = col elif '请假结束时间' in col_str or '结束时间' in col_str: end_col = col print(f"识别到的列:姓名={name_col}, 事由={reason_col}, 开始时间={start_col}, 结束时间={end_col}") if not all([name_col, start_col, end_col]): raise ValueError("请假单文件缺少必要的列:请假人员、请假开始时间、请假结束时间") # 处理每行数据 for index, row in df.iterrows(): try: # 🔥 改进姓名处理逻辑 raw_name = row[name_col] print(f"\n处理请假记录 {index + 1}:") print(f" 原始姓名: '{raw_name}' (类型: {type(raw_name)})") # 跳过空行或标题行 if pd.isna(raw_name) or str(raw_name).strip() == '' or str(raw_name).strip() == 'nan': print(f" 跳过空姓名") continue name = self._normalize_student_name(str(raw_name).strip()) if not name: print(f" 姓名标准化后为空,跳过") continue reason = str(row[reason_col]).strip() if reason_col and pd.notna(row[reason_col]) else "请假" start_time_raw = row[start_col] end_time_raw = row[end_col] print(f" 标准化姓名: '{name}'") print(f" 事由: '{reason}'") print(f" 开始时间原始值: {start_time_raw} (类型: {type(start_time_raw)})") print(f" 结束时间原始值: {end_time_raw} (类型: {type(end_time_raw)})") # 转换时间格式 start_date = self._convert_excel_date(start_time_raw) end_date = self._convert_excel_date(end_time_raw) if start_date and end_date: leave_record = { 'name': name, 'reason': reason, 'start_date': start_date, 'end_date': end_date, 'raw_start': start_time_raw, 'raw_end': end_time_raw } leave_records.append(leave_record) print(f" ✅ 成功添加请假记录: {start_date} 到 {end_date}") else: print(f" ❌ 时间转换失败,跳过此记录") except Exception as e: print(f" ❌ 处理第 {index + 1} 行时出错: {e}") continue print(f"\n📊 成功解析请假记录 {len(leave_records)} 条") for i, record in enumerate(leave_records, 1): print(f" {i}. {record['name']}: {record['start_date']} 到 {record['end_date']} ({record['reason']})") return leave_records except Exception as e: logger.error(f"解析请假单文件失败: {e}") raise def _convert_excel_date(self, date_value) -> Optional[str]: """转换Excel中的日期值为标准日期格式""" if pd.isna(date_value): return None try: print(f" 转换日期: {date_value} (类型: {type(date_value)})") # 如果是数字(Excel日期序列号) if isinstance(date_value, (int, float)): # Excel日期起始点是1900-01-01,但需要处理Excel的闰年错误 if date_value > 59: # 1900-03-01之后 date_value -= 1 # 转换为日期 excel_date = datetime(1900, 1, 1) + timedelta(days=date_value - 1) result = excel_date.strftime('%Y-%m-%d') print(f" 数字转换结果: {result}") return result # 如果是字符串 elif isinstance(date_value, str): date_value = date_value.strip() # 尝试解析各种日期格式 date_formats = [ '%Y-%m-%d', '%Y/%m/%d', '%m/%d/%Y', '%d/%m/%Y', '%Y-%m-%d %H:%M:%S', '%Y/%m/%d %H:%M:%S' ] for fmt in date_formats: try: parsed_date = datetime.strptime(date_value, fmt) result = parsed_date.strftime('%Y-%m-%d') print(f" 字符串转换结果: {result}") return result except ValueError: continue # 如果都不匹配,尝试pandas的日期解析 try: parsed_date = pd.to_datetime(date_value) result = parsed_date.strftime('%Y-%m-%d') print(f" pandas转换结果: {result}") return result except: pass # 如果是datetime对象 elif isinstance(date_value, datetime): result = date_value.strftime('%Y-%m-%d') print(f" datetime转换结果: {result}") return result # 如果是pandas的Timestamp elif hasattr(date_value, 'strftime'): result = date_value.strftime('%Y-%m-%d') print(f" timestamp转换结果: {result}") return result except Exception as e: print(f" ❌ 日期转换失败: {date_value} -> {e}") return None def apply_leave_records(self, attendance_data: Dict, leave_records: List[Dict], week_start: str, week_end: str) -> Dict: """将请假记录应用到考勤数据中""" print(f"\n🔄 开始应用请假记录到考勤数据") print(f"考勤数据覆盖周期: {week_start} 到 {week_end}") print(f"请假记录数量: {len(leave_records)}") week_start_date = datetime.strptime(week_start, '%Y-%m-%d').date() week_end_date = datetime.strptime(week_end, '%Y-%m-%d').date() # 遍历每个学生的考勤数据 for student_name, daily_data in attendance_data.items(): print(f"\n👤 处理学生: {student_name}") # 查找该学生的请假记录 student_leaves = [leave for leave in leave_records if leave['name'] == student_name] if not student_leaves: print(f" ℹ️ 无请假记录") continue print(f" 📋 找到请假记录 {len(student_leaves)} 条") # 处理每条请假记录 for leave in student_leaves: leave_start = datetime.strptime(leave['start_date'], '%Y-%m-%d').date() leave_end = datetime.strptime(leave['end_date'], '%Y-%m-%d').date() print(f" 📝 处理请假: {leave['start_date']} 到 {leave['end_date']} ({leave['reason']})") # 遍历请假期间的每一天 current_date = leave_start while current_date <= leave_end: date_str = current_date.strftime('%Y-%m-%d') # 只处理在考勤周期内的日期 if week_start_date <= current_date <= week_end_date: print(f" 📅 处理日期: {date_str}") if date_str in daily_data: day_data = daily_data[date_str] original_status = day_data.get('status') print(f" 原状态: {original_status}") # 🔥 修改:优先设置为请假状态,即使有打卡记录 if day_data.get('status') in ['absent', 'workday']: # 检查是否有有效的打卡记录 has_valid_punch = any( record.get('status') in ['normal', 'late', 'early_leave'] and record.get('time') for record in day_data.get('records', []) ) if has_valid_punch: # 有打卡记录的情况下,仍然设置为请假,但保留打卡信息 day_data['status'] = 'leave_with_punch' # 新状态:请假但有打卡 day_data['leave_reason'] = leave['reason'] print(f" 🎯 转换为请假(有打卡): {leave['reason']}") else: # 无打卡记录,设置为纯请假 day_data['status'] = 'leave' day_data['leave_reason'] = leave['reason'] print(f" 🎯 转换为请假: {leave['reason']}") else: print(f" ℹ️ 非工作日或其他状态,不处理") else: # 如果该日期没有考勤记录,创建请假记录 daily_data[date_str] = { 'status': 'leave', 'leave_reason': leave['reason'], 'records': [], 'check_in_time': None, 'check_out_time': None } print(f" ➕ 创建请假记录") else: print(f" ⏭️ 日期 {date_str} 不在考勤周期内,跳过") current_date += timedelta(days=1) print(f"\n✅ 请假记录应用完成") return attendance_data def import_leave_records_to_database(self, leave_records: List[Dict]) -> int: """将请假记录导入到数据库""" from app.models import LeaveRecord success_count = 0 try: for leave in leave_records: try: # 查找学生 student = Student.query.filter_by(name=leave['name']).first() if not student: print(f"未找到学生: {leave['name']}") continue # 检查是否已存在相同的请假记录 existing_leave = LeaveRecord.query.filter_by( student_number=student.student_number, leave_start_date=datetime.strptime(leave['start_date'], '%Y-%m-%d').date(), leave_end_date=datetime.strptime(leave['end_date'], '%Y-%m-%d').date() ).first() if existing_leave: # 更新现有记录 existing_leave.leave_reason = leave['reason'] existing_leave.status = '已批准' # 假设上传的请假单都是已批准的 print(f"更新请假记录: {leave['name']}") else: # 创建新记录 leave_record = LeaveRecord( student_number=student.student_number, leave_start_date=datetime.strptime(leave['start_date'], '%Y-%m-%d').date(), leave_end_date=datetime.strptime(leave['end_date'], '%Y-%m-%d').date(), leave_reason=leave['reason'], status='已批准' # 假设上传的请假单都是已批准的 ) db.session.add(leave_record) print(f"创建请假记录: {leave['name']}") success_count += 1 except Exception as e: print(f"处理请假记录失败 {leave['name']}: {e}") continue db.session.commit() print(f"请假记录导入完成: {success_count} 条") except Exception as e: db.session.rollback() logger.error(f"请假记录导入失败: {e}") raise return success_count ================================================================================ File: ./app/utils/auth_helpers.py ================================================================================ from functools import wraps from flask import redirect, url_for, flash, request from flask_login import current_user def admin_required(f): """要求管理员权限的装饰器""" @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: flash('请先登录', 'error') return redirect(url_for('auth.login', next=request.url)) if not current_user.is_admin(): flash('权限不足,需要管理员权限', 'error') return redirect(url_for('student.dashboard')) return f(*args, **kwargs) return decorated_function def student_required(f): """要求学生权限的装饰器""" @wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: flash('请先登录', 'error') return redirect(url_for('auth.login', next=request.url)) return f(*args, **kwargs) return decorated_function ================================================================================ File: ./app/utils/data_import.py ================================================================================ import pandas as pd import re from datetime import datetime, timedelta, time from typing import Dict, List, Tuple, Optional from app.utils.database import get_db_connection import logging logger = logging.getLogger(__name__) class AttendanceDataImporter: def __init__(self): self.work_time_rules = { 'morning': { 'work_start': time(9, 45), 'work_end': time(11, 30), 'card_start': time(6, 0), 'card_end': time(12, 0) }, 'afternoon': { 'work_start': time(13, 30), 'work_end': time(18, 30), 'card_start': time(13, 30), 'card_end': time(18, 30) }, 'evening': { 'work_start': time(19, 0), 'work_end': time(23, 30), 'card_start': time(19, 0), 'card_end': time(23, 30) } } def parse_xlsx_file(self, file_path: str) -> Dict: """解析xlsx文件""" try: df = pd.read_excel(file_path) logger.info(f"成功读取文件: {file_path}") return self._process_dataframe(df) except Exception as e: logger.error(f"读取文件失败: {e}") raise def _process_dataframe(self, df: pd.DataFrame) -> Dict: """处理DataFrame数据""" results = {} # 获取日期列(跳过前几列的统计数据) date_columns = [col for col in df.columns if '2025-' in str(col)] for _, row in df.iterrows(): name = row['姓名'] if pd.isna(name): continue # 解析每日考勤数据 daily_data = {} for date_col in date_columns: date_str = str(date_col).split()[0] # 提取日期部分 attendance_str = str(row[date_col]) daily_data[date_str] = self._parse_daily_attendance(attendance_str) results[name] = daily_data return results def _parse_daily_attendance(self, attendance_str: str) -> Dict: """解析单日考勤字符串""" if pd.isna(attendance_str) or attendance_str == 'nan': return {'status': 'absent', 'records': []} if '休息' in attendance_str: return self._parse_weekend_attendance(attendance_str) # 解析工作日考勤 records = [] parts = attendance_str.split(',') time_periods = ['morning_in', 'morning_out', 'afternoon_in', 'afternoon_out', 'evening_in', 'evening_out'] for i, part in enumerate(parts): if i >= len(time_periods): break part = part.strip() period = time_periods[i] if '缺卡' in part: records.append({'period': period, 'status': 'missing', 'time': None}) elif '正常' in part: time_match = re.search(r'\((\d{2}:\d{2})\)', part) card_time = time_match.group(1) if time_match else None records.append({'period': period, 'status': 'normal', 'time': card_time}) elif '迟到' in part: time_match = re.search(r'\((\d{2}:\d{2})\)', part) late_match = re.search(r'迟到(\d+)分钟', part) card_time = time_match.group(1) if time_match else None late_minutes = int(late_match.group(1)) if late_match else 0 records.append({ 'period': period, 'status': 'late', 'time': card_time, 'late_minutes': late_minutes }) elif '早退' in part: time_match = re.search(r'\((\d{2}:\d{2})\)', part) early_match = re.search(r'早退(\d+)分钟', part) card_time = time_match.group(1) if time_match else None early_minutes = int(early_match.group(1)) if early_match else 0 records.append({ 'period': period, 'status': 'early_leave', 'time': card_time, 'early_minutes': early_minutes }) return {'status': 'workday', 'records': records} def _parse_weekend_attendance(self, attendance_str: str) -> Dict: """解析周末考勤""" if '休息(-,-)' in attendance_str: return {'status': 'weekend_rest', 'records': []} # 解析周末加班 time_match = re.search(r'休息打卡\((\d{2}:\d{2}),?(\d{2}:\d{2})?\)', attendance_str) if time_match: start_time = time_match.group(1) end_time = time_match.group(2) if time_match.group(2) else None return { 'status': 'weekend_work', 'records': [{'start': start_time, 'end': end_time}] } return {'status': 'weekend_rest', 'records': []} def calculate_weekly_statistics(self, daily_data: Dict, week_start: str, week_end: str) -> Dict: """计算周统计数据""" stats = { 'actual_work_hours': 0.0, 'class_work_hours': 0.0, 'absent_days': 0, 'overtime_hours': 0.0 } start_date = datetime.strptime(week_start, '%Y-%m-%d') end_date = datetime.strptime(week_end, '%Y-%m-%d') current_date = start_date while current_date <= end_date: date_str = current_date.strftime('%Y-%m-%d') is_weekday = current_date.weekday() < 5 # 0-4是工作日 if date_str in daily_data: day_data = daily_data[date_str] if day_data['status'] == 'workday': day_stats = self._calculate_daily_hours(day_data['records'], is_weekday) stats['actual_work_hours'] += day_stats['actual_hours'] if is_weekday: stats['class_work_hours'] += day_stats['actual_hours'] else: stats['overtime_hours'] += day_stats['actual_hours'] elif day_data['status'] == 'weekend_work': overtime = self._calculate_weekend_overtime(day_data['records']) stats['actual_work_hours'] += overtime stats['overtime_hours'] += overtime elif day_data['status'] == 'absent' and is_weekday: stats['absent_days'] += 1 elif is_weekday: stats['absent_days'] += 1 current_date += timedelta(days=1) return stats def _calculate_daily_hours(self, records: List[Dict], is_weekday: bool) -> Dict: """计算每日工作时长""" total_hours = 0.0 # 处理早上时段 morning_in = None morning_out = None afternoon_in = None afternoon_out = None evening_in = None evening_out = None for record in records: if record['period'] == 'morning_in' and record['status'] in ['normal', 'late'] and record['time']: morning_in = datetime.strptime(record['time'], '%H:%M').time() elif record['period'] == 'morning_out' and record['status'] in ['normal', 'early_leave'] and record['time']: morning_out = datetime.strptime(record['time'], '%H:%M').time() elif record['period'] == 'afternoon_in' and record['status'] in ['normal', 'late'] and record['time']: afternoon_in = datetime.strptime(record['time'], '%H:%M').time() elif record['period'] == 'afternoon_out' and record['status'] in ['normal', 'early_leave'] and record[ 'time']: afternoon_out = datetime.strptime(record['time'], '%H:%M').time() elif record['period'] == 'evening_in' and record['status'] in ['normal', 'late'] and record['time']: evening_in = datetime.strptime(record['time'], '%H:%M').time() elif record['period'] == 'evening_out' and record['status'] in ['normal', 'early_leave'] and record['time']: evening_out = datetime.strptime(record['time'], '%H:%M').time() # 计算各时段工时 if morning_in and morning_out: morning_hours = self._calculate_time_diff(morning_in, morning_out) total_hours += morning_hours if afternoon_in and afternoon_out: afternoon_hours = self._calculate_time_diff(afternoon_in, afternoon_out) total_hours += afternoon_hours if evening_in and evening_out: evening_hours = self._calculate_time_diff(evening_in, evening_out) total_hours += evening_hours return {'actual_hours': total_hours} def _calculate_weekend_overtime(self, records: List[Dict]) -> float: """计算周末加班时长""" if not records or not records[0].get('start'): return 0.0 start_time = datetime.strptime(records[0]['start'], '%H:%M').time() end_time = None if records[0].get('end'): end_time = datetime.strptime(records[0]['end'], '%H:%M').time() if start_time and end_time: return self._calculate_time_diff(start_time, end_time) return 0.0 def _calculate_time_diff(self, start_time: time, end_time: time) -> float: """计算时间差(小时)""" start_minutes = start_time.hour * 60 + start_time.minute end_minutes = end_time.hour * 60 + end_time.minute if end_minutes < start_minutes: # 跨天 end_minutes += 24 * 60 diff_minutes = end_minutes - start_minutes return round(diff_minutes / 60.0, 1) def import_to_database(self, data: Dict, week_start: str, week_end: str): """导入数据到数据库""" conn = get_db_connection() cursor = conn.cursor() try: for name, daily_data in data.items(): # 获取学生信息 cursor.execute("SELECT student_number FROM students WHERE name = %s", (name,)) student_result = cursor.fetchone() if not student_result: logger.warning(f"未找到学生: {name}") continue student_number = student_result[0] # 计算周统计 weekly_stats = self.calculate_weekly_statistics(daily_data, week_start, week_end) # 插入周考勤汇总 insert_weekly_sql = """ INSERT INTO weekly_attendance (student_number, name, week_start_date, week_end_date, actual_work_hours, class_work_hours, absent_days, overtime_hours) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE actual_work_hours = VALUES(actual_work_hours), class_work_hours = VALUES(class_work_hours), absent_days = VALUES(absent_days), overtime_hours = VALUES(overtime_hours), updated_at = CURRENT_TIMESTAMP """ cursor.execute(insert_weekly_sql, ( student_number, name, week_start, week_end, weekly_stats['actual_work_hours'], weekly_stats['class_work_hours'], weekly_stats['absent_days'], weekly_stats['overtime_hours'] )) weekly_record_id = cursor.lastrowid # 插入每日考勤明细 self._insert_daily_details(cursor, weekly_record_id, student_number, daily_data, week_start, week_end) conn.commit() logger.info("数据导入成功") except Exception as e: conn.rollback() logger.error(f"数据导入失败: {e}") raise finally: cursor.close() conn.close() def _insert_daily_details(self, cursor, weekly_record_id: int, student_number: str, daily_data: Dict, week_start: str, week_end: str): """插入每日考勤明细""" start_date = datetime.strptime(week_start, '%Y-%m-%d') end_date = datetime.strptime(week_end, '%Y-%m-%d') current_date = start_date while current_date <= end_date: date_str = current_date.strftime('%Y-%m-%d') if date_str in daily_data: day_data = daily_data[date_str] status = self._get_daily_status(day_data) # 插入每日记录 insert_daily_sql = """ INSERT INTO daily_attendance_details (weekly_record_id, student_number, attendance_date, status, remarks) VALUES (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE status = VALUES(status), remarks = VALUES(remarks) """ remarks = self._generate_remarks(day_data) cursor.execute(insert_daily_sql, ( weekly_record_id, student_number, current_date.date(), status, remarks )) current_date += timedelta(days=1) def _get_daily_status(self, day_data: Dict) -> str: """获取每日状态""" if day_data['status'] == 'absent': return '缺勤' elif day_data['status'] == 'weekend_rest': return '休息' elif day_data['status'] == 'weekend_work': return '加班' else: # 检查是否有迟到 for record in day_data['records']: if record.get('status') == 'late': return '迟到' return '正常' def _generate_remarks(self, day_data: Dict) -> str: """生成备注信息""" if day_data['status'] == 'absent': return '缺勤' elif day_data['status'] == 'weekend_rest': return '休息日' elif day_data['status'] == 'weekend_work': return '周末加班' remarks = [] for record in day_data['records']: if record.get('status') == 'late': remarks.append(f"迟到{record.get('late_minutes', 0)}分钟") elif record.get('status') == 'early_leave': remarks.append(f"早退{record.get('early_minutes', 0)}分钟") elif record.get('status') == 'missing': remarks.append("缺卡") return '; '.join(remarks) if remarks else '正常' ================================================================================ File: ./app/models/user.py ================================================================================ from app.models import db from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime class User(UserMixin, db.Model): __tablename__ = 'users' user_id = db.Column(db.Integer, primary_key=True) student_number = db.Column(db.String(20), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) role = db.Column(db.Enum('student', 'admin'), default='student') is_active = db.Column(db.Boolean, default=True) last_login = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.utcnow) def get_id(self): return str(self.user_id) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) def is_admin(self): return self.role == 'admin' def __repr__(self): return f'' ================================================================================ File: ./app/models/attendance.py ================================================================================ from app.models import db from datetime import datetime class WeeklyAttendance(db.Model): __tablename__ = 'weekly_attendance' record_id = db.Column(db.Integer, primary_key=True) student_number = db.Column(db.String(20), db.ForeignKey('students.student_number'), nullable=False) name = db.Column(db.String(50), nullable=False) week_start_date = db.Column(db.Date, nullable=False, index=True) week_end_date = db.Column(db.Date, nullable=False, index=True) actual_work_hours = db.Column(db.Numeric(5, 1), default=0) class_work_hours = db.Column(db.Numeric(5, 1), default=0) absent_days = db.Column(db.Integer, default=0) overtime_hours = db.Column(db.Numeric(5, 1), default=0) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 学生关系 student_info = db.relationship('Student', backref='weekly_attendance', foreign_keys=[student_number], primaryjoin="WeeklyAttendance.student_number==Student.student_number") def __repr__(self): return f'' class DailyAttendanceDetail(db.Model): __tablename__ = 'daily_attendance_details' detail_id = db.Column(db.Integer, primary_key=True) weekly_record_id = db.Column(db.Integer, db.ForeignKey('weekly_attendance.record_id')) student_number = db.Column(db.String(20), db.ForeignKey('students.student_number'), nullable=False) attendance_date = db.Column(db.Date, nullable=False, index=True) status = db.Column(db.String(20), default='正常') check_in_time = db.Column(db.Time) check_out_time = db.Column(db.Time) remarks = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 关系 weekly_record = db.relationship('WeeklyAttendance', backref='daily_details') student_info = db.relationship('Student', backref='daily_attendance', foreign_keys=[student_number], primaryjoin="DailyAttendanceDetail.student_number==Student.student_number") def __repr__(self): return f'' class LeaveRecord(db.Model): __tablename__ = 'leave_records' leave_id = db.Column(db.Integer, primary_key=True) student_number = db.Column(db.String(20), db.ForeignKey('students.student_number'), nullable=False) leave_start_date = db.Column(db.Date, nullable=False) leave_end_date = db.Column(db.Date, nullable=False) leave_reason = db.Column(db.Text) status = db.Column(db.Enum('待审批', '已批准', '已拒绝'), default='待审批') created_at = db.Column(db.DateTime, default=datetime.utcnow) # 学生关系 student_info = db.relationship('Student', backref='leave_records', foreign_keys=[student_number], primaryjoin="LeaveRecord.student_number==Student.student_number") def __repr__(self): return f'' ================================================================================ File: ./app/models/__init__.py ================================================================================ from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() # 导入所有模型 from app.models.user import User from app.models.student import Student from app.models.attendance import WeeklyAttendance, DailyAttendanceDetail, LeaveRecord __all__ = ['db', 'User', 'Student', 'WeeklyAttendance', 'DailyAttendanceDetail', 'LeaveRecord'] ================================================================================ File: ./app/models/student.py ================================================================================ from app.models import db from datetime import datetime class Student(db.Model): __tablename__ = 'students' student_id = db.Column(db.Integer, primary_key=True) student_number = db.Column(db.String(20), db.ForeignKey('users.student_number'), unique=True, nullable=False) name = db.Column(db.String(50), nullable=False, index=True) gender = db.Column(db.Enum('男', '女'), nullable=False) grade = db.Column(db.Integer, nullable=False, index=True) phone = db.Column(db.String(11)) supervisor = db.Column(db.String(50), index=True) college = db.Column(db.String(100)) major = db.Column(db.String(100)) degree_type = db.Column(db.Enum('专硕', '学博', '学硕', '专博')) status = db.Column(db.Enum('在读', '毕业'), default='在读') enrollment_date = db.Column(db.Date) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 用户关系 user = db.relationship('User', backref='student', foreign_keys=[student_number], primaryjoin="Student.student_number==User.student_number") def get_latest_attendance(self, limit=10): """获取最近的考勤记录""" from app.models.attendance import WeeklyAttendance return WeeklyAttendance.query.filter_by(student_number=self.student_number) \ .order_by(WeeklyAttendance.week_start_date.desc()) \ .limit(limit) def get_attendance_by_date_range(self, start_date, end_date): """获取指定日期范围的考勤记录""" from app.models.attendance import WeeklyAttendance return WeeklyAttendance.query.filter_by(student_number=self.student_number) \ .filter(WeeklyAttendance.week_start_date >= start_date, WeeklyAttendance.week_end_date <= end_date) \ .all() def __repr__(self): return f'' ================================================================================ File: ./app/static/css/admin.css ================================================================================ ================================================================================ File: ./app/static/css/style.css ================================================================================ /* 全局样式 */ :root { --primary-color: #0d6efd; --secondary-color: #6c757d; --success-color: #198754; --danger-color: #dc3545; --warning-color: #ffc107; --info-color: #0dcaf0; --sidebar-width: 250px; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; } /* 导航栏样式 */ .navbar-brand { font-weight: 600; font-size: 1.25rem; } .navbar-nav .nav-link { font-weight: 500; padding: 0.5rem 1rem; border-radius: 0.375rem; margin: 0 0.25rem; transition: all 0.3s ease; } .navbar-nav .nav-link:hover { background-color: rgba(255, 255, 255, 0.1); } .navbar-nav .nav-link.active { background-color: rgba(255, 255, 255, 0.2); font-weight: 600; } /* 主要内容区域 */ .main-content { margin-top: 76px; /* 导航栏高度 */ min-height: calc(100vh - 76px); padding-bottom: 2rem; } .full-page { min-height: 100vh; } /* 卡片样式 */ .card { border: none; border-radius: 0.75rem; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); transition: box-shadow 0.15s ease-in-out; } .card:hover { box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); } .card-header { background-color: #fff; border-bottom: 1px solid #dee2e6; font-weight: 600; border-radius: 0.75rem 0.75rem 0 0 !important; } /* 按钮样式 */ .btn { border-radius: 0.5rem; font-weight: 500; padding: 0.5rem 1rem; transition: all 0.3s ease; } .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; } .btn-lg { padding: 0.75rem 1.5rem; font-size: 1.125rem; } /* 表格样式 */ .table { border-radius: 0.5rem; overflow: hidden; } .table th { background-color: #f8f9fa; border-bottom: 2px solid #dee2e6; font-weight: 600; padding: 1rem 0.75rem; } .table td { padding: 0.75rem; vertical-align: middle; } .table-hover tbody tr:hover { background-color: rgba(0, 0, 0, 0.025); } /* 分页样式 */ .pagination { border-radius: 0.5rem; } .page-link { border-radius: 0.375rem; margin: 0 0.125rem; border: 1px solid #dee2e6; color: var(--primary-color); } .page-link:hover { background-color: #e9ecef; border-color: #adb5bd; } .page-item.active .page-link { background-color: var(--primary-color); border-color: var(--primary-color); } /* 表单样式 */ .form-control { border-radius: 0.5rem; border: 1px solid #ced4da; padding: 0.75rem; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } .form-control:focus { border-color: #86b7fe; box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); } .form-select { border-radius: 0.5rem; padding: 0.75rem; } /* 徽章样式 */ .badge { font-weight: 500; padding: 0.375rem 0.75rem; border-radius: 0.375rem; } /* 状态颜色 */ .status-normal { color: var(--success-color); } .status-late { color: var(--warning-color); } .status-absent { color: var(--danger-color); } .status-leave { color: var(--info-color); } /* 统计卡片 */ .stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 1rem; padding: 1.5rem; margin-bottom: 1rem; } .stat-card .stat-number { font-size: 2.5rem; font-weight: 700; line-height: 1; } .stat-card .stat-label { font-size: 0.875rem; opacity: 0.9; margin-top: 0.5rem; } /* 登录页面样式 */ .min-vh-100 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .login-card { backdrop-filter: blur(10px); background: rgba(255, 255, 255, 0.95); } /* 响应式设计 */ @media (max-width: 768px) { .main-content { margin-top: 66px; padding: 1rem; } .card { margin-bottom: 1rem; } .table-responsive { border-radius: 0.5rem; } .btn-group-vertical { width: 100%; } .btn-group-vertical .btn { margin-bottom: 0.5rem; } } /* 加载动画 */ .spinner-border-sm { width: 1rem; height: 1rem; } /* 自定义滚动条 */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } ::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } /* 打印样式 */ @media print { .navbar, .btn, .pagination { display: none !important; } .main-content { margin-top: 0; } .card { box-shadow: none; border: 1px solid #000; } } ================================================================================ File: ./app/static/js/main.js ================================================================================ // 基础JavaScript功能 document.addEventListener('DOMContentLoaded', function() { // 初始化所有提示框 var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); // 自动隐藏alert消息 const alerts = document.querySelectorAll('.alert'); alerts.forEach(function(alert) { setTimeout(function() { const bsAlert = new bootstrap.Alert(alert); bsAlert.close(); }, 5000); // 5秒后自动关闭 }); }); ================================================================================ File: ./app/static/js/admin.js ================================================================================ ================================================================================ File: ./app/templates/auth/student_profile.html ================================================================================ {% extends "layout/base.html" %} {% block title %}个人信息 - CHM考勤管理系统{% endblock %} {% block content %}

个人信息

{% if user_info %}
基本信息
{{ user_info.student_number }}
{% if user_info.name %} {{ user_info.name }} {% else %} 未设置 {% endif %}
{% if user_info.gender %} {{ user_info.gender }} {% else %} 未设置 {% endif %}
{% if user_info.grade %} {{ user_info.grade }}级 {% else %} 未设置 {% endif %}
{% if user_info.phone %} {{ user_info.phone }} {% else %} 未设置 {% endif %}
{% if user_info.supervisor %} {{ user_info.supervisor }} {% else %} 未分配 {% endif %}
{% if user_info.college %} {{ user_info.college }} {% else %} 未设置 {% endif %}
{% if user_info.major %} {{ user_info.major }} {% else %} 未设置 {% endif %}
{% if user_info.degree_type %} {{ user_info.degree_type }} {% else %} 未设置 {% endif %}
{% if user_info.enrollment_date %} {{ user_info.enrollment_date.strftime('%Y年%m月%d日') }} {% else %} 未设置 {% endif %}
账户信息
{{ user_info.user_id }}
学生
{% if user_info.is_active %} 活跃 {% else %} 已禁用 {% endif %}
{% if user_info.status %}
{{ user_info.status }}
{% endif %}
{% if user_info.last_login %} {{ user_info.last_login.strftime('%Y-%m-%d %H:%M:%S') }} {% else %} 从未登录 {% endif %}
{{ user_info.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
无法获取用户信息,请联系管理员
{% endif %}
{% endblock %} ================================================================================ File: ./app/templates/auth/login.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}登录 - CHM考勤管理系统{% endblock %} {% block content %}

CHM考勤系统

请使用学号和密码登录

请输入学号
请输入密码
如有登录问题,请联系管理员
使用说明
  • 查看个人考勤记录
  • 申请请假审批
  • 个人统计分析
  • 修改个人密码
{% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/auth/admin_profile.html ================================================================================ {% extends "layout/base.html" %} {% block title %}个人信息 - CHM考勤管理系统{% endblock %} {% block content %}

个人信息

管理员信息
{% if user_info %}
{{ user_info.user_id }}
{{ user_info.student_number }}
{% if user_info.role == 'admin' %}管理员{% else %}普通用户{% endif %}
{% if user_info.is_active %} 活跃 {% else %} 已禁用 {% endif %}
{% if user_info.last_login %} {{ user_info.last_login.strftime('%Y-%m-%d %H:%M:%S') }} {% else %} 从未登录 {% endif %}
{{ user_info.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
无法获取用户信息
{% endif %}
{% endblock %} ================================================================================ File: ./app/templates/auth/change_password.html ================================================================================ {% extends "layout/base.html" %} {% block title %}修改密码 - CHM考勤管理系统{% endblock %} {% block content %}
安全设置
密码要求:
  • 长度至少6位
  • 必须包含字母和数字
  • 建议使用字母、数字和特殊字符的组合
取消
{% endblock %} {% block extra_css %} {% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/layout/base.html ================================================================================ {% block title %}CHM考勤管理系统{% endblock %} {% block extra_css %}{% endblock %} {% if current_user.is_authenticated %} {% include 'layout/nav.html' %} {% endif %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
{% for category, message in messages %} {% endfor %}
{% endif %} {% endwith %} {% block content %}{% endblock %}
{% if current_user.is_authenticated %}
© 2025 CHM考勤管理系统. All rights reserved.
{% endif %} {% block extra_js %}{% endblock %} ================================================================================ File: ./app/templates/layout/nav.html ================================================================================ ================================================================================ File: ./app/templates/student/apply_leave.html ================================================================================ ================================================================================ File: ./app/templates/student/leave_records.html ================================================================================ ================================================================================ File: ./app/templates/student/attendance.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}我的考勤 - CHM考勤管理系统{% endblock %} {% block content %}

我的考勤记录

{% if total_stats %}
总考勤周数
{{ total_stats.total_weeks }}周
总出勤时长
{{ "%.1f"|format(total_stats.total_actual_hours) }}h
班内工作
{{ "%.1f"|format(total_stats.total_class_hours) }}h
迟到次数
{{ total_stats.total_late_count }}次
旷工天数
{{ total_stats.total_absent_days }}天
周均时长
{{ "%.1f"|format(total_stats.avg_weekly_hours) }}h
{% endif %}
筛选条件
重置 {% if attendance_records %} {% endif %}
考勤记录列表
{% if attendance_records %} 共 {{ pagination.total }} 条记录 {% endif %}
{% if attendance_records %}
{% for record in attendance_records %} {% endfor %}
周次 实际工作时长 班内工作时长 迟到次数 旷工天数 加班时长 记录时间 操作
{{ record.week_start_date.strftime('%Y-%m-%d') }} 至 {{ record.week_end_date.strftime('%Y-%m-%d') }}
{{ "%.1f"|format(record.actual_work_hours) }}h {{ "%.1f"|format(record.class_work_hours) }}h {% if record.late_count > 0 %} {{ record.late_count }}次 {% else %} 0次 {% endif %} {% if record.absent_days > 0 %} {{ record.absent_days }}天 {% else %} 0天 {% endif %} {% if record.overtime_hours > 0 %} {{ "%.1f"|format(record.overtime_hours) }}h {% else %} 0h {% endif %} {{ record.created_at.strftime('%m-%d %H:%M') }} 查看详情
{% if pagination.pages > 1 %} {% endif %} {% else %}
暂无考勤记录

当前筛选条件下没有找到考勤记录

返回首页
{% endif %}
{% if attendance_records %}
本期统计分析
{{ "%.1f"|format(total_stats.avg_weekly_hours) }}h
周均出勤
{{ "%.1f"|format((total_stats.total_class_hours / total_stats.total_actual_hours * 100) if total_stats.total_actual_hours > 0 else 0) }}%
班内工作率
{{ "%.1f"|format((total_stats.total_overtime_hours / total_stats.total_weeks) if total_stats.total_weeks > 0 else 0) }}h
周均加班
{% endif %}
{% endblock %} {% block extra_css %} {% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/student/dashboard.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}学生主页 - CHM考勤管理系统{% endblock %} {% block content %}

欢迎回来,{{ student.name }}!

学号:{{ student.student_number }} | 学院:{{ student.college }} | 导师:{{ student.supervisor }}

{{ total_records }}

考勤记录

{{ "%.1f"|format(total_work_hours) }}

总工作时长(小时)

{{ total_absent_days }}

旷工天数

{{ pending_leaves|length }}

待审批请假

最近考勤记录
查看全部
{% if recent_attendance %}
{% for record in recent_attendance %} {% endfor %}
周次 实际工作时长 班内工作时长 旷工天数 加班时长
{{ record.week_start_date.strftime('%m-%d') }}{{ record.week_end_date.strftime('%m-%d') }} {{ record.actual_work_hours }}h {{ record.class_work_hours }}h {% if record.absent_days > 0 %} {{ record.absent_days }}天 {% else %} 0天 {% endif %} {{ record.overtime_hours }}h
{% else %}

暂无考勤记录

{% endif %}
{% if pending_leaves %}
待审批请假
{% for leave in pending_leaves %}
{{ leave.leave_start_date.strftime('%Y-%m-%d') }}{{ leave.leave_end_date.strftime('%Y-%m-%d') }}
待审批
{{ leave.leave_reason[:30] }}...
{% endfor %} 查看所有请假记录
{% endif %}
个人信息
姓名:
{{ student.name }}
性别:
{{ student.gender }}
年级:
{{ student.grade }}级
专业:
{{ student.major }}
学位:
{{ student.degree_type }}
{% if student.phone %}
电话:
{{ student.phone }}
{% endif %}
{% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/student/statistics.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}个人统计 - CHM考勤管理系统{% endblock %} {% block content %}

个人统计分析

基本信息
学号: {{ student.student_number }}
姓名: {{ student.name }}
年级: {{ student.grade }}级
入学日期: {{ student.enrollment_date.strftime('%Y-%m-%d') if student.enrollment_date else '未设置' }}
筛选条件
重置
{% if all_time_stats %}
入学以来总体表现

{{ all_time_stats.attendance_weeks }}

总考勤周数

{{ "%.1f"|format(all_time_stats.total_work_hours) }}

总工作时长(h)

{{ "%.1f"|format(all_time_stats.total_class_hours) }}

班内工作(h)

{{ all_time_stats.total_late_count }}

迟到次数

{{ all_time_stats.total_absent_days }}

旷工天数

{{ "%.1f"|format(all_time_stats.attendance_rate) }}%

出勤率
{% endif %}
筛选期间考勤周数
{{ total_stats.attendance_weeks }}周
总出勤时长
{{ "%.1f"|format(total_stats.total_work_hours) }}h
迟到次数
{{ total_stats.total_late_count }}次
周均工作时长
{{ "%.1f"|format(total_stats.avg_weekly_hours) }}h
月度考勤统计
最近12周趋势
详细考勤记录
{% if attendance_records %}
{% for record in attendance_records %} {% endfor %}
周次 实际工作时长 班内工作时长 加班时长 旷工天数 考勤状态 操作
{{ record.week_start_date.strftime('%Y-%m-%d') }} 至 {{ record.week_end_date.strftime('%Y-%m-%d') }}
{{ "%.1f"|format(record.actual_work_hours) }}h {{ "%.1f"|format(record.class_work_hours) }}h {% if record.overtime_hours > 0 %} {{ "%.1f"|format(record.overtime_hours) }}h {% else %} 0h {% endif %} {% if record.absent_days > 0 %} {{ record.absent_days }}天 {% else %} 0天 {% endif %} {% set performance_score = (record.actual_work_hours / 40 * 100) if record.actual_work_hours else 0 %} {% if performance_score >= 80 %} 优秀 {% elif performance_score >= 60 %} 良好 {% else %} 待改善 {% endif %} 详情
{% else %}
暂无统计数据

当前筛选条件下没有找到考勤记录

{% endif %}
快捷操作
{% endblock %} {% block extra_css %} {% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/student/attendance_details.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}考勤详情 - CHM考勤管理系统{% endblock %} {% block content %}

我的考勤详情

学生信息

学号: {{ record.student_number }}

姓名: {{ record.name }}

{% if student %}

年级: {{ student.grade }}

学院: {{ student.college or '未设置' }}

{% endif %}
{% if student %}

专业: {{ student.major or '未设置' }}

导师: {{ student.supervisor or '未设置' }}

学位类型: {{ student.degree_type or '未设置' }}

状态: {{ student.status }}

{% endif %}
考勤周期

开始日期: {{ record.week_start_date.strftime('%Y年%m月%d日') }}

结束日期: {{ record.week_end_date.strftime('%Y年%m月%d日') }}

创建时间: {{ record.created_at.strftime('%Y-%m-%d %H:%M') }}

更新时间: {{ record.updated_at.strftime('%Y-%m-%d %H:%M') }}

实际出勤时长
{{ "%.1f"|format(record.actual_work_hours) }}小时
班内工作时长
{{ "%.1f"|format(record.class_work_hours) }}小时
旷工天数
{{ record.absent_days }}天
加班时长
{{ "%.1f"|format(record.overtime_hours) }}小时
每日考勤明细 (点击详情按钮查看详细时段信息)
{% if daily_details %}
{% for detail in daily_details %} {% endfor %}
日期 星期 考勤状态 签到时间 签退时间 工作时长 备注 操作
{{ detail.attendance_date.strftime('%m-%d') }} {{ detail.attendance_date.strftime('%Y') }} {% set weekday = detail.attendance_date.weekday() %} {% if weekday == 0 %}周一 {% elif weekday == 1 %}周二 {% elif weekday == 2 %}周三 {% elif weekday == 3 %}周四 {% elif weekday == 4 %}周五 {% elif weekday == 5 %}周六 {% else %}周日 {% endif %} {% if weekday >= 5 %} 休息日 {% endif %} {% if detail.status == '正常' %} {{ detail.status }} {% elif '迟到' in detail.status %} {{ detail.status }} {% elif detail.status == '缺勤' %} {{ detail.status }} {% elif detail.status == '请假' %} {{ detail.status }} {% elif detail.status == '休息' %} {{ detail.status }} {% elif detail.status == '加班' %} {{ detail.status }} {% else %} {{ detail.status }} {% endif %} {% if detail.check_in_time %} {{ detail.check_in_time.strftime('%H:%M') }} {% else %} 未打卡 {% endif %} {% if detail.check_out_time %} {{ detail.check_out_time.strftime('%H:%M') }} {% else %} 未打卡 {% endif %} {% if detail.duration_hours %} {{ detail.duration_hours }}h {% else %} - {% endif %} {% if detail.summary_remarks %} {{ detail.summary_remarks }} {% else %} - {% endif %} {% if detail.status not in ['休息', '缺勤'] and detail.detailed_info %} {% endif %}
{% else %}
暂无每日考勤明细

该考勤周期内没有详细的打卡记录

{% endif %}
本周考勤统计
{{ present_days }}
正常天数
{{ late_days }}
迟到天数
{{ absent_days }}
缺勤天数
{{ "%.1f"|format(avg_daily_hours) }}h
日均时长

{{ "%.1f"|format((present_days / max(total_days, 1) * 100)) }}%
出勤率
{{ "%.1f"|format((record.class_work_hours / max(record.actual_work_hours, 1) * 100)) }}%
班内工作率
{{ total_days }}
考勤天数
最近记录对比
{% if recent_records %}
{% for record_item in recent_records %} {% endfor %}
周期 出勤时长 旷工天数 对比
{{ record_item.week_start_date.strftime('%m-%d') }} {{ "%.1f"|format(record_item.actual_work_hours) }}h {% if record_item.absent_days > 0 %} {{ record_item.absent_days }} {% else %} 0 {% endif %} {% set diff = record.actual_work_hours - record_item.actual_work_hours %} {% if diff > 0 %} +{{ "%.1f"|format(diff) }}h {% elif diff < 0 %} {{ "%.1f"|format(diff) }}h {% else %} - {% endif %}
{% else %}

暂无历史记录

{% endif %}
{% if late_days > 0 or absent_days > 0 %} {% endif %}
{% endblock %} {% block extra_css %} {% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/attendance_management.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}考勤管理 - CHM考勤管理系统{% endblock %} {% block content %}
搜索筛选
考勤记录
{% if attendance_records %} 共 {{ pagination.total }} 条记录 {% endif %}
{% if attendance_records %}
{% for record in attendance_records %} {% endfor %}
学号 姓名 考勤周期 出勤时长 班内工作 旷工天数 迟到次数 加班时长 记录时间 操作
{{ record.student_number }} {{ record.name }} {{ record.week_start_date.strftime('%Y-%m-%d') }}
至 {{ record.week_end_date.strftime('%Y-%m-%d') }}
{{ "%.1f"|format(record.actual_work_hours) }}h {{ "%.1f"|format(record.class_work_hours) }}h {% if record.absent_days > 0 %} {{ record.absent_days }}天 {% else %} 0天 {% endif %} {% set late_count = record.late_count if record.late_count is defined else 0 %} {% if late_count > 0 %} {{ late_count }}次 {% else %} 0次 {% endif %} {% if record.overtime_hours > 0 %} {{ "%.1f"|format(record.overtime_hours) }}h {% else %} 0h {% endif %} {{ record.created_at.strftime('%m-%d %H:%M') }}
{% if pagination.pages > 1 %} {% endif %} {% else %}
暂无考勤记录

还没有上传任何考勤数据

立即上传考勤数据
{% endif %}
{% if attendance_records %}
当前筛选统计
{{ pagination.total }}
总记录
{{ attendance_records|sum(attribute='actual_work_hours')|round(1) }}h
总出勤
{{ attendance_records|sum(attribute='absent_days') }}
旷工
{% set total_leave = statistics.total_leave_days if statistics and statistics.total_leave_days else 0 %} {{ total_leave }}
请假
{% set total_late = attendance_records|sum(attribute='late_count') if attendance_records[0].late_count is defined else 0 %} {{ total_late }}
迟到次数
{{ attendance_records|sum(attribute='overtime_hours')|round(1) }}h
总加班
{{ "%.1f"|format((attendance_records|sum(attribute='actual_work_hours')) / (attendance_records|length) if attendance_records|length > 0 else 0) }}h
平均出勤
{% endif %}
{% endblock %} {% block extra_css %} {% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/edit_student.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}编辑学生 - {{ student.name }} - CHM考勤管理系统{% endblock %} {% block content %}
学生基本信息
学号: {{ student.student_number }}
学号不可修改

{% if student.user %} {% endif %}
取消
{% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/student_detail.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}学生详情 - {{ student.name }} - CHM考勤管理系统{% endblock %} {% block content %}
基本信息
编辑
学号: {{ student.student_number }}
姓名: {{ student.name }}
性别: {% if student.gender == '男' %} {{ student.gender }} {% else %} {{ student.gender }} {% endif %}
年级: {{ student.grade }}级
手机号: {% if student.phone %} {{ student.phone }} {% else %} 未填写 {% endif %}
学院: {% if student.college %} {{ student.college }} {% else %} 未填写 {% endif %}
专业: {% if student.major %} {{ student.major }} {% else %} 未填写 {% endif %}
导师: {% if student.supervisor %} {{ student.supervisor }} {% else %} 未分配 {% endif %}
学位类型: {% if student.degree_type %} {{ student.degree_type }} {% else %} 未填写 {% endif %}
状态: {% if student.status == '在读' %} 在读 {% else %} 毕业 {% endif %}
入学日期:
{% if student.enrollment_date %} {{ student.enrollment_date.strftime('%Y年%m月%d日') }} {% else %} 未填写 {% endif %}
注册时间:
{{ student.created_at.strftime('%Y年%m月%d日 %H:%M') }}
考勤统计
{{ "%.1f"|format(total_work_hours) }}
总工作时长(小时)
{{ total_absent_days }}
旷工天数

{{ attendance_records|length }}
考勤记录数
账户信息
{% if student.user %}
账户状态: {% if student.user.is_active %} 正常 {% else %} 已禁用 {% endif %}
最后登录:
{% if student.user.last_login %} {{ student.user.last_login.strftime('%Y-%m-%d %H:%M') }} {% else %} 从未登录 {% endif %}
{% else %}

该学生暂无账户信息

{% endif %}
最近考勤记录
查看全部
{% if attendance_records %}
{% for record in attendance_records %} {% endfor %}
周期 实际工作时长 班内工作时长 旷工天数 加班时长 创建时间
{{ record.week_start_date.strftime('%m-%d') }} 至 {{ record.week_end_date.strftime('%m-%d') }}
{{ record.week_start_date.strftime('%Y年') }}
{{ "%.1f"|format(record.actual_work_hours or 0) }}h {{ "%.1f"|format(record.class_work_hours or 0) }}h {% if record.absent_days > 0 %} {{ record.absent_days }}天 {% else %} 0天 {% endif %} {% if record.overtime_hours > 0 %} {{ "%.1f"|format(record.overtime_hours) }}h {% else %} 0h {% endif %} {{ record.created_at.strftime('%m-%d %H:%M') }}
{% else %}

暂无考勤记录

上传考勤数据
{% endif %}
{% if leave_records %}
最近请假记录
查看全部
{% for leave in leave_records %} {% endfor %}
请假日期 请假原因 申请时间 状态
{{ leave.leave_start_date.strftime('%Y-%m-%d') }} {% if leave.leave_start_date != leave.leave_end_date %} 至 {{ leave.leave_end_date.strftime('%Y-%m-%d') }} {% endif %}
{{ leave.leave_reason }}
{{ leave.created_at.strftime('%Y-%m-%d %H:%M') }} {% if leave.status == '待审批' %} 待审批 {% elif leave.status == '已批准' %} 已批准 {% else %} 已拒绝 {% endif %}
{% endif %}
{% endblock %} {% block extra_js %} {% endblock %} {% block extra_css %} {% endblock %} ================================================================================ File: ./app/templates/admin/add_student.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}添加学生 - CHM考勤管理系统{% endblock %} {% block content %}
学生基本信息
学号将作为登录账号使用
学生可在登录后自行修改密码

取消
{% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/dashboard.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}管理员控制台 - CHM考勤管理系统{% endblock %} {% block content %}

管理员控制台

加载中...
学生总数
{{ total_students or 0 }}
考勤记录总数
{{ total_attendance_records or 0 }}
待审批请假
{{ pending_leaves or 0 }}
本周新记录
{{ recent_records or 0 }}
学院分布
{% if college_stats %}
{% for college, count in college_stats %} {% endfor %}
学院 学生数
{{ college or '未知学院' }} {{ count }}
{% else %}

暂无数据

{% endif %}
导师排行(TOP 10)
{% if supervisor_stats %}
{% for supervisor, count in supervisor_stats %} {% endfor %}
导师 学生数
{{ supervisor or '未知导师' }} {{ count }}
{% else %}

暂无数据

{% endif %}
{% if recent_leaves %}
最近请假申请
查看全部
{% for leave in recent_leaves %} {% endfor %}
学号 请假日期 请假原因 申请时间 操作
{{ leave.student_number }} {{ leave.leave_start_date.strftime('%Y-%m-%d') }} {% if leave.leave_start_date != leave.leave_end_date %} 至 {{ leave.leave_end_date.strftime('%Y-%m-%d') }} {% endif %} {{ leave.leave_reason[:50] }}{% if leave.leave_reason|length > 50 %}...{% endif %} {{ leave.created_at.strftime('%m-%d %H:%M') }}
{% endif %}
{% endblock %} {% block extra_css %} {% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/upload_attendance.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}上传考勤数据 - CHM考勤管理系统{% endblock %} {% block content %}
上传考勤数据
必须上传考勤记录Excel文件
如有请假记录,请上传请假单Excel文件
导入说明:
  • 考勤记录文件:包含姓名列和每日考勤数据,系统会自动计算工作时长、迟到次数等
  • 请假单文件:包含请假人员、请假开始时间、请假结束时间等信息
  • 处理规则:
    • 请假时间内的缺卡记录会自动转换为请假
    • 请假时间内的正常打卡记录(正常、迟到、早退)保持不变
  • 如果记录已存在,将会更新现有数据
  • 请确保学生信息已在系统中注册
注意事项:
  • 请假单中的时间格式会自动转换(支持数字格式和标准日期格式)
  • 请假人员姓名必须与学生表中的姓名完全一致
  • 建议先上传考勤记录,再选择性上传请假单
返回
{% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/statistics.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}统计报表 - CHM考勤管理系统{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

统计报表

{{ overall_stats.total_students }}

总学生数

{{ "%.1f"|format(overall_stats.total_work_hours) }}

总出勤时长(小时)

{{ overall_stats.total_absent_days }}

总缺勤天数

{{ overall_stats.total_late_count }}

总迟到次数

重置
{% for grade_label, students in grade_groups.items() %}

{{ grade_label }} ({{ students|length }}人)

{% for student in students %}
{{ student.name }}
{{ student.student_number }}
{% if student.total_work_hours >= 200 %} 优秀 {% elif student.total_work_hours >= 100 %} 良好 {% elif student.total_work_hours >= 50 %} 一般 {% else %} 待改进 {% endif %}
{{ "%.1f"|format(student.total_work_hours) }}
总工时
{{ student.total_absent_days }}
缺勤天数
{{ student.total_late_count }}
迟到次数
{{ student.avg_weekly_hours }}
周均工时
{{ student.college or '未设置' }} | {{ student.supervisor or '未设置' }}
{% endfor %}
{% endfor %} {% if not grade_groups %}
没有找到符合条件的学生
{% endif %}
月度考勤趋势
学院分布
{% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/student_list.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}学生管理 - CHM考勤管理系统{% endblock %} {% block content %}

学生管理

学生列表 (共 {{ pagination.total }} 人)
{% if students %}
{% for student in students %} {% endfor %}
学号 姓名 性别 年级 学院 导师 学位类型 状态 操作
{{ student.student_number }} {{ student.name }} {% if student.phone %}
{{ student.phone }} {% endif %}
{{ student.gender }} {{ student.grade }}级 {{ student.college or '-' }} {{ student.supervisor or '-' }} {% if student.degree_type %} {{ student.degree_type }} {% else %} - {% endif %} {% if student.status == '在读' %} 在读 {% else %} 毕业 {% endif %}
{% if pagination.pages > 1 %} {% endif %} {% else %}

暂无学生数据

添加第一个学生
{% endif %}
{% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/templates/admin/attendance_details.html ================================================================================ {% extends 'layout/base.html' %} {% block title %}考勤详情 - CHM考勤管理系统{% endblock %} {% block content %}

考勤详情

学生信息

学号: {{ weekly_record.student_number }}

姓名: {{ weekly_record.name }}

{% if student %}

年级: {{ student.grade }}

学院: {{ student.college or '未设置' }}

{% endif %}
{% if student %}

专业: {{ student.major or '未设置' }}

导师: {{ student.supervisor or '未设置' }}

学位类型: {{ student.degree_type or '未设置' }}

状态: {{ student.status }}

{% endif %}
考勤周期

开始日期: {{ weekly_record.week_start_date.strftime('%Y年%m月%d日') }}

结束日期: {{ weekly_record.week_end_date.strftime('%Y年%m月%d日') }}

创建时间: {{ weekly_record.created_at.strftime('%Y-%m-%d %H:%M') }}

更新时间: {{ weekly_record.updated_at.strftime('%Y-%m-%d %H:%M') }}

实际出勤时长
{{ "%.1f"|format(weekly_record.actual_work_hours) }}小时
班内工作时长
{{ "%.1f"|format(weekly_record.class_work_hours) }}小时
旷工天数
{{ weekly_record.absent_days }}天
加班时长
{{ "%.1f"|format(weekly_record.overtime_hours) }}小时
每日考勤明细 (点击日期查看详细时段信息)
{% if daily_details %}
{% for detail in daily_details %} {% endfor %}
日期 星期 考勤状态 签到时间 签退时间 工作时长 备注 操作
{{ detail.attendance_date.strftime('%m-%d') }} {% set weekday = detail.attendance_date.weekday() %} {% if weekday == 0 %}周一 {% elif weekday == 1 %}周二 {% elif weekday == 2 %}周三 {% elif weekday == 3 %}周四 {% elif weekday == 4 %}周五 {% elif weekday == 5 %}周六 {% else %}周日 {% endif %} {% if detail.status == '正常' %} {{ detail.status }} {% elif '迟到' in detail.status %} {{ detail.status }} {% elif detail.status == '缺勤' %} {{ detail.status }} {% elif detail.status == '请假' %} {{ detail.status }} {% elif detail.status == '休息' %} {{ detail.status }} {% elif detail.status == '加班' %} {{ detail.status }} {% else %} {{ detail.status }} {% endif %} {% if detail.check_in_time %} {{ detail.check_in_time.strftime('%H:%M') }} {% else %} 未打卡 {% endif %} {% if detail.check_out_time %} {{ detail.check_out_time.strftime('%H:%M') }} {% else %} 未打卡 {% endif %} {% if detail.duration_hours %} {{ detail.duration_hours }}h {% else %} - {% endif %} {% if detail.summary_remarks %} {{ detail.summary_remarks }} {% else %} - {% endif %} {% if detail.status not in ['休息', '缺勤'] and detail.detailed_info %} {% endif %}
{% else %}
暂无每日考勤明细

该考勤周期内没有详细的打卡记录

{% endif %}
考勤统计分析
{{ present_days }}
正常天数
{{ late_days }}
迟到天数
{{ absent_days }}
缺勤天数
{{ "%.1f"|format(avg_daily_hours) }}h
日均时长
历史对比
{% if historical_records %}
{% for record in historical_records %} {% endfor %}
周期 出勤时长 旷工天数
{{ record.week_start_date.strftime('%m-%d') }} {{ "%.1f"|format(record.actual_work_hours) }}h {% if record.absent_days > 0 %} {{ record.absent_days }} {% else %} 0 {% endif %}
{% else %}

暂无历史记录

{% endif %}
{% endblock %} {% block extra_css %} {% endblock %} {% block extra_js %} {% endblock %} ================================================================================ File: ./app/routes/auth.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, session from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import check_password_hash, generate_password_hash from app.models import db, User, Student from app.utils.database import safe_commit from datetime import datetime import re auth_bp = Blueprint('auth', __name__) @auth_bp.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: if current_user.is_admin(): return redirect(url_for('admin.dashboard')) else: return redirect(url_for('student.dashboard')) if request.method == 'POST': student_number = request.form.get('student_number', '').strip() password = request.form.get('password', '') remember = request.form.get('remember', False) if not student_number or not password: flash('请输入学号和密码', 'error') return render_template('auth/login.html') # 查找用户 user = User.query.filter_by(student_number=student_number).first() if user and user.check_password(password): if not user.is_active: flash('账户已被禁用,请联系管理员', 'error') return render_template('auth/login.html') # 更新最后登录时间 user.last_login = datetime.utcnow() success, error = safe_commit() if not success: flash('系统错误,请稍后重试', 'error') return render_template('auth/login.html') login_user(user, remember=remember) # 获取重定向地址 next_page = request.args.get('next') if next_page: return redirect(next_page) # 根据用户角色重定向 if user.is_admin(): flash(f'欢迎回来,管理员!', 'success') return redirect(url_for('admin.dashboard')) else: # 获取学生姓名 student = Student.query.filter_by(student_number=student_number).first() name = student.name if student else student_number flash(f'欢迎回来,{name}!', 'success') return redirect(url_for('student.dashboard')) else: flash('学号或密码错误', 'error') return render_template('auth/login.html') @auth_bp.route('/logout') @login_required def logout(): logout_user() flash('您已成功退出登录', 'info') return redirect(url_for('auth.login')) @auth_bp.route('/change_password', methods=['GET', 'POST']) @login_required def change_password(): if request.method == 'POST': current_password = request.form.get('current_password', '') new_password = request.form.get('new_password', '') confirm_password = request.form.get('confirm_password', '') # 验证输入 if not all([current_password, new_password, confirm_password]): flash('请填写所有密码字段', 'error') return render_template('auth/change_password.html') # 验证当前密码 if not current_user.check_password(current_password): flash('当前密码不正确', 'error') return render_template('auth/change_password.html') # 验证新密码与确认密码是否一致 if new_password != confirm_password: flash('新密码与确认密码不匹配', 'error') return render_template('auth/change_password.html') # 密码长度验证 if len(new_password) < 6: flash('新密码长度至少6位', 'error') return render_template('auth/change_password.html') # 密码复杂度验证(可选) if not re.search(r'^(?=.*[a-zA-Z])(?=.*\d).+$', new_password): flash('新密码必须包含字母和数字', 'error') return render_template('auth/change_password.html') # 检查新密码是否与当前密码相同 if current_user.check_password(new_password): flash('新密码不能与当前密码相同', 'error') return render_template('auth/change_password.html') try: # 更新密码 current_user.set_password(new_password) success, error = safe_commit() if success: flash('密码修改成功', 'success') # 根据用户角色重定向 if current_user.is_admin(): return redirect(url_for('auth.profile')) else: return redirect(url_for('auth.profile')) else: flash(f'密码修改失败: {error}', 'error') except Exception as e: flash(f'密码修改失败: {str(e)}', 'error') return render_template('auth/change_password.html') @auth_bp.route('/profile') @login_required def profile(): """用户个人信息页面""" try: if current_user.is_admin(): # 管理员信息页面 user_info = { 'user_id': current_user.user_id, 'student_number': current_user.student_number, 'role': current_user.role, 'is_active': current_user.is_active, 'last_login': current_user.last_login, 'created_at': current_user.created_at } return render_template('auth/admin_profile.html', user_info=user_info) else: # 学生信息页面 student = Student.query.filter_by(student_number=current_user.student_number).first() user_info = { 'user_id': current_user.user_id, 'student_number': current_user.student_number, 'role': current_user.role, 'is_active': current_user.is_active, 'last_login': current_user.last_login, 'created_at': current_user.created_at, # 学生特有信息 'name': student.name if student else None, 'gender': student.gender if student else None, 'grade': student.grade if student else None, 'phone': student.phone if student else None, 'supervisor': student.supervisor if student else None, 'college': student.college if student else None, 'major': student.major if student else None, 'degree_type': student.degree_type if student else None, 'status': student.status if student else None, 'enrollment_date': student.enrollment_date if student else None } return render_template('auth/student_profile.html', user_info=user_info, student=student) except Exception as e: flash(f'获取个人信息失败: {str(e)}', 'error') if current_user.is_admin(): return redirect(url_for('admin.dashboard')) else: return redirect(url_for('student.dashboard')) ================================================================================ File: ./app/routes/__init__.py ================================================================================ ================================================================================ File: ./app/routes/admin.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_file from flask_login import login_required, current_user from app.models import db, User, Student, WeeklyAttendance, DailyAttendanceDetail, LeaveRecord from app.utils.auth_helpers import admin_required from app.utils.database import safe_add_and_commit, safe_commit, safe_delete_and_commit from datetime import datetime, timedelta from sqlalchemy import and_, or_, desc, func import pandas as pd import io import re from werkzeug.security import generate_password_hash from app.utils.attendance_importer import AttendanceDataImporter from werkzeug.utils import secure_filename import os import tempfile admin_bp = Blueprint('admin', __name__) @admin_bp.route('/dashboard') @admin_required def dashboard(): """管理员主页""" # 统计数据 total_students = Student.query.count() total_attendance_records = WeeklyAttendance.query.count() pending_leaves = LeaveRecord.query.filter_by(status='待审批').count() # 最近一周的考勤统计 week_ago = datetime.now().date() - timedelta(days=7) recent_records = WeeklyAttendance.query.filter( WeeklyAttendance.week_start_date >= week_ago ).count() # 按学院统计学生数量 college_stats = db.session.query( Student.college, func.count(Student.student_id).label('count') ).group_by(Student.college).all() # 按导师统计学生数量 supervisor_stats = db.session.query( Student.supervisor, func.count(Student.student_id).label('count') ).group_by(Student.supervisor).order_by(desc('count')).limit(10).all() # 最近的请假申请 recent_leaves = LeaveRecord.query.filter_by( status='待审批' ).order_by(desc(LeaveRecord.created_at)).limit(5).all() return render_template('admin/dashboard.html', total_students=total_students, total_attendance_records=total_attendance_records, pending_leaves=pending_leaves, recent_records=recent_records, college_stats=college_stats, supervisor_stats=supervisor_stats, recent_leaves=recent_leaves) @admin_bp.route('/students') @admin_required def student_list(): """学生列表""" page = request.args.get('page', 1, type=int) per_page = 20 # 搜索和筛选 search = request.args.get('search', '').strip() college = request.args.get('college', '').strip() supervisor = request.args.get('supervisor', '').strip() grade = request.args.get('grade', '', type=str) query = Student.query if search: query = query.filter(or_( Student.name.contains(search), Student.student_number.contains(search) )) if college: query = query.filter(Student.college == college) if supervisor: query = query.filter(Student.supervisor == supervisor) if grade: try: grade_int = int(grade) query = query.filter(Student.grade == grade_int) except ValueError: pass pagination = query.order_by(Student.student_number).paginate( page=page, per_page=per_page, error_out=False ) students = pagination.items # 获取筛选选项 colleges = db.session.query(Student.college).distinct().all() colleges = [c[0] for c in colleges if c[0]] supervisors = db.session.query(Student.supervisor).distinct().all() supervisors = [s[0] for s in supervisors if s[0]] grades = db.session.query(Student.grade).distinct().all() grades = sorted([g[0] for g in grades if g[0]]) return render_template('admin/student_list.html', students=students, pagination=pagination, colleges=colleges, supervisors=supervisors, grades=grades, search=search, selected_college=college, selected_supervisor=supervisor, selected_grade=grade) @admin_bp.route('/students/') @admin_required def student_detail(student_number): """学生详细信息""" student = Student.query.filter_by(student_number=student_number).first_or_404() # 获取考勤记录 attendance_records = WeeklyAttendance.query.filter_by( student_number=student_number ).order_by(desc(WeeklyAttendance.week_start_date)).limit(10).all() # 获取请假记录 leave_records = LeaveRecord.query.filter_by( student_number=student_number ).order_by(desc(LeaveRecord.created_at)).limit(10).all() # 统计数据 total_work_hours = db.session.query( func.sum(WeeklyAttendance.actual_work_hours) ).filter_by(student_number=student_number).scalar() or 0 total_absent_days = db.session.query( func.sum(WeeklyAttendance.absent_days) ).filter_by(student_number=student_number).scalar() or 0 return render_template('admin/student_detail.html', student=student, attendance_records=attendance_records, leave_records=leave_records, total_work_hours=float(total_work_hours), total_absent_days=int(total_absent_days)) @admin_bp.route('/attendance') @admin_required def attendance_management(): """考勤管理""" # ========== 导出功能处理 ========== if request.args.get('export') == 'excel': try: return export_attendance_data() except Exception as e: flash(f'导出失败: {str(e)}', 'error') # 移除export参数,重定向到正常页面 args = dict(request.args) args.pop('export', None) return redirect(url_for('admin.attendance_management', **args)) # ================================== from sqlalchemy import desc, func, case, or_ page = request.args.get('page', 1, type=int) per_page = 50 # 筛选条件 start_date = request.args.get('start_date') end_date = request.args.get('end_date') student_search = request.args.get('student_search', '').strip() sort_by = request.args.get('sort_by', 'week_start_date_desc') # 默认按周开始日期降序 print(f"收到排序参数: {sort_by}") # 调试信息 # 构建基础查询,同时计算迟到次数 query = db.session.query( WeeklyAttendance, func.coalesce( func.sum( case( (DailyAttendanceDetail.status.like('%迟到%'), 1), else_=0 ) ), 0 ).label('late_count') ).outerjoin( DailyAttendanceDetail, WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id ).group_by(WeeklyAttendance.record_id) # 应用筛选条件 if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() query = query.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: flash('开始日期格式错误', 'error') if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() query = query.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: flash('结束日期格式错误', 'error') if student_search: query = query.filter(or_( WeeklyAttendance.name.contains(student_search), WeeklyAttendance.student_number.contains(student_search) )) # 处理排序 if sort_by and '_' in sort_by: field, direction = sort_by.rsplit('_', 1) print(f"排序字段: {field}, 方向: {direction}") # 调试信息 if direction not in ['asc', 'desc']: direction = 'desc' # 根据字段设置排序 if field == 'actual_work_hours': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.actual_work_hours)) else: query = query.order_by(WeeklyAttendance.actual_work_hours) elif field == 'class_work_hours': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.class_work_hours)) else: query = query.order_by(WeeklyAttendance.class_work_hours) elif field == 'absent_days': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.absent_days)) else: query = query.order_by(WeeklyAttendance.absent_days) elif field == 'overtime_hours': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.overtime_hours)) else: query = query.order_by(WeeklyAttendance.overtime_hours) elif field == 'late_count': # 按迟到次数排序 if direction == 'desc': query = query.order_by(desc('late_count')) else: query = query.order_by('late_count') elif field == 'created_at': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.created_at)) else: query = query.order_by(WeeklyAttendance.created_at) elif field == 'week_start_date': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.week_start_date)) else: query = query.order_by(WeeklyAttendance.week_start_date) else: # 未知字段,使用默认排序 query = query.order_by(desc(WeeklyAttendance.week_start_date)) else: # 默认排序:按周开始日期降序 query = query.order_by(desc(WeeklyAttendance.week_start_date)) # 执行分页查询 try: pagination = query.paginate( page=page, per_page=per_page, error_out=False ) except Exception as e: print(f"查询分页失败: {e}") flash('查询数据时出现错误', 'error') return redirect(url_for('admin.dashboard')) # 处理结果,将迟到次数添加到记录对象中 attendance_records = [] for record, late_count in pagination.items: # 给记录对象添加迟到次数属性 record.late_count = int(late_count) if late_count else 0 attendance_records.append(record) print(f"查询结果: {len(attendance_records)} 条记录") # 调试信息 if attendance_records: print(f"第一条记录迟到次数: {attendance_records[0].late_count}") # 调试信息 # 更新pagination对象的items pagination.items = attendance_records # ========== 计算请假统计 ========== statistics = None if attendance_records: # 获取当前筛选结果中的所有考勤记录ID record_ids = [record.record_id for record in attendance_records] # 统计请假天数 leave_count = DailyAttendanceDetail.query.filter( DailyAttendanceDetail.weekly_record_id.in_(record_ids), DailyAttendanceDetail.status == '请假' ).count() statistics = { 'total_leave_days': leave_count } # 确保总是返回模板 return render_template('admin/attendance_management.html', attendance_records=attendance_records, pagination=pagination, start_date=start_date, end_date=end_date, student_search=student_search, sort_by=sort_by, statistics=statistics) @admin_bp.route('/upload/attendance', methods=['GET', 'POST']) @admin_required def upload_attendance(): """上传考勤数据""" if request.method == 'POST': # 检查考勤记录文件 if 'attendance_file' not in request.files: flash('请选择考勤记录文件', 'error') return render_template('admin/upload_attendance.html') attendance_file = request.files['attendance_file'] if attendance_file.filename == '': flash('请选择考勤记录文件', 'error') return render_template('admin/upload_attendance.html') # 检查请假单文件(可选) leave_file = request.files.get('leave_file') has_leave_file = leave_file and leave_file.filename != '' week_start = request.form.get('week_start') week_end = request.form.get('week_end') if not week_start or not week_end: flash('请选择周开始和结束日期', 'error') return render_template('admin/upload_attendance.html') if attendance_file and attendance_file.filename.endswith(('.xlsx', '.xls')): attendance_filename = secure_filename(attendance_file.filename) leave_filename = secure_filename(leave_file.filename) if has_leave_file else None # 使用临时文件 attendance_temp_file = None leave_temp_file = None try: # 保存考勤记录文件 attendance_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx') attendance_file.save(attendance_temp_file.name) attendance_temp_file.close() # 保存请假单文件(如果有) if has_leave_file and leave_file.filename.endswith(('.xlsx', '.xls')): leave_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx') leave_file.save(leave_temp_file.name) leave_temp_file.close() # 处理数据 importer = AttendanceDataImporter() # 解析考勤数据 attendance_data = importer.parse_xlsx_file(attendance_temp_file.name) # 解析请假数据(如果有) leave_data = None if leave_temp_file: try: leave_data = importer.parse_leave_file(leave_temp_file.name) flash(f'成功解析请假记录 {len(leave_data)} 条', 'info') except Exception as e: flash(f'请假单解析失败:{str(e)}', 'warning') leave_data = None # 应用请假数据到考勤数据 if leave_data: attendance_data = importer.apply_leave_records(attendance_data, leave_data, week_start, week_end) # 导入到数据库 success_count, error_count, error_messages = importer.import_to_database( attendance_data, week_start, week_end) # 如果有请假数据,同时保存到请假记录表 if leave_data: leave_success_count = importer.import_leave_records_to_database(leave_data) flash(f'请假记录导入:{leave_success_count} 条', 'info') if success_count > 0: message = f'导入完成:成功 {success_count} 条,失败 {error_count} 条' if has_leave_file: message += f',已处理请假记录' flash(message, 'success') if error_messages: for msg in error_messages[:5]: # 只显示前5个错误 flash(msg, 'warning') else: flash('导入失败,请检查文件格式和数据', 'error') for msg in error_messages[:3]: flash(msg, 'error') except Exception as e: flash(f'文件处理失败:{str(e)}', 'error') logger.error(f"文件处理失败: {str(e)}", exc_info=True) finally: # 删除临时文件 try: if attendance_temp_file: os.unlink(attendance_temp_file.name) if leave_temp_file: os.unlink(leave_temp_file.name) except: pass return redirect(url_for('admin.attendance_management')) else: flash('请上传Excel文件(.xlsx或.xls)', 'error') return render_template('admin/upload_attendance.html') # 添加一个新的路由来删除考勤记录 @admin_bp.route('/attendance//delete', methods=['POST']) @admin_required def delete_attendance_record(record_id): """删除考勤记录""" try: record = WeeklyAttendance.query.get_or_404(record_id) db.session.delete(record) db.session.commit() flash('考勤记录删除成功', 'success') except Exception as e: db.session.rollback() flash(f'删除失败: {str(e)}', 'error') return redirect(url_for('admin.attendance_management')) @admin_bp.route('/statistics') @admin_required def statistics(): """统计报表""" from sqlalchemy import desc, func, case, or_, and_ from datetime import datetime, timedelta # 获取筛选参数 search = request.args.get('search', '').strip() grade_filter = request.args.get('grade', '').strip() college_filter = request.args.get('college', '').strip() supervisor_filter = request.args.get('supervisor', '').strip() start_date = request.args.get('start_date', '') end_date = request.args.get('end_date', '') # 构建基础查询 base_query = db.session.query( Student.student_number, Student.name, Student.grade, Student.college, Student.supervisor, Student.degree_type, Student.enrollment_date, func.coalesce(func.sum(WeeklyAttendance.actual_work_hours), 0).label('total_work_hours'), func.coalesce(func.sum(WeeklyAttendance.class_work_hours), 0).label('total_class_hours'), func.coalesce(func.sum(WeeklyAttendance.overtime_hours), 0).label('total_overtime_hours'), func.coalesce(func.sum(WeeklyAttendance.absent_days), 0).label('total_absent_days'), func.count(WeeklyAttendance.record_id).label('attendance_weeks'), # 计算迟到次数 func.coalesce( func.sum( case( (DailyAttendanceDetail.status.like('%迟到%'), 1), else_=0 ) ), 0 ).label('total_late_count') ).outerjoin( WeeklyAttendance, Student.student_number == WeeklyAttendance.student_number ).outerjoin( DailyAttendanceDetail, WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id ) # 应用日期筛选 if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() base_query = base_query.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: flash('开始日期格式错误', 'error') if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() base_query = base_query.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: flash('结束日期格式错误', 'error') # 应用学生筛选 if search: base_query = base_query.filter(or_( Student.name.contains(search), Student.student_number.contains(search) )) if grade_filter: try: grade_int = int(grade_filter) base_query = base_query.filter(Student.grade == grade_int) except ValueError: pass if college_filter: base_query = base_query.filter(Student.college == college_filter) if supervisor_filter: base_query = base_query.filter(Student.supervisor == supervisor_filter) # 按学生分组 base_query = base_query.group_by(Student.student_id) # 获取学生统计数据 students_stats = base_query.all() # 年级映射函数 def get_grade_label(grade, degree_type): if degree_type in ['学博', '专博']: return f'博士{grade}年级' else: if grade == 1: return '研一' elif grade == 2: return '研二' elif grade == 3: return '研三' else: return f'研{grade}' # 处理学生数据并按年级分组 grade_groups = {} all_students_data = [] for stat in students_stats: grade_label = get_grade_label(stat.grade, stat.degree_type) student_data = { 'student_number': stat.student_number, 'name': stat.name, 'grade': stat.grade, 'grade_label': grade_label, 'college': stat.college, 'supervisor': stat.supervisor, 'degree_type': stat.degree_type, 'enrollment_date': stat.enrollment_date, 'total_work_hours': float(stat.total_work_hours), 'total_class_hours': float(stat.total_class_hours), 'total_overtime_hours': float(stat.total_overtime_hours), 'total_absent_days': int(stat.total_absent_days), 'total_late_count': int(stat.total_late_count), 'attendance_weeks': int(stat.attendance_weeks), 'avg_weekly_hours': round(float(stat.total_work_hours) / max(stat.attendance_weeks, 1), 1) if stat.attendance_weeks > 0 else 0 } all_students_data.append(student_data) if grade_label not in grade_groups: grade_groups[grade_label] = [] grade_groups[grade_label].append(student_data) # 按出勤时长排序每个年级的学生 for grade in grade_groups: grade_groups[grade].sort(key=lambda x: x['total_work_hours'], reverse=True) # 总体统计 overall_stats = { 'total_students': len(all_students_data), 'total_work_hours': sum(s['total_work_hours'] for s in all_students_data), 'total_absent_days': sum(s['total_absent_days'] for s in all_students_data), 'total_late_count': sum(s['total_late_count'] for s in all_students_data), 'avg_work_hours_per_student': round( sum(s['total_work_hours'] for s in all_students_data) / max(len(all_students_data), 1), 1) } # 🔥 修正月度统计查询 monthly_query = db.session.query( func.date_format(WeeklyAttendance.week_start_date, '%Y-%m').label('month'), func.count(WeeklyAttendance.record_id).label('record_count'), func.sum(WeeklyAttendance.actual_work_hours).label('total_hours'), func.sum(WeeklyAttendance.absent_days).label('total_absent') ).group_by('month').order_by('month') # 应用相同的筛选条件到月度统计 if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() monthly_query = monthly_query.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: pass if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() monthly_query = monthly_query.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: pass # 如果有学生筛选,需要关联学生表 if search or grade_filter or college_filter or supervisor_filter: monthly_query = monthly_query.join( Student, WeeklyAttendance.student_number == Student.student_number ) if search: monthly_query = monthly_query.filter(or_( Student.name.contains(search), Student.student_number.contains(search) )) if grade_filter: try: grade_int = int(grade_filter) monthly_query = monthly_query.filter(Student.grade == grade_int) except ValueError: pass if college_filter: monthly_query = monthly_query.filter(Student.college == college_filter) if supervisor_filter: monthly_query = monthly_query.filter(Student.supervisor == supervisor_filter) monthly_stats = monthly_query.all() # 🔥 修正按学院统计查询 college_query = db.session.query( Student.college, func.count(Student.student_id).label('student_count'), func.coalesce(func.sum(WeeklyAttendance.actual_work_hours), 0).label('total_hours') ).outerjoin(WeeklyAttendance, Student.student_number == WeeklyAttendance.student_number) # 应用筛选条件到学院统计 if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() college_query = college_query.filter( or_(WeeklyAttendance.week_start_date.is_(None), WeeklyAttendance.week_start_date >= start_date_obj) ) except ValueError: pass if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() college_query = college_query.filter( or_(WeeklyAttendance.week_end_date.is_(None), WeeklyAttendance.week_end_date <= end_date_obj) ) except ValueError: pass college_stats = college_query.group_by(Student.college).all() # 获取筛选选项 colleges = db.session.query(Student.college).distinct().all() colleges = [c[0] for c in colleges if c[0]] supervisors = db.session.query(Student.supervisor).distinct().all() supervisors = [s[0] for s in supervisors if s[0]] grades = db.session.query(Student.grade).distinct().all() grades = sorted([g[0] for g in grades if g[0]]) print("=== 调试信息 ===") print(f"月度统计数据: {monthly_stats}") print(f"学院统计数据: {college_stats}") print("===============") return render_template('admin/statistics.html', grade_groups=grade_groups, all_students_data=all_students_data, overall_stats=overall_stats, monthly_stats=monthly_stats, college_stats=college_stats, colleges=colleges, supervisors=supervisors, grades=grades, search=search, selected_grade=grade_filter, selected_college=college_filter, selected_supervisor=supervisor_filter, start_date=start_date, end_date=end_date) @admin_bp.route('/statistics/export') @admin_required def export_statistics(): """导出统计数据""" import pandas as pd from io import BytesIO # 获取所有学生统计数据(复用上面的查询逻辑) students_query = db.session.query( Student.student_number, Student.name, Student.grade, Student.college, Student.supervisor, Student.degree_type, func.coalesce(func.sum(WeeklyAttendance.actual_work_hours), 0).label('total_work_hours'), func.coalesce(func.sum(WeeklyAttendance.class_work_hours), 0).label('total_class_hours'), func.coalesce(func.sum(WeeklyAttendance.overtime_hours), 0).label('total_overtime_hours'), func.coalesce(func.sum(WeeklyAttendance.absent_days), 0).label('total_absent_days'), func.count(WeeklyAttendance.record_id).label('attendance_weeks') ).outerjoin( WeeklyAttendance, Student.student_number == WeeklyAttendance.student_number ).group_by(Student.student_id).all() # 转换为DataFrame data = [] for stat in students_query: grade_label = f'博士{stat.grade}年级' if stat.degree_type in ['学博', '专博'] else f'研{stat.grade}' data.append({ '学号': stat.student_number, '姓名': stat.name, '年级': grade_label, '学院': stat.college, '导师': stat.supervisor, '学位类型': stat.degree_type, '总出勤时长(小时)': float(stat.total_work_hours), '班内工作时长(小时)': float(stat.total_class_hours), '加班时长(小时)': float(stat.total_overtime_hours), '缺勤天数': int(stat.total_absent_days), '考勤周数': int(stat.attendance_weeks), '周均工作时长': round(float(stat.total_work_hours) / max(stat.attendance_weeks, 1), 1) }) df = pd.DataFrame(data) # 创建Excel文件 output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name='学生考勤统计', index=False) output.seek(0) filename = f"学生考勤统计_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=filename ) @admin_bp.route('/students/add', methods=['GET', 'POST']) @admin_required def add_student(): """添加学生""" if request.method == 'POST': try: data = request.get_json() if request.is_json else request.form # 检查学号是否已存在 if Student.query.filter_by(student_number=data['student_number']).first(): if request.is_json: return jsonify({'success': False, 'message': '学号已存在'}) else: flash('学号已存在', 'error') return render_template('admin/add_student.html') # 创建用户账户 user = User( student_number=data['student_number'], password_hash=generate_password_hash(data.get('password', '123456')), role='student' ) success, error = safe_add_and_commit(user) if not success: if request.is_json: return jsonify({'success': False, 'message': f'创建用户失败: {error}'}) else: flash(f'创建用户失败: {error}', 'error') return render_template('admin/add_student.html') # 创建学生记录 student = Student( student_number=data['student_number'], name=data['name'], gender=data['gender'], grade=int(data['grade']), phone=data.get('phone', ''), supervisor=data.get('supervisor', ''), college=data.get('college', ''), major=data.get('major', ''), degree_type=data.get('degree_type') if data.get('degree_type') else None, status=data.get('status', '在读'), enrollment_date=datetime.strptime(data['enrollment_date'], '%Y-%m-%d').date() if data.get( 'enrollment_date') else None ) success, error = safe_add_and_commit(student) if success: if request.is_json: return jsonify({'success': True, 'message': '学生添加成功'}) else: flash('学生添加成功', 'success') return redirect(url_for('admin.student_list')) else: if request.is_json: return jsonify({'success': False, 'message': f'添加失败: {error}'}) else: flash(f'添加失败: {error}', 'error') except Exception as e: if request.is_json: return jsonify({'success': False, 'message': f'添加失败: {str(e)}'}) else: flash(f'添加失败: {str(e)}', 'error') return render_template('admin/add_student.html') @admin_bp.route('/students//edit', methods=['GET', 'POST']) @admin_required def edit_student(student_number): """编辑学生信息""" student = Student.query.filter_by(student_number=student_number).first_or_404() if request.method == 'POST': try: data = request.get_json() if request.is_json else request.form # 更新学生信息 student.name = data['name'] student.gender = data['gender'] student.grade = int(data['grade']) student.phone = data.get('phone', '') student.supervisor = data.get('supervisor', '') student.college = data.get('college', '') student.major = data.get('major', '') student.degree_type = data.get('degree_type') if data.get('degree_type') else None student.status = data.get('status', '在读') if data.get('enrollment_date'): student.enrollment_date = datetime.strptime(data['enrollment_date'], '%Y-%m-%d').date() success, error = safe_commit() if success: if request.is_json: return jsonify({'success': True, 'message': '学生信息更新成功'}) else: flash('学生信息更新成功', 'success') return redirect(url_for('admin.student_detail', student_number=student_number)) else: if request.is_json: return jsonify({'success': False, 'message': f'更新失败: {error}'}) else: flash(f'更新失败: {error}', 'error') except Exception as e: if request.is_json: return jsonify({'success': False, 'message': f'更新失败: {str(e)}'}) else: flash(f'更新失败: {str(e)}', 'error') # GET请求,返回学生数据用于编辑 if request.is_json: return jsonify({ 'success': True, 'student': { 'student_number': student.student_number, 'name': student.name, 'gender': student.gender, 'grade': student.grade, 'phone': student.phone, 'supervisor': student.supervisor, 'college': student.college, 'major': student.major, 'degree_type': student.degree_type, 'status': student.status, 'enrollment_date': student.enrollment_date.strftime('%Y-%m-%d') if student.enrollment_date else '' } }) return render_template('admin/edit_student.html', student=student) @admin_bp.route('/students//delete', methods=['POST']) @admin_required def delete_student(student_number): """删除学生""" try: student = Student.query.filter_by(student_number=student_number).first_or_404() student_name = student.name # 删除学生记录(用户记录会因为外键约束自动删除) success, error = safe_delete_and_commit(student) if success: if request.is_json: return jsonify({'success': True, 'message': f'学生 {student_name} 删除成功'}) else: flash(f'学生 {student_name} 删除成功', 'success') return redirect(url_for('admin.student_list')) else: if request.is_json: return jsonify({'success': False, 'message': f'删除失败: {error}'}) else: flash(f'删除失败: {error}', 'error') except Exception as e: if request.is_json: return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) else: flash(f'删除失败: {str(e)}', 'error') return redirect(url_for('admin.student_list')) @admin_bp.route('/students/batch_action', methods=['POST']) @admin_required def batch_action(): """批量操作学生""" try: data = request.get_json() action = data.get('action') student_numbers = data.get('student_numbers', []) if not student_numbers: return jsonify({'success': False, 'message': '请选择要操作的学生'}) if action == 'delete': # 批量删除 students = Student.query.filter(Student.student_number.in_(student_numbers)).all() for student in students: db.session.delete(student) success, error = safe_commit() if success: return jsonify({'success': True, 'message': f'成功删除 {len(student_numbers)} 个学生'}) else: return jsonify({'success': False, 'message': f'删除失败: {error}'}) elif action == 'graduate': # 批量设为毕业 Student.query.filter(Student.student_number.in_(student_numbers)).update( {'status': '毕业'}, synchronize_session=False ) success, error = safe_commit() if success: return jsonify({'success': True, 'message': f'成功将 {len(student_numbers)} 个学生设为毕业状态'}) else: return jsonify({'success': False, 'message': f'操作失败: {error}'}) else: return jsonify({'success': False, 'message': '无效的操作'}) except Exception as e: return jsonify({'success': False, 'message': f'操作失败: {str(e)}'}) @admin_bp.route('/students//reset_password', methods=['POST']) @admin_required def reset_student_password(student_number): """重置学生密码""" try: user = User.query.filter_by(student_number=student_number).first_or_404() # 重置为默认密码 new_password = request.get_json().get('password', '123456') if request.is_json else '123456' user.password_hash = generate_password_hash(new_password) success, error = safe_commit() if success: if request.is_json: return jsonify({'success': True, 'message': '密码重置成功'}) else: flash('密码重置成功', 'success') return redirect(url_for('admin.student_detail', student_number=student_number)) else: if request.is_json: return jsonify({'success': False, 'message': f'重置失败: {error}'}) else: flash(f'重置失败: {error}', 'error') except Exception as e: if request.is_json: return jsonify({'success': False, 'message': f'重置失败: {str(e)}'}) else: flash(f'重置失败: {str(e)}', 'error') return redirect(url_for('admin.student_detail', student_number=student_number)) @admin_bp.route('/students//toggle_status', methods=['POST']) @admin_required def toggle_student_status(student_number): """切换学生账户状态""" try: user = User.query.filter_by(student_number=student_number).first_or_404() user.is_active = not user.is_active success, error = safe_commit() if success: status_text = '启用' if user.is_active else '禁用' if request.is_json: return jsonify({'success': True, 'message': f'账户{status_text}成功'}) else: flash(f'账户{status_text}成功', 'success') else: if request.is_json: return jsonify({'success': False, 'message': f'操作失败: {error}'}) else: flash(f'操作失败: {error}', 'error') except Exception as e: if request.is_json: return jsonify({'success': False, 'message': f'操作失败: {str(e)}'}) else: flash(f'操作失败: {str(e)}', 'error') return redirect(url_for('admin.student_detail', student_number=student_number)) @admin_bp.route('/attendance//details') @admin_required def attendance_record_details(record_id): """查看考勤记录详情""" from datetime import datetime, timedelta import json # 获取周考勤汇总记录 weekly_record = WeeklyAttendance.query.get_or_404(record_id) # 获取学生信息 student = Student.query.filter_by(student_number=weekly_record.student_number).first() # 获取该周的每日考勤明细 daily_details = DailyAttendanceDetail.query.filter_by( weekly_record_id=record_id ).order_by(DailyAttendanceDetail.attendance_date).all() # 处理每日详情,计算工作时长和解析详细信息 processed_daily_details = [] for detail in daily_details: processed_detail = { 'detail_id': detail.detail_id, 'attendance_date': detail.attendance_date, 'status': detail.status, 'check_in_time': detail.check_in_time, 'check_out_time': detail.check_out_time, 'remarks': detail.remarks, 'duration_hours': None, 'detailed_info': None } # 计算工作时长 if detail.check_in_time and detail.check_out_time: try: # 创建完整的datetime对象 start_datetime = datetime.combine(detail.attendance_date, detail.check_in_time) end_datetime = datetime.combine(detail.attendance_date, detail.check_out_time) # 如果结束时间小于开始时间,说明跨天了 if end_datetime < start_datetime: end_datetime += timedelta(days=1) duration = (end_datetime - start_datetime).total_seconds() / 3600 processed_detail['duration_hours'] = round(duration, 1) except Exception as e: print(f"计算工作时长失败: {e}") processed_detail['duration_hours'] = None # 解析详细信息 if detail.remarks: try: if detail.remarks.startswith('{'): remarks_data = json.loads(detail.remarks) processed_detail['detailed_info'] = remarks_data.get('details') processed_detail['summary_remarks'] = remarks_data.get('summary', detail.remarks) else: processed_detail['summary_remarks'] = detail.remarks except: processed_detail['summary_remarks'] = detail.remarks processed_daily_details.append(processed_detail) # 计算统计数据 total_days = len(processed_daily_details) present_days = len([d for d in processed_daily_details if d['status'] == '正常']) late_days = len([d for d in processed_daily_details if '迟到' in d['status']]) absent_days = len([d for d in processed_daily_details if d['status'] == '缺勤']) # 计算平均每日工作时长 if processed_daily_details: avg_daily_hours = weekly_record.actual_work_hours / max(present_days, 1) else: avg_daily_hours = 0 # 获取该学生的历史考勤记录(用于对比) historical_records = WeeklyAttendance.query.filter_by( student_number=weekly_record.student_number ).filter(WeeklyAttendance.record_id != record_id).order_by( desc(WeeklyAttendance.week_start_date) ).limit(5).all() return render_template('admin/attendance_details.html', weekly_record=weekly_record, student=student, daily_details=processed_daily_details, total_days=total_days, present_days=present_days, late_days=late_days, absent_days=absent_days, avg_daily_hours=avg_daily_hours, historical_records=historical_records) @admin_bp.route('/attendance//edit', methods=['GET', 'POST']) @admin_required def edit_attendance_record(record_id): """编辑考勤记录""" weekly_record = WeeklyAttendance.query.get_or_404(record_id) if request.method == 'POST': try: data = request.get_json() if request.is_json else request.form # 更新周考勤记录 weekly_record.actual_work_hours = float(data.get('actual_work_hours', 0)) weekly_record.class_work_hours = float(data.get('class_work_hours', 0)) weekly_record.absent_days = int(data.get('absent_days', 0)) weekly_record.overtime_hours = float(data.get('overtime_hours', 0)) success, error = safe_commit() if success: flash('考勤记录更新成功', 'success') return redirect(url_for('admin.attendance_record_details', record_id=record_id)) else: flash(f'更新失败: {error}', 'error') except Exception as e: flash(f'更新失败: {str(e)}', 'error') return render_template('admin/edit_attendance_record.html', weekly_record=weekly_record) def export_attendance_data(): """导出考勤数据到Excel""" from sqlalchemy import desc, func, case, or_ from io import BytesIO try: # 获取筛选参数 start_date = request.args.get('start_date') end_date = request.args.get('end_date') student_search = request.args.get('student_search', '').strip() sort_by = request.args.get('sort_by', 'week_start_date_desc') # 构建查询 query = db.session.query( WeeklyAttendance, func.coalesce( func.sum( case( (DailyAttendanceDetail.status.like('%迟到%'), 1), else_=0 ) ), 0 ).label('late_count') ).outerjoin( DailyAttendanceDetail, WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id ).group_by(WeeklyAttendance.record_id) # 应用筛选条件 if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() query = query.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: pass if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() query = query.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: pass if student_search: query = query.filter(or_( WeeklyAttendance.name.contains(student_search), WeeklyAttendance.student_number.contains(student_search) )) # 应用排序 if sort_by and '_' in sort_by: field, direction = sort_by.rsplit('_', 1) if direction not in ['asc', 'desc']: direction = 'desc' if field == 'actual_work_hours': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.actual_work_hours)) else: query = query.order_by(WeeklyAttendance.actual_work_hours) elif field == 'week_start_date': if direction == 'desc': query = query.order_by(desc(WeeklyAttendance.week_start_date)) else: query = query.order_by(WeeklyAttendance.week_start_date) else: query = query.order_by(desc(WeeklyAttendance.week_start_date)) else: query = query.order_by(desc(WeeklyAttendance.week_start_date)) # 获取所有记录 results = query.all() if not results: flash('没有数据可导出', 'warning') args = request.args.copy() args.pop('export', None) return redirect(url_for('admin.attendance_management', **args)) # 准备数据 data = [] for record, late_count in results: data.append({ '学号': record.student_number, '姓名': record.name, '周开始日期': record.week_start_date.strftime('%Y-%m-%d'), '周结束日期': record.week_end_date.strftime('%Y-%m-%d'), '实际出勤时长(小时)': float(record.actual_work_hours), '班内工作时长(小时)': float(record.class_work_hours), '旷工天数': int(record.absent_days), '迟到次数': int(late_count) if late_count else 0, '加班时长(小时)': float(record.overtime_hours), '记录创建时间': record.created_at.strftime('%Y-%m-%d %H:%M:%S') }) # 创建DataFrame df = pd.DataFrame(data) # 创建Excel文件 output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name='考勤记录', index=False) # 调整列宽 workbook = writer.book worksheet = writer.sheets['考勤记录'] for column in worksheet.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 30) worksheet.column_dimensions[column_letter].width = adjusted_width output.seek(0) # 生成文件名 timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"考勤记录_{timestamp}.xlsx" return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=filename ) except Exception as e: flash(f'导出失败: {str(e)}', 'error') # 移除export参数,重定向到正常页面 args = request.args.copy() args.pop('export', None) return redirect(url_for('admin.attendance_management', **args)) ================================================================================ File: ./app/routes/student.py ================================================================================ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask_login import login_required, current_user from app.models import db, Student, WeeklyAttendance, DailyAttendanceDetail, LeaveRecord from app.utils.auth_helpers import student_required from app.utils.database import safe_add_and_commit from datetime import datetime, timedelta from sqlalchemy import and_, or_, desc student_bp = Blueprint('student', __name__) @student_bp.route('/dashboard') @student_required def dashboard(): """学生主页""" if current_user.is_admin(): return redirect(url_for('admin.dashboard')) student = Student.query.filter_by(student_number=current_user.student_number).first() if not student: flash('学生信息不存在,请联系管理员', 'error') return redirect(url_for('auth.logout')) # 获取最近的考勤记录 recent_attendance = WeeklyAttendance.query.filter_by( student_number=current_user.student_number ).order_by(desc(WeeklyAttendance.week_start_date)).limit(5).all() # 统计数据 total_records = WeeklyAttendance.query.filter_by( student_number=current_user.student_number ).count() total_work_hours = db.session.query( db.func.sum(WeeklyAttendance.actual_work_hours) ).filter_by(student_number=current_user.student_number).scalar() or 0 total_absent_days = db.session.query( db.func.sum(WeeklyAttendance.absent_days) ).filter_by(student_number=current_user.student_number).scalar() or 0 # 获取未审批的请假记录 pending_leaves = LeaveRecord.query.filter_by( student_number=current_user.student_number, status='待审批' ).order_by(desc(LeaveRecord.created_at)).all() return render_template('student/dashboard.html', student=student, recent_attendance=recent_attendance, total_records=total_records, total_work_hours=float(total_work_hours), total_absent_days=int(total_absent_days), pending_leaves=pending_leaves) @student_bp.route('/attendance') @student_required def attendance(): """考勤记录页面""" from sqlalchemy import desc, func, case, or_ page = request.args.get('page', 1, type=int) per_page = 20 # 日期筛选 start_date = request.args.get('start_date') end_date = request.args.get('end_date') # 构建基础查询,同时计算迟到次数 query = db.session.query( WeeklyAttendance, func.coalesce( func.sum( case( (DailyAttendanceDetail.status.like('%迟到%'), 1), else_=0 ) ), 0 ).label('late_count') ).outerjoin( DailyAttendanceDetail, WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id ).filter( WeeklyAttendance.student_number == current_user.student_number ).group_by(WeeklyAttendance.record_id) # 应用筛选条件 if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() query = query.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: flash('开始日期格式错误', 'error') if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() query = query.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: flash('结束日期格式错误', 'error') # 执行分页查询 pagination = query.order_by(desc(WeeklyAttendance.week_start_date)).paginate( page=page, per_page=per_page, error_out=False ) # 处理结果,将迟到次数添加到记录对象中 attendance_records = [] for record, late_count in pagination.items: record.late_count = int(late_count) if late_count else 0 attendance_records.append(record) # 更新pagination对象的items pagination.items = attendance_records # 计算总体统计 total_stats = None if attendance_records: # 计算所有记录的统计信息 all_records_query = db.session.query( WeeklyAttendance, func.coalesce( func.sum( case( (DailyAttendanceDetail.status.like('%迟到%'), 1), else_=0 ) ), 0 ).label('late_count') ).outerjoin( DailyAttendanceDetail, WeeklyAttendance.record_id == DailyAttendanceDetail.weekly_record_id ).filter( WeeklyAttendance.student_number == current_user.student_number ).group_by(WeeklyAttendance.record_id) # 应用相同的筛选条件 if start_date: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() all_records_query = all_records_query.filter(WeeklyAttendance.week_start_date >= start_date_obj) if end_date: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() all_records_query = all_records_query.filter(WeeklyAttendance.week_end_date <= end_date_obj) all_records = all_records_query.all() total_actual_hours = sum(record.actual_work_hours for record, _ in all_records) total_class_hours = sum(record.class_work_hours for record, _ in all_records) total_absent_days = sum(record.absent_days for record, _ in all_records) total_overtime_hours = sum(record.overtime_hours for record, _ in all_records) total_late_count = sum(late_count for _, late_count in all_records) # 计算请假天数 record_ids = [record.record_id for record, _ in all_records] total_leave_days = 0 if record_ids: total_leave_days = DailyAttendanceDetail.query.filter( DailyAttendanceDetail.weekly_record_id.in_(record_ids), DailyAttendanceDetail.status == '请假' ).count() total_stats = { 'total_weeks': len(all_records), 'total_actual_hours': total_actual_hours, 'total_class_hours': total_class_hours, 'total_absent_days': total_absent_days, 'total_overtime_hours': total_overtime_hours, 'total_late_count': total_late_count, 'total_leave_days': total_leave_days, 'avg_weekly_hours': total_actual_hours / max(len(all_records), 1) } return render_template('student/attendance.html', attendance_records=attendance_records, pagination=pagination, start_date=start_date, end_date=end_date, total_stats=total_stats) @student_bp.route('/attendance//details') @student_required def attendance_details(record_id): """考勤详细信息""" from datetime import datetime, timedelta import json # 获取周考勤汇总记录(确保只能查看自己的记录) record = WeeklyAttendance.query.filter_by( record_id=record_id, student_number=current_user.student_number ).first_or_404() # 获取学生信息 student = Student.query.filter_by(student_number=current_user.student_number).first() # 获取该周的每日考勤明细 daily_details = DailyAttendanceDetail.query.filter_by( weekly_record_id=record_id ).order_by(DailyAttendanceDetail.attendance_date).all() # 处理每日详情,计算工作时长和解析详细信息 processed_daily_details = [] for detail in daily_details: processed_detail = { 'detail_id': detail.detail_id, 'attendance_date': detail.attendance_date, 'status': detail.status, 'check_in_time': detail.check_in_time, 'check_out_time': detail.check_out_time, 'remarks': detail.remarks, 'duration_hours': None, 'detailed_info': None } # 计算工作时长 if detail.check_in_time and detail.check_out_time: try: # 创建完整的datetime对象 start_datetime = datetime.combine(detail.attendance_date, detail.check_in_time) end_datetime = datetime.combine(detail.attendance_date, detail.check_out_time) # 如果结束时间小于开始时间,说明跨天了 if end_datetime < start_datetime: end_datetime += timedelta(days=1) duration = (end_datetime - start_datetime).total_seconds() / 3600 processed_detail['duration_hours'] = round(duration, 1) except Exception as e: print(f"计算工作时长失败: {e}") processed_detail['duration_hours'] = None # 解析详细信息 if detail.remarks: try: if detail.remarks.startswith('{'): remarks_data = json.loads(detail.remarks) processed_detail['detailed_info'] = remarks_data.get('details') processed_detail['summary_remarks'] = remarks_data.get('summary', detail.remarks) else: processed_detail['summary_remarks'] = detail.remarks except: processed_detail['summary_remarks'] = detail.remarks processed_daily_details.append(processed_detail) # 计算统计数据 total_days = len(processed_daily_details) present_days = len([d for d in processed_daily_details if d['status'] == '正常']) late_days = len([d for d in processed_daily_details if '迟到' in d['status']]) absent_days = len([d for d in processed_daily_details if d['status'] == '缺勤']) # 计算平均每日工作时长 if processed_daily_details: avg_daily_hours = record.actual_work_hours / max(present_days, 1) else: avg_daily_hours = 0 # 获取该学生最近的其他考勤记录(用于对比) recent_records = WeeklyAttendance.query.filter_by( student_number=current_user.student_number ).filter(WeeklyAttendance.record_id != record_id).order_by( desc(WeeklyAttendance.week_start_date) ).limit(5).all() return render_template('student/attendance_details.html', record=record, student=student, daily_details=processed_daily_details, total_days=total_days, present_days=present_days, late_days=late_days, absent_days=absent_days, avg_daily_hours=avg_daily_hours, recent_records=recent_records) @student_bp.route('/statistics') @login_required def statistics(): """学生个人统计""" from sqlalchemy import desc, func, case from datetime import datetime, timedelta # 获取当前学生信息 student = Student.query.filter_by(student_number=current_user.student_number).first_or_404() # 获取筛选参数 start_date = request.args.get('start_date', '') end_date = request.args.get('end_date', '') # 构建考勤记录查询 attendance_query = WeeklyAttendance.query.filter_by( student_number=current_user.student_number ) # 应用日期筛选 if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() attendance_query = attendance_query.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: flash('开始日期格式错误', 'error') if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() attendance_query = attendance_query.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: flash('结束日期格式错误', 'error') # 获取考勤记录,按周排序 attendance_records = attendance_query.order_by(desc(WeeklyAttendance.week_start_date)).all() # 计算统计数据 total_stats = { 'total_work_hours': sum(record.actual_work_hours for record in attendance_records), 'total_class_hours': sum(record.class_work_hours for record in attendance_records), 'total_overtime_hours': sum(record.overtime_hours for record in attendance_records), 'total_absent_days': sum(record.absent_days for record in attendance_records), 'attendance_weeks': len(attendance_records) } # 计算迟到次数 total_late_count = db.session.query( func.sum( case( (DailyAttendanceDetail.status.like('%迟到%'), 1), else_=0 ) ) ).join( WeeklyAttendance, DailyAttendanceDetail.weekly_record_id == WeeklyAttendance.record_id ).filter(WeeklyAttendance.student_number == current_user.student_number) if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() total_late_count = total_late_count.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: pass if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() total_late_count = total_late_count.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: pass total_stats['total_late_count'] = int(total_late_count.scalar() or 0) # 计算平均值 if total_stats['attendance_weeks'] > 0: total_stats['avg_weekly_hours'] = round(total_stats['total_work_hours'] / total_stats['attendance_weeks'], 1) total_stats['avg_weekly_class_hours'] = round( total_stats['total_class_hours'] / total_stats['attendance_weeks'], 1) else: total_stats['avg_weekly_hours'] = 0 total_stats['avg_weekly_class_hours'] = 0 # 按月统计 monthly_stats = db.session.query( func.date_format(WeeklyAttendance.week_start_date, '%Y-%m').label('month'), func.count(WeeklyAttendance.record_id).label('record_count'), func.sum(WeeklyAttendance.actual_work_hours).label('total_hours'), func.sum(WeeklyAttendance.class_work_hours).label('class_hours'), func.sum(WeeklyAttendance.overtime_hours).label('overtime_hours'), func.sum(WeeklyAttendance.absent_days).label('absent_days') ).filter_by(student_number=current_user.student_number) if start_date: try: start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() monthly_stats = monthly_stats.filter(WeeklyAttendance.week_start_date >= start_date_obj) except ValueError: pass if end_date: try: end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() monthly_stats = monthly_stats.filter(WeeklyAttendance.week_end_date <= end_date_obj) except ValueError: pass monthly_stats = monthly_stats.group_by('month').order_by('month').all() # 最近几周的趋势数据 recent_weeks = attendance_query.order_by(desc(WeeklyAttendance.week_start_date)).limit(12).all() recent_weeks.reverse() # 按时间正序排列 # 计算入学以来的总体表现 all_time_stats = None if student.enrollment_date: all_time_query = WeeklyAttendance.query.filter_by( student_number=current_user.student_number ) all_records = all_time_query.all() if all_records: # 计算入学以来的总统计 enrollment_weeks = (datetime.now().date() - student.enrollment_date).days // 7 all_time_stats = { 'total_work_hours': sum(record.actual_work_hours for record in all_records), 'total_class_hours': sum(record.class_work_hours for record in all_records), 'total_overtime_hours': sum(record.overtime_hours for record in all_records), 'total_absent_days': sum(record.absent_days for record in all_records), 'attendance_weeks': len(all_records), 'enrollment_weeks': enrollment_weeks, 'attendance_rate': round(len(all_records) / max(enrollment_weeks, 1) * 100, 1) if enrollment_weeks > 0 else 0 } # 计算入学以来的迟到次数 all_time_late_count = db.session.query( func.sum( case( (DailyAttendanceDetail.status.like('%迟到%'), 1), else_=0 ) ) ).join( WeeklyAttendance, DailyAttendanceDetail.weekly_record_id == WeeklyAttendance.record_id ).filter(WeeklyAttendance.student_number == current_user.student_number).scalar() all_time_stats['total_late_count'] = int(all_time_late_count or 0) return render_template('student/statistics.html', student=student, attendance_records=attendance_records, total_stats=total_stats, monthly_stats=monthly_stats, recent_weeks=recent_weeks, all_time_stats=all_time_stats, start_date=start_date, end_date=end_date) ================================================================================ File: ./config/config.py ================================================================================ import os from dotenv import load_dotenv load_dotenv() class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ENGINE_OPTIONS = { 'pool_recycle': 300, 'pool_pre_ping': True, 'pool_size': 10, 'max_overflow': 20 } # 分页配置 STUDENTS_PER_PAGE = 20 ATTENDANCE_PER_PAGE = 50 # 文件上传配置 MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB UPLOAD_FOLDER = 'uploads' ALLOWED_EXTENSIONS = {'csv', 'xlsx', 'xls'} class DevelopmentConfig(Config): DEBUG = True class ProductionConfig(Config): DEBUG = False config = { 'development': DevelopmentConfig, 'production': ProductionConfig, 'default': DevelopmentConfig } ================================================================================ File: ./config/database.py ================================================================================ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker import os DATABASE_URL = os.environ.get('DATABASE_URL') engine = create_engine(DATABASE_URL, echo=False) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() def get_db(): db = SessionLocal() try: yield db finally: db.close() ================================================================================ File: ./config/__init__.py ================================================================================ ================================================================================ File: ./tests/test_auth.py ================================================================================ ================================================================================ File: ./tests/__init__.py ================================================================================ ================================================================================ File: ./tests/test_models.py ================================================================================