uniapp之ios开发及支付整体流程爬坑记录

前言

在写这篇记录的时候,关于ios的支付已经对接的差不多了,下一步就是测试好了直接发版,总共花了好几周的时间,从0到1对于首次做ios支付来说,确实很多坑。

其实业务层面很简单,甚至比安卓支付还简单,因为支付的整体流程uniapp那边已经提供好了,甚至可以直接套模板。主要坑在于不了解ios内购这套东西,及其细节处理。

该APP使用的是uv-ui组件库,uv-ui 破釜沉舟之兼容vue3+2、nvue、app、h5、小程序等多端基于uni-app和uView2.x的生态框架,支持单独导入,开箱即用,利剑出击。

准备工作

最重要的就是准备证书、描述文件等环节。现在uniapp开发ios没有这两样东西,不能真机运行。还需要区分测试证书和正式证书,本地真机运行只能使用测试证书,正式证书只能打包上传后台到TestFlight中下载测试或提App Store审核(即时到了这步,内购也只能使用沙箱环境支付,非正式支付。只有App Store审核通过后才能走真实支付,这里建议开发灰度测试功能,后续会详细讲解)。

注册账号及创建APP等准备工作都是产品去做的,所以对此流程可能会有遗漏,所以只记录大概我所了解。

  1. 创建你的 Apple ID:https://developer.apple.com/account
  2. 创建一个App应用:
    • 登录iTunes Store,点击我的App
    • 新建一个App(如果App已经创建,直接点击App进入就行了)
    • 填写App的基本信息
  3. 创建证书、描述证书等(Certificates, Identifiers & Profiles):Apple后台、uniapp申请证书引导
    • 需要mac电脑
    • 证书及描述文档要分别创建开发版(Development)和发行版(Distribution),参考uniapp申请证书引导
    • 本地调试使用Development,打包上线审核使用Distribution
  4. 最终需要的东西:
    • Bundle ID(AppID)- 创建证书的时候会填写,类似安卓的证书名(推荐反域名+app标识
    • 证书 - xx.p12文件
    • 描述文件 - xx.mobileprovision文件
    • 证书私钥密码 - 创建证书的时候填写的密码
  5. 首次运行需要进行基座签名
    • 我是使用的 爱思助手 进行的基座签名
    • 具体使用请参考uniapp提供的基座签名指南,描述的很详细,跟着操作就OK
  6. 如果有ios内购项目,添加内购项目:
    • 点击我的App进入App Store
    • 选择功能/App内购买项目
    • 创建App内购买项目:类型、产品 ID等信息
    • 最后须知:ios内购的模式是充值,只能是创建固定的金额进行支付,并且平台抽成30%
  7. 添加沙盒账号
    • 回到iTunes Store首页,点击用户和访问权限 进入 用户和访问
    • 点击沙箱测试员
    • 添加沙箱测试账号信息:沙箱账号的邮件地址是需要没有注册过Apple ID的邮箱,知道这点很重要
    • 总结:沙箱测试账号的作用,在后面测试支付的时候会让输入账号和密码,这时候就需要用到这个沙箱账号了,否则其他账号密码是没用的

真机运行

  1. 完成上面的所有准备,就可以直接使用hbuilderX运行到苹果手机上面
  2. 总之,在使用hbuilderX运行到真机的时候,提示缺什么,我们就需要按照上面的方法准备什么

正式开发

业务开发
  1. 使用app-nvue技术开发ios,90%的代码与安卓都是通用的,毕竟多平台跨端开发,只是有些兼容性问题,需要单独处理而已,具体问题具体分析。
  2. 具体开发的内容就不做详细的介绍,接下来把ios内购买项目做简单的记录。
ios内购开发
  1. 参考文档:uniapp之苹果应用内支付,在开发之前一定要对该文档进行通读和了解,很多开发代码合流程都在这里面。
  2. 后端需要准备两个接口:
    • 接口1:生成业务订单号,前端需要获取后做相关关联
    • 接口2:最后一步,在服务器端请求苹果服务器验证票据
  3. 前端开发步骤:
    • 写好充值页面
    • 创建公共文件iap.js,封装的支付相关处理逻辑,方便后续调用,代码是现成的,直接到示例代码复制
    • 确认充值相关逻辑,完整示例代码,该示例里面需要完善两个接口的逻辑。注意:示例代码可能造成丢单情况,需要配合本地缓存进行处理,参考下面的完整示例。
    • 为了方便理解,我把开发中的完整代码贴在下面:

充值页面混入pay.ios.js:

import { Iap, IapTransactionState } from "@/common/js/iap.js"
export default {data() {return {title: "iap",loadingIOS: false,disabled: true,productId: "",productList: [],isError: false}},methods: {async payInitIOS() {uni.showLoading({mask: true,title: '苹果验证中,请稍等'});this.isError = false;// 创建实例this._iap = new Iap({products: [this.productId] // 苹果开发者中心创建})try {// 初始化,获取iap支付通道await this._iap.init();// 从苹果服务器获取产品列表this.productList = await this._iap.getProduct();this.productList[0].checked = true;this.productId = this.productList[0].productid;// 填充产品列表,启用界面this.disabled = false;} catch (e) {this.isError = true;uni.showModal({title: "init",content: e.message,showCancel: false});} finally {if (this._iap._ready && !this.isError) {this.restore();} else {uni.hideLoading();}}},async restore() {// 检查上次用户已支付且未关闭的订单,可能出现原因:首次绑卡,网络中断等异常// 在此处检查用户是否登陆// uni.showLoading({// 	mask: true,// 	title: '苹果验证中,请稍等'// });try {// 从苹果服务器检查未关闭的订单,可选根据 username 过滤,和调用支付时透传的值一致const transactions = await this._iap.restoreCompletedTransactions({username: ''});if (!transactions.length) {return;}// 开发者业务逻辑,从服务器获取当前用户未完成的订单列表,和本地的比较// 此处省略for (let i = 0; i < transactions.length; i++) {const transaction = transactions[i];switch (transaction.transactionState) {case IapTransactionState.purchased:this.isError = true;// 用户已付款,在此处请求开发者服务器,在服务器端请求苹果服务器验证票据uni.showLoading({mask: true,title: '您有一笔订单正在处理中...'})const order_sn = transaction.payment.username || uni.getStorageSync('IOSPAYORDERID');if(!order_sn) {this.isError = false;return await this._iap.finishTransaction(transaction);}let result = await this.validatePaymentResult({product_id: transaction.payment.productid,order_sn: order_sn,receipt: transaction.transactionReceipt, // 不可作为订单唯一标识transactionIdentifier: transaction.transactionIdentifier}, 0);// 验证通过,交易结束,关闭订单if (result) {await this._iap.finishTransaction(transaction);}break;case IapTransactionState.failed:this.isError = false;// 关闭未支付的订单await this._iap.finishTransaction(transaction);break;default:break;}}} catch (e) {// 为了兼容高版本机型在取消订单时候出现的错误,重启后不存在if(e.code == -100 && e.errMsg.indexOf("本地没有响应要移除的事务")>-1){this.isError = false;return;}this.isError = true;uni.showModal({title: `restore${e.errCode}`,content: e.message,showCancel: false});} finally {if (!this.isError) {this.paymentIOS();} else {uni.hideLoading();}}},async paymentIOS() {if (this.loadingIOS == true) {return;}this.loadingIOS = true;uni.showLoading({mask: true,title: '支付处理中...'});try {// 从开发者服务器创建订单const orderId = await this.createOrder({productId: this.productId});// orderId存在本地,防止丢失uni.setStorageSync('IOSPAYORDERID', orderId);// 请求苹果支付const transaction = await this._iap.requestPayment({productid: this.productId,username: orderId,manualFinishTransaction: true,quantity: 1});// 在此处请求开发者服务器,在服务器端请求苹果服务器验证票据await this.validatePaymentResult({product_id: this.productId,order_sn: transaction.payment.username || orderId,receipt: transaction.transactionReceipt, // 不可作为订单唯一标识transactionIdentifier: transaction.transactionIdentifier});// 验证成功后关闭订单await this._iap.finishTransaction(transaction);// 支付成功this.paySccuess();} catch (e) {uni.$uv.toast('支付取消或失败');} finally {this.loadingIOS = false;uni.hideLoading();}},createOrder({ productId }) {return new Promise((resolve, reject) => {this.getOrderInfo({ product_id: productId }).then(res => {resolve(res.order_no);})})},/*** 充值,e.code = 201 或 then返回均代表 处理成功* @param {Object} data 订单数据*/validatePaymentResult(data, type = 1) {return new Promise((resolve, reject) => {const fn = (loading = 1) => {this.validatePayment(data, loading).then(res => {// 处理成功uni.hideLoading();if (type == 0) {this.successTip();}resolve(true);}).catch(e => {if (e.code == 201) { //处理成功-订单已更新this.successTip();uni.hideLoading();resolve(true);} else {setTimeout(() => {fn(0);}, 3000)}})}fn(type == 0 ? 0 : 1);});},applePriceChange(e) {this.productId = e.detail.value;},successTip() {uni.showModal({title: '温馨提示',content: '您的待处理订单已经处理成功,充值金额已到您的账户余额中,请注查收!',showCancel: false,confirmText: '我知道了'});}}
}

测试支付

  1. 其实ios内购在整个业务逻辑并不复杂,不需要像其他支付进行轮询监听等逻辑,ios内部已经做好了这些事情。
  2. 开始测试就需要对ios这个后台有所了解,我也是第一次接触,所以更多的时间是摸索后台怎么设置,我就讲讲我到底经历了哪些问题:
    • 第一步实例化支付就失败了,后来发现是内购项目未创建,必须先创建购买项目,代码中需要使用产品ID。
    • 购买项目创建好后,实例化等逻辑可以走通了,支付之前会弹出一个让输入账号密码的弹窗。一开始我以为是输入自己的AppleID和密码,试了下输入后提交就失败了。在这里也卡了不少时间,后来发现这里是输入沙箱账号,后来创建了沙箱账号成功支付。
    • 测试过程中,发现有丢单情况,可能是由于网络或者后端验证失败等其他原因,导致最后断单了,只有重启APP才能补单,但是有些机型发现请求苹果支付this._iap.requestPayment传的username参数也会丢失,导致补单的时候和我们的订单关联不上,所以后端无法做最后的验证票据,我们的处理方式是配合本地缓存进行处理,如果只是单个订单支付,就可以这样处理,这里的坑在后面专门进行说明,上述完整示例代码中也有体现。
    • 沙箱环境测试通过,接下来想使用正式支付。开始使用TestFlight测试(测试人员可以在这个软件上安装app进行测试);后来发现在发布App Store审核上线之前,都只能进行沙箱环境支付,这个确实有点坑。经过咨询官方给的解决方案:设置几个账号进行灰度测试,以前没有这个概念,现在终于明白了,灰度测试是这样使用的,经过商量就开发了灰度测试功能,就是固定几个账号才能在上线后支付,等支付没问题后,再放开所有账号支付权限。

开发过程中遇到的坑

首次开发ios及其内购买项目,遇到坑是正常的,感谢这次机会,至少让我得到了成长,接下来就讲讲整个ios开发遇到了哪些坑:

坑一:本地没有响应要移除的事务

如果输入沙箱账号和密码支付后未完成后续验证,杀掉APP进程,重启APP进行补单。这时候肯定会检测到未支付的订单,就需要手动关闭订单this._iap.finishTransaction。但是某些苹果机型一直反馈错误信息:undefined.Payment_appleiap:本地没有响应要移除的事务,https://ask.dcloud.net.cn/article/282

原因分析:在6s机型没有这问题,在7等机型会有这个问题,导致支付流程不能往下执行

解决方案:捕捉到此错误,然后就当正确的逻辑处理,在上述完整示例代码中也有体现

catch (e) {// 为了兼容高版本机型在取消订单时候出现的错误,重启后不存在if(e.code == -100 && e.errMsg.indexOf("本地没有响应要移除的事务")>-1){this.isError = false;return;}
<!--后面的逻辑在省略-->
坑二:不能真实支付

本地只能沙箱账号进行支付测试,怎么办?

解决方案:根据uni官方的回复,灰度测试,设置几个固定账号进行上线后测试,其他账号暂不支持支付。官方回答:https://ask.dcloud.net.cn/question/179074?notification_id-1321394__rf-false__item_id-254173#!answer_254173

坑三:丢单+补单

输入密码支付过程中,杀掉进程,会造成丢单情况

原因分析:由于网络或者用户主动关闭APP等情况,支付流程断掉,如果根据username进行订单关联,可能有些机型在补单的时候丢失该值,最终导致丢单,这在ios是正常情况

解决方案:

  1. 根据业务需求,配合本地缓存将订单记录,在补单的时候好做对应
  2. 可以不使用订单号,据说ios没得订单号的概念,直接后端进行验证,这种方案我们没试过
  3. 这里有个keep客户端开发也遇到丢单的情况,经过多次测试修改,最终的流程和我们的处理方案一致,这个很有参考意义:根治顽疾:Keep客户端 In-App Purchase 掉单踩坑指南
坑四:打包上传

打包上传到iTunes Store,versionCode每次上传都得高于上一次,versionName可以不变

上传到iTunes Store的工具推荐(必须mac):通过 Transporter App 上传 App 的二进制文件

坑五:打开APP苹果手机发烧严重

同一套代码,在安卓机没问题。但是在ios发现发烧很严重,打开APP就开始发烧。

原因分析:1. 开始以为是本地基座的问题,其实仔细想想不会是这个问题,uniapp不会这么拉胯;2. 经过代码排查,发现是因为image标签使用了@load,我们APP中恰好有很多图片展示,这应该是ios这边的机制比较耗CPU,导致发热严重。

解决方案:去掉image上的@load,取消图片加载效果,只做图片失败效果

坑六:uniapp打包提示:打包时未添加OAuth模块

原因分析:代码中使用了uni.preLogin相关,但是ios并未涉及相关模块,所以在ios端屏蔽掉就OK了。

解决方案:参考文档:http://www.codingwhy.com/view/12174.html

坑七:审核多次驳回
  1. 后续补充

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/97843.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

​LeetCode解法汇总121. 买卖股票的最佳时机

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给定一个数…

创作2周年?浅记一下~

前言&#xff1a; 最近确实有点缺乏去更新博客的动力&#xff0c;一晃两年过去了&#xff0c;其实也是我新入职公司的两年&#xff0c;两年虽然不长&#xff0c;但是确实发生了太多事情值得去记录下来... 机缘 说是机缘也不是算是&#xff0c;第一次写博客是刚好在CSDN里面查资…

CentOS如何查找java安装路径

目 录 背景 详细步骤 1.使用指令查看有关javad安装路径 2.填入java路径 3.查找java安装路径 4.配置文件展示 背景 准备部署分布式hadoop的时候&#xff0c;校验hadoop版本发现java没配置 但是又有java版本信息 详细步骤 1.使用指令查看有关javad安装路径 java -verb…

高级IO(Linux)

高级IO 五种IO模型高级IO重要概念同步通信 vs 异步通信阻塞 vs 非阻塞 非阻塞IOfcntl实现函数SetNoBlock轮询方式读取标准输入 I/O多路转接之select初识selectselect函数原型参数解释参数timeout取值关于fd_set结构关于timeval结构函数返回值三级目录 理解select执行过程socket…

微信小程序——CSS3渐变

SS3 渐变&#xff08;gradients&#xff09;可以在两个或多个指定的颜色之间显示平稳的过渡。CSS3 定义了两种类型的渐变&#xff08;gradients&#xff09;&#xff1a; 说明 1、线性渐变&#xff08;Linear Gradients&#xff09;- 向下/向上/向左/向右/对角方向&#xff1…

Java版本企业电子招投标采购系统源码之项目说明和开发类型源码

项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及…

mysqld_multi测试

mysqld_multi测试 mysql版本&#xff1a;5.7.25-log 在OS上分别安装了两套mysql&#xff0c; data目录为/mysql/mysql3306、 /mysql/mysql3307 。 端口分别为3306 、3307 配置文件为&#xff1a; /mysql/mysql3306/my.cnf /mysql/mysql3307/my.cnf 参考文档&#xff1a; htt…

练[FBCTF2019]RCEService

[FBCTF2019]RCEService 文章目录 [FBCTF2019]RCEService掌握知识解题思路关键paylaod 掌握知识 ​ json字符串格式&#xff0c;命令失效(修改环境变量)–绝对路径使用linux命令&#xff0c;%0a绕过preg_match函数&#xff0c;代码审计 解题思路 打开题目链接&#xff0c;发现…

总结js中常见的层次选择器

js中的层次选择器可以用于选择和操作DOM树中的元素&#xff0c;根据元素的层级关系进行选择。以下是js中常见的层次选择器&#xff1a; 1. getElementById&#xff1a;使用元素的ID属性进行选择。通过给元素设置唯一的ID属性&#xff0c;可以使用getElementById方法选择该元素…

Python爬虫——爬虫基础模块和类库(附实践项目)

一、简单介绍 Python爬虫是使用Python编程语言开发的一种自动化程序&#xff0c;用于从互联网上获取信息。通过模拟浏览器的行为&#xff0c;爬虫可以访问网页、解析网页内容&#xff0c;并提取所需的数据。 python的爬虫大致可以分为通用爬虫和专用爬虫&#xff1a; 通用爬虫…

【Linux】Vim使用总结

【Linux】Vim使用总结 Vim 的三种模式命令行模式1. 移动2.复制&#xff0c;粘贴&#xff0c;剪切3.撤销4.大小写切换&#xff0c;替换&#xff0c;删除 插入模式底行模式 Vim 的三种模式 一进入VIM就是处于一般模式&#xff08;命令模式&#xff09;&#xff0c;该模式下只能输…

ES 关于 remote_cluster 的一记小坑

最近有小伙伴找到我们说 Kibana 上添加不了 Remote Cluster&#xff0c;填完信息点 Save 直接跳回原界面了。具体页面&#xff0c;就和没添加前一样。 我们和小伙伴虽然隔着网线但还是进行了深入、详细的交流&#xff0c;梳理出来了如下信息&#xff1a; 两个集群&#xff1a;…

架构师-软件工程习题选择题

架构师-软件工程习题选择题

不同数据类型在单片机内存中占多少字节?

文章目录 前言一、不同编译器二、C51* 指针型 三、sizeof结构体联合体 前言 在C语言中&#xff0c;数据类型指的是用于声明不同类型的变量或者函数的一个广泛的系统。变量的类型决定了变量存储占用的空间 一、不同编译器 类型16位编译器大小32位编译器大小64位编译器大小char…

LINGO-1 - 自动驾驶的 视觉语言动作模型

文章目录 LINGO-1: Exploring Natural Language for Autonomous Driving https://wayve.ai/thinking/lingo-natural-language-autonomous-driving/ 【LINGO-1&#xff1a;将自然语言应用于无人驾驶增强学习和可解释性】 探索将视觉、语言和行动相结合的视觉语言行动模型(VLAM)…

运维常见的22个故障排查和10个问题解决技巧大汇总!

作为运维&#xff0c;多多少少会碰见这样那样的问题或故障&#xff0c;从中总结经验&#xff0c;查找问题&#xff0c;汇总并分析故障的原因&#xff0c;这是一个运维工程师良好的习惯。每一次技术的突破&#xff0c;都经历着苦闷&#xff0c;伴随着快乐&#xff0c;可我们还是…

HTTPS工作过程,国家为什么让http为什么要换成https,Tomcat在MAC M1电脑如何安装,Tomcat的详细介绍

目录 引言 一、HTTPS工作过程 二、Tomcat 在访达中找到下载好的Tomcat文件夹&#xff08;这个要求按顺序&#xff09; zsh: permission denied TOMCAT的各部分含义&#xff1a; 引言 在密码中一般是&#xff1a;明文密钥->密文&#xff08;加密&#xff09; &#xff…

机器学习笔记 - 深入研究spaCy库及其使用技巧

一、简述 spaCy 是一个用于 Python 中高级自然语言处理的开源库。它专为生产用途而设计,这意味着它不仅功能强大,而且快速高效。spaCy 在学术界和工业界广泛用于各种 NLP 任务,例如标记化、词性标注、命名实体识别等。 安装,这里使用阿里的源。 pip install spacy…

三十二、【进阶】hash索引结构

1、hash索引结构 &#xff08;1&#xff09;简述&#xff1a; hash索引&#xff0c;就是采用一定的hash算法&#xff0c;将键值换算成新的hash值&#xff0c;映射到对应的槽位上&#xff0c;然后存储在hash表中。 &#xff08;2&#xff09;图示&#xff1a; 2、hash索引结构…

elasticsearch深度分页问题

一、深度分页方式from size es 默认采用的分页方式是 from size 的形式&#xff0c;在深度分页的情况下&#xff0c;这种使用方式效率是非常低的&#xff0c;比如我们执行如下查询 1 GET /student/student/_search 2 { 3 "query":{ 4 "match_all":…