206 lines
7.3 KiB
TypeScript
206 lines
7.3 KiB
TypeScript
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
|