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)