UNIAPP-苹果内购全链路实践:从客户端到SpringBoot服务端

张开发
2026/4/14 10:08:49 15 分钟阅读

分享文章

UNIAPP-苹果内购全链路实践:从客户端到SpringBoot服务端
1. 苹果内购基础准备在开始UNIAPP与SpringBoot的苹果内购集成前需要完成苹果开发者账号的基础配置。我遇到过不少开发者卡在这一步其实核心就四个关键点开发者账号类型选择个人账号和公司账号都能实现内购但虚拟商品必须使用商务管理权限。去年有个客户用个人账号提交审核被拒三次后来发现是账号类型权限不足。建议直接注册公司开发者账号年费同样是99美元。证书配置的坑Identifiers创建时务必勾选In-App Purchase选项这个选项一旦漏选就无法补加。我习惯同时创建两个证书开发证书iOS Development用于真机调试分发证书iOS Distribution用于正式发布银行与税务信息这里最容易耽误时间。税务表单的W-8BEN表格需要填写美国税号豁免中文界面翻译可能有歧义。建议在提交后预留2小时生效时间有次我测试时发现状态一直显示等待用户信息其实是苹果后台同步延迟。产品ID命名技巧在App Store Connect创建内购项目时产品ID建议采用公司缩写.产品类型.金额的格式如YC.PREMIUM.50。曾有个项目用随机字符串导致后期对账困难修改又要重新审核。消耗型项目还要注意配置沙箱测试账号最好用专用邮箱后缀如test.com。2. UNIAPP客户端集成实战2.1 支付通道获取的两种方式UNIAPP提供了原生封装和5API两种调用方式实测发现三个重要差异点类型支持差异// UNIAPP封装版返回any类型 const uniChannel res.providers.find(c c.id appleiap) // 5API返回PlusPaymentPaymentChannel类型 const plusChannel channels.find(i i.id appleiap)方法完整性5API缺少restoreCompletedTransactions等关键方法这在处理丢单时会很麻烦。我做过对比测试原生封装版支持6个完整API5版只有3个基础方法执行效率在iPhone12上实测原生封装版的通道获取速度快15%左右。建议高频支付场景用原生方案。2.2 产品列表拉取策略这里有个容易忽略的要点苹果返回的产品列表可能包含本地化价格信息。推荐两种业务方案方案A纯客户端拉取// 获取苹果产品详情 const productList await uni.request({ url: https://api.storekit.itunes.apple.com/v1/app/inAppPurchases, header: { Authorization: Bearer ${token}, Content-Type: application/json } })优点实时获取最新价格 缺点需要处理苹果API的复杂鉴权方案B服务端中转推荐// SpringBoot示例 GetMapping(/products) public ListProduct getProducts() { // 先从数据库读取产品配置 ListProduct dbProducts productService.list(); // 与苹果API返回数据做匹配 return dbProducts.stream() .map(p - { p.setApplePrice(queryApplePrice(p.getIosId())); return p; }).collect(Collectors.toList()); }2.3 支付发起与状态处理支付代码看似简单但有几个魔鬼细节await uni.requestPayment({ provider: appleiap, orderInfo: { productid: com.your.product, // 必须与苹果后台一致 manualFinishTransaction: true, // 关键参数 quantity: 1, }, success: (res) { // 这里要立即调用服务端验单 verifyReceipt(res.transactionReceipt); } });关键参数manualFinishTransaction这个布尔值决定支付流程的生死。设为true时支付完成后不会自动关闭订单必须手动调用finishTransaction避免重复支付时拿到相同transactionId3. SpringBoot服务端关键实现3.1 凭证验证的防坑指南苹果的验证接口有两个环境生产环境https://buy.itunes.apple.com/verifyReceipt沙箱环境https://sandbox.itunes.apple.com/verifyReceipt自动环境切换逻辑private String verifyReceipt(String receipt) { // 先尝试生产环境 String result callAppleAPI(PRODUCTION_URL, receipt); if(result.contains(21007)) { // 如果是沙箱收据自动切换环境 return callAppleAPI(SANDBOX_URL, receipt); } return result; }SSL证书处理苹果接口需要HTTPS调用但Java默认的证书验证会失败。需要重写验证逻辑SSLContext ssl SSLContext.getInstance(SSL); ssl.init(null, new TrustManager[] { new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) {} public void checkServerTrusted(X509Certificate[] chain, String authType) {} public X509Certificate[] getAcceptedIssuers() { return null; } } }, null); HttpsURLConnection.setDefaultSSLSocketFactory(ssl.getSocketFactory());3.2 订单状态机设计完整的支付状态流转应该包含stateDiagram [*] -- PENDING: 创建订单 PENDING -- PAID: 客户端支付成功 PAID -- VERIFYING: 发起苹果验证 VERIFYING -- COMPLETED: 验证通过 VERIFYING -- FAILED: 验证失败 FAILED -- REFUNDED: 执行退款对应的SpringBoot实体设计Entity public class AppleOrder { Id private String transactionId; Enumerated(EnumType.STRING) private OrderStatus status; // 枚举值 private LocalDateTime verifyTime; private BigDecimal amount; // 关联用户ID private Long userId; }3.3 资金入账的并发控制高并发场景下要防止重复入账我推荐两种方案数据库层面UPDATE user_balance SET balance balance 50 WHERE user_id 123 AND NOT EXISTS ( SELECT 1 FROM payment_log WHERE transaction_id ABC123 )Java代码层面Transactional public synchronized void addBalance(String transactionId) { if(paymentLogRepository.existsByTransactionId(transactionId)) { throw new BusinessException(重复交易); } // 执行余额增加操作 }4. 全链路异常处理方案4.1 客户端丢单恢复苹果支付有个经典问题用户付款后客户端崩溃。解决方案是启动时检查未完成订单// App.vue的onLaunch中 uni.getProvider({ service: payment, success: (res) { const iap res.providers.find(c c.id appleiap); iap.restoreCompletedTransactions({ manualFinishTransaction: true }, (transactions) { if(transactions.length 0) { // 提交服务端补单 retryPayment(transactions[0]); } }); } });4.2 服务端验证重试机制苹果接口有时会返回21005等临时错误需要实现指数退避重试private String callAppleWithRetry(String url, String receipt) { int retry 0; while(retry 3) { try { return callAppleAPI(url, receipt); } catch (Exception e) { retry; Thread.sleep(1000 * (int)Math.pow(2, retry)); // 1s, 2s, 4s } } throw new RuntimeException(苹果接口调用失败); }4.3 对账系统设计建议每天凌晨跑对账任务检查三方状态一致性-- 找出状态不一致的订单 SELECT o.order_id FROM orders o LEFT JOIN apple_receipts a ON o.transaction_id a.transaction_id WHERE o.status PAID AND (a.status IS NULL OR a.status ! 0)这套UNIAPPSpringBoot的苹果内购方案已经在电商、知识付费等场景验证过。最复杂的不是技术实现而是对苹果各种边界case的处理。建议开发阶段多测试网络中断、进程被杀等异常场景确保资金安全万无一失。

更多文章