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:
17
server/src/db/connection.ts
Normal file
17
server/src/db/connection.ts
Normal 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
51
server/src/db/init.ts
Normal 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
48
server/src/db/schema.sql
Normal 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
17
server/src/db/seed.sql
Normal 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);
|
||||
Reference in New Issue
Block a user