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,17 @@
import mysql from 'mysql2/promise'
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'xiaocai',
password: process.env.DB_PASSWORD || 'xiaocai123',
database: process.env.DB_NAME || 'xiaocai',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
})
;(pool as any).on('error', (err: any) => {
console.error('[DB] Pool error:', err?.message || err)
})
export default pool

51
server/src/db/init.ts Normal file
View File

@@ -0,0 +1,51 @@
import mysql from 'mysql2/promise'
import pool from './connection'
import { readFileSync } from 'fs'
import { join } from 'path'
export async function initDatabase() {
const dbName = process.env.DB_NAME || 'xiaocai'
// 先用不指定数据库的连接,确保数据库存在
const bootstrap = await mysql.createConnection({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'xiaocai',
password: process.env.DB_PASSWORD || 'xiaocai123',
multipleStatements: true,
})
await bootstrap.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`)
await bootstrap.end()
// 用独立连接执行建表和填充(需要 multipleStatements
const initConn = await mysql.createConnection({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'xiaocai',
password: process.env.DB_PASSWORD || 'xiaocai123',
database: dbName,
multipleStatements: true,
})
try {
const schemaPath = join(__dirname, 'schema.sql')
const seedPath = join(__dirname, 'seed.sql')
const schema = readFileSync(schemaPath, 'utf-8')
const cleanSchema = schema
.replace(/CREATE DATABASE.*?;/gi, '')
.replace(/USE\s+\w+;/gi, '')
await initConn.query(cleanSchema)
const seed = readFileSync(seedPath, 'utf-8')
await initConn.query(seed)
console.log('[DB] Database initialized')
} catch (err: any) {
if (err.code === 'ER_DUP_ENTRY' || err.errno === 1062) {
console.log('[DB] Already initialized, skipping seed')
} else {
console.error('[DB] Init failed:', err.message)
}
} finally {
await initConn.end()
}
}

48
server/src/db/schema.sql Normal file
View File

@@ -0,0 +1,48 @@
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
openid VARCHAR(100) UNIQUE,
session_key VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT DEFAULT 0 COMMENT '0=默认分类, 其他=用户自定义',
name VARCHAR(50) NOT NULL,
icon VARCHAR(50) NOT NULL,
color VARCHAR(20) NOT NULL,
type ENUM('expense', 'income') NOT NULL,
sort_order INT DEFAULT 0,
is_custom TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_name_type (user_id, name, type),
INDEX idx_user_custom (user_id, is_custom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount INT NOT NULL COMMENT '单位:分',
type ENUM('expense', 'income') NOT NULL,
category_id INT,
note VARCHAR(200),
date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_date (user_id, date),
INDEX idx_user_type_date (user_id, type, date),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS budgets (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount INT NOT NULL COMMENT '单位:分',
month VARCHAR(7) NOT NULL COMMENT '格式2026-05',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_month (user_id, month),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

17
server/src/db/seed.sql Normal file
View File

@@ -0,0 +1,17 @@
INSERT IGNORE INTO categories (user_id, name, icon, color, type, sort_order) VALUES
(0, '餐饮', 'Coffee', '#FF8C69', 'expense', 1),
(0, '饮品', 'Coffee', '#FF8C69', 'expense', 2),
(0, '购物', 'ShoppingBag', '#FFB347', 'expense', 3),
(0, '交通', 'Car', '#6C9BCF', 'expense', 4),
(0, '住房', 'Home', '#7BC67E', 'expense', 5),
(0, '医疗', 'Heart', '#FF6B6B', 'expense', 6),
(0, '教育', 'BookOpen', '#6C9BCF', 'expense', 7),
(0, '娱乐', 'Film', '#B39DDB', 'expense', 8),
(0, '服饰', 'Shirt', '#FFD166', 'expense', 9),
(0, '美容', 'Smile', '#FF8C69', 'expense', 10),
(0, '通讯', 'Smartphone', '#6C9BCF', 'expense', 11),
(0, '其他', 'MoreHorizontal', '#BFB3B3', 'expense', 12),
(0, '工资', 'DollarSign', '#7BC67E', 'income', 1),
(0, '副业', 'TrendingUp', '#7BC67E', 'income', 2),
(0, '理财', 'PieChart', '#7BC67E', 'income', 3),
(0, '其他收入', 'Plus', '#BFB3B3', 'income', 4);