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,91 @@
import { Router, Response } from 'express'
import pool from '../db/connection'
import { AuthRequest } from '../middleware/auth'
import { getCurrentMonth, getMonthRange } from '../utils/date'
const router = Router()
router.get('/overview', async (req: AuthRequest, res: Response) => {
try {
const { month } = req.query
const m = (month as string) || getCurrentMonth()
const range = getMonthRange(m)
if (!range) return res.status(400).json({ code: 40001, message: '月份格式无效' })
const { startDate, endDate } = range
const [rows] = await pool.query(
`SELECT
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as expense,
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as income,
COUNT(*) as count
FROM transactions WHERE user_id = ? AND date >= ? AND date <= ?`,
[req.userId, startDate, endDate]
)
const row = (rows as any[])[0]
const now = new Date()
const [year, mon] = m.split('-').map(Number)
const isCurrentMonth = year === now.getFullYear() && mon === now.getMonth() + 1
const elapsedDays = isCurrentMonth ? now.getDate() : new Date(year, mon, 0).getDate()
const daily = elapsedDays > 0 ? Math.round(row.expense / elapsedDays) : 0
res.json({
code: 0,
data: { expense: row.expense, income: row.income, count: row.count, daily }
})
} catch (err) {
console.error('[Stats] overview error:', err)
res.status(500).json({ code: 50000, message: '服务器错误' })
}
})
router.get('/category', async (req: AuthRequest, res: Response) => {
try {
const { month, type = 'expense' } = req.query
const m = (month as string) || getCurrentMonth()
const range = getMonthRange(m)
if (!range) return res.status(400).json({ code: 40001, message: '月份格式无效' })
const { startDate, endDate } = range
const [rows] = await pool.query(
`SELECT c.id, COALESCE(c.name, '未分类') as name, COALESCE(c.icon, '?') as icon, COALESCE(c.color, '#BFB3B3') as color, SUM(t.amount) as amount, COUNT(t.id) as count
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.user_id = ? AND t.type = ? AND t.date >= ? AND t.date <= ?
GROUP BY c.id
ORDER BY amount DESC`,
[req.userId, type, startDate, endDate]
)
res.json({ code: 0, data: rows })
} catch (err) {
console.error('[Stats] category error:', err)
res.status(500).json({ code: 50000, message: '服务器错误' })
}
})
router.get('/trend', async (req: AuthRequest, res: Response) => {
try {
const { month, type = 'expense' } = req.query
const m = (month as string) || getCurrentMonth()
const range = getMonthRange(m)
if (!range) return res.status(400).json({ code: 40001, message: '月份格式无效' })
const { startDate, endDate } = range
const [rows] = await pool.query(
`SELECT date, SUM(amount) as amount
FROM transactions
WHERE user_id = ? AND type = ? AND date >= ? AND date <= ?
GROUP BY date
ORDER BY date`,
[req.userId, type, startDate, endDate]
)
res.json({ code: 0, data: rows })
} catch (err) {
console.error('[Stats] trend error:', err)
res.status(500).json({ code: 50000, message: '服务器错误' })
}
})
export default router