AI人脸识别打卡签到系统已经实现阶段性完成,完整代码已上传至代码仓,后续第二阶段将延用后端并且开发一个AI人脸识别移动端,将使用微信开发者工具进行实现。
AI考勤系统
阶段性开发日志 v0.99 RC
项目代号:ai-attendance-system
文档日期:2026年6月16日
技术栈:React 18 + Express + SQLite + Python FastAPI
一、项目概览
AI考勤系统是一套基于人脸识别的智能考勤管理平台,支持多租户、多终端部署,涵盖考勤打卡、请假加班、薪资计算、智能排班、访客管理、数据报表等完整业务流程。系统采用前后端分离架构,前端基于 React 18 + Vite + TypeScript,后端基于 Express + SQLite/PostgreSQL,人脸识别服务基于 Python FastAPI + InsightFace。
1.1 核心数据
数据库表:27张 | API端点:149个 | 前端页面:30个 | 路由模块:28个
测试用例:208个 | i18n翻译键:958个/语言 | 部署方案:4种
核心依赖:25个 | 开发依赖:30个 | 数据库索引:25+个
1.2 技术栈
- 前端:React 18.3 + React Router 7 + Zustand 5 + ECharts 6 + Tailwind CSS 3
- 后端:Express 4 + better-sqlite3 12 + jsonwebtoken 9 + winston 3
- 人脸服务:Python FastAPI + InsightFace + ONNX Runtime + OpenCV
- 构建工具:Vite 6 + TypeScript 5.8 + Vitest 4 + Playwright 1.6
- 部署方案:Docker + PM2 + Vercel + Electron
二、功能模块详解
2.1 认证与安全模块
认证模块是整个系统的安全基石,负责用户登录验证、JWT Token管理、密码策略执行、速率限制防护等核心安全功能。系统采用 JWT + Refresh Token 双令牌机制,Access Token 有效期24小时,Refresh Token 有效期7天,存储在数据库中支持主动撤销。
2.1.1 登录验证逻辑
登录流程包含速率限制检查、用户查询、密码验证、哈希自动升级、Token生成、登录日志记录等步骤。密码验证兼容旧版SHA-256和新版scrypt两种格式,登录成功后自动将旧哈希升级为scrypt格式。
router.post('/login', async (req, res) => {
const ip = req.ip || 'unknown'
if (!checkRateLimit(ip)) {
return res.status(429).json({ error: '登录尝试过多,请15分钟后再试' })
}
const user = db.get('SELECT * FROM user WHERE username=? AND status=?', username, 'active')
if (!user || !verifyPassword(password, user.password_hash)) {
db.run('INSERT INTO login_log (username, ip, success) VALUES (?, ?, 0)', username, ip)
return res.status(401).json({ error: '用户名或密码错误' })
}
// 自动升级旧密码哈希 SHA-256 -> scrypt
if (needsUpgrade(user.password_hash)) {
const newHash = hashPasswordSecure(password)
db.run('UPDATE user SET password_hash=? WHERE id=?', newHash, user.id)
}
const accessToken = generateToken({ id, username, role, emp_id, tenant_id })
const refreshToken = jwt.sign({ id, type: 'refresh' }, JWT_SECRET, { expiresIn: '7d' })
db.run('INSERT OR REPLACE INTO refresh_token (user_id, token, expires_at) VALUES (?, ?, ?)',
user.id, refreshToken, expiresAt)
res.json({ success: true, data: { token: accessToken, refreshToken, user } })
})
2.1.2 密码哈希与验证
系统使用scrypt算法进行密码哈希,生成64字节密钥+16字节随机盐,使用timingSafeEqual防止时序攻击。旧版SHA-256哈希在登录时自动升级,无需用户手动操作。
function hashPasswordSecure(password: string): string {
const salt = randomBytes(16).toString('hex')
const key = scryptSync(password, salt, 64)
return `$scrypt$${salt}$${key.toString('hex')}`
}
function verifyPassword(password: string, hash: string): boolean {
if (hash.startsWith('$scrypt$')) {
const [, , salt, key] = hash.split('$')
const derivedKey = scryptSync(password, salt, 64)
return timingSafeEqual(Buffer.from(key, 'hex'), derivedKey)
}
// 兼容旧SHA-256格式
const sha256Hash = createHash('sha256').update(password).digest('hex')
return sha256Hash === hash
}
2.1.3 登录速率限制
基于IP地址的登录速率限制,5次失败后锁定15分钟,防止暴力破解攻击。每10分钟自动清理过期的限制记录,避免内存泄漏。
const loginAttempts = new Map<string, { count: number; lastAttempt: number }>()
const MAX_ATTEMPTS = 5
const LOCKOUT_MS = 15 * 60 * 1000
function checkRateLimit(ip: string): boolean {
const now = Date.now()
const attempt = loginAttempts.get(ip)
if (!attempt) { loginAttempts.set(ip, { count: 1, lastAttempt: now }); return true }
if (now – attempt.lastAttempt > LOCKOUT_MS) {
loginAttempts.set(ip, { count: 1, lastAttempt: now }); return true
}
if (attempt.count >= MAX_ATTEMPTS) return false
attempt.count++; attempt.lastAttempt = now; return true
}
2.1.4 JWT认证中间件
所有API请求(除公开路径外)必须携带有效的JWT Token。中间件从Authorization头或query参数中提取Token,验证签名和有效期,将解码后的用户信息注入req.user供后续路由使用。
const publicPaths = ['/api/auth/login', '/api/auth/refresh', '/api/health', …]
app.use((req, res, next) => {
if (publicPaths.some(p => req.path.startsWith(p))) return next()
const token = req.headers.authorization?.split(' ')[1] || req.query.token
if (!token) return res.status(401).json({ error: '未授权访问' })
try {
req.user = jwt.verify(token, jwtSecret)
next()
} catch { res.status(401).json({ error: '登录已过期' }) }
})
2.1.5 Refresh Token机制
Refresh Token存储在数据库中,有效期7天,支持主动撤销。前端在Access Token过期时自动使用Refresh Token获取新的Access Token,实现无感刷新。
router.post('/refresh', (req, res) => {
const { refreshToken } = req.body
const decoded = jwt.verify(refreshToken, JWT_SECRET)
if (decoded.type !== 'refresh') return res.status(401)
const stored = db.get('SELECT * FROM refresh_token WHERE user_id=? AND token=?', decoded.id, refreshToken)
if (!stored) return res.status(401)
const newAccessToken = jwt.sign({ id, username, role, … }, JWT_SECRET, { expiresIn: '24h' })
res.json({ success: true, data: { token: newAccessToken } })
})
2.1.6 人脸特征加密
人脸特征数据使用AES-256-GCM算法加密存储,16字节随机IV + 16字节认证标签,确保数据机密性和完整性。加密密钥通过文件或环境变量管理,文件权限设为0600。
encrypt(data: string): string {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-gcm', this.key, iv)
const encrypted = Buffer.concat([cipher.update(data, 'utf-8'), cipher.final()])
const authTag = cipher.getAuthTag()
return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted.toString('base64')}`
}
decrypt(encrypted: string): string {
const [ivB64, tagB64, dataB64] = encrypted.split(':')
const decipher = crypto.createDecipheriv('aes-256-gcm', this.key, Buffer.from(ivB64, 'base64'))
decipher.setAuthTag(Buffer.from(tagB64, 'base64'))
return Buffer.concat([decipher.update(Buffer.from(dataB64, 'base64')), decipher.final()]).toString('utf-8')
}
2.2 考勤管理模块
考勤管理是系统的核心业务模块,支持人脸识别打卡、二维码打卡、GPS打卡三种方式,提供考勤记录查询、今日统计、PDF/Excel导出等功能。考勤终端支持活体检测,防止照片欺骗。
2.2.1 考勤记录查询
支持分页查询、日期范围过滤、部门过滤、工号搜索。普通员工只能查看自己的记录,管理员可查看全部。查询结果关联员工表获取姓名和部门信息。
router.get('/', (req, res) => {
let whereClause = 'WHERE a.tenant_id = ?'; const params = [tenantId]
if (date) { whereClause += ' AND date(a.capture_time) = ?'; params.push(date) }
if (department) { whereClause += ' AND e.department = ?'; params.push(department) }
if (req.user.role === 'employee') {
whereClause += ' AND a.emp_id = ?'; params.push(req.user.emp_id)
}
const records = db.all(`SELECT a.*, e.name, e.department
FROM attendance_record a LEFT JOIN employee e ON a.emp_id = e.emp_id
${whereClause} ORDER BY a.capture_time DESC LIMIT ? OFFSET ?`, …params, pageSize, offset)
res.json({ success: true, data: { items: records, total, page, page_size: pageSize } })
})
2.2.2 今日考勤统计
实时统计当日出勤情况,包括应到人数、实到人数、迟到人数、缺勤人数,以及最近10条打卡记录,供仪表盘实时展示。
router.get('/today-stats', (req, res) => {
const total = db.get("SELECT COUNT(*) as count FROM employee WHERE status='active'")?.count || 0
const present = db.get("SELECT COUNT(DISTINCT emp_id) as count FROM attendance_record WHERE type='check_in' AND date(capture_time)=?")?.count || 0
const late = db.get("SELECT COUNT(DISTINCT emp_id) as count … WHERE status='late' …")?.count || 0
const absent = total – present
const recentRecords = db.all("SELECT a.*, e.name … ORDER BY capture_time DESC LIMIT 10")
res.json({ success: true, data: { total, present, late, absent, recentRecords } })
})
2.2.3 考勤终端打卡
前端考勤终端页面通过摄像头实时采集人脸图像,发送到人脸识别服务进行检测和比对,识别成功后自动创建考勤记录。支持活体检测防止照片欺骗,离线模式下自动存入队列等待网络恢复后上传。
// Scanner.tsx 核心检测循环
const detectAndSearch = async () => {
const formData = new FormData()
formData.append('image', captureFrame())
const result = await apiPostForm('/api/fr/detect-and-search', formData)
if (result.faces?.length > 0) {
const face = result.faces[0]
if (face.liveness?.is_real === false) {
showToast('疑似照片,请真人打卡'); return
}
await apiPost('/api/attendance', {
emp_id: face.emp_id, capture_time: new Date().toISOString(),
device_id: deviceId, face_score: face.score
})
checkedInSet.add(face.emp_id) // 60秒内不重复打卡
playSuccessSound()
}
}
2.3 员工管理模块
员工管理模块提供员工信息的完整CRUD操作,支持批量导入、模板下载、部门列表查询。人脸特征数据加密存储,临时解密文件使用随机数据覆写后删除,防止磁盘恢复攻击。
2.3.1 员工列表查询
支持分页、搜索(工号/姓名)、部门过滤。查询结果按创建时间倒序排列,返回员工基本信息和人脸注册状态。
router.get('/', (req, res) => {
let whereClause = 'WHERE tenant_id = ?'; const params = [tenantId]
if (search) { whereClause += ' AND (emp_id LIKE ? OR name LIKE ?)'; params.push(`%${search}%`, `%${search}%`) }
if (department) { whereClause += ' AND department = ?'; params.push(department) }
const employees = db.all(`SELECT * FROM employee ${whereClause}
ORDER BY created_at DESC LIMIT ? OFFSET ?`, …params, pageSize, offset)
res.json({ success: true, data: { items: employees, total, page } })
})
2.3.2 人脸特征安全擦除
当需要临时解密人脸特征文件进行比对时,使用完毕后先用随机数据覆写文件内容,再执行删除操作,确保敏感数据无法从磁盘恢复。
function rencryptFeatureFile(decryptedPath: string): void {
const absolutePath = path.resolve(process.cwd(), decryptedPath)
if (fs.existsSync(absolutePath)) {
const stat = fs.statSync(absolutePath)
const randomData = crypto.randomBytes(stat.size)
fs.writeFileSync(absolutePath, randomData) // 随机数据覆写
fs.unlinkSync(absolutePath) // 安全删除
}
}
2.4 请假管理模块
请假管理模块支持多种请假类型(年假、事假、病假等),提供请假申请、审批(通过/驳回)、撤销、假期余额查询等功能。审批通过后自动更新考勤记录,将请假日期的打卡状态标记为"leave"。
2.4.1 请假申请
员工提交请假申请时,系统自动检查日期冲突,防止重复请假。申请创建后自动向所有管理员发送审批通知。
router.post('/requests', (req, res) => {
const conflict = db.get(`SELECT id FROM leave_request
WHERE emp_id=? AND status IN ('pending','approved')
AND start_date <= ? AND end_date >= ?`, emp_id, end_date, start_date)
if (conflict) return res.status(400).json({ error: '该日期范围已有请假申请' })
const result = db.run('INSERT INTO leave_request (…) VALUES (…)', …)
// 发送审批通知给管理员
const admins = db.all("SELECT id FROM user WHERE role='admin' AND status='active'")
for (const admin of admins) {
notificationDispatcher.dispatchLeaveApproval(admin.id, '', leaveInfo)
}
res.json({ success: true, data: { id: result.lastInsertRowid } })
})
2.4.2 审批通过
管理员审批通过后,系统自动将请假日期范围内的考勤记录状态更新为"leave",并发送结果通知给申请人。
router.post('/requests/:id/approve', (req, res) => {
db.run("UPDATE leave_request SET status='approved', approver_id=?, approve_time=datetime('now','localtime')", …)
// 更新考勤记录为请假状态
db.run(`UPDATE attendance_record SET status='leave', reason=?
WHERE emp_id=? AND date(capture_time) >= ? AND date(capture_time) <= ?
AND status IN ('normal','late','early','absent')`, …)
notificationDispatcher.dispatchLeaveResult(applicantUser.id, '', leaveInfo)
res.json({ success: true })
})
2.5 加班管理模块
加班管理模块与请假模块结构类似,支持加班申请、审批、撤销、统计等功能。加班时长自动计入薪资计算。
router.post('/', (req, res) => {
const { emp_id, date, start_time, end_time, hours, reason } = req.body
const result = db.run('INSERT INTO overtime_request (…) VALUES (…)', …)
const admins = db.all("SELECT id FROM user WHERE role='admin' AND status='active'")
for (const admin of admins) {
notificationDispatcher.dispatchOvertimeApproval(admin.id, '', overtimeInfo)
}
res.json({ success: true, data: { id: result.lastInsertRowid } })
})
2.6 薪资管理模块
薪资管理模块支持自定义薪资规则(基本工资、迟到扣款、加班费率等),一键生成月度薪资,自动计算迟到扣款、早退扣款、缺勤扣款、加班费等,支持CSV导出。
router.post('/generate', (req, res) => {
const rules = db.all('SELECT * FROM salary_rule WHERE enabled=1 ORDER BY priority')
const ruleMap = {}; rules.forEach(r => { ruleMap[r.type] = r })
const baseSalary = ruleMap.base?.value || 0
const lateDeductPerTime = ruleMap.late_deduct?.value || 0
const overtimeRate = ruleMap.overtime_pay?.value || 1.5
for (const emp of employees) {
const lateCount = …; const overtimeHours = …
const lateDeduct = lateCount * lateDeductPerTime
const dailySalary = baseSalary / 22
const overtimePay = overtimeHours * (dailySalary / 8) * overtimeRate
const totalSalary = baseSalary – lateDeduct – earlyDeduct – absenceDeduct + overtimePay
db.run('INSERT INTO salary_record (…) VALUES (…) ON CONFLICT DO UPDATE SET …', …)
}
res.json({ success: true, data: { generated, year, month } })
})
2.7 智能排班模块
排班模块支持排班模板管理(设置上下班时间、迟到/早退容忍时间)和员工排班分配(按星期分配模板)。支持批量排班、部门排班、事务性操作保证数据一致性。
router.post('/employee/:emp_id', (req, res) => {
const { schedules } = req.body
db.exec('BEGIN TRANSACTION')
try {
db.run('DELETE FROM employee_schedule WHERE emp_id=? AND tenant_id=?', emp_id, tenantId)
for (const s of schedules) {
db.run('INSERT INTO employee_schedule (emp_id, schedule_id, weekday, tenant_id) VALUES (?, ?, ?, ?)',
emp_id, s.schedule_id, s.weekday, tenantId)
}
db.exec('COMMIT')
} catch (err) { db.exec('ROLLBACK'); throw err }
})
2.8 人脸识别服务
人脸识别服务基于Python FastAPI框架,使用InsightFace引擎进行人脸检测、特征提取和比对。支持三种活体检测模式:静默活体(无感知分析)、动作挑战(眨眼/摇头/张嘴)、双重验证。后端通过fr-proxy路由代理请求到Python服务。
router.post('/extract', async (req, res) => {
const rawBody = await collectRawBody(req)
const response = await fetch(`${FR_SERVICE_URL}/fr/extract`, {
method: 'POST', headers: { 'Content-Type': contentType }, body: rawBody,
})
const data = await response.json()
res.status(response.status).json({ success: true, data })
})
2.9 访客管理模块
访客管理模块提供访客登记、人脸注册、签到签退、状态管理等功能。访客信息包含姓名、手机、公司、来访目的、接待人等字段,支持有效期设置。所有操作均按租户隔离。
2.10 通知系统
通知系统支持站内通知和邮件通知两种渠道。站内通知实时推送(WebSocket),邮件通知基于nodemailer。通知偏好可按类型配置,支持测试通知功能。请假审批、加班审批等业务事件自动触发通知。
2.11 智能报表模块
报表模块提供出勤趋势分析、部门统计、迟到热力图、异常列表、个人汇总等多种报表视图,支持Excel导出。数据按租户隔离,普通员工只能查看个人数据。
2.12 多租户与计费模块
系统支持多租户数据隔离,每个租户有独立的员工、考勤、设置等数据。计费模块提供用量统计、账单生成、财务概览等功能。支付模块集成微信支付和支付宝,支持订单管理、退款、模拟确认等。
租户注册功能当前已禁用(返回403),仅允许管理员通过后台创建租户。
2.13 授权管理模块
授权管理模块基于机器码+授权码机制,支持4种授权类型(试用版、标准版、专业版、企业版)。机器码通过系统硬件信息生成,授权码由管理端生成后激活。
2.14 数据迁移模块
数据迁移模块支持CSV格式的员工、考勤、排班数据导入,提供模板下载、数据预览、执行导入、导入历史等功能。同时支持SQLite到PostgreSQL的数据库迁移,通过DbMigrationService管理版本化迁移。
2.15 监控与运维模块
监控模块提供Prometheus格式的指标输出、JSON格式的API指标、仪表盘聚合数据。错误监控系统支持错误趋势分析、错误列表、健康状态、告警规则配置。结构化日志系统支持JSON Lines格式文件输出,按模块分类,包含traceId、tenantId等上下文信息。
class Logger {
private module: string; private _traceId?: string; private _tenantId?: string
private log(level, message, meta?, error?) {
const entry = { timestamp, level, message, module: this.module, traceId, tenantId }
if (error) entry.error = { name: error.name, message: error.message, stack: error.stack }
addToBuffer(entry); outputToConsole(entry); outputToFile(entry)
}
withTrace(traceId) { return new Logger(this.module, traceId, …) }
startTimer() { const start = Date.now(); return { end(msg) { this.log(INFO, msg, { duration: Date.now()-start }) } } }
}
2.16 隐私合规模块
隐私合规模块遵循GDPR要求,提供隐私协议管理、用户同意记录、数据导出(JSON格式)、账户删除等功能。用户可以查看自己的隐私同意状态,随时导出或删除个人数据。
三、前端架构
3.1 路由配置
前端采用HashRouter + React.lazy实现代码分割,所有30个页面均按需加载。路由分为公开路由(登录、安装)、管理路由(需认证+Layout包裹)、终端路由(需认证+ScannerLayout包裹)三类。
const Dashboard = lazy(() => import('@/pages/Dashboard'))
const Employees = lazy(() => import('@/pages/Employees'))
const Scanner = lazy(() => import('@/pages/Scanner'))
// … 30+ 页面懒加载
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute><Layout /></ProtectedRoute>}>
<Route path="/" element={<Dashboard />} />
<Route path="/employees" element={<Employees />} />
{/* … 20+ 管理路由 … */}
</Route>
<Route element={<ProtectedRoute><ScannerLayout /></ProtectedRoute>}>
<Route path="/scanner" element={<Scanner />} />
</Route>
</Routes>
3.2 状态管理
使用Zustand进行轻量级状态管理,认证状态持久化到localStorage,支持Token自动刷新和登出时服务端撤销。
export const useAuthStore = create<AuthState>((set, get) => ({
token: localStorage.getItem('auth_token'),
user: safeParse('auth_user', null),
isAuthenticated: !!localStorage.getItem('auth_token'),
setAuth: (token, user, refreshToken?) => {
localStorage.setItem('auth_token', token)
setRefreshToken(refreshToken || null)
set({ token, user, isAuthenticated: true })
},
logout: () => {
const rt = getRefreshToken()
if (rt) fetch('/api/auth/logout', { method: 'POST', body: JSON.stringify({ refreshToken: rt }) })
localStorage.removeItem('auth_token'); setRefreshToken(null)
set({ token: null, user: null, isAuthenticated: false })
},
}))
3.3 Token自动刷新
前端封装了fetchWithRefresh函数,当API请求返回401时自动使用Refresh Token获取新的Access Token并重试请求,实现无感刷新。
export async function refreshAccessToken(): Promise<string | null> {
const token = getRefreshToken()
if (!token) return null
const res = await fetch('/api/auth/refresh', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: token })
})
const data = await res.json()
if (data.success && data.data.token) {
localStorage.setItem('auth_token', data.data.token)
return data.data.token
}
return null
}
3.4 表单验证
使用Zod库定义表单验证Schema,覆盖员工表单、请假表单、加班表单、改密表单等核心场景,统一错误提示格式。
export const schemas = {
employeeId: z.string().min(1, '工号不能为空').max(20).regex(/^[A-Za-z0-9_-]+$/),
employeeName: z.string().min(1, '姓名不能为空').max(50),
leaveReason: z.string().min(1, '请假原因不能为空').max(200),
password: z.string().min(6, '密码至少6位'),
}
export const employeeFormSchema = z.object({ emp_id: schemas.employeeId, … })
export const leaveFormSchema = z.object({ … })
export function validateForm<T>(schema, data) { … }
3.5 国际化
系统支持中文和英文两种语言,共958个翻译键,覆盖所有页面和组件。翻译系统基于自定义Hook实现,支持运行时切换语言,翻译内容持久化到localStorage。
四、安全审计记录
4.1 审计概要
Phase 3安全审计共发现12个安全问题,其中严重3个、高危5个、中危4个,全部已修复。
4.2 严重问题修复
4.2.1 修改密码端点公开访问
问题:/api/auth/change-password 在 publicPaths 中,无需JWT认证即可访问,攻击者可暴力破解修改任意用户密码。修复:移出公开路径,使用JWT中的user.id代替请求体中的user_id,添加速率限制。
4.2.2 硬编码默认密码
问题:新建租户时管理员密码硬编码为"admin123"。修复:改用crypto.randomBytes(12)生成随机密码,并设置must_change_password=1。
4.2.3 跨租户数据泄露
问题:settings、visitors、holidays、audit等表的查询未按tenant_id过滤,允许跨租户访问数据。修复:所有查询添加WHERE tenant_id=?条件,数据库迁移添加tenant_id列和复合唯一索引。
4.3 高危问题修复
- visitors删除/更新无tenant_id检查 — 已添加WHERE条件
- holidays删除无tenant_id检查 — 已添加WHERE条件
- audit日志无tenant_id过滤 — 已添加WHERE条件
- 错误响应泄露内部信息 — db-migrate路由错误信息改为通用消息
- .env.example弱默认JWT_SECRET — 移除默认值,添加生成命令提示
4.4 中危问题修复
- error-tracker端点缺少管理员权限检查 — 已添加requireAdmin中间件
- 输入净化未覆盖req.query — 已扩展中间件同时净化query参数
- 注册/安装接口无速率限制 — 已添加authLimiter(5次/分钟)
- 临时解密文件未安全擦除 — 已添加randomBytes覆写+unlink
4.5 安全措施总览
- 密码哈希:scrypt + salt + timingSafeEqual防时序攻击
- 人脸加密:AES-256-GCM + 随机IV + 认证标签
- 登录限速:5次/15分钟/IP锁定
- JWT:生产环境强制设置JWT_SECRET,24小时过期
- CORS:白名单校验,生产环境不允许任意源
- CSP:生产环境启用Content-Security-Policy
- 输入净化:XSS过滤中间件覆盖body和query
- 审计日志:所有敏感操作记录audit_log
- 路径遍历防护:备份文件名检查..、/、\\
五、测试体系
5.1 测试覆盖
项目共23个测试文件,208个测试用例,覆盖API路由、前端组件、E2E场景和工具函数。
5.2 API测试
15个API测试文件,167个测试用例,覆盖所有核心路由模块。使用Vitest + supertest框架,每个测试文件独立创建测试数据库,测试完成后自动清理。
5.3 前端组件测试
3个前端测试文件,14个测试用例,覆盖Login、ChangePassword、Attendance核心页面。使用Vitest + Testing Library + jsdom环境,Mock了API调用和i18n。
5.4 E2E测试
4个Playwright E2E测试文件,12个测试场景,覆盖认证流程、页面导航、员工管理、API健康检查。使用Chromium浏览器,兼容中英文界面。
test('should login successfully', async ({ page }) => {
await page.goto('/')
await usernameInput.fill('admin')
await passwordInput.fill('admin123')
await page.locator('button[type="submit"]').click()
await expect(page).toHaveURL(/\\/dashboard/, { timeout: 10000 })
})
六、构建与部署
6.1 Vite构建配置
使用Vite 6构建,配置了React插件、路径别名、Bundle可视化分析、代码分割策略。resolve.alias强制React单实例,resolve.dedupe防止重复打包。
export default defineConfig({
plugins: [react(), tsconfigPaths(), visualizer({ filename: 'dist/stats.html' })],
build: {
outDir: 'dist/frontend',
rollupOptions: { output: { manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('echarts')) return 'vendor-charts'
if (id.includes('exceljs')) return 'vendor-utils'
if (id.includes('lucide')) return 'vendor-ui'
return 'vendor'
}
} } },
},
resolve: { dedupe: ['react', 'react-dom'], alias: { react: path.resolve(__dirname, 'node_modules/react') } },
server: { host: 'localhost', hmr: { protocol: 'ws', host: 'localhost' },
proxy: { '/api': { target: 'http://localhost:3001', changeOrigin: true, ws: true } } }
})
6.2 TypeScript配置
已启用strict模式,包含noUnusedLocals、noUnusedParameters、noFallthroughCasesInSwitch等严格检查选项。
{
"compilerOptions": {
"target": "ES2020", "module": "ESNext", "jsx": "react-jsx",
"strict": true, "noUnusedLocals": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true,
"baseUrl": "./", "paths": { "@/*": ["./src/*"] }
}
}
6.3 Docker部署
Docker采用多阶段构建:阶段1构建前端,阶段2运行时包含Node.js + Python人脸服务。强制设置JWT_SECRET环境变量,配置健康检查。
FROM node:20-alpine AS frontend-builder
WORKDIR /app; COPY package*.json ./; RUN npm ci; COPY . .; RUN npm run build
FROM node:20-alpine
RUN apk add –no-cache python3 py3-pip
COPY –from=frontend-builder /app/dist ./dist
COPY api ./api; COPY face_recognition_service ./face_recognition_service
RUN cd face_recognition_service && pip install -r requirements.txt
EXPOSE 3001 3002; VOLUME ["/app/data"]
6.4 其他部署方案
- PM2:双进程(API + 人脸服务),内存限制512M/1G,最大重启10次
- Vercel:API路由重写 + SPA fallback,适合轻量部署
- Electron:桌面应用打包,NSIS安装程序,包含Python人脸服务
七、数据库设计
7.1 表结构总览
系统共27张数据库表,采用SQLite WAL模式,启用外键约束,设置busy_timeout=5000ms。所有业务表均包含tenant_id字段实现多租户隔离。
7.2 核心表设计
7.2.1 employee(员工表)
字段:id, emp_id, name, department, position, phone, email, photo_path, feature_path, status, hire_date, face_registered, tenant_id, created_at, updated_at
7.2.2 attendance_record(考勤记录表)
字段:id, emp_id, capture_time, verify_method, device_id, photo_path, confidence, latitude, longitude, ip_address, type, face_score, status, reason, tenant_id
7.2.3 user(用户表)
字段:id, username, password_hash, role, emp_id, tenant_id, status, must_change_password, last_login, created_at, updated_at
7.2.4 leave_request(请假申请表)
字段:id, emp_id, leave_type_id, start_date, end_date, start_half, end_half, days, reason, status, approver_id, approve_time, tenant_id, created_at
7.2.5 salary_record(薪资记录表)
字段:id, emp_id, year, month, base_salary, overtime_pay, bonus, late_deduct, early_deduct, absence_deduct, leave_deduct, total_deduct, net_salary, tenant_id, created_at
7.3 索引策略
共25+个索引,覆盖主要查询字段:员工工号、考勤时间、部门、状态、租户ID等。复合唯一索引(key, tenant_id)确保设置项按租户隔离。
7.4 数据库类型定义
为所有数据库查询结果定义了TypeScript接口(api/types/db.ts),共25个接口,替代了大部分as any类型断言,提升类型安全性。
export interface EmployeeRow {
id: number; emp_id: string; name: string; department: string
position: string; phone: string | null; email: string | null
status: string; hire_date: string | null; face_registered: number
tenant_id: number | null; created_at: string; updated_at: string
}
export interface AttendanceRecordRow {
id: number; emp_id: string; capture_time: string
verify_method: string; device_id: string | null
face_score: number | null; status: string; reason: string | null
tenant_id: number | null
}
八、版本演进记录
8.1 v0.9.0(2026-06-09)
初始版本,完成28个前端页面、27个API路由模块、人脸识别服务、多租户支持、计费支付系统等核心功能。
8.2 v0.95.0(2026-06-10)
- Zod表单验证(Employee/Leave/Overtime/ChangePassword)
- Empty组件重写+9个列表页集成
- console清理+JSON Lines文件日志
- 9个新API测试文件
- Refresh Token机制
- 密码策略(8位+大小写+数字)
8.3 v0.97.0(2026-06-12)
- TypeScript strict模式启用
- any类型从~100处减少至<20处
- 25个数据库行类型接口
- 前端组件测试框架(Vitest + Testing Library)
- Bundle分析与优化(visualizer + manualChunks)
8.4 v0.99 RC(2026-06-16)
- 安全审计:12个安全问题全部修复
- E2E测试框架(Playwright + Chromium)
- 后端console全部替换为structured logger
- CHANGELOG建立
- 密码策略调整为6位最低要求
- 注册功能关闭,仅允许admin账号登录
- React多实例问题修复(移除react-dev-locator + resolve.alias)
九、后续开发计划
9.1 v1.0 正式版
- 生产环境压力测试验证
- 支付流程生产环境验证
- 技术文档完善
- 正式发布
9.2 v1.1(1-2个月后)
- 数据看板增强(趋势分析、异常检测)
- 自动排班算法优化
- 钉钉/企微集成
9.3 v1.2(3-4个月后)
- 移动端App(React Native/Taro)
- 推送通知(FCM/APNs)
- 生物识别快捷登录
9.4 v1.3(5-6个月后)
- 多语言扩展(日语/韩语)
- Redis分布式缓存
- Kubernetes部署配置
- 异地备份(S3/OSS)

![[特殊字符]DeepSeek‑Harness(DSH)小白保姆教程-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260816085112-6a817a009aabf-220x150.png)