feat: 优化通知处理逻辑,忽略未命中规则的通知并更新规则同步说明
This commit is contained in:
@@ -98,10 +98,17 @@ export const useTransactionEntry = () => {
|
||||
const queue = await fetchNotificationQueue()
|
||||
const manualQueue = []
|
||||
for (const item of queue) {
|
||||
const { transaction, rule, requiresConfirmation } = transformNotificationToTransaction(
|
||||
item,
|
||||
{ ruleSet: ruleSet.value, date: item.createdAt },
|
||||
)
|
||||
const { transaction, rule, requiresConfirmation } = transformNotificationToTransaction(item, {
|
||||
ruleSet: ruleSet.value,
|
||||
date: item.createdAt,
|
||||
})
|
||||
|
||||
// 如果没有任何规则命中(rule 为空),默认忽略该通知,避免系统通知等噪音进入待确认列表
|
||||
if (!rule) {
|
||||
await acknowledgeNotification(item.id)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!requiresConfirmation) {
|
||||
await transactionStore.addTransaction(transaction)
|
||||
await acknowledgeNotification(item.id)
|
||||
|
||||
@@ -3,8 +3,8 @@ const fallbackRules = [
|
||||
{
|
||||
id: 'alipay-expense',
|
||||
label: '支付宝消费',
|
||||
channels: ['支付宝', '支付', 'Alipay'],
|
||||
keywords: ['支付', '扣款', '支出', '已消费', '成功支付'],
|
||||
channels: ['支付宝', 'Alipay'],
|
||||
keywords: ['支付', '扣款', '支出', '消费', '成功支付'],
|
||||
direction: 'expense',
|
||||
defaultCategory: 'Food',
|
||||
autoCapture: false,
|
||||
@@ -14,7 +14,7 @@ const fallbackRules = [
|
||||
{
|
||||
id: 'alipay-income',
|
||||
label: '支付宝收款',
|
||||
channels: ['支付宝', '支付', 'Alipay'],
|
||||
channels: ['支付宝', 'Alipay'],
|
||||
keywords: ['到账', '收入', '收款', '入账'],
|
||||
direction: 'income',
|
||||
defaultCategory: 'Income',
|
||||
@@ -25,7 +25,7 @@ const fallbackRules = [
|
||||
{
|
||||
id: 'wechat-expense',
|
||||
label: '微信消费',
|
||||
channels: ['微信', '微信支付', 'WeChat'],
|
||||
channels: ['微信支付', '微信', 'WeChat'],
|
||||
keywords: ['支付', '支出', '扣款', '消费成功'],
|
||||
direction: 'expense',
|
||||
defaultCategory: 'Groceries',
|
||||
@@ -36,7 +36,7 @@ const fallbackRules = [
|
||||
{
|
||||
id: 'wechat-income',
|
||||
label: '微信收款',
|
||||
channels: ['微信', '微信支付', 'WeChat'],
|
||||
channels: ['微信支付', '微信', 'WeChat'],
|
||||
keywords: ['收款', '到账', '入账', '转入'],
|
||||
direction: 'income',
|
||||
defaultCategory: 'Income',
|
||||
@@ -98,28 +98,30 @@ const fallbackRules = [
|
||||
id: 'transit-card-expense',
|
||||
label: '公交出行',
|
||||
channels: [
|
||||
'公交',
|
||||
'地铁',
|
||||
'乘车码',
|
||||
'交通联合',
|
||||
'一卡通',
|
||||
'杭州通互联互通卡',
|
||||
'杭州通',
|
||||
'北京一卡通',
|
||||
'上海公共交通卡',
|
||||
'深圳通',
|
||||
'苏州公交卡',
|
||||
'杭州通互联互通卡',
|
||||
'交通联合',
|
||||
'乘车码',
|
||||
'地铁',
|
||||
'公交',
|
||||
'Mi Pay',
|
||||
'Huawei Pay',
|
||||
'华为钱包',
|
||||
'小米智能卡',
|
||||
'小米钱包',
|
||||
'OPPO 钱包',
|
||||
],
|
||||
keywords: ['扣款', '支出', '乘车', '刷卡', '过闸','行程中','地铁','公交','进站','出站'],
|
||||
// 对公交卡类通知,只要来源匹配即可视为出行支出,不强制正文关键字
|
||||
keywords: [],
|
||||
direction: 'expense',
|
||||
defaultCategory: 'Transport',
|
||||
autoCapture: true,
|
||||
// 公交卡类通知里商户往往不重要,给一个统一名称
|
||||
// 优先从正文中提取「文三路->祥园路」之类的起点-终点信息作为“商户”
|
||||
merchantPattern: /([\u4e00-\u9fa5]{2,}\s*(?:-|—|>|→|至|到)\s*[\u4e00-\u9fa5]{2,})/,
|
||||
// 若无法提取线路,则使用统一名称
|
||||
defaultMerchant: '公交/地铁出行',
|
||||
},
|
||||
]
|
||||
@@ -128,7 +130,19 @@ let cachedRules = [...fallbackRules]
|
||||
let remoteRuleFetcher = async () => []
|
||||
|
||||
/**
|
||||
* 允许后续接入服务端规则同步,只需在应用启动时调用并注入实际的 fetch 函数
|
||||
* 允许后续接入服务端规则同步,只需在应用启动时调用并注入实际的 fetch 函数。
|
||||
* 服务端可以返回一组规则对象,形如:
|
||||
* {
|
||||
* id: string;
|
||||
* label?: string;
|
||||
* enabled?: boolean; // false 时在本地完全忽略此规则
|
||||
* priority?: number; // 预留优先级字段(当前仅在服务端使用)
|
||||
* channels?: string[]; // 匹配通知来源(标题 / 包名)
|
||||
* keywords?: string[]; // 匹配通知正文的关键字
|
||||
* direction?: 'income' | 'expense';
|
||||
* defaultCategory?: string;
|
||||
* autoCapture?: boolean;
|
||||
* }
|
||||
* @param {(currentRules: Array) => Promise<Array>} fetcher
|
||||
*/
|
||||
export const setRemoteRuleFetcher = (fetcher) => {
|
||||
@@ -178,18 +192,28 @@ const extractMerchant = (text, rule) => {
|
||||
const m = text.match(rule.merchantPattern)
|
||||
if (m?.[1]) return m[1].trim()
|
||||
}
|
||||
const generic = text.match(/向\s*([\u4e00-\u9fa5A-Za-z0-9\-\s&]+)/)
|
||||
if (generic?.[1]) return generic[1].trim()
|
||||
|
||||
const genericPatterns = [
|
||||
/向\s*([\u4e00-\u9fa5A-Za-z0-9\-\s&]+)/,
|
||||
/商户[::]?\s*([\u4e00-\u9fa5A-Za-z0-9\-\s&]+)/,
|
||||
]
|
||||
|
||||
for (const pattern of genericPatterns) {
|
||||
const m = text.match(pattern)
|
||||
if (m?.[1]) return m[1].trim()
|
||||
}
|
||||
|
||||
const parts = text.split(/:|:/)
|
||||
if (parts.length > 1) {
|
||||
const candidate = parts[1].split(/[,,\s]/)[0]
|
||||
if (candidate) return candidate.trim()
|
||||
}
|
||||
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
const matchRule = (notification, rule) => {
|
||||
if (!rule) return false
|
||||
if (!rule || rule.enabled === false) return false
|
||||
const channel = notification?.channel || ''
|
||||
const text = notification?.text || ''
|
||||
const channelHit = !rule.channels?.length || rule.channels.some((item) => channel.includes(item))
|
||||
@@ -234,4 +258,3 @@ export const transformNotificationToTransaction = (notification, options = {}) =
|
||||
|
||||
return { transaction, rule, requiresConfirmation }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user