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

74
server/src/index.ts Normal file
View File

@@ -0,0 +1,74 @@
import 'dotenv/config'
import express from 'express'
import cors from 'cors'
import { authMiddleware } from './middleware/auth'
import { initDatabase } from './db/init'
import pool from './db/connection'
import authRoutes from './routes/auth'
import transactionRoutes from './routes/transaction'
import statsRoutes from './routes/stats'
import budgetRoutes from './routes/budget'
import categoryRoutes from './routes/category'
// Warn if token secret is using default fallback
if (!process.env.TOKEN_SECRET) {
console.warn('[Security] TOKEN_SECRET not set — using default fallback. Set TOKEN_SECRET in .env for production!')
}
const app = express()
const PORT = process.env.PORT || 3000
const ALLOWED_ORIGINS = (process.env.CORS_ORIGINS || '').split(',').filter(Boolean)
app.use(cors({
origin: (origin, callback) => {
// No origin = WeChat mini-program request (always allow)
if (!origin) return callback(null, true)
// If no CORS_ORIGINS configured, allow all (dev mode)
if (ALLOWED_ORIGINS.length === 0) return callback(null, true)
// Check whitelist
if (ALLOWED_ORIGINS.includes(origin)) return callback(null, true)
callback(null, false)
},
credentials: true,
}))
app.use(express.json({ limit: '10kb' }))
app.use(authMiddleware)
app.use('/api/auth', authRoutes)
app.use('/api/transactions', transactionRoutes)
app.use('/api/stats', statsRoutes)
app.use('/api/budget', budgetRoutes)
app.use('/api/categories', categoryRoutes)
app.get('/api/health', async (_req, res) => {
try {
await pool.query('SELECT 1')
res.json({ status: 'ok', db: 'connected', time: new Date().toISOString() })
} catch {
res.status(503).json({ status: 'error', db: 'disconnected', time: new Date().toISOString() })
}
})
let server: any
initDatabase().then(() => {
server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
})
// Graceful shutdown
function shutdown(signal: string) {
console.log(`[Server] ${signal} received, shutting down gracefully...`)
server?.close(() => {
pool.end().then(() => {
console.log('[Server] All connections closed')
process.exit(0)
})
})
// Force exit after 10s
setTimeout(() => process.exit(1), 10000)
}
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))