superlishunqin e7fa4bc030 first commit
2025-06-11 19:56:34 +08:00

438 lines
17 KiB
Python

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/<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()
# 处理每日详情,计算工作时长和解析详细信息
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)