Skip to content

Commit fbbdd73

Browse files
authored
Merge pull request #4 from mingyush/dev04
v1.2 新增数据一致性修复功能
2 parents 0c4b3f6 + 7c96806 commit fbbdd73

17 files changed

Lines changed: 768 additions & 104 deletions

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# 更新日志 (Changelog)
2+
3+
## 🌟 版本 1.2.0
4+
**日期**: 2026年3月
5+
**亮点**: 数据一致性、性能监控及系统稳定性修复
6+
7+
### ✨ 新增功能 (Features)
8+
1. **数据一致性修复工具**:
9+
- 分离式的修复脚本 `scripts/fix_data_consistency.js` 与内嵌在“系统设置”中的一键修复面板。
10+
- 自动扫描并合并不规则的重复学期记录(解决多线程或由于设备拷贝造成的同步写入导致的重复学期创建)。
11+
- 自动追溯缺失的历史积分变更记录,生成“系统初始化余额归集”记录,将数据库底层分值对齐到 `point_records` 历史流水线上,确保班级大屏不再因跨学期断层显示 0 分。
12+
2. **进程与内存深度监控**:
13+
- 优化 `errorHandler.js` 中的内存监控模块。从单一的 V8 堆内存(`heapUsed`)监控,升级为对 Node.js 的常驻内存集(`rss`)与服务器系统级真实可用内存(`os.freemem()`)的双重监控,彻底消除非真实超载导致的高频系统警告和告警邮件。
14+
15+
### 🐛 Bug 修复 (Bug Fixes)
16+
1. **彻底解决教师面板“最近操作”无数据问题**:
17+
- 修复了因为代码缺失导致的选中学生时没有执行 `loadRecentOperations()` 钩子的问题。在教师后台侧边栏点击学生时,右下方的“操作历史记录”能立刻以极低延迟精准展现。
18+
2. **修复积分加、减算法中的正负倒置错误**:
19+
- 修正了在进行独立积分扣除时底层处理库产生“- - = +”式的负向重叠写入数据库的问题。
20+
3. **彻底修复跨系统的 `MODULE_NOT_FOUND` 大小写崩溃**:
21+
- 修正在 macOS 环境下 `Require()` 大小不敏感但在 NAS(Linux 核心)部署时找不到 `systemService.js` / `SystemService.js` 导致的程序 `status=1/FAILURE` 无限重启崩溃现象,将全系统 import 完全标准化大小写。
22+
4. **修复由瞬时高并发产生的 SQLite 锁库异常 (Database is Locked)**:
23+
- 修改了 `config.js` 等接口多次新建独立 `dataAccess` 子实例导致 SQLite 并行事务处理超载的问题。改为各功能继承并复用同一实例,大大提高了密集数据刷新时的引擎稳定性。
24+
25+
---
26+
27+
## 🔄 版本 1.1.0 (原标为 2026 V2 升级包)
28+
**日期**: 2026年3月早期
29+
**亮点**: 学期隔离和全新管理体系
30+
31+
1. **学期积分隔离系统 (Semester Isolation)**:
32+
- 彻底解决了跨学期“积分清零”污染流水线的问题。
33+
- 所有积分记录严格绑定 `semester_id`。系统切换学期时执行底层静默抹零,不产生额外扣分账单。
34+
- 日榜、周榜排除了系统操作行为的影响,大屏展示永远只显示当下的真实奋斗轨迹。
35+
2. **内嵌超级管理员体系**:
36+
- `admin` 账号成为系统底层保护机制的一部分(不可见,无法被普通教师账号删除、编辑)。
37+
- 提供给班主任或系统环境部署者唯一的最高控制权,保护系统根源数据不被下发平展级教师误篡改。
38+
3. **数据库重建与测试隔离**:
39+
- 加入了全新的基于原始JSON模版的数据库重置引擎。支持一键初始化跨学期的基础数据架构,无损保留基础用户结构。
40+
4. **前端UI体系优化**:
41+
- 修正了顶部菜单遮挡屏幕区域、教师与学生状态栏因内容产生悬浮跳动的大量交互级 Bug。

README.md

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
77
## 🎯 功能特点
88

9-
- **🖥️ 大屏展示**: 实时显示学生积分排行榜,支持教室投影
10-
- **👨‍🏫 教师管理**: 简单直观的积分加减操作界面
11-
- **👨‍🎓 学生查询**: 个人积分查看和奖品预约功能
12-
- **💾 数据持久化**: 基于JSON文件的可靠数据存储
13-
- **⚡ 实时更新**: 使用Server-Sent Events实现实时数据推送
14-
- **🔐 权限控制**: 教师和学生分离的权限管理
15-
- **📊 数据统计**: 总积分、日积分、周积分多维度排行
16-
- **🎁 奖品系统**: 完整的商品预约和兑换流程
9+
- **🖥️ 大屏展示**: 实时显示学生积分排行榜,支持教室平时/上课双模式投影
10+
- **👨‍🏫 教师管理**: 简单直观的积分加减操作界面,支持多维度的数据追踪
11+
- **👨‍🎓 学生查询**: 个人积分查看和奖品在线预约功能
12+
- **💾 数据库引擎**: 基于高性能 SQLite 3 引擎的数据存储,保障并发读写安全
13+
- **⚡ 实时更新**: 使用 Server-Sent Events 实现大屏与前端数据的无刷新推送
14+
- **🔐 权限控制**: 教师、学生、系统管理员(Admin)、班主任(导演角色)四级权限分离
15+
- **📊 数据统计**: 总积分、日榜、周榜多维度排行,且按学期严格隔离
16+
- **🎁 奖品系统**: 完整的商品预留库存、预约和兑换审核流程
1717

1818
## 📸 系统界面展示
1919

@@ -125,27 +125,20 @@
125125
## 🛠️ 技术栈
126126

127127
- **后端**: Node.js + Express.js
128-
- **前端**: HTML5 + CSS3 + 原生JavaScript
129-
- **数据存储**: JSON文件系统
128+
- **前端**: HTML5 + CSS3 + 原生JavaScript (零构建依赖)
129+
- **数据库引擎**: SQLite 3 (node-sqlite3)
130130
- **实时通信**: Server-Sent Events (SSE)
131-
- **认证**: JWT Token
132-
- **测试**: Jest + Supertest
133-
134-
## 🔄 核心功能升级 (2026 V2)
135-
136-
在最初期版本的基础上,系统进行了深度重构与功能升级(V2版):
137-
138-
1. **学期积分隔离隔离 (Semester Isolation)**:
139-
- 彻底解决了跨学期“积分清零”污染流水线的问题。
140-
- 所有积分记录严格绑定 `semester_id`。系统切换学期时执行底层静默抹零,不产生额外扣分账单。
141-
- 日榜、周榜排除了系统操作行为的影响,大屏展示永远只显示当下的真实奋斗轨迹。
142-
2. **内嵌超级管理员体系**:
143-
- `admin` 账号成为系统底层保护机制的一部分(无法被普通教师账号删除、编辑)。
144-
- 提供给班主任/系统管理者唯一的最高控制权,确保数据源的稳定性。
145-
3. **数据库重建与测试隔离**:
146-
- 加入了全新的基于原始JSON模版的数据库重置引擎。支持一键初始化跨学期的基础数据架构。
147-
4. **前端UI体系优化**:
148-
- 修正了顶部菜单遮挡、状态栏悬浮跳动等大量页面交互 Bug。
131+
- **认证安全**: JWT Token (带有强保护与过期策略)
132+
- **测试框架**: Jest + Supertest
133+
134+
## 🔄 版本更新说明
135+
136+
本系统持续在快速迭代并修复生产环境中遇到的各种细节问题。有关最新版本的功能扩展、修复清单与底层结构修改细节,请参阅:
137+
138+
👉 **[查看完整更新日志 (CHANGELOG) ](CHANGELOG.md)**
139+
140+
- **1.2.0**:主要新增数据一致性智能修复、真实服务器级系统资源告警隔离、修复基于大小写引发的群晖NAS模块加载崩溃、修复“最近操作”列表缺失。
141+
- **1.1.0**:增加“学期积分完全隔离系统”、超管降级防删体系、修正部分导致系统交互跳动的 UI 层 Bug。
149142

150143
## 升级与交叉部署指南 (含 NAS 部署)
151144

api/config.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const DataAccess = require('../utils/dataAccess');
33
const sseService = require('../services/sseService');
44
const { authenticateToken, requireTeacher } = require('./auth');
55
const { asyncHandler, createError, operationLogger } = require('../middleware/errorHandler');
6+
const SystemService = require('../services/systemService');
67
const router = express.Router();
78

89
const dataAccess = new DataAccess();
@@ -256,4 +257,34 @@ router.post('/reset-points/toggle', authenticateToken, requireTeacher,
256257
})
257258
);
258259

260+
/**
261+
* 修复数据一致性
262+
* POST /api/config/fix-data
263+
* 需要教师权限 (admin/director)
264+
*/
265+
router.post('/fix-data', authenticateToken, requireTeacher,
266+
operationLogger('修复数据一致性'),
267+
asyncHandler(async (req, res) => {
268+
try {
269+
// 【修复】3. 复用模块全局的 dataAccess 实例进行数据连接,而不新开隐含 SQLite 连接
270+
const systemService = new SystemService(dataAccess);
271+
272+
const results = await systemService.repairDataConsistency();
273+
274+
res.json({
275+
success: true,
276+
message: '数据一致性修复完成',
277+
data: results
278+
});
279+
} catch (error) {
280+
console.error('[FixData API] Error:', error);
281+
res.status(500).json({
282+
success: false,
283+
message: '数据一致性修复失败: ' + error.message,
284+
stack: error.stack
285+
});
286+
}
287+
})
288+
);
289+
259290
module.exports = router;

api/sse.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,16 @@ router.post('/test', authenticateToken, requireTeacher, (req, res) => {
166166
module.exports = {
167167
router,
168168
broadcastSSEMessage,
169-
getConnectionCount: () => sseConnections.size
169+
getConnectionCount: () => sseConnections.size,
170+
closeAllConnections: () => {
171+
if (sseConnections.size > 0) {
172+
console.log(`正在断开 ${sseConnections.size} 个存活的 SSE 连接...`);
173+
sseConnections.forEach(conn => {
174+
if (conn.response && !conn.response.writableEnded) {
175+
conn.response.end();
176+
}
177+
});
178+
sseConnections.clear();
179+
}
180+
}
170181
};

middleware/errorHandler.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const fs = require('fs').promises;
22
const path = require('path');
3+
const os = require('os');
34

45
/**
56
* 错误类型定义
@@ -586,18 +587,35 @@ class ErrorMonitor {
586587
*/
587588
async checkSystemResources(alerts) {
588589
try {
589-
const memoryUsage = process.memoryUsage();
590-
const memoryUsagePercent = memoryUsage.heapUsed / memoryUsage.heapTotal;
590+
const totalMem = os.totalmem();
591+
const freeMem = os.freemem();
592+
const usedMem = totalMem - freeMem;
593+
const memoryUsagePercent = usedMem / totalMem;
594+
const memoryUsage = process.memoryUsage(); // 保留用于记录进程数据
591595

592-
// 检查内存使用率
596+
// 检查服务器物理内存使用率
593597
if (memoryUsagePercent > 0.9) { // 90%内存使用率
594598
const alertKey = 'HIGH_MEMORY_USAGE';
595599
if (this.shouldSendAlert(alertKey)) {
596600
alerts.push({
597601
type: alertKey,
598-
message: `内存使用率${(memoryUsagePercent * 100).toFixed(2)}%过高`,
602+
message: `服务器物理内存使用率${(memoryUsagePercent * 100).toFixed(2)}%过高`,
603+
severity: 'WARNING',
604+
data: { systemUsage: memoryUsagePercent, processMemory: memoryUsage }
605+
});
606+
}
607+
}
608+
609+
// 也检查一下 Node 进程本身是否占用过高 (超过 500MB rss)
610+
const processMemUsageMB = memoryUsage.rss / (1024 * 1024);
611+
if (processMemUsageMB > 500) {
612+
const alertKey = 'HIGH_PROCESS_MEMORY';
613+
if (this.shouldSendAlert(alertKey)) {
614+
alerts.push({
615+
type: alertKey,
616+
message: `Node进程内存占用过高: ${processMemUsageMB.toFixed(2)}MB`,
599617
severity: 'WARNING',
600-
data: { memoryUsage, memoryUsagePercent }
618+
data: { processMemory: memoryUsage }
601619
});
602620
}
603621
}
@@ -734,13 +752,17 @@ class ErrorMonitor {
734752

735753
const errorRate = statistics.totalErrors / Math.max(1, statistics.totalErrors + 100); // 假设正常请求数
736754
const memoryUsage = process.memoryUsage();
737-
const memoryUsagePercent = memoryUsage.heapUsed / memoryUsage.heapTotal;
755+
756+
// 将健康检查修改为基于系统的实际内存分配或者 RSS
757+
const totalMem = os.totalmem();
758+
const freeMem = os.freemem();
759+
const memoryUsagePercent = (totalMem - freeMem) / totalMem;
738760

739761
// 综合评估健康状态
740762
const healthChecks = {
741763
errorRate: errorRate <= this.errorThresholds.errorRate,
742764
errorCount: statistics.totalErrors <= this.errorThresholds.errorCount,
743-
memoryUsage: memoryUsagePercent <= 0.9,
765+
memoryUsage: memoryUsagePercent <= 0.9 && (memoryUsage.rss / (1024 * 1024)) < 500, // 系统不足90% 或 进程不足500MB
744766
uptime: process.uptime() < 7 * 24 * 60 * 60 // 7天
745767
};
746768

models/dataModels.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ class Order {
207207
constructor(data = {}) {
208208
this.id = data.id || this.generateId();
209209
this.studentId = data.studentId || '';
210+
this.semesterId = data.semesterId || null;
210211
this.productId = data.productId || '';
211212
this.status = data.status || 'pending'; // 'pending', 'confirmed', 'cancelled'
212213
this.reservedAt = data.reservedAt || new Date().toISOString();
@@ -256,6 +257,7 @@ class Order {
256257
return {
257258
id: this.id,
258259
studentId: this.studentId,
260+
semesterId: this.semesterId,
259261
productId: this.productId,
260262
status: this.status,
261263
reservedAt: this.reservedAt,

public/css/teacher.css

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,42 @@
394394
.operations-list {
395395
max-height: 200px;
396396
overflow-y: auto;
397+
display: flex;
398+
flex-direction: column;
399+
gap: 10px;
400+
}
401+
402+
.operation-item {
403+
background: #f1f3f5;
404+
padding: 10px;
405+
border-radius: 6px;
406+
font-size: 0.9em;
407+
}
408+
409+
.operation-header {
410+
display: flex;
411+
justify-content: space-between;
412+
margin-bottom: 5px;
413+
}
414+
415+
.operation-reason {
416+
font-weight: bold;
417+
}
418+
419+
.operation-points.add {
420+
color: #28a745;
421+
}
422+
423+
.operation-points.subtract,
424+
.operation-points.purchase {
425+
color: #dc3545;
426+
}
427+
428+
.operation-footer {
429+
display: flex;
430+
justify-content: space-between;
431+
color: #6c757d;
432+
font-size: 0.85em;
397433
}
398434

399435
/* 预约管理样式 */
@@ -613,6 +649,16 @@
613649
color: white;
614650
}
615651

652+
.backup-manager-btn {
653+
background: #6f42c1;
654+
color: white;
655+
}
656+
657+
.repair-btn {
658+
background: #fd7e14;
659+
color: white;
660+
}
661+
616662
.reset-btn.danger {
617663
background: #dc3545;
618664
color: white;
@@ -1419,10 +1465,11 @@
14191465
width: 100%;
14201466
border-collapse: collapse;
14211467
background: white;
1422-
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1468+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
14231469
}
14241470

1425-
.data-table th, .data-table td {
1471+
.data-table th,
1472+
.data-table td {
14261473
padding: 12px 15px;
14271474
text-align: left;
14281475
border-bottom: 1px solid #ddd;

public/js/display.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,8 @@ async function adjustPoints(studentId, points) {
453453

454454
} catch (error) {
455455
console.error('积分操作失败:', error);
456-
showMessage('积分操作失败,请重试', 'error');
456+
// 使用服务器返回的具体错误信息,如果没有则使用默认提示
457+
showMessage(error.message || '积分操作失败,请重试', 'error');
457458
} finally {
458459
// 重新启用按钮
459460
setTimeout(() => {

0 commit comments

Comments
 (0)