Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions android/app/src/main/kotlin/com/tntlikely/beecount/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
private val CHANNEL = "notification_channel"
private val RECORD_CHANNEL = "com.beecount.api/broadcast"
private val INSTALL_CHANNEL = "com.tntlikely.beecount/install"
private val SCREENSHOT_CHANNEL = "com.tntlikely.beecount/screenshot"
private val LOGGER_CHANNEL = "com.beecount.logger"
private val SHARE_CHANNEL = "com.tntlikely.beecount/share"
private val AUTO_BILL_ACTION = "com.tntlikely.beecount.AUTO_BILLING"

private var screenshotObserver: ScreenshotObserver? = null

Expand Down Expand Up @@ -136,6 +138,55 @@ class MainActivity: FlutterActivity() {
android.util.Log.e("MainActivity", "==========================================")
android.util.Log.e("MainActivity", "configureFlutterEngine 被调用!!!")
android.util.Log.e("MainActivity", "==========================================")

// 分别建立连接
val recordChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, RECORD_CHANNEL)
// 1. 定义广播接收器 (监听来自 Xposed 或 ADB 的广播)
val receiver = object : android.content.BroadcastReceiver() {
override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
val action = intent?.action
println("📡 [Native] 收到广播: $action")

if (action == AUTO_BILL_ACTION) {

// 【兼容取值】优先取 amount (BeeCount),没有则取 money (钱迹格式)
val rawAmount = intent.extras?.get("amount") ?: intent.extras?.get("money")
val amount = rawAmount.toString().replace(",", "").toDoubleOrNull() ?: 0.0

// 【兼容备注】优先取备注,备注为空则取分类名
val rawRemark = intent.getStringExtra("remark") ?: ""
val rawCateName = intent.getStringExtra("cateName") ?: ""
val finalRemark = if (rawRemark.isNotEmpty()) rawRemark else rawCateName

val type = intent.getIntExtra("type", 0)
val rawAccount = intent.getStringExtra("account") ?: ""

val args = mapOf(
"amount" to amount,
"remark" to finalRemark,
"category" to rawCateName,
"account" to rawAccount,
"type" to if (type == 1) "income" else "expense",
"timestamp" to System.currentTimeMillis()
)

// 通过之前定义的 recordChannel 发送
recordChannel.invokeMethod("addTransaction", args)
}
}
}

// 2. 注册过滤器
val filter = android.content.IntentFilter()
filter.addAction(AUTO_BILL_ACTION)


// 3. 注册接收器(适配 Android 14+ 安全要求)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(receiver, filter, android.content.Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(receiver, filter)
}

// 日志桥接的MethodChannel
val loggerChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, LOGGER_CHANNEL)
Expand Down
54 changes: 54 additions & 0 deletions docs/Xposed/ADB示例.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 🐝 BeeCount 自动化记账 API 测试指南

## 1. 核心配置
- **广播 Action**: `com.tntlikely.beecount.AUTO_BILLING`
- **目标包名**: `com.tntlikely.beecount.dev.debug`

## 2. 参数说明
- `amount`: 金额 (支持 "1,234.56")
- `account`: 账户名 (需与 App 内一致)
- `type`: 0 为支出, 1 为收入
- `remark`: 备注 (可选)

---

## 3. 支付宝测试 (Alipay)

### 🔴 支出
adb shell am broadcast -a com.tntlikely.beecount.AUTO_BILLING -p com.tntlikely.beecount.dev.debug --es amount "12.80" --es account "支付宝" --ei type 0 --es remark "便利店消费"

### 🔵 收入
adb shell am broadcast -a com.tntlikely.beecount.AUTO_BILLING -p com.tntlikely.beecount.dev.debug --es amount "100.00" --es account "支付宝" --ei type 1 --es remark "余额宝收益"

---

## 4. 微信测试 (WeChat)

### 🔴 支出
adb shell am broadcast -a com.tntlikely.beecount.AUTO_BILLING -p com.tntlikely.beecount.dev.debug --es amount "8.50" --es account "微信" --ei type 0 --es remark "早饭包子"

### 🔵 收入
adb shell am broadcast -a com.tntlikely.beecount.AUTO_BILLING -p com.tntlikely.beecount.dev.debug --es amount "88.88" --es account "微信" --ei type 1 --es remark "收到红包"

---

## 5. 现金测试 (Cash)

### 🔴 支出
adb shell am broadcast -a com.tntlikely.beecount.AUTO_BILLING -p com.tntlikely.beecount.dev.debug --es amount "10.00" --es account "现金" --ei type 0 --es remark "打车零钱"

### 🔵 收入
adb shell am broadcast -a com.tntlikely.beecount.AUTO_BILLING -p com.tntlikely.beecount.dev.debug --es amount "50.00" --es account "现金" --ei type 1 --es remark "捡到钱了"

## 运行效果演示

以下是功能实现的实际运行截图:

### 运行效果演示

| 1. 支付宝收入测试 | 2. 支付宝支出测试 |
| :---: | :---: |
| ![记账结果](./docs/Xposed/示例3.jpg) | ![记账结果](./docs/Xposed/示例3.jpg) |

### 3. 手机端最终入账展示
![记账结果](./docs/Xposed/示例3.jpg)
Binary file added docs/Xposed/示例1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Xposed/示例2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Xposed/示例3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
141 changes: 86 additions & 55 deletions lib/services/automation/auto_billing_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ class AutoBillingService {
/// [text] 快捷指令传递的识别文本
/// [showNotification] 是否显示通知(默认true)
/// 返回:交易记录ID,失败返回null
/// 核心:直接处理文本并自动记账
Future<int?> processText(
String text, {
bool showNotification = true,
Expand All @@ -362,8 +363,38 @@ class AutoBillingService {
);
}

// 直接解析文本(无需OCR)
final ocrResult = _ocrService.parsePaymentText(text);
// 🔥【核心修改】:支持结构化暗号解析 (XPOSED:金额|备注|分类)
OcrResult ocrResult;
String? manualCategory; // 记录传过来的分类名
String? typeStr;
String? manualAccount;
if (text.startsWith("XPOSED:")) {
print("🚀 [精准模式] 检测到 Xposed 结构化数据");
final content = text.substring(7); // 去掉 "XPOSED:"
final parts = content.split('|');

// 解析格式:金额|备注|分类|收支方向|账户
final double? amount = parts.length > 0 ? double.tryParse(parts[0]) : null;
final String? note = (parts.length > 1 && parts[1].isNotEmpty) ? parts[1] : null;
manualCategory = (parts.length > 2 && parts[2].isNotEmpty) ? parts[2] : null;
typeStr = parts.length > 3 ? parts[3] : '0';
manualAccount = parts.length > 4 && parts[4].isNotEmpty ? parts[4] : null;
ocrResult = OcrResult(
amount: amount,
note: note,
time: DateTime.now(),
rawText: text,
allNumbers: [],
suggestedCategoryId: null,
);
if (typeStr == '1' || typeStr == 'income') {
// 这里我们可以在创建交易前做个标记,或者直接在这里把金额变成负数(如果你的底层逻辑支持)
print("💰 检测到收入信号,准备记入收入");
}
} else {
// 普通模式:走原有的正则解析逻辑
ocrResult = _ocrService.parsePaymentText(text);
}

if (ocrResult.amount == null) {
print('❌ 未能识别出金额');
Expand All @@ -379,32 +410,30 @@ class AutoBillingService {

print('✅ 识别成功: 金额=${ocrResult.amount}, 备注=${ocrResult.note}');

// 更新通知状态
if (showNotification) {
await _showNotification(
id: notificationId,
title: '✅ 识别成功',
body: '正在创建交易记录...',
);
}
// 获取分类逻辑
// 1. 根据 typeStr 动态判定搜索方向
// typeStr 是前面从 parts[3] 解析出来的结果
final isIncome = (typeStr == '1' || typeStr == 'income');
final String searchDirection = isIncome ? 'income' : 'expense';

print('🔍 [自动化判断] 记账方向: $searchDirection');

// 获取分类并创建交易
// 2. 获取分类逻辑 (注意:括号里改成了变量 searchDirection)
final repo = _container.read(repositoryProvider);
final topLevelCategories = await repo.getTopLevelCategories('expense');
final topLevelCategories = await repo.getTopLevelCategories(searchDirection);

final allCategories = <Category>[];
allCategories.addAll(topLevelCategories);
// 获取所有子分类
for (final category in topLevelCategories) {
final subCategories = await repo.getSubCategories(category.id);
allCategories.addAll(subCategories);
}

// 过滤出可用分类(排除有子分类的父分类)
final categories = CategoryHierarchy.getUsableCategories(allCategories);

// 匹配分类:优先使用传过来的分类名进行智能匹配
final suggestedCategoryId = CategoryMatcher.smartMatch(
merchant: ocrResult.note,
fullText: ocrResult.rawText,
fullText: manualCategory ?? ocrResult.rawText, // 如果有手动分类,优先按它匹配
categories: categories,
);

Expand All @@ -418,58 +447,52 @@ class AutoBillingService {
);

// 创建交易记录
final txId = await _createTransaction(resultWithCategory);
// 创建交易记录:多传一个 typeStr 参数
final txId = await _createTransaction(
resultWithCategory,
typeOverride: typeStr, // 👈 把刚才解析的 '1' 传下去
accountOverride: manualAccount,
);

if (txId != null) {
// 刷新统计信息
_container.read(statsRefreshProvider.notifier).state++;
print('✅ 交易创建成功: id=$txId');
if (showNotification) {
await _showNotification(
id: notificationId,
title: '✅ 记账成功',
body: '已自动创建支出记录: ¥${ocrResult.amount}',
body: '金额: ¥${ocrResult.amount}${ocrResult.note != null ? ' (${ocrResult.note})' : ''}',
);
}
return txId;
} else {
print('❌ 交易创建失败');
if (showNotification) {
await _showNotification(
id: notificationId,
title: '❌ 创建失败',
body: '无法创建交易记录',
);
}
return null;
}
return null;
} catch (e) {
print('❌ [AutoBilling] 文本处理失败: $e');
if (showNotification) {
await _showNotification(
id: 1002,
title: '❌ 处理失败',
body: '错误: $e',
);
}
return null;
} finally {
final totalElapsed =
DateTime.now().millisecondsSinceEpoch - totalStartTime;
final totalElapsed = DateTime.now().millisecondsSinceEpoch - totalStartTime;
print('⏱️ [性能] 文本处理完成, 总耗时=${totalElapsed}ms');
}
}

/// 创建交易记录
/// [billingTypes] 记账方式列表,用于添加标签
/// [autoAddTags] 是否自动添加标签
Future<int?> _createTransaction(
OcrResult result, {
List<String>? billingTypes,
bool autoAddTags = true,
String? typeOverride, // 1. 接收从管道传来的类型信号
String? accountOverride,
}) async {
try {
// 获取当前账本ID(优先从Provider读取,失败则从SharedPreferences读取,最后从数据库获取默认账本)
// --- 🔥 逻辑注入:强制指定类型 ---
String? forceType;
if (typeOverride == '1' || typeOverride == 'income') {
forceType = 'income';
} else if (typeOverride == '0' || typeOverride == 'expense') {
forceType = 'expense';
}

// 2. 获取当前账本ID(保留你原有的三方案逻辑)
int? ledgerId;

// 方案1: 尝试从Provider读取
Expand Down Expand Up @@ -497,7 +520,6 @@ class AutoBillingService {
if (ledgers.isNotEmpty) {
ledgerId = ledgers.first.id;
print('✅ 从数据库获取默认账本ID: $ledgerId');
// 保存到SharedPreferences供下次使用
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_ledgerIdKey, ledgerId!);
}
Expand All @@ -508,29 +530,25 @@ class AutoBillingService {
return null;
}

print('📝 准备创建交易: ledgerId=$ledgerId');

// 使用共享的BillCreationService创建交易
// 3. 初始化记账核心服务
final repo = _container.read(repositoryProvider);
final billCreationService = BillCreationService(repo);

// 准备备注
String? note;
if (result.note != null) {
note = result.note!;
}

// 获取 l10n(使用系统语言设置)
// 4. 准备备注与语言环境
String? note = result.note;
final systemLocale = PlatformDispatcher.instance.locale;
final l10n = lookupAppLocalizations(systemLocale);

// 5. ⚠️ 真正执行创建(注意最后多了一个 type 参数)
final transactionId = await billCreationService.createBillTransaction(
result: result,
ledgerId: ledgerId,
note: note,
billingTypes: billingTypes,
l10n: l10n,
autoAddTags: autoAddTags,
type: forceType, // 🔥 这里把强制指定的类型传进去,解决“反了”的问题
accountName: accountOverride,// 👈 核心修改 2:把解析出来的“微信/支付宝”传下去
);

if (transactionId != null) {
Expand All @@ -541,14 +559,27 @@ class AutoBillingService {
logger.warning('AutoBilling', '创建交易记录失败');
}

// 🌟 核心修复:记账成功后,强制失效相关的缓存和状态
if (transactionId != null) {
// 1. 刷新首页交易缓存(解决“微信/支付宝”显示不出来的关键)
_container.invalidate(cachedTransactionsProvider);

// 2. 刷新账户统计(让首页顶部的余额、资产数字立刻变动)
_container.invalidate(allAccountStatsProvider);

// 3. 刷新总计统计(支出/收入总和)
_container.invalidate(allAccountsTotalStatsProvider);

print('🔔 [AutoBilling] 已发出 UI 刷新指令');
}

return transactionId;
} catch (e) {
print('❌ 创建交易记录失败: $e');
print('❌ 错误堆栈: ${StackTrace.current}');
rethrow;
}
}

/// 显示通知
Future<void> _showNotification({
required int id,
Expand Down Expand Up @@ -577,4 +608,4 @@ class AutoBillingService {
void dispose() {
_ocrService.dispose();
}
}
}
Loading