news 2026/9/16 18:46:57

Android收款监控链路设计:通知监听、事件推送与状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android收款监控链路设计:通知监听、事件推送与状态机

简介:一份基于Android平台开发的微信/支付宝收款监控系统源码,面向移动端开发者及有个人收款管理需求的用户。项目核心解决个人账户无需单独签约支付接口即可实现即时到账监控的问题,通过应用内监听与通知机制辅助收款记录,适合自由职业者、小微商户或独立开发者使用。资源共159个文件,包含Java源码49个、XML布局与配置37个、PNG图片42个,以及JAR依赖库、MP3音频提示和Gradle工程配置,压缩包仅2.94MB,结构紧凑,便于导入Android Studio直接分析。工程涵盖界面展示、支付监听、结果解析等模块,并配有属性文件与License声明,有助于开发者梳理完整收款监控流程,同时图片与音频素材也可直接用于界面和提示音二次定制。目前已有537人学习浏览,适合具备一定Android基础、希望快速借鉴实现思路并做功能扩展的工程师参考学习。

1. 收款监控系统为什么绕不开 Android 监听端

收款监控的核心不是“收钱”,而是“在钱到账的瞬间拿到一个可信的事件”。服务端的支付宝/微信回调能精确告诉你交易结果,但小微商户、门店播报、多店归集这类场景里,收银终端往往不在开发者手里,甚至只是店员手机上挂着收款码。这时候,Android 端就成了唯一能统一捕获微信和支付宝到账事件的监听点。标题里的“设计源码”其实是在讲一条完整的链路:事件源(通知栏、无障碍、系统广播)→ 中间服务(事件解析、金额提取)→ 业务出口(本地 HTTP 推送、语音播报、上报服务器)。这套结构对 5 年以上的工程师来说,难点不在某个 API,而在怎么在厂商 ROM 的后台限制下保住这条链路的稳定性。

2. 广播式监听设计:从系统事件到本地 HTTP 推送的链路搭建

2.1 为什么先做事件源抽象而不是直接写通知监听

微信和支付宝的到账提醒,表面上都来自通知栏,但两个 App 的推送通道、通知栏展示策略、甚至是否允许读取通知都各不相同。常见做法是先把“事件源”抽象成统一的接口:收到通知、收到无障碍节点变化、收到系统广播,最终都归一成一个PaymentEvent。这样后续切换监听策略时,业务代码不用动,只换监听源实现。

我一般会在PaymentEvent里保留三个字段:type(微信/支付宝/未知)、amount(金额,单位分)、rawText(原始通知文本,用于排错)。金额单位用分而不是元,是因为浮点数在 JSON 序列化和数据库存储里都可能丢精度,而且支付宝回调里的total_amount是字符串形式的元,换算分的时候要自己处理,避免直接Double.parseDouble后乘 100。

public class PaymentEvent { public static final int TYPE_WECHAT = 1; public static final int TYPE_ALIPAY = 2; public int type; public long amount; // 单位:分 public String rawText; // 原始通知文本 public long occurredAt; // 事件发生时间戳,毫秒 }

这段代码本身没有逻辑,但它决定了后面所有监听源实现类的返回类型。rawText一定要存,因为通知解析的正则规则会频繁调整,没有原始文本做回归验证,改了规则都没法确认是否误伤。occurredAt用事件发生时间而不是接收时间,避免系统通知堆积导致的时间漂移。

2.2 用系统广播做兜底触发:App 被杀后怎么重新拉起

通知监听服务最大的问题是:用户手动划掉 App 或厂商清理后台后,NotificationListenerService可能被系统解绑,但 App 进程还活着,业务就进入了“静默失联”状态。兜底方案是动态注册一个BroadcastReceiver,监听ACTION_SCREEN_ONACTION_USER_PRESENTACTION_BOOT_COMPLETED这类高频事件,在里面检查监听服务是否仍然连接。

下面是几个实践下来值得注册的 action:

Action触发时机用途
android.intent.action.SCREEN_ON屏幕点亮检查服务存活,重建通知通道
android.intent.action.USER_PRESENT解锁完成高概率伴随用户查看手机,适合做轮询补偿
android.intent.action.BOOT_COMPLETED开机完成拉起常驻服务
android.net.conn.CONNECTIVITY_CHANGE网络切换补偿上报失败的消息

注册方式要用Context.registerReceiver动态注册,别写进 Manifest。Android 14(targetSdk 34)对静态广播接收者做了大量限制,SCREEN_ONUSER_PRESENT这类隐式广播在清单注册里根本收不到,动态注册是唯一可靠路径。注册时机放在Application.onCreate里,和监听服务解绑回调互不干扰。

2.3 把解析结果推给业务侧:内置 HTTP Server 的端点设计

终端设备上的解析结果要送到收银台或后台系统,最省事的方式是在 App 里内置一个轻量 HTTP 服务,局域网内 POST JSON。避免引入 GRPC 或者自研 TCP 长连接,对单机监听场景来说维护成本远大于收益。JDK 自带的com.sun.net.httpserver.HttpServer在 Android 上可用,代码量控制在 80 行以内。

HttpServer server = HttpServer.create(new InetSocketAddress(9527), 0); server.createContext("/payment/notify", exchange -> { if (!"POST".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(405, -1); exchange.close(); return; } String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); boolean ok = pushService.dispatch(body); // 分发到业务处理链 byte[] resp = ok ? "{\"code\":0}".getBytes() : "{\"code\":500}".getBytes(); exchange.sendResponseHeaders(200, resp.length); exchange.getResponseBody().write(resp); exchange.close(); }); server.setExecutor(Executors.newFixedThreadPool(4)); server.start();

这里的关键参数是端口和线程池。端口要避开微信和支付宝内置浏览器的常见代理端口,我一般取 9000 以上的奇数端口。线程池线程数 4 到 8 是经验值,监听场景的并发峰值不会超过 10 个请求,线程数再多反而浪费内存。注意readAllBytes()在 Java 9+ 才可用,Android 的 desugar 支持有限,稳妥写法是手动循环读取。

这个端点的dispatch方法内部要做三件事:验签、幂等去重、落库。验签用的 token 在 App 和后台各配一份,请求头带X-Auth-Token,别把 token 放到 URL 参数里,因为局域网内网 HTTP 抓包实在太容易了,URL 会被各种中间设备记日志。

3. NotificationListenerService 实现微信/支付宝到账监听的最小可运行方案

3.1 通知栏监听为什么是到账监控的“通用入口”

微信和支付宝的收款到账通知,最终都会走系统通知栏展示。即使 App 在后台被省电策略限制,通知栏消息仍会被系统接管。所以NotificationListenerService是监听渠道里覆盖最广、误报最少的一条路。无障碍服务能看更多东西,但需要用户额外开启权限,而且被手机厂商杀服务也是常态。

需要澄清一个容易误解的点:NotificationListenerService拿到的只是通知的内容,拿不到 App 内部进程的通信数据。微信把金额显示在通知里,我们就能读到;如果哪一天微信改成“你有一笔收款到账”不带金额,那这条路就废了,只能靠无障碍节点逐级遍历界面,或者直接放弃金额展示改为语音播报原文。

3.2 最小可用的监听服务代码

<nbsp;

class PayNotificationListener : NotificationListenerService() { override fun onNotificationPosted(sbn: StatusBarNotification?) { val extras = sbn?.notification?.extras ?: return val title = extras.getCharSequence(Notification.EXTRA_TITLE).toString() val text = extras.getCharSequence(Notification.EXTRA_TEXT).toString() val app = sbn.packageName val amount = when { app == WECHAT_PACKAGE && title.contains("微信支付") -> extractAmount(text) app == ALIPAY_PACKAGE && title.contains("支付宝") -> extractAmount(text) else -> return } if (amount > 0) { val event = PaymentEvent(type = if (app == WECHAT_PACKAGE) TYPE_WECHAT else TYPE_ALIPAY, amount = amount, rawText = text) dispatcher.enqueue(event) } } private fun extractAmount(text: String): Long { val matcher = AMOUNT_REGEX.find(text) ?: return 0 val yuan = matcher.groupValues[1].toDoubleOrNull() ?: return 0 return (yuan * 100).roundToLong() } companion object { const val WECHAT_PACKAGE = "com.tencent.mm" const val ALIPAY_PACKAGE = "com.eg.android.AlipayGphone" val AMOUNT_REGEX = Regex("收款([0-9]+\\.[0-9]{2})元") } }

这段代码有四个细节值得讲。第一,extras取文本用的是EXTRA_TEXT,但部分 ROM 会把通知内容放在EXTRA_TEXT_LINES(一个CharSequence[])里,只读EXTRA_TEXT会是空,所以要加 fallback 逻辑。第二,正则收款([0-9]+\\.[0-9]{2})元只匹配到分,微信的 “收款0.01元” 和 “收款100.00元” 都能命中,但如果通知文案改动成 “已收款100.00元”,正则就失效了,这就是rawText的意义——到时候改正则再跑回归。第三,金额乘 100 用roundToLong(),避免浮点乘法的精度问题。第四,dispatcher.enqueue内部要有一个有界队列,队列满时丢弃最早的事件,防止内存溢出。

3.3 权限配置和通知读取开关

要在AndroidManifest.xml里声明服务,并配好BIND_NOTIFICATION_LISTENER_SERVICE权限。这个权限是系统签名权限,普通 App 必须在系统设置里手动开启“通知使用权”,没有代码可以绕过,别在这个点上浪费时间。

<service android:name=".PayNotificationListener" android:label="收款监控服务" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> <intent-filter> <action android:name="android.service.notification.NotificationListenerService" /> </intent-filter> </service>

配置里android:permission写错了服务会直接无法绑定,这是最常见的低级错误。判断用户是否已授权的标准代码是查询NotificationManager.getEnabledListenerPackages(),返回的包名列表里存在自己的包名才算开启,不要用canUse之类的自定义状态,会误导排查。

3.4 通知文本特征与金额提取的适配边界

微信和支付宝的通知文本结构会随版本调整,这里给出一份我整理的适配表,标注了稳定场景和失效风险场景:

场景微信通知特征支付宝通知特征
个人收款码到账“微信支付收款到账 12.00 元”“支付宝到账 12.00 元”
商家扫码枪“微信支付-xxx店 收款 12.00 元”“向你付款 12.00 元”
群收款“群收款- 已收款 12.00 元”无对等场景
退款通知“退款到账” 无金额或负数“退款成功” 无金额
红包“收到红包” 金额在标题栏无对等场景

退款和红包都要单独挡掉。你只监控“收款”,但通知里“退款到账”会带正向金额,直接匹配就会误报。我一般加一层上下文判断:微信的退款通知标题带“退款”,支付宝的退款通知标题带“退款成功”,命中后直接返回amount = 0。红包同理,标题有“红包”就不解析。

4. 支付宝回调与二维码状态机的集成方式

4.1 服务端支付宝回调与 Android 本地监听的职责边界

支付宝的异步回调(notify_url)是服务端的事,收到回调说明交易已经真实完成,这是账务级凭证。Android 端通知监听只是“播报”级别的事实,两者不应混为一谈。实际项目里,我会把服务端回调作为最终入账依据,Android 监听的结果只做两件事:实时语音播报、触发终端界面刷新。

这样分工的好处是,即使 Android 端因为厂商省电策略漏听了消息,服务端回调还能兜底记账。反过来,如果服务端宕机,Android 端仍然能保证店员先知道钱到账,这就是监控系统的核心价值。连接两条链路的介质是订单号out_trade_no:扫码支付时,终端生成订单号并显示二维码,服务端回调带着同一订单号返回,Android 端轮询自己的订单接口就能知道“这笔单子到底入账没有”。

4.2 关键回调参数与验签字段说明

支付宝回调 POST 到开发者服务器的参数中,有几个和收款监控直接相关的字段:

参数名示例用途
out_trade_no20250101120001商户订单号,关联本地订单
trade_no202501012200141234支付宝交易号,对账用
trade_statusTRADE_SUCCESS交易状态,只有这个值才入账
total_amount12.00订单金额,字符串类型
seller_id2088xxxx收款方 PID,多门店做归属用
signbase64字符串RSA2 签名,验签核心

trade_status是状态机跳转的核心驱动。WAIT_BUYER_PAY表示等待付款,TRADE_SUCCESS表示已付款,TRADE_FINISHED表示交易完成且不可退款。收款监控的关注点其实只在WAIT_BUYER_PAYTRADE_SUCCESS的跳变上,TRADE_FINISHED是退款链路才要关心的。

4.3 二维码收款的最小状态机代码

在终端设备上,每张收款码对应一个订单,状态变化是“待支付 → 已支付 → 已通知”。这个状态机用枚举加单次流转校验就能写清楚,不需要引入 WorkFlow 引擎:

public enum OrderState { PENDING("待支付"), PAID("已支付"), NOTIFIED("已通知"); private final String desc; OrderState(String desc) { this.desc = desc; } public boolean canTransitTo(OrderState target) { return (this == PENDING && target == PAID) || (this == PAID && target == NOTIFIED); } @Override public String toString() { return desc; } }

状态流转的判断被收敛在canTransitTo里,所有入口都必须经过它。这个设计不是为了炫技,而是收款链路里的订单状态必须是单向不可逆的——PAID永远不能回到PENDING,如果业务上需要撤销,应生成逆向退款单,而不是改状态。NOTIFIED状态存在的意义是保证“已通知”这个动作的幂等性,防止 Android 端重试推送造成重复播报。

4.4 轮询二维码状态的实测命令

在开发调试阶段,不一定要等真钱到账,可以用支付宝沙箱环境配合 curl 模拟回调。先在后端接口里加一个 dev-only 的触发端点:

curl -X POST http://localhost:8080/api/mock/alipay/callback \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "out_trade_no=20250101120001&trade_status=TRADE_SUCCESS&total_amount=12.00&seller_id=2088123456789012"

这个请求模拟了支付宝服务器向notify_url发起的回调。注意trade_status参数要严格对应,TRADE_SUCCESSWAIT_BUYER_PAY是两个最常用的测试值。用沙箱调试比用真实账号扫码快得多,而且能精确控制金额和状态,适合做状态机的单元验证。seller_id在多门店场景下可以多传几组值,验证归属逻辑是否正确分流。

5. 用 adb 验证监听链路和保活状态的具体技巧

5.1 确认 NotificationListenerService 是否被系统解绑

命令行直接查当前应用的监听服务绑定状态:

adb shell dumpsys notification --noredact | grep -A 5 "mListeningPackages"

正常输出里能看到你的包名出现在mListeningPackages,如果只有mUserSetPackages有你的包名,说明用户在设置里开了开关,但服务因异常退出还没重新绑定。接着再查:

adb shell dumpsys activity services 你的包名

输出里的app=ProcessRecord段会显示进程是否存活,intent=Intent段会显示flg=0x10000000,其中0x10000000表示该服务是系统通过BIND_NOTIFICATION_LISTENER_SERVICE权限启动的。两个命令配合使用,能快速定位是“没开权限”还是“服务崩了”。

5.2 验证通知解析是否生效的快速手段

不需要真的收一笔钱,用adb直接给微信发一条模拟通知不现实,但可以换个思路——把解析逻辑单独抽成纯函数,然后写个带 main 的 Java 类,在本地跑单元测试。我在项目里会保留一个notify_parser_test.json,里面放几十条历史通知原文,每次改正则后跑一遍断言。命令如下:

./gradlew :app:testDebugUnitTest --tests "*.PaymentTextParserTest"

测试通过后再上车真机。真机验证的兜底手段是加一个 debug 页,手动粘贴任意通知文本,点“解析”输出金额和来源。这样测试时不依赖真实收款事件,效率高得多。最后一招,直接在onNotificationPosted里加 logcat 输出,用adb logcat | grep PaymentListener跟踪实时事件流,确认事件是否进入分发队列。

5.3 前台服务参数设置的关键点

如果项目要求服务更稳,可以把监听服务升级成前台服务。startForeground必须传入 channel id 和通知实例,Android 12 以后startForeground还要求同时声明FOREGROUND_SERVICE权限,并且后台启动前台服务有ForegroundServiceStartNotAllowedException限制。写代码时集中做一层封装,避免startForeground被极端场景抛异常:

try { service.startForeground(NOTIFY_ID, buildNotification(context)); } catch (ForegroundServiceStartNotAllowedException ignored) { // 后台启动受限,等待下个系统广播再自启 }

不要在这里做复杂的重试逻辑,系统广播会再次触发检查,过度重试只会增加耗电和崩溃率。保活的最终防线其实是产品层面:引导用户把 App 加入厂商的“耗电保护白名单”,这一步要放到设置引导页里反复提醒,纯靠代码对抗厂商策略是不现实的。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 18:46:35

SSM框架学生信息管理系统实战:从Maven搭建到部署详解

简介&#xff1a;基于SSM框架的学生信息管理系统完整项目&#xff0c;含Java源码、配置及数据库文件&#xff0c;面向Java Web开发者、课程设计及毕业设计学生。系统覆盖学生信息管理、成绩管理、班级管理、用户权限管理、操作日志等模块&#xff0c;采用模块化设计&#xff0c…

作者头像 李华
网站建设 2026/9/16 18:45:48

多平台优惠券回收源码:金融级交易闭环实现

简介&#xff1a;这是一套面向PHP开发者与电商系统学习者的2024年多平台礼物回收类优惠券商城源码&#xff0c;聚焦于优惠券秒杀、拼团、限时折扣及余额宝理财等高频业务场景&#xff0c;解决闲置电商权益变现与轻量级SaaS化商城快速搭建需求。资源包共2005个文件&#xff0c;含…

作者头像 李华
网站建设 2026/9/16 18:45:21

CS1237电容式传感器驱动开发:C语言裸机SPI精准控制指南

简介&#xff1a;本资源是一份基于C语言开发的CS1237硬件驱动程序实现&#xff0c;面向嵌入式系统开发者、Linux内核模块初学者及需要对接特定外设的工程师&#xff0c;解决CS1237类设备在操作系统中识别、初始化与数据交互的核心问题。压缩包为RAR格式&#xff0c;共含2个关键…

作者头像 李华
网站建设 2026/9/16 18:44:52

书霸AI|官网shubaai.com|公众号搜书霸AI写作

很多人写开题报告时&#xff0c;真正卡住的并不是打字&#xff0c;而是不知道从哪里开始&#xff1a;研究问题不够明确&#xff0c;研究内容彼此脱节&#xff0c;研究方法写得笼统&#xff0c;参考文献也不知道如何筛选。结果往往是反复修改标题&#xff0c;却始终没有形成一条…

作者头像 李华