2025-09-14 02:00:19 +08:00

564 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.py中添加的缺勤次数计算函数
def calculate_absent_count_for_record(record):
"""为单个考勤记录计算真实的缺勤次数"""
import json
from app.models import DailyAttendanceDetail
# 获取该记录的每日明细
daily_details = DailyAttendanceDetail.query.filter_by(
weekly_record_id=record.record_id
).all()
absent_count = 0
for detail in daily_details:
# 处理完全缺勤的情况
if detail.status == '缺勤' and (not detail.remarks or not detail.remarks.startswith('{')):
# 完全缺勤的天数,早上+下午都缺勤
absent_count += 2 # 早上缺勤1次 + 下午缺勤1次
continue
if detail.remarks and detail.remarks.startswith('{'):
try:
remarks_data = json.loads(detail.remarks)
details_info = remarks_data.get('details', {})
# 统计缺勤次数
# 检查早上缺勤morning_in AND morning_out 都missing
morning_data = details_info.get('morning', {})
morning_in_time = morning_data.get('in')
morning_out_time = morning_data.get('out')
morning_in_status = morning_data.get('status', 'missing')
if ((not morning_in_time or morning_in_status == 'missing') and
(not morning_out_time)):
absent_count += 1
# 检查下午缺勤afternoon_in AND afternoon_out 都missing
afternoon_data = details_info.get('afternoon', {})
afternoon_in_time = afternoon_data.get('in')
afternoon_out_time = afternoon_data.get('out')
afternoon_in_status = afternoon_data.get('status', 'missing')
if ((not afternoon_in_time or afternoon_in_status == 'missing') and
(not afternoon_out_time)):
absent_count += 1
except (json.JSONDecodeError, KeyError, AttributeError):
continue
return absent_count
def add_absent_count_to_records(records):
"""为考勤记录列表添加缺勤次数计算属性"""
total_absent_count = 0
for record in records:
absent_count = calculate_absent_count_for_record(record)
record.absent_count = absent_count
total_absent_count += absent_count
return total_absent_count
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
# 获取所有考勤记录并计算真实缺勤次数
all_records = WeeklyAttendance.query.filter_by(student_number=current_user.student_number).all()
total_absent_count = add_absent_count_to_records(all_records)
# 获取未审批的请假记录
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_count=int(total_absent_count),
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)
# 为记录添加缺勤次数计算
record_list = [record for record, _ in all_records]
total_absent_count = add_absent_count_to_records(record_list)
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_count': total_absent_count,
'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/<int:record_id>/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()
# 🔥 新增:计算真实的缺勤次数和迟到次数
def calculate_real_counts(daily_details_list):
"""计算真实的缺勤次数和迟到次数"""
late_count = 0
absent_count = 0
for detail in daily_details_list:
# 🔥 处理完全缺勤的情况
if detail.status == '缺勤' and (not detail.remarks or not detail.remarks.startswith('{')):
# 完全缺勤的天数,早上+下午都缺勤
absent_count += 2 # 早上缺勤1次 + 下午缺勤1次
continue
if detail.remarks and detail.remarks.startswith('{'):
try:
remarks_data = json.loads(detail.remarks)
details_info = remarks_data.get('details', {})
# 统计迟到次数
for period in ['morning', 'afternoon', 'evening']:
period_data = details_info.get(period, {})
if period_data.get('status') == 'late':
late_count += 1
# 统计缺勤次数
# 检查早上缺勤morning_in AND morning_out 都missing
morning_data = details_info.get('morning', {})
morning_in_time = morning_data.get('in')
morning_out_time = morning_data.get('out')
morning_in_status = morning_data.get('status', 'missing')
if ((not morning_in_time or morning_in_status == 'missing') and
(not morning_out_time)):
absent_count += 1
# 检查下午缺勤afternoon_in AND afternoon_out 都missing
afternoon_data = details_info.get('afternoon', {})
afternoon_in_time = afternoon_data.get('in')
afternoon_out_time = afternoon_data.get('out')
afternoon_in_status = afternoon_data.get('status', 'missing')
if ((not afternoon_in_time or afternoon_in_status == 'missing') and
(not afternoon_out_time)):
absent_count += 1
except (json.JSONDecodeError, KeyError, AttributeError):
continue
return late_count, absent_count
# 🔥 计算真实的次数并添加到record对象中
real_late_count, real_absent_count = calculate_real_counts(daily_details)
record.late_count = real_late_count
record.absent_count = real_absent_count
# 处理每日详情,计算工作时长和解析详细信息
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()
# 🔥 修改:传递 weekly_record 而不是 record以匹配模板
return render_template('student/attendance_details.html',
weekly_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()
# 计算真实缺勤次数
add_absent_count_to_records(attendance_records)
# 计算统计数据
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_count': sum(getattr(record, 'absent_count', 0) 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
# 🔥 修复:先计算缺勤次数,再定义字典
add_absent_count_to_records(all_records)
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_count': sum(getattr(record, 'absent_count', 0) 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)