在中小企业日常运营中,考勤管理和午餐统计看似简单,实则暗藏诸多痛点。作为经历过多次考勤系统迭代的开发者,我发现市面上现成解决方案往往存在两个极端:要么是功能臃肿的SaaS平台,包含大量用不上的模块;要么是过于简陋的Excel模板,难以应对多人协作场景。
这个项目的诞生源于一家50人规模科技公司的实际需求。他们原有的纸质签到+微信群接龙订餐方式存在三大问题:
经过需求梳理,我们确定了系统必须具备的五个核心能力:
提示:在中小型系统设计中,建议先明确"必须解决的核心痛点",避免陷入功能蔓延的陷阱。我们砍掉了指纹识别、移动端APP等非必要需求,专注做好基础体验。
面对这个项目,技术选型主要考虑三个维度:
经过对比测试,最终技术栈如下:
| 技术组件 | 选型理由 | 替代方案对比 |
|---|---|---|
| Node.js | 事件驱动适合IO密集型应用,npm生态丰富 | PHP配置复杂,Java太重 |
| SQLite | 单文件数据库,备份只需拷贝一个文件 | MySQL需要单独服务,MongoDB事务支持弱 |
| Express | 轻量灵活,中间件机制完善 | Koa生态插件较少,NestJS过度设计 |
| Vanilla JS | 无需构建工具,调试直观 | Vue/React会增加学习成本 |
初始设计的三个版本充分体现了业务理解的深入过程:
V1.0 简单结构
sql复制CREATE TABLE attendance (
id INTEGER PRIMARY KEY,
user_id INTEGER,
date TEXT,
clock_in TEXT,
clock_out TEXT,
meal_flag INTEGER
);
V2.0 引入客饭分离
sql复制-- 新增独立客饭表
CREATE TABLE guest_meals (
id INTEGER PRIMARY KEY,
date TEXT,
count INTEGER
);
V3.0 最终方案
sql复制-- 采用星型模型设计
CREATE TABLE daily_summary (
date TEXT PRIMARY KEY,
total_meals INTEGER,
guest_meals INTEGER
);
关键改进点:
前端采用"乐观锁定"策略防止重复提交:
javascript复制// 前端防抖逻辑
let lastSubmitTime = 0;
function handleClock() {
const now = Date.now();
if (now - lastSubmitTime < 5000) {
alert('操作过于频繁');
return;
}
lastSubmitTime = now;
// 提交逻辑...
}
后端使用SQLite的原子操作保证数据准确:
sql复制-- 使用事务确保完整性
BEGIN TRANSACTION;
INSERT INTO attendance(user_id, date, clock_in)
VALUES(123, '2023-06-01', '09:00')
ON CONFLICT(user_id, date) DO UPDATE SET clock_in = excluded.clock_in;
COMMIT;
浏览器时区问题是个经典坑点,我们的解决方案是:
javascript复制// 强制使用东八区时间
process.env.TZ = 'Asia/Shanghai';
javascript复制// 使用date-fns替代原生Date对象
import { format, addDays } from 'date-fns';
function generateWeekDates(baseDate) {
return Array.from({ length: 7 }).map((_, i) => {
const date = addDays(baseDate, i);
return {
dateObj: date,
dateStr: format(date, 'yyyy-MM-dd'),
dayOfWeek: format(date, 'EEEE')
};
});
}
javascript复制// 合并系统周末和自定义节假日
function getDayStyle(date, holidays) {
const isWeekend = [0, 6].includes(date.getDay());
const isHoliday = holidays.includes(format(date, 'yyyy-MM-dd'));
return isWeekend || isHoliday ? 'holiday-style' : '';
}
使用流式处理避免内存溢出:
javascript复制const xlsx = require('node-xlsx');
const fs = require('fs');
function exportReport(startDate, endDate) {
// 分页查询数据
const summaryData = getSummaryData(startDate, endDate);
const detailData = getDetailData(startDate, endDate);
// 构建Excel缓冲区
const buffer = xlsx.build([
{ name: '汇总', data: summaryData },
{ name: '明细', data: detailData }
]);
// 流式写入文件
const stream = fs.createWriteStream('report.xlsx');
stream.write(buffer);
stream.end();
}
实测数据量达到10万条记录时,内存占用仍能保持在50MB以下。
编写自动化安装脚本deploy.sh:
bash复制#!/bin/bash
# 安装Node.js
if ! command -v node &> /dev/null; then
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs
fi
# 初始化项目
npm install
sqlite3 attendance.db < schema.sql
# 配置系统服务
cat > /etc/systemd/system/attendance.service <<EOF
[Unit]
Description=Attendance System
After=network.target
[Service]
ExecStart=/usr/bin/node /opt/attendance/server.js
Restart=always
User=node
Environment=NODE_ENV=production
WorkingDirectory=/opt/attendance
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable attendance
systemctl start attendance
采用双保险机制:
sql复制-- 使用SQLite自带备份命令
.backup /backup/attendance_$(date +%Y%m%d).db
javascript复制// 使用rsync同步到NAS
const { exec } = require('child_process');
exec(`rsync -avz /backup/ user@nas:/company_backup/attendance/`);
在压力测试时发现周视图加载缓慢,通过以下步骤定位问题:
EXPLAIN QUERY PLAN分析SQL:sql复制EXPLAIN QUERY PLAN
SELECT * FROM attendance WHERE date BETWEEN ? AND ?;
sql复制CREATE INDEX idx_attendance_date ON attendance(date);
针对高频访问的节假日数据,采用两级缓存:
javascript复制const holidayCache = {
memory: new Map(),
async get(year) {
if (this.memory.has(year)) {
return this.memory.get(year);
}
const data = await db.query(
'SELECT date FROM holidays WHERE strftime("%Y", date) = ?',
[year]
);
this.memory.set(year, data);
return data;
}
};
实现基础安全防护:
javascript复制// 使用参数化查询
db.run('INSERT INTO users(name) VALUES(?)', [name]);
javascript复制// 前端转义输出
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
javascript复制// 生成随机Token
app.use((req, res, next) => {
res.locals.csrfToken = crypto.randomBytes(16).toString('hex');
next();
});
系统预留了三个扩展接口:
javascript复制// 考勤异常通知
function checkAbnormal(record) {
if (record.clock_in > '09:30') {
triggerWebhook('late', record);
}
}
javascript复制// 动态加载模块
function loadPlugin(name) {
const plugin = require(`./plugins/${name}`);
app.use(plugin.routes());
}
javascript复制// 支持Excel导入
app.post('/api/import', upload.single('file'), (req, res) => {
const data = xlsx.parse(req.file.buffer);
processImportData(data);
});
这套系统最终在客户环境稳定运行了两年多,期间仅需偶尔维护。它的成功验证了一个道理:针对特定场景的精简设计,往往比大而全的系统更实用。对于开发者而言,深入理解业务需求比追求技术新颖性更重要。