Flutter App 支付成功语音播报方案

Flutter App 支付成功语音播报方案:TTS、后台限制和 SignalR 怎么用

一句话结论

这个需求可以做,而且不一定要买外接收款音箱,也不一定要接第三方语音接口。

最适合第一期上线的方案是:

后端确认支付成功
→ 通知 Flutter App
→ Flutter App 用本地 TTS 播报
→ 播报“付款成功,到账 XX 元”

这里真正难的不是“怎么让手机说话”,而是“付款成功这件事怎么实时、可靠地传到 App”。

需求背景

门店希望会员付款后,前台设备能立刻播报收款结果,比如:

付款成功,到账 99 元

覆盖场景包括:

会员扫码购买商品
会员扫码办卡、续费、储值
会员扫码购买私教课、团课
小程序线上支付
后台/收银台生成收款码后会员扫码付款

对应的播报文案可以先定三类:

支付成功:付款成功,到账 XX 元
支付超时:支付超时,请重新扫码付款
支付失败:付款失败,请检查支付账户后重试

同时,财务中心继续展示支付记录明细,比如支付时间、金额、支付方式、业务类型、会员信息、订单号等。

Flutter 语音播报本身怎么做

Flutter 端可以使用本地 TTS,也就是 Text To Speech,文字转语音。

你可以把它理解为:

App 把“付款成功,到账 99 元”这句话交给系统
系统用手机自己的语音能力读出来

它不是录音,所以不需要麦克风权限。

它不是第三方语音合成接口,所以没有额外接口费用。

它一般也不是读取本地音乐文件,所以不需要媒体文件读取权限。

项目里如果已经引入了 flutter_tts,可以封装一个支付播报服务:

import 'package:flutter_tts/flutter_tts.dart';

class PaymentVoiceService {
  static final FlutterTts _tts = FlutterTts();
  static bool _inited = false;

  static Future<void> init() async {
    if (_inited) {
      return;
    }

    await _tts.setLanguage('zh-CN');
    await _tts.setVolume(1.0);
    await _tts.setSpeechRate(0.5);
    await _tts.setPitch(1.0);
    _inited = true;
  }

  static Future<void> speakSuccess(String amount) async {
    await speak('付款成功,到账$amount元');
  }

  static Future<void> speakTimeout() async {
    await speak('支付超时,请重新扫码付款');
  }

  static Future<void> speakFailed() async {
    await speak('付款失败,请检查支付账户后重试');
  }

  static Future<void> speak(String text) async {
    await init();
    await _tts.stop();
    await _tts.speak(text);
  }
}

这个服务后面可以被“收款页轮询结果”“SignalR 收到支付成功消息”“晨报播报”等多个地方复用。

支付结果怎么通知 App

这块是核心。

会员付款成功后,通常是支付平台先通知后端。比如微信、支付宝、汇付等支付平台,会把支付结果回调给服务端。

所以完整链路应该是:

会员扫码付款
→ 支付平台确认支付成功
→ 支付平台回调后端
→ 后端更新订单状态、写财务流水
→ 后端通知 App
→ App 本地 TTS 播报

前端不能凭空知道会员已经付款了。前端必须通过某种方式拿到后端的支付结果。

常见有三种做法。

方案一:轮询订单状态

这是第一期最容易落地的方式。

流程是:

App 生成收款订单
→ App 展示二维码
→ App 每隔 2 秒查一次订单状态
→ 查到成功后播报
→ 超过 2 到 5 分钟未支付,就提示支付超时

优点:

实现简单
不需要长连接
适合当前 App 停留在收款二维码页面的场景

缺点:

只能监听当前订单
如果会员在小程序里自己付款,App 不一定知道该查哪笔订单
App 切后台后,定时器可能不稳定

所以轮询适合做第一期,但不是完整终局方案。

方案二:SignalR 实时通知

你前面说的 “singler” 应该是 SignalR。

SignalR 是微软 .NET 生态里常用的实时通信框架。它可以让服务端主动通知客户端。

大白话说:

不是 App 一直问后端“付了吗?付了吗?”
而是后端一旦知道“付了”,马上告诉 App

SignalR 底层会优先使用 WebSocket;如果不支持,会降级到 Server-Sent Events;再不行会用 Long Polling。业务层不用太关心底层到底是哪一种。

支付播报里可以这样用:

App 登录后连接 SignalR
→ App 告诉后端自己属于哪个门店
→ 后端把这个连接加入门店分组
→ 某门店有支付成功
→ 后端向这个门店分组推送 paymentSuccess
→ App 收到消息后播报

服务端推给 App 的消息可以约定成这样:

{
  "event": "paymentSuccess",
  "orderId": "202606180001",
  "branchId": "10001",
  "amount": "99.00",
  "payType": "微信支付",
  "businessType": "商品消费",
  "payTime": "2026-06-18 10:30:00"
}

App 收到后做三件事:

判断是不是当前门店的订单
判断 orderId 有没有播报过
调用 TTS 播报“付款成功,到账 99 元”

Flutter 前端如何接 SignalR

Flutter 没有官方的一等公民 SignalR 客户端,一般会使用社区包。比较常见的是:

signalr_netcore
signalr_core

如果你们后端是 ASP.NET Core SignalR,可以先评估 signalr_netcore,它的包说明里写到支持 WebSocket、Server-Sent Events、Long Polling,并支持自动重连。

依赖示例:

dependencies:
  signalr_netcore:

封装一个支付 SignalR 服务:

import 'dart:async';
import 'package:signalr_netcore/signalr_client.dart';

typedef PaymentSuccessCallback = Future<void> Function(
  Map<String, dynamic> data,
);

class PaymentSignalRService {
  HubConnection? _connection;
  bool _starting = false;

  Future<void> start({
    required String hubUrl,
    required String token,
    required PaymentSuccessCallback onPaymentSuccess,
  }) async {
    if (_starting) {
      return;
    }

    if (_connection?.state == HubConnectionState.Connected) {
      return;
    }

    _starting = true;

    final httpOptions = HttpConnectionOptions(
      accessTokenFactory: () async => token,
    );

    final connection = HubConnectionBuilder()
        .withUrl(hubUrl, options: httpOptions)
        .withAutomaticReconnect(
          retryDelays: [0, 2000, 5000, 10000, 30000, null],
        )
        .build();

    connection.on('paymentSuccess', (arguments) async {
      final first = arguments?.isNotEmpty == true ? arguments!.first : null;
      if (first is Map) {
        await onPaymentSuccess(Map<String, dynamic>.from(first));
      }
    });

    connection.onclose(({error}) {
      _connection = null;
    });

    _connection = connection;

    try {
      await connection.start();
    } finally {
      _starting = false;
    }
  }

  Future<void> stop() async {
    final connection = _connection;
    _connection = null;
    await connection?.stop();
  }
}

业务层使用时大概这样:

final paymentSignalRService = PaymentSignalRService();

Future<void> startPaymentListener() async {
  await paymentSignalRService.start(
    hubUrl: 'https://你的域名/paymentHub',
    token: '用户登录 token',
    onPaymentSuccess: (data) async {
      final orderId = data['orderId']?.toString() ?? '';
      final amount = data['amount']?.toString() ?? '0';

      if (orderId.isEmpty) {
        return;
      }

      final hasSpoken = await PaymentBroadcastDedup.hasSpoken(orderId);
      if (hasSpoken) {
        return;
      }

      await PaymentBroadcastDedup.markSpoken(orderId);
      await PaymentVoiceService.speakSuccess(amount);
    },
  );
}

这里还需要一个去重工具,避免同一笔订单重复播报:

class PaymentBroadcastDedup {
  static final Set<String> _spokenOrderIds = <String>{};

  static Future<bool> hasSpoken(String orderId) async {
    return _spokenOrderIds.contains(orderId);
  }

  static Future<void> markSpoken(String orderId) async {
    _spokenOrderIds.add(orderId);
  }
}

正式项目里不要只放内存,建议把最近播报过的订单号存到 SharedPreferences 或项目已有的本地存储里。否则 App 重启后,短时间内同一笔订单可能又被播一次。

SignalR 解决什么,不解决什么

SignalR 解决的是:

服务端怎么实时通知客户端

它不解决的是:

App 被系统挂起后还能不能一直运行
App 被用户杀掉后还能不能收到消息
iOS 后台能不能直接 TTS 播报

所以它不是后台保活技术。

可以这样记:

App 前台:SignalR 很适合,实时性好
App 后台:SignalR 不一定稳定,取决于系统限制
App 被杀:SignalR 收不到,只能靠系统推送

后台播报能不能做

要分平台。

Android 和鸿蒙相对可做,但最好不要只依赖 Flutter 层的定时器或普通长连接。更稳的是做“前台服务”:

用户开启收款语音播报
→ App 启动一个常驻前台服务
→ 状态栏显示“正在监听收款播报”
→ 收到支付消息后调用 TTS 播报

这通常会涉及:

Android 13+ 通知权限 POST_NOTIFICATIONS
前台服务 Service 配置
部分国产机型的后台运行/自启动/电池优化设置引导

iOS 更严格。App 进入后台后,系统通常会挂起 App,不能承诺一直保持 SignalR 连接,也不建议承诺后台直接 TTS 播报。

iOS 更稳的产品设计是:

支付成功后发通知
→ 用户点通知回到 App
→ App 自动播报详情

是否需要应用市场开通能力

如果只是 App 前台付款语音播报:

不需要应用市场额外开通能力
不需要麦克风权限
不需要录音权限
不需要第三方语音接口

如果是 App 打开时播报每日晨报:

也不需要额外市场能力

如果要做后台提醒或推送:

Android 13+ 需要通知权限
iOS 需要通知授权和 APNs 推送能力
华为/鸿蒙如果用华为推送,需要在 AppGallery Connect / HMS 开通 Push Kit
小米、OPPO、vivo 等厂商推送,也要各自平台配置

如果要做 Android 后台持续监听并播报:

需要前台服务配置
需要通知栏常驻提示
可能需要用户允许后台运行

APNs 和厂商推送到底是什么

APNs 全称是 Apple Push Notification service,也就是苹果官方推送服务。

大白话说:

APNs 就是 iPhone 上 App 收通知必须走的苹果官方通道

为什么需要它?

因为 iOS 不允许普通 App 在后台一直开着网络连接等消息。App 切到后台一段时间后,系统可能会把它挂起。这个时候 SignalR、WebSocket、SSE 这些长连接都不一定还活着。

但是苹果自己的 APNs 通道是系统级能力。即使 App 没打开,系统也可以收到苹果服务器下发的通知,然后展示给用户。

链路大概是:

后端发现支付成功
→ 后端请求苹果 APNs
→ 苹果 APNs 把通知发到用户 iPhone
→ iPhone 系统弹出通知
→ 用户点击通知打开 App
→ App 再播报详情

所以 APNs 不是一个 Flutter 插件,也不是一段前端代码能单独完成的东西。它需要后端、iOS 证书、Apple Developer 后台一起配合。

iOS 侧通常要做这些配置:

Apple Developer 后台打开 Push Notifications 能力
Xcode 里给 Runner 打开 Push Notifications capability
如果需要后台通知处理,再打开对应 Background Modes
App 首次使用通知时,向用户申请通知权限
后端保存设备 token
后端用 APNs key 或证书向苹果推送通知

用户看到的权限弹窗大概就是:

“菲特云”想给您发送通知

用户允许后,App 才能正常弹通知。

Android 这边也有类似概念,只是更碎。

国内 Android 手机有很多厂商,比如华为、小米、OPPO、vivo、荣耀等。它们都有自己的推送通道。为什么不用一个统一的就好了?因为国内很多手机对后台进程限制很严,App 被杀掉后,普通网络连接基本就断了。要想 App 没打开也能收到通知,通常要接厂商推送。

常见厂商配置大概是:

华为/鸿蒙:AppGallery Connect / HMS Push Kit
小米:小米开放平台推送
OPPO:OPPO PUSH
vivo:vivo 推送
荣耀:荣耀推送

Android 13 以后,即使接了推送,也还需要用户授权通知权限:

POST_NOTIFICATIONS

所以厂商推送可以理解为:

App 不在前台时,系统或厂商帮你把通知送到手机

它适合做:

支付成功通知
每日晨报提醒
活动提醒
待办提醒

但它不太适合直接等同于“后台语音播报”。尤其是 iOS,推送通常负责把用户叫回来,真正的详细语音播报建议在用户打开 App 后执行。

每日晨报怎么设计更稳

每日晨报可以分两种。

第一种:用户打开 App 后播报。

早上用户打开 App
→ App 请求昨日/今日营收数据
→ TTS 播报:
  早上好,昨日门店营收 12345 元,新增会员 8 人

这个方案简单、稳定、权限少。

第二种:App 没打开也要早上 8 点自动响。

这个就复杂很多。

Android 可以通过通知、定时任务、前台服务尽量实现,但受系统和厂商限制。

iOS 不建议做成后台强制语音播报,更建议做通知提醒:

今日晨报已生成,点击查看

用户点进 App 后再播报详细内容。

推荐落地路线

第一期建议这样做:

1. 封装 PaymentVoiceService,统一负责 TTS 播报
2. 收款二维码页面先做订单状态轮询
3. 支付成功、失败、超时分别播报对应文案
4. 加订单号去重,避免重复播报
5. 财务中心展示支付流水详情

第二期再增强实时性:

1. 后端提供 SignalR paymentHub
2. App 登录后连接 SignalR
3. 后端按门店分组推送 paymentSuccess
4. App 收到消息后播报
5. 增加“最近未播报支付记录”补偿接口,防止漏单

第三期处理后台体验:

Android/鸿蒙:评估前台服务 + 通知权限 + 后台运行引导
iOS:走 APNs 通知,点击通知回 App 后播报

为什么还需要补偿接口

长连接不是百分百可靠。

网络会断,App 会切后台,系统会限制连接,服务端也可能重启。

所以推荐做双保险:

SignalR:负责实时播报
补偿接口:负责防漏单

补偿接口可以长这样:

GET /payment/unbroadcasted?branchId=xxx&lastTime=xxx

返回最近几分钟当前门店已成功但 App 可能没播报过的订单。

App 拿到后仍然按 orderId 去重:

没播过 → 播报
播过 → 跳过

财务中心需要展示什么

财务中心不是触发播报的核心,但它要承接支付结果,方便对账。

建议支付记录里展示:

支付时间
支付金额
支付方式:微信、支付宝、汇付、储值卡等
业务类型:商品消费、办卡、续费、储值、私教课、团课
会员姓名和手机号
订单号
支付状态:成功、失败、超时、退款
收款门店
操作来源:小程序、收款码、后台开单
备注

这样后面排查“有没有播报”“有没有到账”“是不是同一笔订单”会更方便。

测试重点

测试时不要只测“能不能说话”,还要测这些情况:

同一笔订单是否只播报一次
连续多笔支付是否按顺序播报
手机静音/媒体音量为 0 时表现如何
中文 TTS 不可用时是否有提示
App 前台时 SignalR 是否能实时收到
App 切后台后 Android 是否还能收到
iOS 切后台后是否改走通知
网络断开重连后是否能继续收到消息
SignalR 漏消息后补偿接口是否能补播
财务中心记录是否和播报金额一致

最后总结

这个需求可以拆成三层看:

语音层:Flutter 本地 TTS,负责把文字读出来
消息层:轮询或 SignalR,负责把支付结果送到 App
系统层:前台、后台、推送、权限,决定 App 能不能持续运行

最现实的上线方案是:

前台收款播报:TTS + 订单状态轮询
实时增强:SignalR 推送支付成功
防漏单:最近支付成功记录补偿
后台体验:Android/鸿蒙评估前台服务,iOS 使用通知唤醒
每日晨报:优先做 App 打开后自动播报

这样做既能快速上线,又不会一开始就被后台保活、厂商推送、iOS 审核这些复杂问题拖住。

参考资料

posted @ 2026-06-18 16:59  jialiangzai  阅读(16)  评论(0)    收藏  举报