SUNMI P2 支付后如何从 Besteron 返回应用
结论
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 不能可靠解决这个问题。它只能调整当前任务栈中 Activity 的顺序,无法关闭 Besteron 的 Activity,也无法绕过 Android 11 对后台启动 Activity 的限制。
支付流程需要主动提供返回商户应用的机制,例如:
- Besteron 支付 Activity 完成后调用
finish(); - 通过 Activity Result 返回;
- 支付应用调用商户提供的
PendingIntent; - 使用双方约定的 deep link 或 callback URI;
- 使用 Besteron 或 SUNMI/NEXO SDK 提供的关闭、返回或 app-to-app callback 接口。
如果当前集成只有 HTTP 请求和响应,而 Besteron 没有提供这些回调能力,商户应用就无法从后台可靠地关闭第三方支付 UI。此时应先向 Besteron/SUNMI 集成方确认正式的 “return to merchant app” 流程。
为什么 FLAG_ACTIVITY_REORDER_TO_FRONT 没有效果
FLAG_ACTIVITY_REORDER_TO_FRONT 只作用于目标 Activity 所在的任务栈。例如:
MainActivity -> OtherActivity
再次启动 MainActivity 时,该 flag 可以把已有实例移到栈顶。但在当前场景中,前台界面属于 Besteron,通常位于另一个应用或任务栈。商户应用重新排列自己的 Activity,并不会让 Besteron 自动退出。
NEXO HTTP 响应到达时,商户应用已经不在前台。从 Android 10 开始,系统会限制后台应用直接启动 Activity。即使调用:
startActivity(Intent(this, MainActivity::class.java))
系统也可能拒绝将应用切换到前台。增加 NEW_TASK、CLEAR_TOP 或 REORDER_TO_FRONT,并不意味着应用获得了后台启动权限。
代码中还有一个容易忽略的问题:
repeatOnLifecycle(Lifecycle.State.CREATED)
Fragment 进入后台后,只要视图仍然存在,其生命周期通常仍不低于 CREATED。因此,paymentResult.collect 可能恰好在应用不可见时执行。这时从后台调用 startActivity(),就会遇到系统限制。
首选方案:让支付 Activity 正常返回结果
如果 Besteron 支持通过 Intent 发起支付,应使用 Activity Result API,不要只根据 HTTP 响应判断是否应该关闭支付 UI。
private val paymentLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val paymentResult = result.data?.getStringExtra("payment_result")
viewModel.onPaymentCompleted(paymentResult)
} else {
viewModel.onPaymentCancelled()
}
}
private fun startPayment(paymentIntent: Intent) {
paymentLauncher.launch(paymentIntent)
}
支付应用完成处理后,需要执行类似的逻辑:
setResult(
Activity.RESULT_OK,
Intent().putExtra("payment_result", result)
)
finish()
问题的重点不在于商户应用使用哪个 flag,而在于 Besteron 的 Activity 是否会在交易结束后主动调用 finish()。如果支付应用不结束自己的 Activity,商户应用无法替它调用 finish()。
具体的 action、extra 和返回码必须以 Besteron/SUNMI 提供的接口文档为准,不能自行假设。
可选方案:向支付应用传递 PendingIntent
如果 Besteron 支持接收回调 PendingIntent,可以预先创建一个指向现有 MainActivity 的返回入口:
val callbackIntent = Intent(requireContext(), MainActivity::class.java).apply {
action = "com.example.app.ACTION_PAYMENT_RESULT"
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val callbackPendingIntent = PendingIntent.getActivity(
requireContext(),
1001,
callbackIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val paymentIntent = Intent("com.besteron.action.PAY").apply {
putExtra("merchant_callback", callbackPendingIntent)
}
交易完成后,由支付应用发送这个 PendingIntent。相比商户应用在后台自行调用 startActivity(),这种方式更符合 Android 的跨应用返回模型。
该方法有两个前提:
- Besteron 明确支持接收并触发
PendingIntent; - 支付应用完成后仍会关闭自己的支付 Activity。
仅仅把一个未经约定的 extra 放进 Intent 不会生效,支付端必须实现对应协议。
可选方案:deep link 或自定义回调 URI
如果支付服务支持 return URL,可以为 MainActivity 配置 deep link:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="merchantapp"
android:host="payment-result" />
</intent-filter>
</activity>
支付完成后打开:
merchantapp://payment-result?status=success&transactionId=...
已有的 MainActivity 会通过 onNewIntent() 接收回调:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handlePaymentCallback(intent)
}
private fun handlePaymentCallback(intent: Intent) {
if (intent.data?.host != "payment-result") return
val status = intent.data?.getQueryParameter("status")
val transactionId = intent.data?.getQueryParameter("transactionId")
paymentViewModel.handleCallback(status, transactionId)
}
不能只信任 deep link 中携带的支付结果。应使用交易编号向后端或 NEXO 服务重新核验交易状态,避免伪造回调导致错误出票。
恢复已有的 MainActivity
如果支付端能够合法触发返回,可以使用:
val intent = Intent(context, MainActivity::class.java).apply {
action = "com.example.app.ACTION_PAYMENT_RESULT"
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra("transaction_id", transactionId)
}
然后根据实际导航需求在 manifest 中设置:
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true" />
几个 flag 的作用分别是:
FLAG_ACTIVITY_CLEAR_TOP:移除目标 Activity 上方的 Activity;FLAG_ACTIVITY_SINGLE_TOP:目标已在顶部时复用实例,并调用onNewIntent();launchMode="singleTask":尽量复用任务栈中的MainActivity实例。
这些设置只能控制商户应用自己的任务栈,既不能关闭 Besteron,也不能保证后台应用能够自行切换到前台。
应用只有一个 Activity 和多个 Fragment 时,通常不需要创建第二个 MainActivity。支付结果可以交给 Activity 级别的 ViewModel,再由当前 Fragment 在恢复后处理。
调整支付结果和页面导航的处理方式
不要把收到支付结果、启动 Activity、提交订单和页面导航全部耦合在一个 Fragment collector 中。HTTP 响应可能在 Fragment 不可见、视图已经销毁或状态已经保存时到达。
可以让 ViewModel 保存交易状态:
sealed interface CheckoutState {
data object Idle : CheckoutState
data object PaymentProcessing : CheckoutState
data object CreatingTransaction : CheckoutState
data class Completed(
val orderNumber: String
) : CheckoutState
data class Failed(
val message: String
) : CheckoutState
}
Fragment 只在可见且能够安全导航时处理 UI:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.checkoutState.collect { state ->
when (state) {
is CheckoutState.Completed -> {
findNavController().navigate(
R.id.action_checkoutFragment_to_confirmationFragment,
Bundle().apply {
putString("orderNumber", state.orderNumber)
putString("name", args.name)
putFloat("totalPrice", totalPrice.toFloat())
}
)
viewModel.onCompletionConsumed()
}
is CheckoutState.Failed -> {
// 显示失败信息
}
else -> Unit
}
}
}
}
网络请求和支付状态更新由 ViewModel 处理:
fun onPaymentCompleted(paymentResult: PaymentResult) {
viewModelScope.launch {
if (!paymentResult.isSuccessful) {
_checkoutState.value =
CheckoutState.Failed("Payment was not successful")
return@launch
}
_checkoutState.value = CheckoutState.CreatingTransaction
val transaction = repository.createTransaction(paymentResult)
_checkoutState.value = CheckoutState.Completed(
orderNumber = transaction.orderNumber
)
}
}
lifecycleScope 默认运行在主线程,因此原代码中的:
requireActivity().runOnUiThread {
findNavController().navigate(...)
}
通常没有必要。需要处理的是导航发生时 Fragment 是否处于 RESUMED 状态,以及同一事件是否会被重复消费。
如果 paymentResult、transactionResult 属于一次性事件,直接使用会重放旧值的 StateFlow,可能导致屏幕旋转或重新进入页面后重复提交、重复导航。可以使用明确的状态机、事件 ID,或者在成功处理后调用 onCompletionConsumed()。
设备受管场景下的特殊方案
SUNMI P2 可能运行在 kiosk、专用设备或定制系统环境中。如果商户应用是 Device Owner、默认 Launcher,或者 SUNMI 固件提供专用的任务切换接口,设备上可能还有其他能力,例如锁定任务模式或厂商定义的应用切换 API。
这些能力取决于具体固件、设备管理配置和 SUNMI/Besteron SDK,不能当作标准 Android 11 行为。ActivityManager.moveTaskToFront()、REORDER_TASKS 等旧式方案也不适合作为普通应用的可靠支付回跳机制,更不能用来关闭另一个应用的 Activity。
不建议采用的方式
不要尝试以下方案:
- 强制停止 Besteron;
- 使用 AccessibilityService 模拟 Back/Home;
- 通过 shell、root 或隐藏 API 切换任务;
- 使用普通通知或全屏通知伪装支付回跳;
- 不断循环调用
startActivity(); - 收到 HTTP 响应后调用系统 Back,并期望它影响另一个应用;
- 只更换 Intent flag 来规避后台启动限制。
这些方式无法稳定工作,还可能破坏支付应用的状态,造成交易已经扣款但商户端误判失败。
建议的落地顺序
- 查看 Besteron/NEXO 集成文档,确认是否提供
finish、Activity Result、callback Intent、PendingIntent、return URI 或 merchant application ID 等返回机制。 - 确认支付 UI 是由 Intent 启动的独立 Activity、独立任务,还是由系统级支付服务启动。
- 让支付端在成功、失败和取消三种最终状态下都结束支付 Activity,并明确回调商户应用。
- 使用
singleTask或CLEAR_TOP | SINGLE_TOP复用已有的MainActivity。 - 在
MainActivity.onNewIntent()中接收回调,并把结果交给 Activity 级 ViewModel。 - Fragment 在
RESUMED状态下观察结果并执行导航,同时防止重复提交和重复导航。 - 始终通过服务端或 NEXO 查询核验最终支付状态。
如果当前版本的 Besteron 没有提供任何返回机制,需要修复的是支付应用或其设备集成配置。商户应用单方面增加 Intent flags,无法在标准 Android 11 权限模型下可靠地接管前台。
备注:内容仅供参考。
