feat: 智能分类建议 — 根据金额/备注/历史自动推荐分类

This commit is contained in:
2026-06-08 17:21:10 +08:00
parent 057c69800e
commit 6908ffed0e
3 changed files with 176 additions and 2 deletions

View File

@@ -170,4 +170,99 @@ router.get('/trend', async (req: AuthRequest, res: Response) => {
}
})
/** 智能分类建议:根据历史记录的金额和备注推荐分类 */
router.get('/suggest-category', async (req: AuthRequest, res: Response) => {
try {
const userId = req.userId
const { amount, type = 'expense', note = '' } = req.query
if (!amount || !['expense', 'income'].includes(type as string)) {
return res.status(400).json({ code: 40001, message: '参数错误' })
}
const amt = parseInt(amount as string)
const noteStr = (note as string).trim()
// 1. 关键词匹配:从备注中提取关键词,查找历史记录中最常用的分类
let suggestedCategory: any = null
let confidence = 0
if (noteStr) {
// 提取关键词取前4个字符以上的词
const keywords = noteStr.replace(/[,。.!?\s]+/g, ' ').split(' ').filter(w => w.length >= 1)
if (keywords.length > 0) {
const likeClauses = keywords.map(() => 't.note LIKE ?').join(' OR ')
const likeParams = keywords.map(k => `%${k}%`)
const [rows] = await pool.query(
`SELECT t.category_id, c.name, c.color, COUNT(*) as cnt
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.user_id = ? AND t.type = ? AND (${likeClauses})
GROUP BY t.category_id, c.name, c.color
ORDER BY cnt DESC
LIMIT 1`,
[userId, type, ...likeParams]
)
const match = (rows as any[])[0]
if (match) {
suggestedCategory = { id: match.category_id, name: match.name, color: match.color }
// 置信度基于匹配次数
confidence = Math.min(90, 40 + match.cnt * 15)
}
}
}
// 2. 金额相近匹配±30%
if (!suggestedCategory) {
const minAmt = Math.floor(amt * 0.7)
const maxAmt = Math.ceil(amt * 1.3)
const [rows] = await pool.query(
`SELECT t.category_id, c.name, c.color, COUNT(*) as cnt
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.user_id = ? AND t.type = ? AND t.amount BETWEEN ? AND ?
GROUP BY t.category_id, c.name, c.color
ORDER BY cnt DESC
LIMIT 1`,
[userId, type, minAmt, maxAmt]
)
const match = (rows as any[])[0]
if (match) {
suggestedCategory = { id: match.category_id, name: match.name, color: match.color }
confidence = Math.min(70, 20 + match.cnt * 10)
}
}
// 3. 最近使用的分类(兜底)
if (!suggestedCategory) {
const [rows] = await pool.query(
`SELECT t.category_id, c.name, c.color
FROM transactions t
LEFT JOIN categories c ON t.category_id = c.id
WHERE t.user_id = ? AND t.type = ?
ORDER BY t.created_at DESC
LIMIT 1`,
[userId, type]
)
const match = (rows as any[])[0]
if (match) {
suggestedCategory = { id: match.category_id, name: match.name, color: match.color }
confidence = 20
}
}
res.json({
code: 0,
data: {
category: suggestedCategory,
confidence
}
})
} catch (err) {
console.error('[Stats] suggest-category error:', err)
res.status(500).json({ code: 50000, message: '服务器错误' })
}
})
export default router