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:
91
server/src/routes/stats.ts
Normal file
91
server/src/routes/stats.ts
Normal 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
|
||||
Reference in New Issue
Block a user