修复学期功能
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
# 环境变量
|
# 环境变量
|
||||||
.env
|
.env
|
||||||
|
backend/.env
|
||||||
|
frontend/.env
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
# ===========================================
|
||||||
|
# 班级操行分管理系统 - 前端配置
|
||||||
|
#
|
||||||
|
# 开发者: Canglan
|
||||||
|
# 联系方式: admin@sea-studio.top
|
||||||
|
# 版权归属: Sea Network Technology Studio
|
||||||
|
# 许可证: MIT License
|
||||||
|
#
|
||||||
|
# 版权所有 © Sea Network Technology Studio
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# FastAPI 应用配置
|
# FastAPI 应用配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -94,7 +105,7 @@ MONITOR_MAX_ADD=5
|
|||||||
# 班长单次扣分上限(负数)
|
# 班长单次扣分上限(负数)
|
||||||
MONITOR_MAX_SUBTRACT=-5
|
MONITOR_MAX_SUBTRACT=-5
|
||||||
|
|
||||||
# 学习委员单次加减分上限(绝对值)- 正负均不可超过此值
|
# 学习委员单次加减分上限(绝对值)
|
||||||
STUDY_COMMISSIONER_MAX_POINTS=5
|
STUDY_COMMISSIONER_MAX_POINTS=5
|
||||||
|
|
||||||
# 考勤委员单次扣分上限(绝对值)
|
# 考勤委员单次扣分上限(绝对值)
|
||||||
|
|||||||
@@ -51,14 +51,26 @@ class SemesterModel:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_active() -> Optional[Dict[str, Any]]:
|
async def get_active() -> Optional[Dict[str, Any]]:
|
||||||
"""获取当前活跃学期"""
|
"""获取当前活跃学期(优先 is_active 标记,降级为日期范围匹配)"""
|
||||||
sql = """
|
fields = "semester_id, semester_name, start_date, end_date, is_active, is_archived, created_at"
|
||||||
SELECT semester_id, semester_name, start_date, end_date,
|
# 第一优先级:is_active 标记
|
||||||
is_active, is_archived, created_at
|
sql = f"""
|
||||||
|
SELECT {fields}
|
||||||
FROM semesters
|
FROM semesters
|
||||||
WHERE is_active = 1 AND is_archived = 0
|
WHERE is_active = 1 AND is_archived = 0
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
|
result = await execute_one(sql)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
# 第二优先级:日期范围匹配
|
||||||
|
# 注:无日期的学期不会自动匹配为活跃学期(需手动激活)
|
||||||
|
sql = f"""
|
||||||
|
SELECT {fields}
|
||||||
|
FROM semesters
|
||||||
|
WHERE is_archived = 0 AND start_date <= CURDATE() AND (end_date IS NULL OR end_date >= CURDATE())
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
return await execute_one(sql)
|
return await execute_one(sql)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -103,6 +115,29 @@ class SemesterModel:
|
|||||||
result = await execute_one(sql, (record_id,))
|
result = await execute_one(sql, (record_id,))
|
||||||
return result['semester_id'] if result else None
|
return result['semester_id'] if result else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_attendance_stats_by_semester(semester_id: int, start_date: str, end_date: str) -> List[Dict]:
|
||||||
|
"""批量查询学期内所有学生的考勤统计"""
|
||||||
|
sql = """
|
||||||
|
SELECT student_id, status, COUNT(*) as cnt
|
||||||
|
FROM attendance_records
|
||||||
|
WHERE (semester_id = %s OR (semester_id IS NULL AND `date` BETWEEN %s AND %s))
|
||||||
|
GROUP BY student_id, status
|
||||||
|
"""
|
||||||
|
return await execute_query(sql, (semester_id, start_date, end_date))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_homework_stats_by_date_range(start_date: str, end_date: str) -> List[Dict]:
|
||||||
|
"""通过作业截止日期范围查询所有学生的作业提交统计"""
|
||||||
|
sql = """
|
||||||
|
SELECT hs.student_id, hs.status, COUNT(*) as cnt
|
||||||
|
FROM homework_submissions hs
|
||||||
|
JOIN assignments a ON hs.assignment_id = a.assignment_id
|
||||||
|
WHERE a.deadline BETWEEN %s AND %s
|
||||||
|
GROUP BY hs.student_id, hs.status
|
||||||
|
"""
|
||||||
|
return await execute_query(sql, (start_date, end_date))
|
||||||
|
|
||||||
|
|
||||||
class SemesterArchiveModel:
|
class SemesterArchiveModel:
|
||||||
"""学期归档快照数据模型"""
|
"""学期归档快照数据模型"""
|
||||||
@@ -114,25 +149,39 @@ class SemesterArchiveModel:
|
|||||||
return 0
|
return 0
|
||||||
sql = """
|
sql = """
|
||||||
INSERT INTO semester_archives
|
INSERT INTO semester_archives
|
||||||
(semester_id, student_id, student_no, student_name, final_points, rank_position, total_students)
|
(semester_id, student_id, student_no, student_name, final_points, rank_position, total_students,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
attendance_present, attendance_absent, attendance_late, attendance_leave,
|
||||||
|
homework_submitted, homework_not_submitted, homework_late)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
"""
|
"""
|
||||||
params_list = [
|
params_list = [
|
||||||
(
|
(
|
||||||
a['semester_id'], a['student_id'], a['student_no'],
|
a['semester_id'], a['student_id'], a['student_no'],
|
||||||
a['student_name'], a['final_points'],
|
a['student_name'], a['final_points'],
|
||||||
a.get('rank_position'), a.get('total_students')
|
a.get('rank_position', 0), a.get('total_students', 0),
|
||||||
|
a.get('attendance_present', 0), a.get('attendance_absent', 0),
|
||||||
|
a.get('attendance_late', 0), a.get('attendance_leave', 0),
|
||||||
|
a.get('homework_submitted', 0), a.get('homework_not_submitted', 0),
|
||||||
|
a.get('homework_late', 0)
|
||||||
)
|
)
|
||||||
for a in archives_data
|
for a in archives_data
|
||||||
]
|
]
|
||||||
return await execute_many(sql, params_list)
|
return await execute_many(sql, params_list)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_by_semester(semester_id: int) -> int:
|
||||||
|
"""删除指定学期的所有归档数据(用于归档操作的幂等性)"""
|
||||||
|
sql = "DELETE FROM semester_archives WHERE semester_id = %s"
|
||||||
|
return await execute_update(sql, (semester_id,))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_by_semester(semester_id: int) -> List[Dict[str, Any]]:
|
async def get_by_semester(semester_id: int) -> List[Dict[str, Any]]:
|
||||||
"""获取学期的归档数据"""
|
"""获取学期的归档数据"""
|
||||||
sql = """
|
sql = """
|
||||||
SELECT archive_id, semester_id, student_id, student_no,
|
SELECT archive_id, semester_id, student_id, student_no,
|
||||||
student_name, final_points, rank_position, total_students, archived_at
|
student_name, final_points, rank_position, total_students,
|
||||||
|
attendance_present, attendance_absent, attendance_late, attendance_leave,
|
||||||
|
homework_submitted, homework_not_submitted, homework_late, archived_at
|
||||||
FROM semester_archives
|
FROM semester_archives
|
||||||
WHERE semester_id = %s
|
WHERE semester_id = %s
|
||||||
ORDER BY rank_position ASC
|
ORDER BY rank_position ASC
|
||||||
@@ -156,7 +205,10 @@ class SemesterArchiveModel:
|
|||||||
sql = """
|
sql = """
|
||||||
SELECT sa.archive_id, sa.semester_id, sa.student_id, sa.student_no,
|
SELECT sa.archive_id, sa.semester_id, sa.student_id, sa.student_no,
|
||||||
sa.student_name, sa.final_points, sa.rank_position,
|
sa.student_name, sa.final_points, sa.rank_position,
|
||||||
sa.total_students, sa.archived_at,
|
sa.total_students, sa.attendance_present, sa.attendance_absent,
|
||||||
|
sa.attendance_late, sa.attendance_leave,
|
||||||
|
sa.homework_submitted, sa.homework_not_submitted, sa.homework_late,
|
||||||
|
sa.archived_at,
|
||||||
s.semester_name, s.start_date, s.end_date
|
s.semester_name, s.start_date, s.end_date
|
||||||
FROM semester_archives sa
|
FROM semester_archives sa
|
||||||
JOIN semesters s ON sa.semester_id = s.semester_id
|
JOIN semesters s ON sa.semester_id = s.semester_id
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
# 版权所有 © Sea Network Technology Studio
|
# 版权所有 © Sea Network Technology Studio
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|
||||||
|
import datetime
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
from models.semester import SemesterModel, SemesterArchiveModel
|
from models.semester import SemesterModel, SemesterArchiveModel
|
||||||
@@ -49,20 +50,41 @@ class SemesterService:
|
|||||||
return {"success": False, "message": "学期名称不能为空"}
|
return {"success": False, "message": "学期名称不能为空"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 自动将之前的活跃学期设为非活跃
|
# 创建学期(不预先 deactivate_all)
|
||||||
await SemesterModel.deactivate_all()
|
|
||||||
|
|
||||||
# 创建新学期并自动设为活跃
|
|
||||||
semester_id = await SemesterModel.create(
|
semester_id = await SemesterModel.create(
|
||||||
semester_name=semester_name.strip(),
|
semester_name=semester_name.strip(),
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date
|
end_date=end_date
|
||||||
)
|
)
|
||||||
|
|
||||||
# 设为活跃学期
|
# 判断新学期的日期范围是否包含今天,决定是否自动激活
|
||||||
await SemesterModel.activate(semester_id)
|
should_activate = False
|
||||||
|
if start_date is not None:
|
||||||
|
try:
|
||||||
|
today = datetime.date.today()
|
||||||
|
s_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||||
|
e_date = (
|
||||||
|
datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||||
|
if end_date is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if s_date <= today and (e_date is None or e_date >= today):
|
||||||
|
should_activate = True
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
should_activate = False
|
||||||
|
|
||||||
logger.info(f"用户[{operator_id}] 创建了新学期: {semester_name}")
|
if should_activate:
|
||||||
|
# 日期范围包含今天,自动激活
|
||||||
|
await SemesterModel.deactivate_all()
|
||||||
|
await SemesterModel.activate(semester_id)
|
||||||
|
logger.info(
|
||||||
|
f"用户[{operator_id}] 创建并激活新学期: {semester_name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 补录历史学期或未来学期,不激活
|
||||||
|
logger.info(
|
||||||
|
f"用户[{operator_id}] 创建补录学期(未激活): {semester_name}"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -120,6 +142,11 @@ class SemesterService:
|
|||||||
if semester['is_archived']:
|
if semester['is_archived']:
|
||||||
return {"success": False, "message": "该学期已归档"}
|
return {"success": False, "message": "该学期已归档"}
|
||||||
|
|
||||||
|
# 校验开始日期:无 start_date 时作业统计会全部归零
|
||||||
|
start_date = semester.get('start_date')
|
||||||
|
if not start_date:
|
||||||
|
return {"success": False, "message": "学期未设置开始日期,无法进行归档"}
|
||||||
|
|
||||||
# 获取所有活跃学生及其当前分数
|
# 获取所有活跃学生及其当前分数
|
||||||
students = await StudentModel.get_all(include_disabled=False)
|
students = await StudentModel.get_all(include_disabled=False)
|
||||||
if not students:
|
if not students:
|
||||||
@@ -127,12 +154,60 @@ class SemesterService:
|
|||||||
|
|
||||||
total_students = len(students)
|
total_students = len(students)
|
||||||
|
|
||||||
|
# 获取学期的日期范围,用于查询考勤和作业统计
|
||||||
|
end_date = semester.get('end_date') or datetime.date.today().isoformat()
|
||||||
|
|
||||||
|
# 批量查询考勤和作业统计
|
||||||
|
attendance_stats = await SemesterModel.get_attendance_stats_by_semester(
|
||||||
|
semester_id, start_date, end_date
|
||||||
|
)
|
||||||
|
homework_stats = await SemesterModel.get_homework_stats_by_date_range(
|
||||||
|
start_date, end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建 attendance_map: {student_id: {status_field: cnt, ...}}
|
||||||
|
attendance_map = {}
|
||||||
|
for stat in attendance_stats:
|
||||||
|
sid = stat['student_id']
|
||||||
|
if sid not in attendance_map:
|
||||||
|
attendance_map[sid] = {
|
||||||
|
'attendance_present': 0, 'attendance_absent': 0,
|
||||||
|
'attendance_late': 0, 'attendance_leave': 0
|
||||||
|
}
|
||||||
|
status_key = {
|
||||||
|
'present': 'attendance_present',
|
||||||
|
'absent': 'attendance_absent',
|
||||||
|
'late': 'attendance_late',
|
||||||
|
'leave': 'attendance_leave'
|
||||||
|
}.get(stat['status'])
|
||||||
|
if status_key:
|
||||||
|
attendance_map[sid][status_key] = stat['cnt']
|
||||||
|
|
||||||
|
# 构建 homework_map: {student_id: {status_field: cnt, ...}}
|
||||||
|
homework_map = {}
|
||||||
|
for stat in homework_stats:
|
||||||
|
sid = stat['student_id']
|
||||||
|
if sid not in homework_map:
|
||||||
|
homework_map[sid] = {
|
||||||
|
'homework_submitted': 0, 'homework_not_submitted': 0,
|
||||||
|
'homework_late': 0
|
||||||
|
}
|
||||||
|
status_key = {
|
||||||
|
'submitted': 'homework_submitted',
|
||||||
|
'not_submitted': 'homework_not_submitted',
|
||||||
|
'late': 'homework_late'
|
||||||
|
}.get(stat['status'])
|
||||||
|
if status_key:
|
||||||
|
homework_map[sid][status_key] = stat['cnt']
|
||||||
|
|
||||||
# 按分数降序排列以计算排名
|
# 按分数降序排列以计算排名
|
||||||
sorted_students = sorted(students, key=lambda s: s['total_points'], reverse=True)
|
sorted_students = sorted(students, key=lambda s: s['total_points'], reverse=True)
|
||||||
|
|
||||||
# 构建归档快照数据
|
# 构建归档快照数据
|
||||||
archives_data = []
|
archives_data = []
|
||||||
for rank, student in enumerate(sorted_students, 1):
|
for rank, student in enumerate(sorted_students, 1):
|
||||||
|
att = attendance_map.get(student['student_id'], {})
|
||||||
|
hw = homework_map.get(student['student_id'], {})
|
||||||
archives_data.append({
|
archives_data.append({
|
||||||
'semester_id': semester_id,
|
'semester_id': semester_id,
|
||||||
'student_id': student['student_id'],
|
'student_id': student['student_id'],
|
||||||
@@ -140,10 +215,17 @@ class SemesterService:
|
|||||||
'student_name': student['name'],
|
'student_name': student['name'],
|
||||||
'final_points': student['total_points'],
|
'final_points': student['total_points'],
|
||||||
'rank_position': rank,
|
'rank_position': rank,
|
||||||
'total_students': total_students
|
'total_students': total_students,
|
||||||
|
'attendance_present': att.get('attendance_present', 0),
|
||||||
|
'attendance_absent': att.get('attendance_absent', 0),
|
||||||
|
'attendance_late': att.get('attendance_late', 0),
|
||||||
|
'attendance_leave': att.get('attendance_leave', 0),
|
||||||
|
'homework_submitted': hw.get('homework_submitted', 0),
|
||||||
|
'homework_not_submitted': hw.get('homework_not_submitted', 0),
|
||||||
|
'homework_late': hw.get('homework_late', 0),
|
||||||
})
|
})
|
||||||
|
# 删除已有的归档数据以保证幂等性,再保存归档快照
|
||||||
# 保存归档快照
|
await SemesterArchiveModel.delete_by_semester(semester_id)
|
||||||
await SemesterArchiveModel.batch_create(archives_data)
|
await SemesterArchiveModel.batch_create(archives_data)
|
||||||
|
|
||||||
# 标记学期为已归档
|
# 标记学期为已归档
|
||||||
|
|||||||
@@ -35,9 +35,16 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">操行分排行榜</div>
|
<div class="card-title">操行分排行榜</div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 12px; gap: 8px;">
|
||||||
|
<span style="font-size: 14px; color: #666;">显示前</span>
|
||||||
|
<input type="number" id="percentileFilter" style="width: 70px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px;" min="1" max="100" value="100" placeholder="1-100">
|
||||||
|
<span style="font-size: 14px; color: #666;">% 的学生</span>
|
||||||
|
<button class="btn btn-sm" style="background: #667eea; color: white;" onclick="applyPercentileFilter()">筛选</button>
|
||||||
|
<button class="btn btn-sm" style="border: 1px solid #ccc; color: #666;" onclick="resetPercentileFilter()">显示全部</button>
|
||||||
|
</div>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>排名</th><th>学号</th><th>姓名</th><th>操行分</th><th>前%</th></tr>
|
<tr><th>排名</th><th>学号</th><th>姓名</th><th>操行分</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="rankingList"></tbody>
|
<tbody id="rankingList"></tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -46,6 +53,8 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
var totalStudents = 0;
|
||||||
|
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
const studentsRes = await apiGet('/api/admin/students');
|
const studentsRes = await apiGet('/api/admin/students');
|
||||||
if (studentsRes && studentsRes.success) {
|
if (studentsRes && studentsRes.success) {
|
||||||
@@ -69,30 +78,51 @@ async function loadDashboard() {
|
|||||||
|
|
||||||
const rankingRes = await apiGet('/api/student/ranking', { limit: 100 });
|
const rankingRes = await apiGet('/api/student/ranking', { limit: 100 });
|
||||||
if (rankingRes && rankingRes.success) {
|
if (rankingRes && rankingRes.success) {
|
||||||
const totalStudents = rankingRes.data.total_students || 0;
|
totalStudents = rankingRes.data.total_students || 0;
|
||||||
let html = '';
|
let html = '';
|
||||||
rankingRes.data.ranking.forEach((student, index) => {
|
rankingRes.data.ranking.forEach((student, index) => {
|
||||||
const rank = index + 1;
|
const rank = index + 1;
|
||||||
let percentile = '--';
|
|
||||||
if (totalStudents > 0) {
|
|
||||||
const pct = Math.floor(rank / totalStudents * 100);
|
|
||||||
percentile = (pct === 0 ? 1 : pct) + '%';
|
|
||||||
}
|
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${rank}</td>
|
<td>${rank}</td>
|
||||||
<td>${escapeHtml(student.student_no)}</td>
|
<td>${escapeHtml(student.student_no)}</td>
|
||||||
<td>${escapeHtml(student.name)}</td>
|
<td>${escapeHtml(student.name)}</td>
|
||||||
<td>${student.total_points}</td>
|
<td>${student.total_points}</td>
|
||||||
<td>前${percentile}</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (rankingRes.data.ranking.length === 0) {
|
if (rankingRes.data.ranking.length === 0) {
|
||||||
html = '<tr><td colspan="5" style="text-align:center;">暂无数据</td></tr>';
|
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('rankingList').innerHTML = html;
|
document.getElementById('rankingList').innerHTML = html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('percentileFilter').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') applyPercentileFilter();
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyPercentileFilter() {
|
||||||
|
const input = document.getElementById('percentileFilter');
|
||||||
|
const percentile = parseInt(input.value);
|
||||||
|
if (isNaN(percentile) || percentile < 1 || percentile > 100) {
|
||||||
|
showToast('请输入 1-100 之间的整数', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rows = document.getElementById('rankingList').querySelectorAll('tr');
|
||||||
|
if (rows.length === 0) return;
|
||||||
|
const showCount = Math.ceil(totalStudents * (percentile / 100));
|
||||||
|
rows.forEach(function(row, index) {
|
||||||
|
row.style.display = index < showCount ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPercentileFilter() {
|
||||||
|
document.getElementById('percentileFilter').value = 100;
|
||||||
|
const rows = document.getElementById('rankingList').querySelectorAll('tr');
|
||||||
|
rows.forEach(function(row) {
|
||||||
|
row.style.display = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
</script>
|
</script>
|
||||||
<script src="/assets/js/admin.js"></script>
|
<script src="/assets/js/admin.js"></script>
|
||||||
|
|||||||
@@ -64,16 +64,20 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<label>学期名称 <span style="color:red;">*</span></label>
|
<label>学期名称 <span style="color:red;">*</span></label>
|
||||||
<input type="text" id="semesterName" required placeholder="如:2025春季学期" maxlength="100">
|
<input type="text" id="semesterName" required placeholder="如:2025春季学期" maxlength="100">
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<button type="button" class="btn btn-sm" style="border: 1px solid #667eea; color: #667eea; margin-right: 6px;" onclick="fillSemesterDates('upper')">上学期(9月-次年2月)</button>
|
||||||
|
<button type="button" class="btn btn-sm" style="border: 1px solid #667eea; color: #667eea;" onclick="fillSemesterDates('lower')">下学期(3月-7月)</button>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>开始日期</label>
|
<label>开始日期</label>
|
||||||
<input type="date" id="semesterStartDate">
|
<input type="date" id="semesterStartDate">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>结束日期</label>
|
<label>结束日期 <small style="color: #999;">(可选)</small></label>
|
||||||
<input type="date" id="semesterEndDate">
|
<input type="date" id="semesterEndDate" placeholder="可选,不确定可不填">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="submit" class="btn btn-primary">创建并激活</button>
|
<button type="submit" class="btn btn-primary">创建学期</button>
|
||||||
<button type="button" class="btn" onclick="closeModal('createSemesterModal')">取消</button>
|
<button type="button" class="btn" onclick="closeModal('createSemesterModal')">取消</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -89,7 +93,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<p id="archiveConfirmText" style="margin: 10px 0;"></p>
|
<p id="archiveConfirmText" style="margin: 10px 0;"></p>
|
||||||
<p style="color: #e74c3c; font-size: 14px;">注意:归档后该学期的操行分记录将不可修改或撤销,但可以查看归档数据。</p>
|
<p style="color: #e74c3c; font-size: 14px;">注意:归档前需确保学期已设置开始日期,否则无法归档。归档后该学期的操行分记录将不可修改或撤销,但可以查看归档数据。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary" onclick="confirmArchive()">确认归档</button>
|
<button type="button" class="btn btn-primary" onclick="confirmArchive()">确认归档</button>
|
||||||
@@ -108,11 +112,23 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" style="text-align:center; border-bottom: none;">基本信息</th>
|
||||||
|
<th rowspan="2" style="vertical-align: middle;">姓名</th>
|
||||||
|
<th rowspan="2" style="vertical-align: middle;">操行分</th>
|
||||||
|
<th colspan="4" style="text-align:center; border-bottom: none;">考勤统计</th>
|
||||||
|
<th colspan="3" style="text-align:center; border-bottom: none;">作业统计</th>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>排名</th>
|
<th>排名</th>
|
||||||
<th>学号</th>
|
<th>学号</th>
|
||||||
<th>姓名</th>
|
<th>出勤</th>
|
||||||
<th>最终操行分</th>
|
<th>缺勤</th>
|
||||||
|
<th>迟到</th>
|
||||||
|
<th>请假</th>
|
||||||
|
<th>已交</th>
|
||||||
|
<th>未交</th>
|
||||||
|
<th>迟交</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="archiveDataList"></tbody>
|
<tbody id="archiveDataList"></tbody>
|
||||||
@@ -130,6 +146,28 @@ var archiveSemesterId = null;
|
|||||||
var archivePage = 1;
|
var archivePage = 1;
|
||||||
var archiveTotalPages = 1;
|
var archiveTotalPages = 1;
|
||||||
|
|
||||||
|
function fillSemesterDates(type) {
|
||||||
|
var now = new Date();
|
||||||
|
var currentYear = now.getFullYear();
|
||||||
|
var currentMonth = now.getMonth() + 1;
|
||||||
|
var startDateInput = document.getElementById('semesterStartDate');
|
||||||
|
var endDateInput = document.getElementById('semesterEndDate');
|
||||||
|
|
||||||
|
if (type === 'upper') {
|
||||||
|
var year = currentMonth >= 6 ? currentYear : currentYear - 1;
|
||||||
|
var endYear = year + 1;
|
||||||
|
var febDay = 28;
|
||||||
|
if ((endYear % 4 === 0 && endYear % 100 !== 0) || endYear % 400 === 0) {
|
||||||
|
febDay = 29;
|
||||||
|
}
|
||||||
|
startDateInput.value = year + '-09-01';
|
||||||
|
endDateInput.value = endYear + '-02-' + febDay;
|
||||||
|
} else if (type === 'lower') {
|
||||||
|
startDateInput.value = currentYear + '-03-01';
|
||||||
|
endDateInput.value = currentYear + '-07-15';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSemesters() {
|
async function loadSemesters() {
|
||||||
const res = await apiGet('/api/semester/list');
|
const res = await apiGet('/api/semester/list');
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
@@ -200,7 +238,7 @@ async function submitCreateSemester() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
showToast('学期创建成功并已激活');
|
showToast(res.message || '学期创建成功');
|
||||||
closeModal('createSemesterModal');
|
closeModal('createSemesterModal');
|
||||||
loadSemesters();
|
loadSemesters();
|
||||||
} else {
|
} else {
|
||||||
@@ -262,10 +300,17 @@ async function viewArchiveData(semesterId, semesterName, page) {
|
|||||||
<td>${escapeHtml(a.student_no)}</td>
|
<td>${escapeHtml(a.student_no)}</td>
|
||||||
<td>${escapeHtml(a.student_name)}</td>
|
<td>${escapeHtml(a.student_name)}</td>
|
||||||
<td>${a.final_points}</td>
|
<td>${a.final_points}</td>
|
||||||
|
<td>${a.attendance_present || 0}</td>
|
||||||
|
<td>${a.attendance_absent || 0}</td>
|
||||||
|
<td>${a.attendance_late || 0}</td>
|
||||||
|
<td>${a.attendance_leave || 0}</td>
|
||||||
|
<td>${a.homework_submitted || 0}</td>
|
||||||
|
<td>${a.homework_not_submitted || 0}</td>
|
||||||
|
<td>${a.homework_late || 0}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (archives.length === 0) {
|
if (archives.length === 0) {
|
||||||
html = '<tr><td colspan="4" style="text-align:center;">暂无归档数据</td></tr>';
|
html = '<tr><td colspan="11" style="text-align:center;">暂无归档数据</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('archiveDataList').innerHTML = html;
|
document.getElementById('archiveDataList').innerHTML = html;
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,23 @@ async function loadSemesterRecords() {
|
|||||||
<div class="semester-stat-label">班级总人数</div>
|
<div class="semester-stat-label">班级总人数</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #eee;">
|
||||||
|
<div style="font-size: 12px; color: #999; margin-bottom: 8px;">考勤统计</div>
|
||||||
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<span style="background: #e8f5e9; color: #388e3c; padding: 2px 8px; border-radius: 10px; font-size: 12px;">出勤 ${record.attendance_present || 0}</span>
|
||||||
|
<span style="background: #ffebee; color: #c62828; padding: 2px 8px; border-radius: 10px; font-size: 12px;">缺勤 ${record.attendance_absent || 0}</span>
|
||||||
|
<span style="background: #fff3e0; color: #e65100; padding: 2px 8px; border-radius: 10px; font-size: 12px;">迟到 ${record.attendance_late || 0}</span>
|
||||||
|
<span style="background: #e3f2fd; color: #1565c0; padding: 2px 8px; border-radius: 10px; font-size: 12px;">请假 ${record.attendance_leave || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<div style="font-size: 12px; color: #999; margin-bottom: 8px;">作业统计</div>
|
||||||
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<span style="background: #e8f5e9; color: #388e3c; padding: 2px 8px; border-radius: 10px; font-size: 12px;">已交 ${record.homework_submitted || 0}</span>
|
||||||
|
<span style="background: #ffebee; color: #c62828; padding: 2px 8px; border-radius: 10px; font-size: 12px;">未交 ${record.homework_not_submitted || 0}</span>
|
||||||
|
<span style="background: #fff3e0; color: #e65100; padding: 2px 8px; border-radius: 10px; font-size: 12px;">迟交 ${record.homework_late || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|||||||
112
sql/init.sql
112
sql/init.sql
@@ -187,6 +187,13 @@ CREATE TABLE IF NOT EXISTS `semester_archives` (
|
|||||||
`final_points` INT NOT NULL COMMENT '学期最终操行分',
|
`final_points` INT NOT NULL COMMENT '学期最终操行分',
|
||||||
`rank_position` INT DEFAULT NULL COMMENT '排名',
|
`rank_position` INT DEFAULT NULL COMMENT '排名',
|
||||||
`total_students` INT DEFAULT NULL COMMENT '班级总人数',
|
`total_students` INT DEFAULT NULL COMMENT '班级总人数',
|
||||||
|
`attendance_present` INT DEFAULT 0 COMMENT '出勤次数',
|
||||||
|
`attendance_absent` INT DEFAULT 0 COMMENT '缺勤次数',
|
||||||
|
`attendance_late` INT DEFAULT 0 COMMENT '迟到次数',
|
||||||
|
`attendance_leave` INT DEFAULT 0 COMMENT '请假次数',
|
||||||
|
`homework_submitted` INT DEFAULT 0 COMMENT '已交作业数',
|
||||||
|
`homework_not_submitted` INT DEFAULT 0 COMMENT '未交作业数',
|
||||||
|
`homework_late` INT DEFAULT 0 COMMENT '迟交作业数',
|
||||||
`archived_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
`archived_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`),
|
FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`),
|
||||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`)
|
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`)
|
||||||
@@ -260,6 +267,111 @@ PREPARE stmt FROM @sql;
|
|||||||
EXECUTE stmt;
|
EXECUTE stmt;
|
||||||
DEALLOCATE PREPARE stmt;
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 迁移:semester_archives 表新增 attendance_present 字段
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||||
|
AND TABLE_NAME = 'semester_archives'
|
||||||
|
AND COLUMN_NAME = 'attendance_present'
|
||||||
|
);
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `semester_archives` ADD COLUMN `attendance_present` INT DEFAULT 0 COMMENT ''出勤次数'' AFTER `total_students`',
|
||||||
|
'SELECT ''semester_archives.attendance_present already exists'' AS message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 迁移:semester_archives 表新增 attendance_absent 字段
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||||
|
AND TABLE_NAME = 'semester_archives'
|
||||||
|
AND COLUMN_NAME = 'attendance_absent'
|
||||||
|
);
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `semester_archives` ADD COLUMN `attendance_absent` INT DEFAULT 0 COMMENT ''缺勤次数'' AFTER `attendance_present`',
|
||||||
|
'SELECT ''semester_archives.attendance_absent already exists'' AS message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 迁移:semester_archives 表新增 attendance_late 字段
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||||
|
AND TABLE_NAME = 'semester_archives'
|
||||||
|
AND COLUMN_NAME = 'attendance_late'
|
||||||
|
);
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `semester_archives` ADD COLUMN `attendance_late` INT DEFAULT 0 COMMENT ''迟到次数'' AFTER `attendance_absent`',
|
||||||
|
'SELECT ''semester_archives.attendance_late already exists'' AS message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 迁移:semester_archives 表新增 attendance_leave 字段
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||||
|
AND TABLE_NAME = 'semester_archives'
|
||||||
|
AND COLUMN_NAME = 'attendance_leave'
|
||||||
|
);
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `semester_archives` ADD COLUMN `attendance_leave` INT DEFAULT 0 COMMENT ''请假次数'' AFTER `attendance_late`',
|
||||||
|
'SELECT ''semester_archives.attendance_leave already exists'' AS message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 迁移:semester_archives 表新增 homework_submitted 字段
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||||
|
AND TABLE_NAME = 'semester_archives'
|
||||||
|
AND COLUMN_NAME = 'homework_submitted'
|
||||||
|
);
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `semester_archives` ADD COLUMN `homework_submitted` INT DEFAULT 0 COMMENT ''已交作业数'' AFTER `attendance_leave`',
|
||||||
|
'SELECT ''semester_archives.homework_submitted already exists'' AS message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 迁移:semester_archives 表新增 homework_not_submitted 字段
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||||
|
AND TABLE_NAME = 'semester_archives'
|
||||||
|
AND COLUMN_NAME = 'homework_not_submitted'
|
||||||
|
);
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `semester_archives` ADD COLUMN `homework_not_submitted` INT DEFAULT 0 COMMENT ''未交作业数'' AFTER `homework_submitted`',
|
||||||
|
'SELECT ''semester_archives.homework_not_submitted already exists'' AS message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 迁移:semester_archives 表新增 homework_late 字段
|
||||||
|
SET @column_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||||
|
AND TABLE_NAME = 'semester_archives'
|
||||||
|
AND COLUMN_NAME = 'homework_late'
|
||||||
|
);
|
||||||
|
SET @sql = IF(@column_exists = 0,
|
||||||
|
'ALTER TABLE `semester_archives` ADD COLUMN `homework_late` INT DEFAULT 0 COMMENT ''迟交作业数'' AFTER `homework_not_submitted`',
|
||||||
|
'SELECT ''semester_archives.homework_late already exists'' AS message'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
-- 插入初始科目(仅语数英,如不存在)
|
-- 插入初始科目(仅语数英,如不存在)
|
||||||
INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VALUES
|
INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VALUES
|
||||||
('语文', 'CHI', 1),
|
('语文', 'CHI', 1),
|
||||||
|
|||||||
Reference in New Issue
Block a user