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 幂等复用 |