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

53
server/src/routes/auth.ts Normal file
View File

@@ -0,0 +1,53 @@
import { Router, Request, Response } from 'express'
import { createHmac } from 'crypto'
import pool from '../db/connection'
const router = Router()
const APPID = process.env.WX_APPID || ''
const SECRET = process.env.WX_SECRET || ''
const TOKEN_SECRET = process.env.TOKEN_SECRET || 'xiaocai-token-secret-change-in-production'
export function signToken(userId: number): string {
const payload = `${userId}:${Date.now()}`
const signature = createHmac('sha256', TOKEN_SECRET).update(payload).digest('hex')
return Buffer.from(`${payload}:${signature}`).toString('base64')
}
router.post('/login', async (req: Request, res: Response) => {
try {
const { code } = req.body
if (!code) {
return res.status(400).json({ code: 40001, message: '缺少code参数' })
}
// 调用微信 code2session 接口
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
const wxRes = await fetch(url)
const wxData = await wxRes.json() as any
if (wxData.errcode) {
console.error('[Auth] WeChat API error:', wxData.errcode, wxData.errmsg)
return res.status(400).json({ code: 40002, message: '微信登录失败' })
}
const { openid, session_key } = wxData
// 查找或创建用户 (atomic: handles race condition)
const [result] = await pool.query(
'INSERT INTO users (openid, session_key) VALUES (?, ?) ON DUPLICATE KEY UPDATE session_key = ?, id = LAST_INSERT_ID(id)',
[openid, session_key, session_key]
)
const userId = (result as any).insertId
// HMAC signed token with expiry
const token = signToken(userId)
res.json({ code: 0, data: { token, userId } })
} catch (err: any) {
console.error('[Auth] Login failed:', err.message)
res.status(500).json({ code: 50000, message: '服务器错误' })
}
})
export default router