Android 中如何处理实体商品购买与支付?
明确结论
Android 应用可以销售实体商品,但此类交易通常不使用 Google Play Billing。开发者需要接入自己的订单系统,并使用第三方支付服务,例如收单银行、支付网关或支持 Google Pay 的支付服务商。
“自行处理支付”不代表必须从零开发一套完整的银行卡平台。更常见也更安全的分工是:
- 应用负责展示商品、管理购物车和处理订单交互。
- 服务端负责校验价格、管理库存和订单状态,以及编排支付流程。
- 持牌支付服务商负责银行卡信息采集、令牌化、收单和 3-D Secure 等环节。
- 应用和业务服务器应尽量避免接触原始卡号及 CVV。
Google Play Billing 主要用于应用内数字内容、功能或订阅。实体商品、配送服务和线下消费通常应使用其他支付方式。相关政策可能调整,上线前应核对目标市场适用的最新版 Google Play 支付政策和支付服务商要求。
推荐的支付架构
常见的支付流程如下:
- 用户在应用中选择实体商品并提交购物车。
- 应用把商品 ID 和数量发送给业务服务器。
- 服务器根据数据库中的数据重新计算价格、税费、优惠和运费,不能直接采用客户端传来的金额。
- 服务器创建待支付订单,再通过支付服务商创建支付会话。
- 应用使用支付服务商的 Android SDK、托管支付页面或 Google Pay 完成支付授权。
- 支付服务商返回一次性令牌或支付结果。
- 服务器调用支付服务商的服务端 API 确认支付。
- 支付服务商通过 Webhook 通知最终状态。
- 服务器验证 Webhook 签名,以幂等方式更新订单并安排发货。
判断订单是否支付成功,应以服务端查询结果或通过签名验证的 Webhook 为准,不能只看 Android 客户端显示的”成功”页面。
Android 端示例
下面是一个不依赖具体支付服务商的简化示例。应用只提交商品信息,金额由服务器计算:
data class CartItem(
val productId: String,
val quantity: Int
)
data class CheckoutRequest(
val items: List<CartItem>,
val shippingAddressId: String
)
data class CheckoutSession(
val orderId: String,
val paymentSessionToken: String
)
interface CheckoutApi {
@POST("checkout/sessions")
suspend fun createCheckoutSession(
@Body request: CheckoutRequest
): CheckoutSession
}
创建支付会话后,把 paymentSessionToken 交给支付服务商提供的官方 Android SDK:
lifecycleScope.launch {
try {
val session = checkoutApi.createCheckoutSession(
CheckoutRequest(
items = cartItems,
shippingAddressId = selectedAddressId
)
)
paymentProvider.presentCheckout(
activity = this@CheckoutActivity,
sessionToken = session.paymentSessionToken
)
} catch (e: Exception) {
showPaymentError("暂时无法发起支付,请稍后重试")
}
}
这里的 paymentProvider 只是示意接口,并非 Android 标准 API。实际代码需要按照所选支付服务商的 SDK 编写。不要自行使用普通 EditText 收集银行卡号、有效期和 CVV,除非团队确实具备相应的支付安全和合规能力。
服务端示例
服务端必须重新读取商品价格并生成订单,不能接受客户端指定的最终金额:
app.post("/checkout/sessions", async (req, res) => {
const userId = req.auth.userId;
const { items, shippingAddressId } = req.body;
const pricedItems = await priceProductsFromDatabase(items);
const totalAmount = calculateTotal(pricedItems);
const order = await orders.create({
userId,
items: pricedItems,
shippingAddressId,
totalAmount,
currency: "CNY",
status: "PAYMENT_PENDING"
});
const paymentSession = await paymentGateway.createSession({
amount: totalAmount,
currency: order.currency,
reference: order.id,
returnUrl: "example://payment-result"
});
res.json({
orderId: order.id,
paymentSessionToken: paymentSession.token
});
});
处理 Webhook 时需要验证签名,并防止重复执行同一个通知:
app.post("/payments/webhook", rawBodyMiddleware, async (req, res) => {
const event = paymentGateway.verifyWebhook(
req.rawBody,
req.headers
);
if (await webhookEvents.hasProcessed(event.id)) {
return res.sendStatus(200);
}
await database.transaction(async (tx) => {
await webhookEvents.markProcessed(tx, event.id);
if (event.type === "payment.succeeded") {
await orders.markPaidIfPending(tx, event.reference);
}
if (event.type === "payment.failed") {
await orders.markPaymentFailedIfPending(tx, event.reference);
}
});
res.sendStatus(200);
});
rawBodyMiddleware、签名算法和事件字段都取决于支付服务商,必须按照其官方文档规定的方式进行验证。
银行卡信息如何处理
应优先使用令牌化方案。银行卡信息直接提交给支付服务商,业务系统只接收类似 paymentMethodToken 的不可逆令牌。需要保存支付方式时,也应保存服务商生成的客户 ID 或支付方式令牌,不要保存完整卡号。
通常可以保存以下展示信息:
- 卡组织,例如 Visa 或 Mastercard。
- 卡号后四位。
- 有效期,但需要结合服务商要求和业务必要性判断。
- 支付服务商生成的支付方式令牌。
以下信息不应保存:
- CVV、CVC 等银行卡安全码。
- 磁道数据。
- PIN 或 PIN block。
- 未经必要保护的完整银行卡号。
- 可以还原银行卡敏感认证数据的日志、截图或分析事件。
即使采用令牌化方案,也要控制访问权限、加密业务数据,并设置合理的数据保留期限。
安全与合规注意事项
PCI DSS
只要系统存储、处理或传输银行卡数据,就可能进入 PCI DSS 的适用范围。支付服务商提供的托管页面、官方移动 SDK 或令牌化组件,可以大幅减少业务系统接触银行卡数据的范围,但通常不会自动免除所有合规义务。
具体适用哪类自评问卷或审计要求,需要由支付服务商、收单机构或合规顾问根据实际数据流确定。
传输和存储安全
所有支付接口都应使用 HTTPS/TLS,并采取以下措施:
- 由服务端验证证书,不提供绕过证书错误的逻辑。
- 密钥和支付网关私钥只保存在服务端。
- 不把密钥硬编码到 APK 中。
- 不在日志、崩溃报告或分析平台中记录卡号、令牌和个人地址。
- 使用 Android Keystore 支持的密钥保护敏感本地数据。
- 不通过明文
SharedPreferences保存支付凭证。 - 对支付、退款和地址修改等操作进行重新认证或风险校验。
Android 客户端中的常量最终都可能被提取,因此不能依靠 APK 隐藏服务器密钥。
支付状态与重复扣款
网络超时不代表支付失败。如果客户端每次重试都创建新交易,可能导致重复扣款。创建订单和确认支付时,应使用唯一订单号和幂等键。
支付结果至少需要区分以下状态:
- 等待授权。
- 需要额外验证。
- 已授权或已支付。
- 明确失败。
- 状态未知,需要服务端查询。
- 已退款或部分退款。
身份验证和欺诈风险
实体商品涉及配送和拒付风险,需要考虑:
- 3-D Secure 或目标地区要求的强客户认证。
- 对账单地址和收货地址进行风险检查。
- 检测设备、账号、订单频率和异常金额。
- 人工审核高风险订单。
- 防止退款、取消、缺货或重复发货造成问题。
是否必须启用 3-D Secure,取决于支付地区、卡组织、收单机构和服务商的规则。
隐私与消费者保护
收货地址、电话号码和订单历史也属于敏感数据。应用需要提供清晰的隐私说明,并根据经营地区处理以下事项:
- 数据收集目的和保存期限。
- 用户访问、删除或更正数据的方式。
- 退款、退货、配送和取消规则。
- 税费、运费和最终价格的展示方式。
- 未成年人交易及受限制商品。
- 当地的数据保护、电子商务和消费者权益要求。
Google Pay 与 Google Play Billing 的区别
两者用途不同:
- Google Play Billing 用于其适用范围内的应用内数字商品和数字服务。
- Google Pay 是一种支付方式,可用于实体商品或其他符合其规则的交易。
Google Pay 通常向支付服务商提供经过保护的支付凭据,但不能代替商品订单、库存、税费、退款和履约系统。使用 Google Pay 时,仍需配置受支持的支付网关或收单处理方式。
实际落地建议
对于大多数销售实体商品的 Android 应用,可以采用以下实现方式:
- 选择能够在经营地区合法提供服务的支付服务商。
- 使用服务商的官方 Android SDK、托管结账页或 Google Pay 集成。
- 让银行卡数据直接进入支付服务商的系统。
- 在服务端计算金额并创建订单。
- 使用签名 Webhook 和服务端查询确认支付状态。
- 对支付创建、支付确认和 Webhook 处理实施幂等控制。
- 上线前完成 PCI DSS、隐私、税务、退款和应用商店政策检查。
因此,Office Depot 一类应用需要自己的商品、订单和履约后台,但不一定自行处理银行卡底层业务。敏感的支付信息采集、令牌化和收单处理,通常由专业支付服务商完成。
备注:内容仅供参考。
