HarmonyOS开发:支付服务Payment Kit
HarmonyOS开发:支付服务Payment Kit
📌 核心要点:Payment Kit让HarmonyOS应用接入华为应用内支付,IAP支付流程、订单安全校验、订单管理与对账,每一环都关乎钱,容不得半点马虎。
背景与动机
你的App要收费了。虚拟商品、会员订阅、增值服务——用户得掏钱,你得收钱。
最简单的做法?让用户转账,你手动确认。一个用户还行,一百个呢?一千个呢?你每天光确认转账就够忙的了,而且用户等不起——付了钱不能立刻用,体验极差。
接入第三方支付?微信支付、支付宝——但它们不支持虚拟商品的支付。苹果和谷歌都要求虚拟商品必须走应用内支付(IAP),华为也一样。
Payment Kit就是华为的IAP方案。用户在App内购买虚拟商品,通过华为支付完成交易,你收到订单回调和货款。整个流程闭环,安全合规。
但支付这东西,每一行代码都关乎钱。支付流程少一步,用户付了钱收不到货;订单校验少一步,黑客能绕过支付直接拿商品;对账少一步,钱少了你都不知道。所以Payment Kit的集成,容不得半点马虎。
核心原理
Payment Kit的IAP支付流程:客户端发起支付 → 华为支付系统处理 → 服务端校验订单 → 发放商品。
flowchart TD
A[用户点击购买] --> B[客户端创建订单]
B --> C[调用Payment Kit支付]
C --> D[华为支付系统]
D --> E{用户确认支付}
E -->|取消| F[返回支付取消]
E -->|确认| G[处理支付]
G --> H{支付结果}
H -->|成功| I[返回PurchaseInfo]
H -->|失败| J[返回支付失败]
I --> K[客户端发送订单到服务端]
K --> L[服务端调用华为校验API]
L --> M{校验结果}
M -->|合法| N[发放商品]
M -->|非法| O[拒绝发放,记录异常]
N --> P[服务端确认消费]
P --> Q[华为标记订单已消费]
Q --> R[客户端更新UI]
classDef client fill:#1565C0,color:#fff,stroke:#0D47A1
classDef pay fill:#E65100,color:#fff,stroke:#BF360C
classDef server fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef result fill:#6A1B9A,color:#fff,stroke:#4A148C
classDef error fill:#C62828,color:#fff,stroke:#B71C1C
class A,B,C,K,R,client
class D,E,F,G,H,I,J,pay
class L,M,N,P,Q,server
class O,error
商品类型
Payment Kit支持两种商品类型:
| 商品类型 | 说明 | 示例 |
|---|---|---|
| 消耗型(Consumable) | 购买后消耗,可重复购买 | 游戏金币、虚拟币、次数卡 |
| 非消耗型(Non-consumable) | 购买后永久拥有 | 去广告、解锁功能、主题 |
注意:HarmonyOS 6之前不支持订阅型商品(Subscription),HarmonyOS 6新增了订阅支持。
订单状态
一个订单从创建到完成,经历以下状态:
- 已创建:用户发起支付,订单创建成功
- 已支付:用户完成支付,但商品尚未发放
- 已消费:服务端确认发放商品,订单完成
- 已退款:用户申请退款,华为审核通过
关键点:已支付 ≠ 已消费。 用户付了钱,但你还没确认发放商品,订单处于"已支付"状态。你必须调用服务端API确认消费,订单才算真正完成。
安全校验
支付安全的核心原则:永远不要信任客户端。
客户端返回的支付结果可以被篡改。黑客可以hook支付回调,伪造支付成功的消息。所以,支付结果的校验必须在服务端完成——服务端调用华为的订单校验API,确认订单真实有效后,再发放商品。
代码实战
基础用法:集成IAP并发起支付
先在AGC控制台配置商品,然后在客户端发起支付。
// PaymentManager.ets
import { iap } from '@kit.PaymentKit';
// 商品定义(和AGC控制台配置一致)
interface ProductInfo {
productId: string; // 商品ID
productType: iap.ProductType; // 商品类型
name: string; // 商品名称
price: string; // 展示价格
}
// 预定义的商品列表
const PRODUCTS: Record<string, ProductInfo> = {
'coin_100': {
productId: 'coin_100',
productType: iap.ProductType.CONSUMABLE,
name: '100金币',
price: '¥6'
},
'coin_500': {
productId: 'coin_500',
productType: iap.ProductType.CONSUMABLE,
name: '500金币',
price: '¥25'
},
'remove_ads': {
productId: 'remove_ads',
productType: iap.ProductType.NONCONSUMABLE,
name: '去广告',
price: '¥18'
}
};
export class PaymentManager {
private static instance: PaymentManager;
private iapService: iap.IAPService | null = null;
private constructor() {}
static getInstance(): PaymentManager {
if (!PaymentManager.instance) {
PaymentManager.instance = new PaymentManager();
}
return PaymentManager.instance;
}
/**
* 初始化IAP服务
*/
async init(): Promise<boolean> {
try {
this.iapService = iap.getService();
console.info('[Payment] IAP服务初始化完成');
return true;
} catch (error) {
console.error(`[Payment] 初始化失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 查询商品信息
* 从华为服务端获取最新的商品信息(名称、价格等)
*/
async queryProducts(productIds: string[]): Promise<iap.Product[]> {
if (!this.iapService) return [];
try {
const products = await this.iapService.queryProducts({
productIds: productIds,
productType: iap.ProductType.CONSUMABLE // 查询消耗型商品
});
console.info(`[Payment] 查询到${products.length}个商品`);
return products;
} catch (error) {
console.error(`[Payment] 查询商品失败: ${JSON.stringify(error)}`);
return [];
}
}
/**
* 发起支付
* 消耗型商品支付流程
*/
async purchaseConsumable(productId: string): Promise<iap PurchaseResult | null> {
if (!this.iapService) return null;
try {
// 创建支付请求
const purchaseRequest: iap.PurchaseRequest = {
productId: productId,
productType: iap.ProductType.CONSUMABLE,
developerPayload: `order_${Date.now()}` // 开发者自定义数据,校验时回传
};
// 发起支付(会拉起华为支付页面)
const result = await this.iapService.createPurchase(purchaseRequest);
console.info(`[Payment] 支付成功: orderId=${result.getOrderId()}`);
return result;
} catch (error) {
const payError = error as iap.IAPError;
switch (payError.code) {
case iap.IAPErrorCode.ORDER_CANCELLED:
console.info('[Payment] 用户取消支付');
break;
case iap.IAPErrorCode.ORDER_FAILED:
console.error('[Payment] 支付失败');
break;
case iap.IAPErrorCode.PRODUCT_NOT_EXIST:
console.error('[Payment] 商品不存在');
break;
default:
console.error(`[Payment] 支付异常: ${JSON.stringify(error)}`);
}
return null;
}
}
/**
* 购买非消耗型商品
*/
async purchaseNonConsumable(productId: string): Promise<iap.PurchaseResult | null> {
if (!this.iapService) return null;
try {
const purchaseRequest: iap.PurchaseRequest = {
productId: productId,
productType: iap.ProductType.NONCONSUMABLE,
developerPayload: `order_${Date.now()}`
};
const result = await this.iapService.createPurchase(purchaseRequest);
console.info(`[Payment] 非消耗型商品购买成功: ${productId}`);
return result;
} catch (error) {
console.error(`[Payment] 购买失败: ${JSON.stringify(error)}`);
return null;
}
}
/**
* 查询已购买的非消耗型商品
* 用于恢复购买(用户换设备后恢复已购商品)
*/
async queryPurchasedProducts(): Promise<iap.PurchaseResult[]> {
if (!this.iapService) return [];
try {
const purchases = await this.iapService.queryPurchases({
productType: iap.ProductType.NONCONSUMABLE
});
console.info(`[Payment] 已购买${purchases.length}个非消耗型商品`);
return purchases;
} catch (error) {
console.error(`[Payment] 查询已购商品失败: ${JSON.stringify(error)}`);
return [];
}
}
}
进阶用法:服务端订单校验与消费确认
客户端支付成功后,必须到服务端校验订单真实性,然后确认消费。
// === 服务端订单校验代码(Node.js) ===
// order-verify-server.js
const axios = require('axios');
const IAP_CONFIG = {
appId: 'your_app_id',
appSecret: 'your_app_secret',
verifyUrl: 'https://subscr-drcn.iap.cloud.huawei.com/v2/applications/purchases/verify',
consumeUrl: 'https://subscr-drcn.iap.cloud.huawei.com/v2/applications/purchases/consume'
};
/**
* 获取IAP AccessToken
*/
async function getIAPAccessToken(): Promise<string> {
const response = await axios.post(
'https://oauth-login.cloud.huawei.com/oauth2/v3/token',
new URLSearchParams({
grant_type: 'client_credentials',
client_id: IAP_CONFIG.appId,
client_secret: IAP_CONFIG.appSecret
})
);
return response.data.access_token;
}
/**
* 校验订单
* 这是支付安全的核心——必须在服务端完成
*/
async function verifyPurchase(
purchaseToken: string,
productId: string,
orderId: string
): Promise<{ valid: boolean; data?: any; error?: string }> {
try {
const token = await getIAPAccessToken();
const response = await axios.post(
IAP_CONFIG.verifyUrl,
{
purchaseToken: purchaseToken,
productId: productId,
orderId: orderId
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
const result = response.data;
if (result.responseCode === '0') {
// 校验通过
console.info(`订单校验通过: orderId=${orderId}, productId=${productId}`);
return {
valid: true,
data: {
orderId: result.orderId,
productId: result.productId,
purchaseTime: result.purchaseTime,
purchaseState: result.purchaseState, // 0=已创建, 1=已取消
consumptionState: result.consumptionState // 0=未消费, 1=已消费
}
};
} else {
console.error(`订单校验失败: ${result.responseCode} - ${result.responseMessage}`);
return { valid: false, error: result.responseMessage };
}
} catch (error) {
console.error(`订单校验异常: ${JSON.stringify(error)}`);
return { valid: false, error: '校验服务异常' };
}
}
/**
* 确认消费
* 商品发放后必须调用,否则订单一直处于"已支付未消费"状态
*/
async function consumePurchase(
purchaseToken: string,
orderId: string
): Promise<boolean> {
try {
const token = await getIAPAccessToken();
const response = await axios.post(
IAP_CONFIG.consumeUrl,
{
purchaseToken: purchaseToken,
orderId: orderId
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
if (response.data.responseCode === '0') {
console.info(`订单已消费: orderId=${orderId}`);
return true;
} else {
console.error(`订单消费失败: ${response.data.responseMessage}`);
return false;
}
} catch (error) {
console.error(`订单消费异常: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 完整的支付处理流程
*/
async function handlePurchase(
purchaseToken: string,
productId: string,
orderId: string,
userId: string
): Promise<{ success: boolean; error?: string }> {
// 1. 校验订单
const verifyResult = await verifyPurchase(purchaseToken, productId, orderId);
if (!verifyResult.valid) {
return { success: false, error: verifyResult.error || '订单校验失败' };
}
// 2. 检查订单是否已消费(防止重复发放)
if (verifyResult.data.consumptionState === 1) {
console.warn(`订单已消费,跳过: orderId=${orderId}`);
return { success: true }; // 已消费,视为成功
}
// 3. 检查订单状态
if (verifyResult.data.purchaseState !== 0) {
return { success: false, error: '订单状态异常' };
}
// 4. 发放商品
try {
await deliverProduct(userId, productId);
console.info(`商品已发放: userId=${userId}, productId=${productId}`);
} catch (error) {
console.error(`商品发放失败: ${JSON.stringify(error)}`);
return { success: false, error: '商品发放失败' };
}
// 5. 确认消费
const consumed = await consumePurchase(purchaseToken, orderId);
if (!consumed) {
// 商品已发放但消费确认失败——记录日志,人工处理
console.error(`商品已发放但消费确认失败: orderId=${orderId}`);
// 不返回失败,因为商品已经发了
}
return { success: true };
}
/**
* 发放商品
*/
async function deliverProduct(userId: string, productId: string): Promise<void> {
switch (productId) {
case 'coin_100':
// 增加100金币
await addCoins(userId, 100);
break;
case 'coin_500':
// 增加500金币
await addCoins(userId, 500);
break;
case 'remove_ads':
// 开通去广告
await enableRemoveAds(userId);
break;
default:
throw new Error(`未知商品: ${productId}`);
}
}
module.exports = { verifyPurchase, consumePurchase, handlePurchase };
完整示例:生产级支付流程
把客户端支付、服务端校验、订单管理、异常处理串成完整链路。
// ProductionPaymentService.ets
import { iap } from '@kit.PaymentKit';
// 支付状态
enum PaymentState {
IDLE = 'idle',
PAYING = 'paying',
VERIFYING = 'verifying',
DELIVERING = 'delivering',
COMPLETED = 'completed',
FAILED = 'failed'
}
// 支付结果
interface PaymentResult {
success: boolean;
orderId: string;
productId: string;
error?: string;
state: PaymentState;
}
export class ProductionPaymentService {
private static instance: ProductionPaymentService;
private iapService: iap.IAPService | null = null;
private paymentState: PaymentState = PaymentState.IDLE;
private stateListeners: Array<(state: PaymentState) => void> = [];
private constructor() {}
static getInstance(): ProductionPaymentService {
if (!ProductionPaymentService.instance) {
ProductionPaymentService.instance = new ProductionPaymentService();
}
return ProductionPaymentService.instance;
}
/**
* 初始化
*/
async init(): Promise<void> {
try {
this.iapService = iap.getService();
// 检查是否有未完成的订单(App崩溃或网络中断导致)
await this.checkPendingPurchases();
console.info('[ProdPayment] 初始化完成');
} catch (error) {
console.error(`[ProdPayment] 初始化失败: ${JSON.stringify(error)}`);
}
}
/**
* 发起支付(完整流程)
*/
async purchase(productId: string, productType: iap.ProductType): Promise<PaymentResult> {
if (!this.iapService) {
return this.failResult(productId, '支付服务未初始化');
}
if (this.paymentState === PaymentState.PAYING) {
return this.failResult(productId, '正在支付中,请稍候');
}
const orderId = `order_${Date.now()}`;
try {
// 1. 发起支付
this.updateState(PaymentState.PAYING);
const purchaseRequest: iap.PurchaseRequest = {
productId: productId,
productType: productType,
developerPayload: orderId
};
const purchaseResult = await this.iapService.createPurchase(purchaseRequest);
if (!purchaseResult) {
return this.failResult(productId, '支付结果为空');
}
// 2. 服务端校验
this.updateState(PaymentState.VERIFYING);
const verifyResult = await this.verifyOnServer(
purchaseResult.getPurchaseToken(),
productId,
purchaseResult.getOrderId()
);
if (!verifyResult.success) {
return this.failResult(productId, verifyResult.error || '订单校验失败');
}
// 3. 发放商品
this.updateState(PaymentState.DELIVERING);
const deliverResult = await this.deliverProduct(productId);
if (!deliverResult) {
return this.failResult(productId, '商品发放失败');
}
// 4. 确认消费(消耗型商品)
if (productType === iap.ProductType.CONSUMABLE) {
await this.confirmConsumption(
purchaseResult.getPurchaseToken(),
purchaseResult.getOrderId()
);
}
// 5. 完成
this.updateState(PaymentState.COMPLETED);
return {
success: true,
orderId: purchaseResult.getOrderId(),
productId: productId,
state: PaymentState.COMPLETED
};
} catch (error) {
this.updateState(PaymentState.FAILED);
const payError = error as iap.IAPError;
let errorMsg = '支付失败';
if (payError.code === iap.IAPErrorCode.ORDER_CANCELLED) {
errorMsg = '用户取消支付';
}
return this.failResult(productId, errorMsg);
}
}
/**
* 检查未完成的订单
* App崩溃或网络中断时,可能有已支付但未消费的订单
*/
private async checkPendingPurchases(): Promise<void> {
if (!this.iapService) return;
try {
// 查询消耗型商品的未消费订单
const consumablePurchases = await this.iapService.queryPurchases({
productType: iap.ProductType.CONSUMABLE
});
for (const purchase of consumablePurchases) {
console.info(`[ProdPayment] 发现未消费订单: ${purchase.getOrderId()}`);
// 重新走校验和消费流程
const verifyResult = await this.verifyOnServer(
purchase.getPurchaseToken(),
purchase.getProductId(),
purchase.getOrderId()
);
if (verifyResult.success) {
await this.deliverProduct(purchase.getProductId());
await this.confirmConsumption(
purchase.getPurchaseToken(),
purchase.getOrderId()
);
console.info(`[ProdPayment] 未消费订单已处理: ${purchase.getOrderId()}`);
}
}
// 查询非消耗型商品的已购记录
const nonConsumablePurchases = await this.iapService.queryPurchases({
productType: iap.ProductType.NONCONSUMABLE
});
// 恢复已购的非消耗型商品
for (const purchase of nonConsumablePurchases) {
await this.deliverProduct(purchase.getProductId());
}
} catch (error) {
console.error(`[ProdPayment] 检查未完成订单失败: ${JSON.stringify(error)}`);
}
}
/**
* 服务端校验
*/
private async verifyOnServer(
purchaseToken: string,
productId: string,
orderId: string
): Promise<{ success: boolean; error?: string }> {
try {
// 调用你的服务端接口校验
// const response = await apiClient.post('/payment/verify', {
// purchaseToken, productId, orderId
// });
// return response.data;
console.info(`[ProdPayment] 服务端校验: orderId=${orderId}`);
return { success: true }; // 简化实现
} catch (error) {
console.error(`[ProdPayment] 服务端校验失败: ${JSON.stringify(error)}`);
return { success: false, error: '校验服务异常' };
}
}
/**
* 发放商品
*/
private async deliverProduct(productId: string): Promise<boolean> {
try {
// 根据商品ID发放对应商品
// 实际项目中调用你的业务服务
console.info(`[ProdPayment] 商品发放: ${productId}`);
return true;
} catch (error) {
console.error(`[ProdPayment] 商品发放失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 确认消费
*/
private async confirmConsumption(
purchaseToken: string,
orderId: string
): Promise<void> {
try {
// 调用服务端确认消费API
// await apiClient.post('/payment/consume', { purchaseToken, orderId });
console.info(`[ProdPayment] 确认消费: orderId=${orderId}`);
} catch (error) {
// 消费确认失败不影响商品发放,记录日志后续处理
console.error(`[ProdPayment] 消费确认失败: ${JSON.stringify(error)}`);
}
}
/**
* 注册状态监听
*/
addStateListener(listener: (state: PaymentState) => void): void {
this.stateListeners.push(listener);
}
private updateState(state: PaymentState): void {
this.paymentState = state;
this.stateListeners.forEach(l => {
try { l(state); } catch (e) { /* 忽略 */ }
});
}
private failResult(productId: string, error: string): PaymentResult {
return {
success: false,
orderId: '',
productId: productId,
error: error,
state: PaymentState.FAILED
};
}
}
// 页面中使用
@Entry
@Component
struct ShopPage {
@State paymentState: PaymentState = PaymentState.IDLE;
@State message: string = '';
async aboutToAppear() {
const service = ProductionPaymentService.getInstance();
await service.init();
service.addStateListener((state) => {
this.paymentState = state;
});
}
async buyCoins(productId: string) {
this.message = '';
const service = ProductionPaymentService.getInstance();
const result = await service.purchase(productId, iap.ProductType.CONSUMABLE);
if (result.success) {
this.message = '购买成功!';
} else {
this.message = result.error || '购买失败';
}
}
build() {
Column() {
Text('金币商城')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 24 })
// 商品列表
Row() {
Column() {
Text('100金币')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text('¥6')
.fontSize(16)
.fontColor('#E65100')
.margin({ top: 8 })
Button('购买')
.margin({ top: 12 })
.enabled(this.paymentState !== PaymentState.PAYING)
.onClick(() => this.buyCoins('coin_100'))
}
.padding(16)
.borderRadius(12)
.backgroundColor('#F5F5F5')
.margin({ right: 12 })
Column() {
Text('500金币')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text('¥25')
.fontSize(16)
.fontColor('#E65100')
.margin({ top: 8 })
Button('购买')
.margin({ top: 12 })
.enabled(this.paymentState !== PaymentState.PAYING)
.onClick(() => this.buyCoins('coin_500'))
}
.padding(16)
.borderRadius(12)
.backgroundColor('#F5F5F5')
}
// 状态提示
if (this.paymentState === PaymentState.PAYING) {
LoadingProgress().width(32).height(32).margin({ top: 24 })
Text('支付中...').fontSize(14).fontColor(Color.Gray).margin({ top: 8 })
}
if (this.message) {
Text(this.message)
.fontSize(16)
.fontColor(this.paymentState === PaymentState.COMPLETED ? '#2E7D32' : '#C62828')
.margin({ top: 16 })
}
}
.padding(24)
.width('100%')
.height('100%')
}
}
踩坑与注意事项
坑1:消耗型商品必须确认消费
消耗型商品支付成功后,如果你不调用消费确认API,订单一直处于"已支付未消费"状态。下次用户再买同一个商品,会收到"您已拥有此商品"的错误。
必须在校验通过并发放商品后,立刻调用消费确认API。 如果确认失败,记录日志,后续补确认。
坑2:未消费订单的恢复
App崩溃、网络中断、用户杀进程——这些情况下,可能存在"已支付但未消费"的订单。你必须在App启动时检查未消费订单,完成校验和消费流程。
否则用户付了钱,商品没到账,客服投诉就来了。
坑3:服务端校验是必须的
客户端的支付结果可以被篡改。如果你只在客户端判断支付是否成功,黑客可以绕过支付直接拿到商品。
所有支付结果的校验必须在服务端完成。 客户端收到支付成功回调后,把订单信息发给服务端,服务端调用华为校验API确认真实性。
坑4:商品ID必须和AGC控制台一致
客户端代码中的商品ID(如coin_100)必须和AGC控制台配置的完全一致,大小写敏感。你在控制台建的是Coin_100,代码里写coin_100,查询商品时直接返回空列表。
坑5:测试环境用测试账号
开发阶段不要用真实账号支付。在AGC控制台添加测试账号,测试账号支付不会真正扣钱。如果你用真实账号测试,每次支付都是真金白银——测试完了还得退款,麻烦。
坑6:对账不能少
华为的结算周期通常是月结。每月你需要在AGC控制台下载交易明细,和你的服务端订单数据对账。如果发现差异(比如华为记录了100笔订单,你只有99笔),需要排查原因。
建议:每天自动对账,发现差异及时处理。别等到月底才发现问题,那时候可能已经无法追溯了。
HarmonyOS 6适配说明
HarmonyOS 6对Payment Kit做了以下重要更新:
- 订阅型商品支持:这是最大的更新。HarmonyOS 6新增了订阅型商品(Subscription),支持月度/年度自动续费订阅。之前只支持一次性购买,现在会员订阅也能走IAP了。
// HarmonyOS 6订阅型商品
const subscriptionRequest: iap.PurchaseRequest = {
productId: 'vip_monthly',
productType: iap.ProductType.SUBSCRIPTION,
developerPayload: `sub_${Date.now()}`
};
const result = await iapService.createPurchase(subscriptionRequest);
- 订阅管理:新增订阅管理API,用户可以查看当前订阅状态、取消订阅、修改订阅计划。
// 查询用户订阅状态
const subscriptions = await iapService.querySubscriptions({
productId: 'vip_monthly'
});
// 取消订阅
await iapService.cancelSubscription(subscriptionId);
-
优惠码支持:新增优惠码(Promo Code)能力。运营可以生成优惠码,用户输入优惠码后以折扣价购买商品或免费试用订阅。
-
沙盒测试增强:沙盒测试环境新增了多种支付场景模拟,包括支付成功、支付失败、退款、订阅续费等,不需要真实支付就能测试所有流程。
-
订单通知推送:新增订单状态变更推送。当订单状态变化(如用户申请退款、订阅续费成功)时,服务端会收到Webhook通知,不需要轮询查询。
总结
Payment Kit让HarmonyOS应用接入IAP变得简单,但"简单"不等于"可以掉以轻心"。支付流程的每一步都关乎钱——校验少了,黑客能白嫖;消费确认忘了,用户买不了第二次;未完成订单不处理,用户付了钱收不到货。
核心记住三点:
- 服务端校验是底线,客户端的支付结果不可信
- 消耗型商品必须确认消费,不确认就买不了第二次
- 启动时检查未完成订单,崩溃和网络中断是常态
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 基础支付简单,服务端校验和订单管理需要严谨的逻辑 |
| 使用频率 | ⭐⭐⭐⭐ 有付费需求的应用必备 |
| 重要程度 | ⭐⭐⭐⭐⭐ 支付出问题就是钱的问题,没有比这更重要的了 |
Payment Kit不是"可选项"——如果你的App有虚拟商品,不走IAP就上不了架。而且支付这东西,宁可多写十行校验代码,也不能少写一行。每一行代码都关乎钱。
- 点赞
- 收藏
- 关注作者
评论(0)