feat: 多班级版班级管理系统 v2.0

技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis

主要功能:
- 多班级完全隔离(class_id 贯穿全系统)
- 后端从 Python FastAPI 重写为 Go Gin(端口 56789)
- 超级管理员独立登录(env 配置路径,默认账密 admin/Admin123)
- 科任老师/课代表新角色
- 课代表作业管理页面
- 排行榜分项排行(操行分/考勤/作业)
- 角色加减分上下限由班主任配置
- 家长改密功能(可开关)
- 班级角色按需开关
- 宿舍号格式:南0-000
- 周度/月度重置功能
- MySQL 5.7 兼容
- Nginx 反向代理部署

开发者: Canglan
版权归属: Sea Network Technology Studio
许可证: Apache License 2.0
This commit is contained in:
2026-06-22 10:21:52 +08:00
commit 124d7f645e
140 changed files with 21103 additions and 0 deletions

View File

@@ -0,0 +1,439 @@
<?php
/**
* 多班级版班级管理系统 - 班级设置页面
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
$page_title = '班级设置';
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id'])) {
header('Location: /index.php');
exit();
}
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '系统管理员'])) {
header('Location: /admin/dashboard.php');
exit();
}
require_once __DIR__ . '/../includes/header.php';
require_once __DIR__ . '/../includes/nav.php';
?>
<div class="container">
<div class="page-header">
<h2>班级设置</h2>
<p class="text-muted">修改当前班级的扣分规则、加减分限制和功能开关(仅班主任可修改)</p>
</div>
<!-- 扣分规则 -->
<div class="card">
<h3>扣分规则</h3>
<div id="deductionRules">
<div class="form-group">
<label>学生初始操行分</label>
<input type="number" id="setting_student_initial_points" value="60" min="0" max="200">
</div>
<div class="form-group">
<label>作业未提交扣分</label>
<input type="number" id="setting_deduction_homework_not_submit" value="2" min="0" max="20">
</div>
<div class="form-group">
<label>作业迟交扣分</label>
<input type="number" id="setting_deduction_homework_late" value="1" min="0" max="20">
</div>
<div class="form-group">
<label>缺勤扣分</label>
<input type="number" id="setting_deduction_attendance_absent" value="3" min="0" max="20">
</div>
<div class="form-group">
<label>迟到扣分</label>
<input type="number" id="setting_deduction_attendance_late" value="1" min="0" max="20">
</div>
<div class="form-group">
<label>请假扣分0=不扣分)</label>
<input type="number" id="setting_deduction_attendance_leave" value="0" min="0" max="20">
</div>
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
</div>
</div>
<!-- 角色加减分上下限 -->
<div class="card">
<h3>角色加减分限制</h3>
<p class="text-muted" style="margin-bottom:15px;">配置各角色单次加减分的上下限</p>
<div id="pointLimits">
<div class="settings-grid">
<div class="form-group">
<label>班长单次加分上限</label>
<input type="number" id="limit_monitor_max_add" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>班长单次扣分下限</label>
<input type="number" id="limit_monitor_max_subtract" value="-5" min="-100" max="0">
</div>
<div class="form-group">
<label>学习委员加分上限</label>
<input type="number" id="limit_study_comm_max_points" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>学习委员扣分下限</label>
<input type="number" id="limit_study_comm_min_points" value="-5" min="-100" max="0">
</div>
<div class="form-group">
<label>考勤委员扣分上限</label>
<input type="number" id="limit_attendance_rep_max_points" value="8" min="0" max="100">
</div>
<div class="form-group">
<label>考勤委员扣分下限</label>
<input type="number" id="limit_attendance_rep_min_points" value="-8" min="-100" max="0">
</div>
<div class="form-group">
<label>劳动委员加分上限</label>
<input type="number" id="limit_labor_rep_max_points" value="1" min="0" max="100">
</div>
<div class="form-group">
<label>劳动委员扣分下限</label>
<input type="number" id="limit_labor_rep_min_points" value="-1" min="-100" max="0">
</div>
<div class="form-group">
<label>志愿委员加分上限</label>
<input type="number" id="limit_volunteer_rep_max_points" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>志愿委员扣分下限</label>
<input type="number" id="limit_volunteer_rep_min_points" value="-5" min="-100" max="0">
</div>
<div class="form-group">
<label>科任老师加分上限</label>
<input type="number" id="limit_subject_teacher_max_points" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>科任老师扣分下限</label>
<input type="number" id="limit_subject_teacher_min_points" value="-5" min="-100" max="0">
</div>
<button class="btn btn-primary" onclick="savePointLimits()">保存加减分限制</button>
</div>
</div>
<!-- 周期重置 -->
<div class="card">
<h3>周期重置</h3>
<p class="text-muted" style="margin-bottom:15px;">按周或按月自动重置学生操行分(需配合定时任务或手动触发)</p>
<div id="periodResetSettings">
<div class="form-group">
<label>重置频率</label>
<select id="setting_reset_frequency" onchange="toggleResetDay()">
<option value="none">不重置(仅学期结算)</option>
<option value="weekly">每周重置</option>
<option value="monthly">每月重置</option>
</select>
</div>
<div class="form-group" id="reset_day_of_week_group" style="display:none;">
<label>每周重置日</label>
<select id="setting_reset_day_of_week">
<option value="1">周一</option>
<option value="2">周二</option>
<option value="3">周三</option>
<option value="4">周四</option>
<option value="5">周五</option>
<option value="6">周六</option>
<option value="7">周日</option>
</select>
</div>
<div class="form-group" id="reset_day_of_month_group" style="display:none;">
<label>每月重置日</label>
<input type="number" id="setting_reset_day_of_month" value="1" min="1" max="28">
<small style="color:#999;">1~28日建议避免月末最后几天</small>
</div>
<button class="btn btn-primary" onclick="savePeriodResetSettings()">保存周期重置设置</button>
</div>
</div>
<!-- 角色开关 -->
<div class="card">
<h3>功能开关</h3>
<p class="text-muted" style="margin-bottom:15px;">控制各角色的功能启用状态</p>
<div id="roleToggles">
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_account_enabled">
<span>家长账号启用</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_password_change_enabled">
<span>家长改密启用</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_view_attendance">
<span>家长查看考勤</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_view_ranking">
<span>家长查看排名</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_student_view_ranking">
<span>学生查看排行榜</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_homework_management">
<span>作业管理模块</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_attendance_management">
<span>考勤管理模块</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_cadre_homework">
<span>课代表作业管理</span>
</label>
</div>
<button class="btn btn-primary" onclick="saveRoleToggles()">保存功能开关</button>
</div>
</div>
</div>
<style>
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.toggle-group {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 15px;
}
.toggle-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
}
.toggle-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: #fff;
cursor: pointer;
}
</style>
<script>
// limitFieldMap: 前端 input ID 后缀 → 后端 class_settings 表 key
var limitFieldMap = {
'monitor_max_add': 'point_limit_班长_max',
'monitor_max_subtract': 'point_limit_班长_min',
'study_comm_max_points': 'point_limit_学习委员_max',
'study_comm_min_points': 'point_limit_学习委员_min',
'attendance_rep_max_points': 'point_limit_考勤委员_max',
'attendance_rep_min_points': 'point_limit_考勤委员_min',
'labor_rep_max_points': 'point_limit_劳动委员_max',
'labor_rep_min_points': 'point_limit_劳动委员_min',
'volunteer_rep_max_points': 'point_limit_志愿委员_max',
'volunteer_rep_min_points': 'point_limit_志愿委员_min',
'subject_teacher_max_points': 'point_limit_科任老师_max',
'subject_teacher_min_points': 'point_limit_科任老师_min'
};
var limitFields = Object.keys(limitFieldMap);
var toggleFields = [
'parent_account_enabled', 'parent_password_change_enabled', 'parent_view_attendance',
'parent_view_ranking', 'student_view_ranking', 'homework_management',
'attendance_management', 'cadre_homework'
];
async function loadSettings() {
var params = {};
if (window.CLASS_ID) {
params.class_id = window.CLASS_ID;
}
var result = await apiGet('/api/config/deduction-rules', params);
if (result && result.success && result.data) {
var d = result.data;
document.getElementById('setting_student_initial_points').value = d.STUDENT_INITIAL_POINTS || 60;
document.getElementById('setting_deduction_homework_not_submit').value = d.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
document.getElementById('setting_deduction_homework_late').value = d.DEDUCTION_HOMEWORK_LATE || 1;
document.getElementById('setting_deduction_attendance_absent').value = d.DEDUCTION_ATTENDANCE_ABSENT || 3;
document.getElementById('setting_deduction_attendance_late').value = d.DEDUCTION_ATTENDANCE_LATE || 1;
document.getElementById('setting_deduction_attendance_leave').value = d.DEDUCTION_ATTENDANCE_LEAVE || 0;
}
}
async function loadPointLimits() {
var result = await apiGet('/api/class/point-limits');
if (result && result.success && result.data) {
var d = result.data.settings || result.data;
limitFields.forEach(function(key) {
var el = document.getElementById('limit_' + key);
var backendKey = limitFieldMap[key];
if (el) {
// 优先读取后端 point_limit_* key兼容旧 key
if (backendKey && d[backendKey] !== undefined) {
el.value = d[backendKey];
} else if (d[key] !== undefined) {
el.value = d[key];
}
}
});
}
}
async function loadRoleToggles() {
var result = await apiGet('/api/class/features');
if (result && result.success && result.data) {
var d = result.data.features || result.data;
toggleFields.forEach(function(key) {
var el = document.getElementById('toggle_' + key);
if (el) {
el.checked = d[key] === true || d[key] === 1 || d[key] === '1';
}
});
}
}
async function saveSettings() {
var settings = [
{ key: 'initial_points', value: document.getElementById('setting_student_initial_points').value },
{ key: 'deduction_homework_not_submit', value: document.getElementById('setting_deduction_homework_not_submit').value },
{ key: 'deduction_homework_late', value: document.getElementById('setting_deduction_homework_late').value },
{ key: 'deduction_attendance_absent', value: document.getElementById('setting_deduction_attendance_absent').value },
{ key: 'deduction_attendance_late', value: document.getElementById('setting_deduction_attendance_late').value },
{ key: 'deduction_attendance_leave', value: document.getElementById('setting_deduction_attendance_leave').value },
];
var success = true;
for (var i = 0; i < settings.length; i++) {
var s = settings[i];
var result = await apiPost('/api/class/settings', { setting_key: s.key, setting_value: s.value });
if (!result || !result.success) {
success = false;
}
}
if (success) {
showToast('班级设置已保存');
} else {
showToast('部分设置保存失败', 'error');
}
}
async function savePointLimits() {
var data = {};
limitFields.forEach(function(key) {
var el = document.getElementById('limit_' + key);
if (el) {
// 使用后端 point_limit_* key 保存,确保 conduct_service 能正确读取
var backendKey = limitFieldMap[key] || key;
data[backendKey] = el.value;
}
});
var result = await apiPost('/api/class/point-limits', data);
if (result && result.success) {
showToast('加减分限制已保存');
} else {
showToast(result && result.message ? result.message : '保存失败', 'error');
}
}
async function saveRoleToggles() {
var success = true;
for (var i = 0; i < toggleFields.length; i++) {
var key = toggleFields[i];
var el = document.getElementById('toggle_' + key);
if (el) {
var result = await apiPost('/api/class/features', {
feature_key: key,
enabled: el.checked ? 1 : 0
});
if (!result || !result.success) {
success = false;
}
}
}
if (success) {
showToast('功能开关已保存');
} else {
showToast('部分开关保存失败', 'error');
}
}
// ========== 周期重置设置 ==========
async function loadPeriodResetSettings() {
var result = await apiGet('/api/class/settings');
if (result && result.success && result.data && result.data.settings) {
var s = result.data.settings;
var freqSelect = document.getElementById('setting_reset_frequency');
freqSelect.value = s['reset_frequency'] || 'none';
toggleResetDay();
if (s['reset_day_of_week']) {
document.getElementById('setting_reset_day_of_week').value = s['reset_day_of_week'];
}
if (s['reset_day_of_month']) {
document.getElementById('setting_reset_day_of_month').value = s['reset_day_of_month'];
}
}
}
function toggleResetDay() {
var freq = document.getElementById('setting_reset_frequency').value;
document.getElementById('reset_day_of_week_group').style.display = (freq === 'weekly') ? 'block' : 'none';
document.getElementById('reset_day_of_month_group').style.display = (freq === 'monthly') ? 'block' : 'none';
}
async function savePeriodResetSettings() {
var freq = document.getElementById('setting_reset_frequency').value;
var settings = [
{ key: 'reset_frequency', value: freq }
];
if (freq === 'weekly') {
settings.push({ key: 'reset_day_of_week', value: document.getElementById('setting_reset_day_of_week').value });
} else if (freq === 'monthly') {
settings.push({ key: 'reset_day_of_month', value: document.getElementById('setting_reset_day_of_month').value });
}
var success = true;
for (var i = 0; i < settings.length; i++) {
var result = await apiPost('/api/class/settings', { setting_key: settings[i].key, setting_value: settings[i].value });
if (!result || !result.success) {
success = false;
}
}
if (success) {
showToast('周期重置设置已保存');
} else {
showToast('部分设置保存失败', 'error');
}
}
document.addEventListener('DOMContentLoaded', function() {
loadSettings();
loadPointLimits();
loadRoleToggles();
loadPeriodResetSettings();
});
</script>
<?php require_once __DIR__ . '/../includes/footer.php'; ?>