PAYATHON 2026

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 的跨应用返回模型。

该方法有两个前提:

  1. Besteron 明确支持接收并触发 PendingIntent;
  2. 支付应用完成后仍会关闭自己的支付 Activity。

仅仅把一个未经约定的 extra 放进 Intent 不会生效,支付端必须实现对应协议。

如果支付服务支持 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 来规避后台启动限制。

这些方式无法稳定工作,还可能破坏支付应用的状态,造成交易已经扣款但商户端误判失败。

建议的落地顺序

  1. 查看 Besteron/NEXO 集成文档,确认是否提供 finish、Activity Result、callback Intent、PendingIntent、return URI 或 merchant application ID 等返回机制。
  2. 确认支付 UI 是由 Intent 启动的独立 Activity、独立任务,还是由系统级支付服务启动。
  3. 让支付端在成功、失败和取消三种最终状态下都结束支付 Activity,并明确回调商户应用。
  4. 使用 singleTask 或 CLEAR_TOP | SINGLE_TOP 复用已有的 MainActivity。
  5. 在 MainActivity.onNewIntent() 中接收回调,并把结果交给 Activity 级 ViewModel。
  6. Fragment 在 RESUMED 状态下观察结果并执行导航,同时防止重复提交和重复导航。
  7. 始终通过服务端或 NEXO 查询核验最终支付状态。

如果当前版本的 Besteron 没有提供任何返回机制,需要修复的是支付应用或其设备集成配置。商户应用单方面增加 Intent flags,无法在标准 Android 11 权限模型下可靠地接管前台。

备注:内容仅供参考。