fix:数据库错误

This commit is contained in:
2025-11-27 10:44:27 +08:00
parent b23514cfe6
commit 074a7f1ff0
9 changed files with 346 additions and 93 deletions

View File

@@ -0,0 +1,143 @@
const fallbackRules = [
{
id: 'alipay-expense',
label: '支付宝消费',
channels: ['支付宝', 'Alipay'],
keywords: ['支付', '扣款', '支出'],
direction: 'expense',
defaultCategory: 'Food',
autoCapture: false,
merchantPattern: /向\s?([\u4e00-\u9fa5A-Za-z0-9\s&]+)/i,
},
{
id: 'alipay-income',
label: '支付宝收入',
channels: ['支付宝', 'Alipay'],
keywords: ['到账', '收入'],
direction: 'income',
defaultCategory: 'Income',
autoCapture: true,
merchantPattern: /来自\s?([\u4e00-\u9fa5A-Za-z0-9\s&]+)/i,
},
{
id: 'wechat-expense',
label: '微信消费',
channels: ['微信', 'WeChat'],
keywords: ['支付', '支出', '扣款'],
direction: 'expense',
defaultCategory: 'Groceries',
autoCapture: false,
merchantPattern: /([\u4e00-\u9fa5A-Za-z0-9\s&]+)/,
},
{
id: 'bank-income',
label: '工资到账',
channels: ['招商银行', '中国银行', '建设银行'],
keywords: ['工资', '薪资', '到账'],
direction: 'income',
defaultCategory: 'Income',
autoCapture: true,
},
]
let cachedRules = [...fallbackRules]
let remoteRuleFetcher = async () => []
/**
* 允许后续接入服务端规则同步,只需在应用启动时调用并注入实际 fetch 函数
* @param {(currentRules: Array) => Promise<Array>} fetcher
*/
export const setRemoteRuleFetcher = (fetcher) => {
remoteRuleFetcher = typeof fetcher === 'function' ? fetcher : remoteRuleFetcher
}
const dedupeRules = (rules) => {
const bucket = new Map()
fallbackRules.forEach((rule) => bucket.set(rule.id, { ...rule }))
rules.forEach((rule) => {
const prev = bucket.get(rule.id) || {}
bucket.set(rule.id, { ...prev, ...rule })
})
return Array.from(bucket.values())
}
export const loadNotificationRules = async () => {
try {
const remote = await remoteRuleFetcher(cachedRules)
if (Array.isArray(remote) && remote.length > 0) {
cachedRules = dedupeRules(remote)
}
} catch (err) {
cachedRules = [...cachedRules]
console.warn('[rules] 使用本地规则,远程规则获取失败:', err)
}
return cachedRules
}
export const getCachedNotificationRules = () => cachedRules
const extractAmount = (text = '') => {
const match = text.match(/(?:¥|¥)\s?(-?\d+(?:\.\d{1,2})?)/)
if (match?.[1]) {
return Math.abs(parseFloat(match[1]))
}
const loose = text.match(/(-?\d+(?:\.\d{1,2})?)/)
return loose?.[1] ? Math.abs(parseFloat(loose[1])) : 0
}
const extractMerchant = (text, rule) => {
if (rule?.merchantPattern instanceof RegExp) {
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 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
const channel = notification?.channel || ''
const text = notification?.text || ''
const channelHit = !rule.channels?.length || rule.channels.some((item) => channel.includes(item))
const keywordHit = !rule.keywords?.length || rule.keywords.some((word) => text.includes(word))
return channelHit && keywordHit
}
/**
* 根据当前规则集生成交易草稿,并返回命中的规则信息
* @param {{ id: string, channel: string, text: string, createdAt: string }} notification
* @param {{ ruleSet?: Array, overrides?: object }} options
* @returns {{ transaction: import('../types/transaction').Transaction, rule?: object, requiresConfirmation: boolean }}
*/
export const transformNotificationToTransaction = (notification, options = {}) => {
const ruleSet = options.ruleSet?.length ? options.ruleSet : cachedRules
const rule = ruleSet.find((item) => matchRule(notification, item))
const amountFromText = extractAmount(notification?.text)
const direction = options.direction || rule?.direction || 'expense'
const normalizedAmount = direction === 'income' ? Math.abs(amountFromText) : -Math.abs(amountFromText)
const merchant =
options.merchant || extractMerchant(notification?.text || '', rule) || 'Unknown'
const date = options.date || notification?.createdAt || new Date().toISOString()
const transaction = {
id: options.id,
merchant,
category: options.category || rule?.defaultCategory || 'Uncategorized',
amount: Number.isFinite(normalizedAmount) ? normalizedAmount : 0,
date: new Date(date).toISOString(),
note: notification?.text || '',
syncStatus: 'pending',
}
const requiresConfirmation =
options.requiresConfirmation ??
!(rule?.autoCapture && transaction.amount !== 0 && merchant !== 'Unknown')
return { transaction, rule, requiresConfirmation }
}

View File

@@ -0,0 +1,64 @@
import { v4 as uuidv4 } from 'uuid'
let remoteNotificationFetcher = async () => []
let remoteNotificationAcknowledger = async () => {}
/**
* 本地模拟通知缓存,真实环境可以由原生插件 push
*/
let localNotificationQueue = [
{
id: uuidv4(),
channel: '支付宝',
text: '支付宝提醒:你向 瑞幸咖啡 支付 ¥18.00',
createdAt: new Date().toISOString(),
},
{
id: uuidv4(),
channel: '微信支付',
text: '微信支付:美团外卖 实付 ¥46.50 · 订单已送达',
createdAt: new Date(Date.now() - 1000 * 60 * 8).toISOString(),
},
{
id: uuidv4(),
channel: '招商银行',
text: '招商银行:工资收入 ¥12500.00 已到账',
createdAt: new Date(Date.now() - 1000 * 60 * 32).toISOString(),
},
]
export const setRemoteNotificationFetcher = (fetcher) => {
remoteNotificationFetcher = typeof fetcher === 'function' ? fetcher : remoteNotificationFetcher
}
export const setRemoteNotificationAcknowledger = (fn) => {
remoteNotificationAcknowledger = typeof fn === 'function' ? fn : remoteNotificationAcknowledger
}
const sortByCreatedAtDesc = (a, b) =>
new Date(b.createdAt || b.id) - new Date(a.createdAt || a.id)
export const fetchNotificationQueue = async () => {
const remote = await remoteNotificationFetcher()
const composed = [...localNotificationQueue, ...(Array.isArray(remote) ? remote : [])]
return composed.sort(sortByCreatedAtDesc)
}
export const pushLocalNotification = (payload) => {
const entry = {
id: payload?.id || uuidv4(),
createdAt: payload?.createdAt || new Date().toISOString(),
channel: payload?.channel || '模拟',
text: payload?.text || '',
}
localNotificationQueue = [entry, ...localNotificationQueue]
}
export const acknowledgeNotification = async (id) => {
localNotificationQueue = localNotificationQueue.filter((item) => item.id !== id)
try {
await remoteNotificationAcknowledger(id)
} catch (error) {
console.warn('[notifications] 远程确认失败,将在下次同步重试', error)
}
}

View File

@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid'
import { getDb } from '../lib/sqlite'
import { getDb, saveDbToStore } from '../lib/sqlite'
const statusMap = {
pending: 0,
@@ -63,6 +63,8 @@ export const insertTransaction = async (payload) => {
],
)
await saveDbToStore()
return {
id,
...sanitized,
@@ -89,6 +91,8 @@ export const updateTransaction = async (payload) => {
],
)
await saveDbToStore()
const result = await db.query(
`
SELECT id, amount, merchant, category, date, note, sync_status
@@ -105,4 +109,5 @@ export const updateTransaction = async (payload) => {
export const deleteTransaction = async (id) => {
const db = await getDb()
await db.run(`DELETE FROM transactions WHERE id = ?`, [id])
await saveDbToStore()
}