feat: 小菜记账 v1.0 - 完整功能实现

核心功能:
- 记账CRUD(支出/收入/分类/备注/日期)
- 统计分析(概览/分类占比/每日趋势)
- 预算管理(按月设置/进度条/超支提醒)
- 数据导出CSV

安全与认证:
- HMAC-SHA256签名token认证
- 用户数据隔离
- 输入验证与错误处理
- CORS配置

前端优化:
- 骨架屏加载
- 账单按日期分组
- 预算页面重构(快捷预设+Numpad)
- SvgIcon组件(H5+微信双端适配)
- 下拉刷新

后端优化:
- 共享日期工具函数
- 数据库连接池优化
- 健康检查端点
- 优雅关闭处理

技术栈:
- 前端:Uni-app (Vue 3 + Pinia)
- 后端:Node.js + Express + MySQL
This commit is contained in:
2026-05-29 16:14:15 +08:00
commit c7f29aa7a7
70 changed files with 32282 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import { Request, Response, NextFunction } from 'express'
import { createHmac } from 'crypto'
export interface AuthRequest extends Request {
userId?: number
}
const TOKEN_SECRET = process.env.TOKEN_SECRET || 'xiaocai-token-secret-change-in-production'
const TOKEN_EXPIRY = 30 * 24 * 60 * 60 * 1000 // 30 days
// 不需要认证的路径
const PUBLIC_PATHS = ['/api/auth/login', '/api/health']
export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
// 公开路径跳过认证
if (PUBLIC_PATHS.some(p => req.path === p)) {
return next()
}
// 从 Authorization header 获取 token
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ code: 40100, message: '未登录' })
}
const token = authHeader.slice(7)
try {
const decoded = Buffer.from(token, 'base64').toString('utf-8')
const parts = decoded.split(':')
if (parts.length !== 3) {
return res.status(401).json({ code: 40100, message: 'token无效' })
}
const [userIdStr, timestamp, signature] = parts
const userId = parseInt(userIdStr)
const ts = parseInt(timestamp)
if (isNaN(userId) || isNaN(ts)) {
return res.status(401).json({ code: 40100, message: 'token无效' })
}
// Check expiry and reject future timestamps
const now = Date.now()
if (now - ts > TOKEN_EXPIRY) {
return res.status(401).json({ code: 40101, message: 'token已过期' })
}
if (ts > now + 60000) {
return res.status(401).json({ code: 40100, message: 'token无效' })
}
// Verify HMAC signature (full length)
const payload = `${userId}:${timestamp}`
const expectedSig = createHmac('sha256', TOKEN_SECRET).update(payload).digest('hex')
if (signature !== expectedSig) {
return res.status(401).json({ code: 40100, message: 'token无效' })
}
req.userId = userId
} catch {
return res.status(401).json({ code: 40100, message: 'token无效' })
}
next()
}