feat: 优化统一规则逻辑

This commit is contained in:
2026-03-12 10:04:29 +08:00
parent d0f68e07b7
commit bf15918136
17 changed files with 859 additions and 600 deletions

63
scripts/rule-smoke.mjs Normal file
View File

@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import { transformNotificationToTransaction } from '../src/services/notificationRuleService.js'
const cases = [
{
name: 'ignore transit operation notice',
notification: {
id: 'n1',
channel: '绍兴公交',
text: '黄酒小镇专线绍兴北站出站该线路已于2025年10月17日起暂停运营',
createdAt: '2026-03-11T00:00:00.000Z',
},
verify(result) {
assert.equal(result.rule, undefined)
},
},
{
name: 'capture transit expense with payment context',
notification: {
id: 'n2',
channel: '绍兴公交',
text: '乘车码扣费2元绍兴北站->黄酒小镇',
createdAt: '2026-03-11T00:00:00.000Z',
},
verify(result) {
assert.equal(result.rule?.id, 'transit-card-expense')
assert.equal(result.transaction.amount, -2)
},
},
{
name: 'capture bank expense',
notification: {
id: 'n3',
channel: '招商银行',
text: '您尾号1234信用卡消费支出3.23元,商户支付宝-上海拉扎斯信息科技有限公司',
createdAt: '2026-03-11T00:00:00.000Z',
},
verify(result) {
assert.equal(result.rule?.id, 'bank-expense')
assert.equal(result.transaction.amount, -3.23)
},
},
{
name: 'capture wechat income',
notification: {
id: 'n4',
channel: '微信支付',
text: '收款到账12.50元,来自张三',
createdAt: '2026-03-11T00:00:00.000Z',
},
verify(result) {
assert.equal(result.rule?.id, 'wechat-income')
assert.equal(result.transaction.amount, 12.5)
},
},
]
for (const testCase of cases) {
const result = transformNotificationToTransaction(testCase.notification)
testCase.verify(result)
}
console.log(`rule smoke passed: ${cases.length} cases`)

View File

@@ -0,0 +1,57 @@
import fs from 'node:fs'
import path from 'node:path'
import notificationRules from '../src/config/notificationRules.js'
const rootDir = path.resolve(import.meta.dirname, '..')
const targetFile = path.join(
rootDir,
'android',
'app',
'src',
'main',
'java',
'com',
'echo',
'app',
'notification',
'NotificationRuleData.kt',
)
const toKotlinString = (value) => JSON.stringify(value).replace(/\$/g, '\\$')
const toKotlinStringList = (values = []) =>
values.length ? `listOf(${values.map((value) => toKotlinString(value)).join(', ')})` : 'emptyList()'
const toKotlinRegex = (spec) => {
if (!spec?.pattern) return 'null'
const flags = spec.flags?.includes('i') ? ', setOf(RegexOption.IGNORE_CASE)' : ''
return `Regex(${toKotlinString(spec.pattern)}${flags})`
}
const toRuleBlock = (rule) => ` NotificationRuleDefinition(
id = ${toKotlinString(rule.id)},
label = ${toKotlinString(rule.label)},
channels = ${toKotlinStringList(rule.channels)},
keywords = ${toKotlinStringList(rule.keywords)},
requiredTextPatterns = ${toKotlinStringList(rule.requiredTextPatterns)},
direction = NotificationRuleDirection.${rule.direction === 'income' ? 'INCOME' : 'EXPENSE'},
defaultCategory = ${toKotlinString(rule.defaultCategory)},
autoCapture = ${rule.autoCapture ? 'true' : 'false'},
merchantPattern = ${toKotlinRegex(rule.merchantPattern)},
defaultMerchant = ${
rule.defaultMerchant ? toKotlinString(rule.defaultMerchant) : 'null'
},
)`
const content = `package com.echo.app.notification
// Generated from src/config/notificationRules.js by scripts/sync-notification-rules.mjs.
internal object NotificationRuleData {
val fallbackRules: List<NotificationRuleDefinition> = listOf(
${notificationRules.map(toRuleBlock).join(',\n')}
)
}
`
fs.writeFileSync(targetFile, content)
console.log(`[rules:sync] wrote ${path.relative(rootDir, targetFile)}`)