feat: add all route modules (auth, records, stats, recurring, categories, export, search)
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const db = require('../config/database');
|
||||
const { generateToken } = require('../middleware/auth');
|
||||
const { asyncHandler, AppError } = require('../middleware/errorHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 注册
|
||||
router.post('/register', asyncHandler(async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('请填写用户名和密码', 400, 'MISSING_FIELDS');
|
||||
}
|
||||
|
||||
if (username.length < 3 || username.length > 20) {
|
||||
throw new AppError('用户名需3-20个字符', 400, 'INVALID_USERNAME');
|
||||
}
|
||||
|
||||
if (password.length < 6 || password.length > 20) {
|
||||
throw new AppError('密码需6-20个字符', 400, 'INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const stmt = db.prepare('INSERT INTO users (username, password) VALUES (?, ?)');
|
||||
const result = stmt.run(username, hashedPassword);
|
||||
|
||||
const token = generateToken(result.lastInsertRowid);
|
||||
console.log(`User registered: ${username}`);
|
||||
|
||||
res.json({ token, username });
|
||||
}));
|
||||
|
||||
// 登录
|
||||
router.post('/login', asyncHandler(async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('请填写用户名和密码', 400, 'MISSING_FIELDS');
|
||||
}
|
||||
|
||||
const stmt = db.prepare('SELECT * FROM users WHERE username = ?');
|
||||
const user = stmt.get(username);
|
||||
|
||||
if (!user) {
|
||||
throw new AppError('用户名或密码错误', 400, 'INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
const validPassword = await bcrypt.compare(password, user.password);
|
||||
if (!validPassword) {
|
||||
throw new AppError('用户名或密码错误', 400, 'INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
const token = generateToken(user.id);
|
||||
console.log(`User logged in: ${username}`);
|
||||
|
||||
res.json({ token, username: user.username });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,98 @@
|
||||
const express = require('express');
|
||||
const db = require('../config/database');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { asyncHandler, AppError } = require('../middleware/errorHandler');
|
||||
const { validateCategoryPayload } = require('../utils/validators');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const defaultCategories = {
|
||||
expense: [
|
||||
{ name: '餐饮', icon: '🍜' },
|
||||
{ name: '买菜', icon: '🥬' },
|
||||
{ name: '交通', icon: '🚗' },
|
||||
{ name: '购物', icon: '🛒' },
|
||||
{ name: '娱乐', icon: '🎮' },
|
||||
{ name: '房租', icon: '🏠' },
|
||||
{ name: '房贷', icon: '🏦' },
|
||||
{ name: '水电', icon: '💧' },
|
||||
{ name: '衣服', icon: '👔' },
|
||||
{ name: '通讯', icon: '📱' },
|
||||
{ name: '日用品', icon: '🧴' },
|
||||
{ name: '美妆', icon: '💄' },
|
||||
{ name: '医疗', icon: '🏥' },
|
||||
{ name: '养娃', icon: '👶' },
|
||||
{ name: '家电', icon: '📺' },
|
||||
{ name: '家具', icon: '🛋️' },
|
||||
{ name: '装修', icon: '🔨' },
|
||||
{ name: '宠物', icon: '🐕' },
|
||||
{ name: '其他', icon: '📝' },
|
||||
],
|
||||
income: [
|
||||
{ name: '工资', icon: '💰' },
|
||||
{ name: '奖金', icon: '🎁' },
|
||||
{ name: '理财', icon: '📈' },
|
||||
{ name: '红包', icon: '🧧' },
|
||||
{ name: '其他', icon: '📝' },
|
||||
],
|
||||
};
|
||||
|
||||
// 获取分类列表
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const stmt = db.prepare('SELECT * FROM categories WHERE user_id = ? ORDER BY type, id');
|
||||
const customCategories = stmt.all(req.userId);
|
||||
|
||||
// 合并默认分类和自定义分类
|
||||
const categories = {
|
||||
expense: [
|
||||
...defaultCategories.expense.map((c) => ({ ...c, is_default: true })),
|
||||
...customCategories.filter((c) => c.type === 'expense'),
|
||||
],
|
||||
income: [
|
||||
...defaultCategories.income.map((c) => ({ ...c, is_default: true })),
|
||||
...customCategories.filter((c) => c.type === 'income'),
|
||||
],
|
||||
};
|
||||
|
||||
res.json(categories);
|
||||
}));
|
||||
|
||||
// 新增自定义分类
|
||||
router.post('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { type, name, icon } = req.body;
|
||||
|
||||
const validationError = validateCategoryPayload(req.body);
|
||||
if (validationError) {
|
||||
throw new AppError(validationError, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO categories (user_id, type, name, icon, is_default)
|
||||
VALUES (?, ?, ?, ?, 0)
|
||||
`);
|
||||
|
||||
const result = stmt.run(req.userId, type, name.trim(), icon.trim());
|
||||
console.log(`Category created: user=${req.userId}, id=${result.lastInsertRowid}`);
|
||||
|
||||
res.json({ id: result.lastInsertRowid, message: '添加成功' });
|
||||
}));
|
||||
|
||||
// 删除自定义分类
|
||||
router.delete('/:id', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const checkStmt = db.prepare('SELECT * FROM categories WHERE id = ? AND user_id = ? AND is_default = 0');
|
||||
const category = checkStmt.get(id, req.userId);
|
||||
|
||||
if (!category) {
|
||||
throw new AppError('分类不存在或无法删除', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
const stmt = db.prepare('DELETE FROM categories WHERE id = ? AND user_id = ?');
|
||||
stmt.run(id, req.userId);
|
||||
console.log(`Category deleted: user=${req.userId}, id=${id}`);
|
||||
|
||||
res.json({ message: '删除成功' });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,115 @@
|
||||
const express = require('express');
|
||||
const db = require('../config/database');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { asyncHandler, AppError } = require('../middleware/errorHandler');
|
||||
const { validateImportPayload, normalizeNote } = require('../utils/validators');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 导出数据
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const recordsStmt = db.prepare('SELECT * FROM records WHERE user_id = ? ORDER BY date DESC');
|
||||
const records = recordsStmt.all(req.userId);
|
||||
|
||||
const recurringStmt = db.prepare('SELECT * FROM recurring_bills WHERE user_id = ?');
|
||||
const recurring = recurringStmt.all(req.userId);
|
||||
|
||||
const categoriesStmt = db.prepare('SELECT * FROM categories WHERE user_id = ?');
|
||||
const categories = categoriesStmt.all(req.userId);
|
||||
|
||||
console.log(`Data exported: user=${req.userId}`);
|
||||
|
||||
res.json({
|
||||
version: '1.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
records,
|
||||
recurring,
|
||||
customCategories: categories,
|
||||
});
|
||||
}));
|
||||
|
||||
// 导入数据
|
||||
router.post('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { records: importRecords, recurring: importRecurring, customCategories: importCategories } = req.body;
|
||||
|
||||
const validationError = validateImportPayload(req.body);
|
||||
if (validationError) {
|
||||
throw new AppError(validationError, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
const importCount = { records: 0, recurring: 0, categories: 0 };
|
||||
|
||||
if (importRecords && Array.isArray(importRecords)) {
|
||||
const insertRecord = db.prepare(`
|
||||
INSERT INTO records (user_id, type, amount, category, date, note, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertMany = db.transaction((records) => {
|
||||
for (const r of records) {
|
||||
insertRecord.run(
|
||||
req.userId,
|
||||
r.type,
|
||||
r.amount,
|
||||
r.category.trim(),
|
||||
r.date,
|
||||
normalizeNote(r.note),
|
||||
r.created_at || new Date().toISOString()
|
||||
);
|
||||
}
|
||||
});
|
||||
insertMany(importRecords);
|
||||
importCount.records = importRecords.length;
|
||||
}
|
||||
|
||||
if (importRecurring && Array.isArray(importRecurring)) {
|
||||
const insertRecurring = db.prepare(`
|
||||
INSERT INTO recurring_bills (user_id, type, amount, category, day, note, is_active, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertRecurringMany = db.transaction((bills) => {
|
||||
for (const b of bills) {
|
||||
insertRecurring.run(
|
||||
req.userId,
|
||||
b.type,
|
||||
b.amount,
|
||||
b.category.trim(),
|
||||
b.day,
|
||||
normalizeNote(b.note),
|
||||
b.is_active !== undefined ? b.is_active : 1,
|
||||
b.created_at || new Date().toISOString()
|
||||
);
|
||||
}
|
||||
});
|
||||
insertRecurringMany(importRecurring);
|
||||
importCount.recurring = importRecurring.length;
|
||||
}
|
||||
|
||||
if (importCategories && Array.isArray(importCategories)) {
|
||||
const insertCategory = db.prepare(`
|
||||
INSERT INTO categories (user_id, type, name, icon, is_default, created_at)
|
||||
VALUES (?, ?, ?, ?, 0, ?)
|
||||
`);
|
||||
|
||||
const insertCategoryMany = db.transaction((cats) => {
|
||||
for (const c of cats) {
|
||||
insertCategory.run(
|
||||
req.userId,
|
||||
c.type,
|
||||
c.name.trim(),
|
||||
c.icon.trim(),
|
||||
c.created_at || new Date().toISOString()
|
||||
);
|
||||
}
|
||||
});
|
||||
insertCategoryMany(importCategories);
|
||||
importCount.categories = importCategories.length;
|
||||
}
|
||||
|
||||
console.log(`Data imported: user=${req.userId}, counts=${JSON.stringify(importCount)}`);
|
||||
|
||||
res.json({ message: '导入成功', count: importCount });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,116 @@
|
||||
const express = require('express');
|
||||
const db = require('../config/database');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { asyncHandler, AppError } = require('../middleware/errorHandler');
|
||||
const { validateRecordPayload, normalizeNote } = require('../utils/validators');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 获取账目列表
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { month, type } = req.query;
|
||||
|
||||
let query = 'SELECT * FROM records WHERE user_id = ?';
|
||||
const params = [req.userId];
|
||||
|
||||
if (month) {
|
||||
query += ' AND date LIKE ?';
|
||||
params.push(`${month}%`);
|
||||
}
|
||||
|
||||
if (type && type !== 'all') {
|
||||
query += ' AND type = ?';
|
||||
params.push(type);
|
||||
}
|
||||
|
||||
query += ' ORDER BY date DESC, id DESC';
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const records = stmt.all(...params);
|
||||
|
||||
res.json(records);
|
||||
}));
|
||||
|
||||
// 新增账目
|
||||
router.post('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { type, amount, category, date, note } = req.body;
|
||||
|
||||
const validationError = validateRecordPayload(req.body);
|
||||
if (validationError) {
|
||||
throw new AppError(validationError, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO records (user_id, type, amount, category, date, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(req.userId, type, amount, category.trim(), date, normalizeNote(note));
|
||||
console.log(`Record created: user=${req.userId}, id=${result.lastInsertRowid}`);
|
||||
|
||||
res.json({ id: result.lastInsertRowid, message: '添加成功' });
|
||||
}));
|
||||
|
||||
// 更新账目
|
||||
router.put('/:id', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { type, amount, category, date, note } = req.body;
|
||||
|
||||
const checkStmt = db.prepare('SELECT * FROM records WHERE id = ? AND user_id = ?');
|
||||
const record = checkStmt.get(id, req.userId);
|
||||
|
||||
if (!record) {
|
||||
throw new AppError('记录不存在', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
const validationError = validateRecordPayload(req.body);
|
||||
if (validationError) {
|
||||
throw new AppError(validationError, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE records SET type = ?, amount = ?, category = ?, date = ?, note = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
`);
|
||||
|
||||
stmt.run(type, amount, category.trim(), date, normalizeNote(note), id, req.userId);
|
||||
console.log(`Record updated: user=${req.userId}, id=${id}`);
|
||||
|
||||
res.json({ message: '更新成功' });
|
||||
}));
|
||||
|
||||
// 删除账目
|
||||
router.delete('/:id', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const checkStmt = db.prepare('SELECT * FROM records WHERE id = ? AND user_id = ?');
|
||||
const record = checkStmt.get(id, req.userId);
|
||||
|
||||
if (!record) {
|
||||
throw new AppError('记录不存在', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
const stmt = db.prepare('DELETE FROM records WHERE id = ? AND user_id = ?');
|
||||
stmt.run(id, req.userId);
|
||||
console.log(`Record deleted: user=${req.userId}, id=${id}`);
|
||||
|
||||
res.json({ message: '删除成功' });
|
||||
}));
|
||||
|
||||
// 批量删除指定分类的记录
|
||||
router.delete('/by-category/:category', authenticate, asyncHandler(async (req, res) => {
|
||||
const { category } = req.params;
|
||||
|
||||
if (!category || typeof category !== 'string' || !category.trim()) {
|
||||
throw new AppError('分类不能为空', 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
const stmt = db.prepare('DELETE FROM records WHERE category = ? AND user_id = ?');
|
||||
const result = stmt.run(category.trim(), req.userId);
|
||||
|
||||
console.log(`Records deleted by category: user=${req.userId}, category=${category}, count=${result.changes}`);
|
||||
|
||||
res.json({ message: `已删除 ${result.changes} 条记录` });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,108 @@
|
||||
const express = require('express');
|
||||
const db = require('../config/database');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { asyncHandler, AppError } = require('../middleware/errorHandler');
|
||||
const { validateRecurringPayload, normalizeNote } = require('../utils/validators');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 获取周期性账单
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const stmt = db.prepare('SELECT * FROM recurring_bills WHERE user_id = ? ORDER BY day ASC');
|
||||
const bills = stmt.all(req.userId);
|
||||
res.json(bills);
|
||||
}));
|
||||
|
||||
// 新增周期性账单
|
||||
router.post('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { type, amount, category, day, note } = req.body;
|
||||
|
||||
const validationError = validateRecurringPayload(req.body);
|
||||
if (validationError) {
|
||||
throw new AppError(validationError, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO recurring_bills (user_id, type, amount, category, day, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(req.userId, type, amount, category.trim(), day, normalizeNote(note));
|
||||
console.log(`Recurring bill created: user=${req.userId}, id=${result.lastInsertRowid}`);
|
||||
|
||||
res.json({ id: result.lastInsertRowid, message: '添加成功' });
|
||||
}));
|
||||
|
||||
// 更新周期性账单
|
||||
router.put('/:id', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { type, amount, category, day, note, is_active } = req.body;
|
||||
|
||||
const checkStmt = db.prepare('SELECT * FROM recurring_bills WHERE id = ? AND user_id = ?');
|
||||
const bill = checkStmt.get(id, req.userId);
|
||||
|
||||
if (!bill) {
|
||||
throw new AppError('账单不存在', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
const validationError = validateRecurringPayload(req.body);
|
||||
if (validationError) {
|
||||
throw new AppError(validationError, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE recurring_bills SET type = ?, amount = ?, category = ?, day = ?, note = ?, is_active = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
`);
|
||||
|
||||
stmt.run(type, amount, category.trim(), day, normalizeNote(note), is_active !== undefined ? is_active : 1, id, req.userId);
|
||||
console.log(`Recurring bill updated: user=${req.userId}, id=${id}`);
|
||||
|
||||
res.json({ message: '更新成功' });
|
||||
}));
|
||||
|
||||
// 删除周期性账单
|
||||
router.delete('/:id', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const checkStmt = db.prepare('SELECT * FROM recurring_bills WHERE id = ? AND user_id = ?');
|
||||
const bill = checkStmt.get(id, req.userId);
|
||||
|
||||
if (!bill) {
|
||||
throw new AppError('账单不存在', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
const stmt = db.prepare('DELETE FROM recurring_bills WHERE id = ? AND user_id = ?');
|
||||
stmt.run(id, req.userId);
|
||||
console.log(`Recurring bill deleted: user=${req.userId}, id=${id}`);
|
||||
|
||||
res.json({ message: '删除成功' });
|
||||
}));
|
||||
|
||||
// 应用周期性账单
|
||||
router.post('/:id/apply', authenticate, asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const checkStmt = db.prepare('SELECT * FROM recurring_bills WHERE id = ? AND user_id = ? AND is_active = 1');
|
||||
const bill = checkStmt.get(id, req.userId);
|
||||
|
||||
if (!bill) {
|
||||
throw new AppError('账单不存在或已禁用', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
// 创建当前日期的账目
|
||||
const today = new Date();
|
||||
const dateStr = today.toISOString().split('T')[0] + ' ' + today.toTimeString().slice(0, 5);
|
||||
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT INTO records (user_id, type, amount, category, date, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
insertStmt.run(req.userId, bill.type, bill.amount, bill.category, dateStr, bill.note || '');
|
||||
console.log(`Recurring bill applied: user=${req.userId}, billId=${id}`);
|
||||
|
||||
res.json({ message: '应用成功' });
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,27 @@
|
||||
const express = require('express');
|
||||
const db = require('../config/database');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { asyncHandler, AppError } = require('../middleware/errorHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 搜索账目
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { q } = req.query;
|
||||
|
||||
if (!q) {
|
||||
throw new AppError('请输入搜索关键词', 400, 'MISSING_QUERY');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM records
|
||||
WHERE user_id = ? AND (category LIKE ? OR note LIKE ?)
|
||||
ORDER BY date DESC, id DESC
|
||||
LIMIT 50
|
||||
`);
|
||||
|
||||
const records = stmt.all(req.userId, `%${q}%`, `%${q}%`);
|
||||
res.json(records);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,123 @@
|
||||
const express = require('express');
|
||||
const db = require('../config/database');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { asyncHandler, AppError } = require('../middleware/errorHandler');
|
||||
const { isValidMonth, VALID_STATS_PERIODS, VALID_STATS_TYPES } = require('../utils/validators');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 获取月度统计
|
||||
router.get('/', authenticate, asyncHandler(async (req, res) => {
|
||||
const { month } = req.query;
|
||||
|
||||
if (!month) {
|
||||
throw new AppError('请指定月份', 400, 'MISSING_MONTH');
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) as totalIncome,
|
||||
SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as totalExpense
|
||||
FROM records
|
||||
WHERE user_id = ? AND date LIKE ?
|
||||
`);
|
||||
|
||||
const result = stmt.get(req.userId, `${month}%`);
|
||||
|
||||
res.json({
|
||||
income: result.totalIncome || 0,
|
||||
expense: result.totalExpense || 0,
|
||||
balance: (result.totalIncome || 0) - (result.totalExpense || 0),
|
||||
});
|
||||
}));
|
||||
|
||||
// 收支趋势统计
|
||||
router.get('/trend', authenticate, asyncHandler(async (req, res) => {
|
||||
const { period = 'month', month } = req.query;
|
||||
|
||||
if (!VALID_STATS_PERIODS.includes(period)) {
|
||||
throw new AppError('统计周期错误', 400, 'INVALID_PERIOD');
|
||||
}
|
||||
|
||||
if (month && !isValidMonth(month)) {
|
||||
throw new AppError('月份格式错误', 400, 'INVALID_MONTH');
|
||||
}
|
||||
|
||||
let dateGroup;
|
||||
if (period === 'year') {
|
||||
dateGroup = "strftime('%Y', date)";
|
||||
} else if (period === 'week') {
|
||||
dateGroup = "strftime('%Y-%W', date)";
|
||||
} else {
|
||||
dateGroup = "strftime('%Y-%m', date)";
|
||||
}
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
${dateGroup} as period,
|
||||
SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) as income,
|
||||
SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as expense
|
||||
FROM records
|
||||
WHERE user_id = ?
|
||||
`;
|
||||
const params = [req.userId];
|
||||
|
||||
if (month) {
|
||||
query += ' AND date LIKE ?';
|
||||
params.push(`${month}%`);
|
||||
}
|
||||
|
||||
query += `
|
||||
GROUP BY ${dateGroup}
|
||||
ORDER BY period DESC
|
||||
LIMIT 12
|
||||
`;
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const result = stmt.all(...params);
|
||||
res.json(result.reverse());
|
||||
}));
|
||||
|
||||
// 分类统计
|
||||
router.get('/category', authenticate, asyncHandler(async (req, res) => {
|
||||
const { month, type = 'all' } = req.query;
|
||||
|
||||
if (!VALID_STATS_TYPES.includes(type)) {
|
||||
throw new AppError('统计类型错误', 400, 'INVALID_TYPE');
|
||||
}
|
||||
|
||||
if (month && !isValidMonth(month)) {
|
||||
throw new AppError('月份格式错误', 400, 'INVALID_MONTH');
|
||||
}
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
category,
|
||||
SUM(amount) as total,
|
||||
type
|
||||
FROM records
|
||||
WHERE user_id = ?
|
||||
`;
|
||||
const params = [req.userId];
|
||||
|
||||
if (month) {
|
||||
query += ' AND date LIKE ?';
|
||||
params.push(`${month}%`);
|
||||
}
|
||||
|
||||
if (type !== 'all') {
|
||||
query += ' AND type = ?';
|
||||
params.push(type);
|
||||
}
|
||||
|
||||
query += `
|
||||
GROUP BY category, type
|
||||
ORDER BY total DESC
|
||||
`;
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const result = stmt.all(...params);
|
||||
res.json(result);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user