自动记账安全说明 与代码公开

很多用户关心自动记账到底在后台做了什么,会不会读取聊天内容、支付密码或者银行卡信息。为了让你用得更放心,小账 Lite 将核心的 自动记账解析逻辑公开展示,用最直接的方式告诉你:我们只关注 与记账相关的金额、类型和来源,不会碰你不愿意被看到的隐私内容。

只监听支付通知

自动记账只会处理来自 微信支付、支付宝 等支付类通知,不会读取聊天记录、朋友圈、图片等内容。逻辑中通过 packageName 精确判断来源。

只解析金额和方向

我们只从通知文本中提取 金额、是“收入还是支出”、来自微信还是支付宝,不会提取银行卡号、手机号、支付密码等任何敏感字段。

本地解析、可控开关

所有解析逻辑都在本地完成,并且有 独立的开关和自定义规则。你可以只开启微信、只开启支付宝,甚至完全关闭自动记账。

自动记账核心代码片段

语言为 Dart,运行在 App 本地,并不会把原始通知内容上传到服务器。

Expense parseTransactionText(String text, String type2) {
  double amount = 0.0;
  String type = '';
  Map<String, String> nameObj = {
    '支出': '其他',
    '收入': '收入',
    '收款': '收入',
    '支付': '其他',
    'wx': '微信',
    'zfb': '支付宝',
  };

  try {
    if (text.contains('收款') || text.contains('收入')) {
      type = '收款';
    } else if (text.contains('支出')) {
      type = '支出';
    } else if (text.contains('支付')) {
      type = '支出';
    }

    RegExp amountRegExp = RegExp(r'(\d+\.\d{2})\s*元');
    Match? amountMatch = amountRegExp.firstMatch(text);

    if (amountMatch != null) {
      amount = double.parse(amountMatch.group(1)!);
    } else {
      RegExp fallbackAmountRegExp =
          RegExp(r'[¥¥]?\s*(\d+\.\d{2})(?=\D|$)');
      Match? fallbackMatch = fallbackAmountRegExp.firstMatch(text);
      if (fallbackMatch != null) {
        amount = double.parse(fallbackMatch.group(1)!);
      }
    }
  } catch (e) {
    print('解析错误: $e');
  }

  return Expense(
    amount: amount,
    category: nameObj[type] ?? '其他',
    description: "AI自动记录-" + nameObj[type2]!,
    date: DateTime.now(),
  );
}

Future<void> _handleAutoExpenseNotification(Map<String, dynamic> data) async {
  final nativeHandled = data["nativeHandled"] == true;
  if (nativeHandled == true) {
    return;
  }
  final prefs = await SharedPreferences.getInstance();
  List<dynamic> customRules = [];
  final rawRules = prefs.getString('auto_book_custom_rules_v1');
  if (rawRules != null && rawRules.isNotEmpty) {
    try {
      customRules = jsonDecode(rawRules) as List<dynamic>;
    } catch (_) {
      customRules = [];
    }
  }

  final wechatEnabled = prefs.getBool('auto_book_wechat_enabled') ?? true;
  final alipayEnabled = prefs.getBool('auto_book_alipay_enabled') ?? true;
  final packageName = data["packageName"] ?? "";
  String text = data["text"] ?? "";
  final normalized = _normalizeText(text);
  final matchedCustom = _matchesCustomRules(normalized, customRules);

  if (packageName != 'com.tencent.mm' &&
      packageName != 'com.eg.android.AlipayGphone' &&
      !matchedCustom) {
    return;
  }

  final isWeChat = packageName == 'com.tencent.mm';
  if (isWeChat && !matchedCustom && !_isNormalWeChatPayNotification(normalized)) {
    return;
  }
  if (isWeChat && !wechatEnabled && !matchedCustom) {
    return;
  }
  if (!isWeChat && !alipayEnabled && !matchedCustom) {
    return;
  }

  final title = (data["title"] ?? "").toString();
  final postTime = (data["postTime"] ?? "").toString();
  final id = (data["id"] ?? "").toString();

  String sig;
  Expense expense;

  if (isWeChat) {
    expense = parseTransactionText(normalized, 'wx');
  } else {
    expense = parseTransactionText(normalized, 'zfb');
    if (expense.amount == 0.0) {
      expense = parseTransactionText(title, 'zfb');
    }
  }

  if (expense.amount == 0.0) {
    return;
  }

  final direction = expense.category == '收入' ? 'in' : 'out';
  String source;
  if (matchedCustom) {
    source = 'custom';
  } else if (isWeChat) {
    source = 'wx';
  } else {
    source = packageName;
  }

  sig = '$source|$direction|${expense.amount.toStringAsFixed(2)}';
  if (_isDuplicate(sig)) {
    return;
  }

  final processedExpense = await _maybeEditAutoExpenseRemark(expense);
  final toAdd = processedExpense ?? expense;
  if (!mounted) {
    return;
  }

  Provider.of<ExpenseProvider>(context, listen: false).addExpense(toAdd);
  await _updateForegroundTodayTotal();
  _markProcessed(sig);

  final ttsEnabled = prefs.getBool('tts_enabled') ?? false;
  if (ttsEnabled) {
    if (expense.category == '其他') {
      await audiotext(
        "已支出${expense.amount.toStringAsFixed(2)}元",
        onError: (msg) {
          print("TTS错误: $msg");
        },
      );
    } else {
      await audiotext(
        "已收入${expense.amount.toStringAsFixed(2)}元",
        onError: (msg) {
          print("TTS错误: $msg");
        },
      );
    }
  }
}

这段代码在安全上做了哪些事情?

1)严格限制来源: 只有包名是 微信支付(com.tencent.mm)、 支付宝(com.eg.android.AlipayGphone) 或你自己配置的自定义规则才会被处理,其它 App 的通知全部直接忽略。

2)用户可控开关: 通过 auto_book_wechat_enabled 和 auto_book_alipay_enabled 两个开关,可以单独关闭微信或支付宝的自动记账。

3)只解析金额与收支方向: 代码里用正则 (\d+\.\d{2}) 元 去提取金额,只关心类似 “88.88 元” 这样的数字,不会把整条通知内容存起来。

4)重复拦截机制: 通过 _isDuplicate(sig) 生成签名,防止同一笔通知被多次记账。

5)本地备注可编辑: 在记账前调用 _maybeEditAutoExpenseRemark ,让你有机会本地修改备注,所有处理都在本机完成。

6)语音播报可选: 只有在你打开 tts_enabled 的情况下, 才会播报 “已支出/已收入 xx 元”,没有任何额外信息被朗读或上报。

数据存储与上传说明

自动记账生成的账单会以 标准 JSON 格式 保存到本地数据库中,如果你开启云端同步,数据会在 加密通道 中上传,仅包含记账必需的字段(金额、分类、时间、来源),不会上传原始通知全文。

如果你仍然对隐私有顾虑,可以选择 关闭自动记账功能,仅使用手动记账, 小账 Lite 的所有核心功能依然可以使用。