1. 项目背景与核心需求
去年接手公司行政部门的数字化改造需求时,发现一个有趣的现象:每天中午11点,行政同事都要拿着纸质表格在办公区来回穿梭,统计近百号人的午餐订餐需求。这种传统方式不仅效率低下,还经常出现漏记、错记的情况。更麻烦的是,月底核对考勤和餐费时,行政和财务部门往往要花上两三天时间手工对账。
这个看似简单的"订饭+考勤"需求,实际上涉及三个核心痛点:
- 订餐统计效率低(人工登记耗时易错)
- 考勤数据与订餐记录割裂(无法自动关联)
- 缺乏可视化报表(财务核算困难)
1.1 技术选型考量
面对这个需求,我首先排除了以下方案:
- 直接采购商业系统(成本高、功能冗余)
- 使用Excel+邮件(无法解决数据关联问题)
- 基于现有OA系统二次开发(接口复杂、周期长)
最终选择Node.js + SQLite的技术栈,主要基于以下判断:
- 开发效率:Node.js的异步特性适合处理高并发的订餐请求
- 部署成本:SQLite无需单独部署数据库服务
- 维护难度:全JavaScript技术栈降低维护门槛
- 扩展空间:后期可轻松对接企业微信/钉钉
关键决策点:系统日均访问量预估<200次,SQLite完全能满足性能需求,且避免了MySQL等数据库的运维负担
2. 系统架构设计
2.1 技术架构图解
(注:实际开发时在白板手绘的架构草图)
code复制[浏览器] ←HTTP→ [Express服务器]
↓
[SQLite]
↑
[考勤模块] [订餐模块]
2.2 数据库设计精要
考虑到数据关系简单但查询频繁的特点,设计了4张核心表:
employees表
sql复制CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
dept TEXT,
wechat_id TEXT UNIQUE
);
meals表
sql复制CREATE TABLE meals (
id INTEGER PRIMARY KEY,
date TEXT NOT NULL,
option_a TEXT NOT NULL,
option_b TEXT NOT NULL,
deadline TEXT NOT NULL
);
orders表
sql复制CREATE TABLE orders (
id INTEGER PRIMARY KEY,
employee_id INTEGER,
meal_id INTEGER,
choice TEXT CHECK(choice IN ('A','B')),
FOREIGN KEY(employee_id) REFERENCES employees(id),
FOREIGN KEY(meal_id) REFERENCES meals(id)
);
attendance表
sql复制CREATE TABLE attendance (
id INTEGER PRIMARY KEY,
employee_id INTEGER,
date TEXT NOT NULL,
status TEXT CHECK(status IN ('present','absent','late')),
FOREIGN KEY(employee_id) REFERENCES employees(id)
);
设计亮点:使用SQLite的CHECK约束保证数据有效性,通过外键关联实现数据一致性
3. 核心功能实现
3.1 订餐模块关键技术点
防重复提交机制
javascript复制router.post('/order', async (req, res) => {
const { wechatId, mealId, choice } = req.body;
// 检查是否已订餐
const existing = await db.get(
`SELECT 1 FROM orders
JOIN employees ON employees.id = orders.employee_id
WHERE employees.wechat_id = ? AND orders.meal_id = ?`,
[wechatId, mealId]
);
if (existing) {
return res.status(400).json({ error: '已订餐,请勿重复提交' });
}
// 写入数据库
await db.run(`INSERT INTO orders (employee_id, meal_id, choice)
VALUES ((SELECT id FROM employees WHERE wechat_id = ?), ?, ?)`,
[wechatId, mealId, choice]);
res.json({ success: true });
});
订餐截止时间控制
javascript复制// 在订餐接口前添加中间件
router.use('/order', async (req, res, next) => {
const meal = await db.get(
`SELECT deadline FROM meals WHERE id = ?`,
[req.body.mealId]
);
if (new Date() > new Date(meal.deadline)) {
return res.status(403).json({ error: '已过订餐截止时间' });
}
next();
});
3.2 考勤联动设计
实现考勤数据自动关联订餐记录的关键SQL:
sql复制-- 获取某日缺勤但订餐的员工列表
SELECT e.name, e.dept
FROM employees e
JOIN orders o ON o.employee_id = e.id
JOIN meals m ON o.meal_id = m.id
LEFT JOIN attendance a ON a.employee_id = e.id AND a.date = m.date
WHERE m.date = ? AND (a.status IS NULL OR a.status = 'absent');
4. 踩坑实录与优化方案
4.1 SQLite并发写入问题
初期测试时发现,当多个用户同时提交订餐请求时,会出现"database is locked"错误。解决方案:
-
启用WAL模式(Write-Ahead Logging)
javascript复制await db.exec('PRAGMA journal_mode = WAL;'); -
实现请求队列机制
javascript复制const orderQueue = new PQueue({ concurrency: 1 }); router.post('/order', (req, res) => { orderQueue.add(() => handleOrder(req, res)); });
4.2 微信接入的坑
原计划通过企业微信API获取用户信息,但遇到两个问题:
- 部分员工未激活企业微信账号
- API调用有频率限制
最终解决方案:
- 改用微信扫码登录(非企业微信)
- 本地缓存用户openid与员工ID的映射关系
- 首次登录时要求填写工号进行绑定
5. 性能优化实践
5.1 数据库索引优化
通过对慢查询日志分析,添加了关键索引:
sql复制CREATE INDEX idx_orders_meal ON orders(meal_id);
CREATE INDEX idx_attendance_date ON attendance(date);
5.2 前端数据缓存
对静态数据采用sessionStorage缓存:
javascript复制// 获取当日菜单
if (!sessionStorage.getItem('todayMeal')) {
fetch('/api/meals/today')
.then(res => res.json())
.then(data => {
sessionStorage.setItem('todayMeal', JSON.stringify(data));
renderMeal(data);
});
} else {
renderMeal(JSON.parse(sessionStorage.getItem('todayMeal')));
}
6. 部署与运维方案
6.1 轻量级部署方案
使用PM2管理进程:
bash复制pm2 start server.js --name "meal-system" --watch
配合nginx反向代理:
nginx复制server {
listen 80;
server_name meal.internal.company.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
}
6.2 数据备份策略
设置每日凌晨3点的自动备份:
bash复制# 备份脚本
#!/bin/bash
BACKUP_DIR=/var/backups/meal-system
DATE=$(date +%Y%m%d)
cp /app/data/meal.db $BACKUP_DIR/meal_$DATE.db
find $BACKUP_DIR -name "*.db" -mtime +7 -delete
7. 实际效果与扩展思考
系统上线三个月后的关键数据:
- 订餐统计时间从30分钟缩短至实时完成
- 考勤-订餐对账时间从3天降为10分钟
- 异常订餐(缺勤但订餐)发现率提升至100%
几个值得分享的改进方向:
- 接入食堂POS系统实现自动核销
- 增加智能推荐算法(根据历史订单推荐餐品)
- 开发移动端快捷操作(如"和昨天一样"按钮)
这个项目给我的最大启示是:解决实际问题不需要追求技术复杂度,用最简单的方案满足核心需求,往往能取得最好的投入产出比。比如SQLite在这种小规模场景下的表现,就完全超出了我最初的预期。