Skip to content

Android 接入(Kotlin) ​

这套接入适合谁 ​

原生 Android 应用(Kotlin)需要在自己的界面里展示应付金额与收款信息, 并在买家付款后刷新订单状态。示例用 Retrofit + OkHttp。

前置准备 ​

  • 已拿到 API KEY(见立即接入)。
  • 已配置收款地址并设为启用。
  • 有一个你自己的服务端(手机 App 装不下 API KEY)。

KEY 不能打进 APK

APK 是可以被反编译的,任何写进包体的字符串都等于公开。App 里的下单请求发往你自己的服务端, 由它带着 KEY 去调平台的开放接口。

安装 ​

kotlin
// app/build.gradle.kts
dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
}

最小可跑示例 ​

这里分两段:你的服务端带 KEY 去下单,App 只跟你的服务端说话。

1. 你的服务端(Kotlin / Ktor 或 OkHttp) ​

kotlin
// 服务端:用 OkHttp 调平台的开放接口
val client = OkHttpClient.Builder()
    .callTimeout(15, TimeUnit.SECONDS)
    .build()

fun createOrder(orderNo: String, amount: String): String {
    val json = """
        {
          "merchantOrderNo": "$orderNo",
          "chainId": 2,
          "tokenId": 1,
          "amount": "$amount",
          "subject": "订单 $orderNo",
          "notifyUrl": "https://your-shop.com/pay/notify"
        }
    """.trimIndent()

    val req = Request.Builder()
        .url("${System.getenv("API_BASE")}/api/pay/open/createOrder")
        .addHeader("X-Pay-Api-Key", System.getenv("PAY_API_KEY"))   // 只在服务端
        .addHeader("Content-Type", "application/json")
        .post(json.toRequestBody("application/json".toMediaType()))
        .build()

    return client.newCall(req).execute().use { it.body!!.string() }
}

2. App 侧:Retrofit 只打你自己的后端 ​

kotlin
data class CheckoutReq(val orderNo: String, val amount: String)

data class CheckoutResp(
    val code: Int,
    val orderNo: String?,
    val payAddress: String?,
    val payAmountDisplay: String?,
    val payAmountMinimal: String?,
    val chainKey: String?,
    val msg: String?,
)

data class StatusResp(
    val code: Int,
    val status: Int,          // 1..6,只认 3/4 为已付款
    val statusText: String?,
    val txHash: String?,
)

interface ShopApi {
    @POST("/api/checkout")
    suspend fun checkout(@Body body: CheckoutReq): CheckoutResp

    @GET("/api/order-status")
    suspend fun status(@Query("orderNo") orderNo: String): StatusResp
}
kotlin
class CheckoutViewModel : ViewModel() {
    private val api = Retrofit.Builder()
        .baseUrl("https://your-shop.com")
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(ShopApi::class.java)

    private val _state = MutableStateFlow<UiState>(UiState.Loading)
    val state: StateFlow<UiState> = _state

    fun start(orderNo: String, amount: String) = viewModelScope.launch {
        try {
            val o = api.checkout(CheckoutReq(orderNo, amount))
            if (o.code != 0) {
                _state.value = UiState.Error(o.msg ?: "下单失败")
                return@launch
            }
            _state.value = UiState.Ready(o)

            // 轮询状态,直到已确认/已完成
            while (true) {
                delay(3_000)
                val s = api.status(o.orderNo!!)
                when (s.status) {
                    1 -> _state.value = UiState.Ready(o)          // 等待付款
                    2 -> _state.value = UiState.Pending(o)        // 已提交,等确认
                    3, 4 -> { _state.value = UiState.Paid(o); break }
                    5 -> { _state.value = UiState.Error("订单已过期"); break }
                    6 -> { _state.value = UiState.Error("待人工认领,请联系客服"); break }
                }
            }
        } catch (e: Exception) {
            _state.value = UiState.Error(e.message ?: "网络错误")
        }
    }
}

也可以不让 App 自己画收银信息,直接打开平台收银台: {cashierBaseUrl}/pay/cashier/m/{merchantNo},两个值从商户侧 GET /api/pay/getMyProfile 取。

回调处理 ​

回调由你的服务端接收(App 不参与)。用 OkHttp 在服务端验签:

kotlin
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

fun verify(rawBody: String, ts: String, sig: String, apiKey: String): Boolean {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(apiKey.toByteArray(), "HmacSHA256"))
    mac.update(ts.toByteArray())        // 时间戳与正文字符串拼接
    mac.update(rawBody.toByteArray())   // 原始报文体,不要重新序列化
    val want = mac.doFinal().joinToString("") { "%02x".format(it) }
    return java.security.MessageDigest.isEqual(want.toByteArray(), sig.toByteArray()) // 常数时间
}

处理要点:

  • 幂等:回调会重试,用 merchantOrderNo 去重,别重复发货。
  • 快返回:约 10 秒超时,重活放后台任务。
  • 以查询为准:关键动作前用查询订单复核。

唤起钱包 ​

Android 上"唤起钱包"依赖钱包 App 的 URL scheme: https://your-shop.com/pay/... 这类链接交给系统选择器即可唤起已安装的钱包。 收款地址与金额以文本 + 二维码同时展示,兼容没有装钱包的用户。

常见问题 ​

问题说明
KEY 能放 BuildConfig 吗不能,APK 可被反编译
要不要用 WebView可选。也可以直接用系统浏览器打开平台收银台链接,少维护一套页面
轮询间隔3–5 秒;进入后台时暂停轮询,回到前台再恢复
怎么判断已付款只认状态 3 / 4,不要用数值大小比较
断网重试会不会多开单不会,同一个 merchantOrderNo 幂等复用

相关 ​

面向全球中小商家的 Web3 支付基础设施