feat: 智能分类建议 — 根据金额/备注/历史自动推荐分类
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user