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