Files
xiaocai/server/src/routes/recurring.ts

206 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Router } from 'express'
import pool from '../db/connection'
import { authMiddleware } from '../middleware/auth'
const router = Router()
router.use(authMiddleware)
/** 获取用户的周期模板列表 */
router.get('/', async (req, res) => {
try {
const userId = (req as any).userId
const [rows] = await pool.execute(
`SELECT rt.*, c.name as category_name, c.icon as category_icon, c.color as category_color
FROM recurring_templates rt
LEFT JOIN categories c ON rt.category_id = c.id
WHERE rt.user_id = ?
ORDER BY rt.is_active DESC, rt.next_date ASC`,
[userId]
)
res.json({ code: 0, data: rows })
} catch (error) {
console.error('Get recurring templates error:', error)
res.status(500).json({ code: 50000, message: '获取失败' })
}
})
/** 创建周期模板 */
router.post('/', async (req, res) => {
try {
const userId = (req as any).userId
const { amount, type, category_id, note, frequency, next_date, end_date, group_id } = req.body
if (!amount || amount <= 0) {
return res.status(400).json({ code: 40001, message: '请输入金额' })
}
if (!type || !['expense', 'income'].includes(type)) {
return res.status(400).json({ code: 40001, message: '请选择类型' })
}
if (!category_id) {
return res.status(400).json({ code: 40001, message: '请选择分类' })
}
if (!frequency || !['weekly', 'monthly', 'yearly'].includes(frequency)) {
return res.status(400).json({ code: 40001, message: '请选择周期' })
}
if (!next_date) {
return res.status(400).json({ code: 40001, message: '请选择开始日期' })
}
const [result] = await pool.execute(
`INSERT INTO recurring_templates (user_id, amount, type, category_id, note, frequency, next_date, end_date, group_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[userId, amount, type, category_id, note || '', frequency, next_date, end_date || null, group_id || null]
)
res.json({ code: 0, data: { id: (result as any).insertId } })
} catch (error) {
console.error('Create recurring template error:', error)
res.status(500).json({ code: 50000, message: '创建失败' })
}
})
/** 更新周期模板 */
router.put('/:id', async (req, res) => {
try {
const userId = (req as any).userId
const { id } = req.params
const { amount, type, category_id, note, frequency, next_date, end_date, is_active } = req.body
// 校验所有权
const [rows] = await pool.execute(
'SELECT id FROM recurring_templates WHERE id = ? AND user_id = ?',
[id, userId]
)
if ((rows as any[]).length === 0) {
return res.status(404).json({ code: 40400, message: '模板不存在' })
}
const updates: string[] = []
const params: any[] = []
if (amount !== undefined) { updates.push('amount = ?'); params.push(amount) }
if (type !== undefined) { updates.push('type = ?'); params.push(type) }
if (category_id !== undefined) { updates.push('category_id = ?'); params.push(category_id) }
if (note !== undefined) { updates.push('note = ?'); params.push(note) }
if (frequency !== undefined) { updates.push('frequency = ?'); params.push(frequency) }
if (next_date !== undefined) { updates.push('next_date = ?'); params.push(next_date) }
if (end_date !== undefined) { updates.push('end_date = ?'); params.push(end_date) }
if (is_active !== undefined) { updates.push('is_active = ?'); params.push(is_active) }
if (updates.length === 0) {
return res.status(400).json({ code: 40001, message: '无更新字段' })
}
params.push(id, userId)
await pool.execute(
`UPDATE recurring_templates SET ${updates.join(', ')} WHERE id = ? AND user_id = ?`,
params
)
res.json({ code: 0 })
} catch (error) {
console.error('Update recurring template error:', error)
res.status(500).json({ code: 50000, message: '更新失败' })
}
})
/** 删除周期模板 */
router.delete('/:id', async (req, res) => {
try {
const userId = (req as any).userId
const { id } = req.params
const [rows] = await pool.execute(
'SELECT id FROM recurring_templates WHERE id = ? AND user_id = ?',
[id, userId]
)
if ((rows as any[]).length === 0) {
return res.status(404).json({ code: 40400, message: '模板不存在' })
}
await pool.execute(
'DELETE FROM recurring_templates WHERE id = ? AND user_id = ?',
[id, userId]
)
res.json({ code: 0 })
} catch (error) {
console.error('Delete recurring template error:', error)
res.status(500).json({ code: 50000, message: '删除失败' })
}
})
/** 同步:检查所有活跃模板,生成到期交易 */
router.post('/sync', async (req, res) => {
try {
const userId = (req as any).userId
// 获取该用户所有活跃且到期next_date <= 今天)的模板
const [templates] = await pool.execute(
`SELECT * FROM recurring_templates
WHERE user_id = ? AND is_active = 1 AND next_date <= CURDATE()
ORDER BY next_date ASC`,
[userId]
)
let generated = 0
const today = new Date()
const todayStr = today.toISOString().slice(0, 10)
for (const t of templates as any[]) {
let next = new Date(t.next_date)
const end = t.end_date ? new Date(t.end_date) : null
// 生成所有错过的交易
while (next <= today) {
if (end && next > end) break
// 检查是否已生成过(避免重复)
const [existing] = await pool.execute(
`SELECT id FROM transactions
WHERE user_id = ? AND recurring_id = ? AND date = ?`,
[userId, t.id, next.toISOString().slice(0, 10)]
)
if ((existing as any[]).length === 0) {
await pool.execute(
`INSERT INTO transactions (user_id, amount, type, category_id, note, date, group_id, recurring_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[userId, t.amount, t.type, t.category_id, t.note,
next.toISOString().slice(0, 10), t.group_id, t.id]
)
generated++
}
// 推进到下一周期
if (t.frequency === 'weekly') {
next = new Date(next.getTime() + 7 * 86400000)
} else if (t.frequency === 'monthly') {
next = new Date(next.getFullYear(), next.getMonth() + 1, next.getDate())
} else if (t.frequency === 'yearly') {
next = new Date(next.getFullYear() + 1, next.getMonth(), next.getDate())
}
}
// 更新 next_date 到下一周期
let newNext: Date
if (t.frequency === 'weekly') {
newNext = new Date(new Date(t.next_date).getTime() + 7 * 86400000)
} else if (t.frequency === 'monthly') {
const d = new Date(t.next_date)
newNext = new Date(d.getFullYear(), d.getMonth() + 1, d.getDate())
} else {
const d = new Date(t.next_date)
newNext = new Date(d.getFullYear() + 1, d.getMonth(), d.getDate())
}
await pool.execute(
'UPDATE recurring_templates SET next_date = ? WHERE id = ?',
[newNext.toISOString().slice(0, 10), t.id]
)
}
res.json({ code: 0, data: { generated } })
} catch (error) {
console.error('Sync recurring error:', error)
res.status(500).json({ code: 50000, message: '同步失败' })
}
})
export default router