公司内部员工使用的iOS客户端应用希望对内开放,不需要发布于AppStore直接能够让内部用户获取,对于Android应用来说这个问题很好解决,直接下发安装包然后就能安装了;但是对于苹果生态来说,这种方式是行不通的,因为苹果本身有一套完备的应用安装体系,除了具备一定特性之外的应用,都必须通过在AppStore上发布然后被用户获取。但是苹果依然对企业内部应用(In-House应用)有所特别对待,即可通过web方式来获取和安装,那么我们需要做的,就是熟悉这一套实现流程。
本项目主要说明后台服务端实现,前期还有很多准备工作,可能涉及到的是苹果开发者账号、企业证书生成、企业证书签名的ipa、应用相关的bundle-identifier等,这些事项基本都是iOS客户端开发同学来操作的,后台项目需要用到的内容都可以找他们提供。
iOS APP
1、必须是由$299购买的企业证书签名过的In-House应用,$99购买的证书签名是无效的。
2、需要提供应用或者证书相关的bundle-identifier信息,因为plist中需要使用。
plist
1、plist文件必须使用固定且完整的xml格式。
2、plist文件中的ipa文件路径无须是https协议下的。
3、plist文件必须通过https协议访问,而且是苹果受信任的企业证书。
1、通过web后台来管理和维护iOS版本。
2、web后台提供iOS应用的上传功能,上传的同时生成和app配套的plist文件。
3、app文件上传成功,web后台维护记录成功之后,会得到safari浏览器访问的路径。
4、Safari浏览器访问到获取应用的路径之后会打开下载页面,点击按钮是通过itms-services协议访问的plist文件。
5、访问该文件之后,手机将会自动弹窗提示当前网站想要安装XXX应用。
6、安装应用完成之后,首次尝试打开应用时,系统会提示该应用未受信任,需要前往手机「设置-通用-描述文件与设备管理」下信任该应用,信任之后将可以正常打开和使用。
1、web后台上传和维护app应用
(展开以显示代码)
1 <!-- Captain&D -->
2 <!-- https://www.cnblogs.com/captainad/ -->
3 <div class="modal inmodal" id="myModal_editApp" tabindex="-1" role="dialog" aria-hidden="true">
4 <div style="width: 1000px" class="modal-dialog">
5 <div class="modal-content animated bounceInRight">
6 <div class="modal-header">
7 <button type="button" class="close" data-dismiss="modal"><span
8 aria-hidden="true">×</span><span class="sr-only">关闭</span>
9 </button>
10 <h5 class="modal-title" id="configTitle" data-lang="">增加/修改应用版本</h5>
11 <input type="hidden" id="versionId" >
12 <input type="hidden" id="appTypeId" >
13 </div>
14 <div class="modal-body">
15 <div class="row">
16 <div class="col-sm-6">
17 <div class="form-group">
18 <label>对外版本号</label>
19 <input type="text" id="versionName" class="form-control" placeholder="下载时显示的apk名称,无需加.apk后缀">
20 </div>
21 <div class="form-group">
22 <label>对内版本号</label>
23 <input type="text" id="versionCode" class="form-control">
24 </div>
25 <div class="form-group">
26 <label id="appfile_title">应用文件</label>
27 <div id="file-pretty">
28 <div class="form-group">
29 <input type="file" name="accountFile" id="appfile" class="form-control" >
30 </div>
31 </div>
32 </div>
33 <div class="form-group">
34 <label>发布版本</label>
35 <div class="checkbox checkbox-success">
36 <input id="checkbox2" type="checkbox">
37 <label for="checkbox2">
38 勾选并保存修改之后,当前版本将发布成博客原创Captain&D在线可用的最新版本
39 </label>
40 </div>
41 </div>
42 <div class="form-group">
43 <
label>是否强制升级</label>
44 <div class="checkbox checkbox-success">
45 <input id="checkbox4" type="checkbox">
46 <label for="checkbox4">
47 当前版本启用之后,用户打开客户端后会立即强制升级成博客原创Captain&D当前版本
48 </label>
49 </div>
50 </div>
51 </div>
52 <div class="col-sm-6">
53 <div class="form-group">
54 <label>升级日志</label>
55 <textarea class="form-control" id="upgradeLog" rows="12" style="resize: none"></textarea>
56 </div>
57 </div>
58 </div>
59 <div class="row">
60 <p style="color:red;display: none" id="errMsg">
61 </p>
62 </div>
63 </div>
64 <div class="modal-footer">
65 <button type="button" class="btn btn-success" id="saveEdit" >保存</button>
66 <button type="button" class="btn btn-white" data-dismiss="modal" data-lang="close">关闭</button>
67 </div>
68 </div>
69 </div>
70 </div>
View Code
2、从页面上传附件相关处理方式
(展开以显示代码)
1 <!-- Captain&D -->
2 <!-- https://www.cnblogs.com/captainad/ -->
3 $("#saveEdit").click(function () {
4 if(validateParam()) return;
6 // 先进行存在性校验
7 var formdate = new FormData();
8 formdate.append('id', $("#versionId").val());
9 formdate.append('versionName', $("#versionName").val());
10 formdate.append('versionCode', $("#versionCode").val());
11 $('#loading-modal').modal("show");
12 $.ajax({
13 url: "versionmng/existsSameAppVersion",
14 type: "post",
15 data: formdate,
16 processData : false,
17 contentType : false,
18 success: function(data1){
19 if(data1.code == 200) {
21 // 正式发起保存请求
22 var checked = $("#checkbox2").is(':checked');
23 var checked1 = $("#checkbox4").is(':checked');
24 var formdate = new FormData();
25 var fils = $("#appfile").get(0).files[0];
26 console.log(fils);
27 formdate.append('appFile', fils);
28 formdate.append('id', $("#versionId").val());
29 formdate.append('appType', $("#appTypeId").val());
30 formdate.append('versionName', $("#versionName").val());
31 formdate.append('versionCode', $("#versionCode").val());
32 formdate.append('upgradeLog', $("#upgradeLog").val());
33 formdate.append('appStatus', checked ? 1 : 0);
34 formdate.append('forcedUpgrade', checked1 ? 1 : 0);
36 $.ajax({
37 url: "versionmng/addAppVersion",
38 type: "post",
39 data: formdate,
40 processData : false,
41 contentType : false,
42 success: function(data){
43 if(data.code == 200) {
44 $("#myModal_editApp").modal("hide");
45 $("#errMsg").html("");
46 $("#errMsg").css("display", "none");
47 swal("Successfully", "新增/修改App应用版本信息博客原创Captain&D成功", "success");
48 initload(pageObj);
49 }else {
50 swal("Failed", data.msg, "error");
51 }
52 $('#loading-modal').modal("hide");
53 }
54 });
56 }else {
57 swal("Failed", data1.msg, "error");
58 $('#loading-modal').modal("hide");
59 }
60 }
61 });
62 })
View Code
3、Captainad通过上传资源到云服务器的方法
(展开以显示代码)
1 /**
2 * 增加应用版本
3 * Captain&D
4 * https://www.cnblogs.com/captainad/
5 */
6 public Result addAppVersion(HttpServletRequest request, @RequestParam(value = "appFile", required = false) MultipartFile file) {
8 ···
10 // 文件处理
11 if(file != null && file.getSize() > 0) {
12 // 检查文件类型
13 String filename = file.getOriginalFilename();
14 String suffix = filename.substring(filename.lastIndexOf("."), filename.length());
15 log.info("file format: {} {}", filename, suffix);
16 if ("1".equals(appType) && !".apk".contains(suffix) || "2".equals(appType) && !".ipa".contains(suffix)) {
17 return Result.builder()
18 .code(ResultLanguage.getResultMessage(ResultMessage.WRONG_APP_FORMAT).getCode())
19 .msg(ResultLanguage.getResultMessage(ResultMessage.WRONG_APP_FORMAT).getMsg()).build();
20 }
21 String appName = "";
22 if("1".equals(appType)) {
23 appName = versionName.replace(" ", "_").replace(".apk", "").concat(".apk");
24 }else {
25 appName = versionName.replace(" ", "_").replace(".ipa", "").concat(".ipa");
26 }
28 try{
29 Map<String, String> fileMap = fileOperationService.uploadFile(appName, "/captainad/app/", file.getInputStream());
30 if(null != fileMap && !fileMap.isEmpty()) {
31 for(Map.Entry<String, String> set : fileMap.entrySet()) {
32 String downloadUrl = set.getKey();
33 String appMd5 = set.getValue();
34 requestMap.put("downloadUrl", new String[]{downloadUrl});
35 requestMap.put("appMd5", new String[]{appMd5});
36 }
37 }else {
38 return Result.builder()
39 .code(ResultLanguage.getResultMessage(ResultMessage.APP_UPLOAD_ERROR).getCode())
40 .msg(ResultLanguage.getResultMessage(ResultMessage.APP_UPLOAD_ERROR).getMsg()).build();
41 }
42 }catch (Exception e) {
43 log.error("上传客户端App文件存在异常。", e);
44 }
45 }
View Code
4、通过拼接字符串生成plist文件
1 /**
2 * 生成iOS应用对应的plist文件
3 * Captain&D
4 * https://www.cnblogs.com/captainad/
5 */
6 private String genIosPlist(CaptainadAppVersionInfo captainadAppVersionInfo){
7 StringBuilder builder = new StringBuilder();
8 builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
9 builder.append("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
10 builder.append("<plist version=\"1.0\">");
11 builder.append("<dict>");
12 builder.append(" <key>items</key>");
13 builder.append(" <array>");
14 builder.append(" <dict>");
15 builder.append(" <key>assets</key>");
16 builder.append(" <array>");
17 builder.append(" <dict>");
18 builder.append(" <key>kind</key>");
19 builder.append(" <string>software-package</string>");
20 builder.append(" <key>url</key>");
21 builder.append(" <string>").append(captainadAppVersionInfo.getDownloadUrl()).append("</string>");
22 builder.append(" </dict>");
23 builder.append(" </array>");
24 builder.append(" <key>metadata</key>");
25 builder.append(" <dict>");
26 builder.append(" <key>bundle-identifier</key>");
27 builder.append(" <string>").append(getSetCacheService.getConfigValue("ios_bundle_identifier")).append("</string>");
28 builder.append(" <key>bundle-version</key>");
29 builder.append(" <string>").append(captainadAppVersionInfo.getVersionCode()).append("</string>");
30 builder.append(" <key>kind</key>");
31 builder.append(" <string>software</string>");
32 builder.append(" <key>title</key>");
33 builder.append(" <string>Captainad App</string>");
34 builder.append(" </dict>");
35 builder.append(" </dict>");
36 builder.append(" </array>");
37 builder.append("</dict>");
38 builder.append("</plist>");
39 String plistName = captainadAppVersionInfo.getVersionName().concat(".plist");
40 try {
41 InputStream is = new ByteArrayInputStream(builder.toString().getBytes("UTF-8"));
42 Map<String, String> fileMap = fileOperationService.uploadFile(plistName, "/captainad/app/plist/", is);
43 if(null != fileMap && !fileMap.isEmpty()) {
44 for(Map.Entry<String, String> entry : fileMap.entrySet()) {
45 log.info("生成的plist的文件地址:{}", entry.getKey());
46 return entry.getKey();
47 }
48 }
49 } catch (Exception e) {
50 log.error("生成plist文件时出现异常。", e);
51 }
52 return null;
5、数据库表设计(展开以显示代码)
1 -- Captain&D
2 -- https://www.cnblogs.com/captainad/
3 CREATE TABLE `captainad_app_version_info` (
4 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
5 `version_name` varchar(64) DEFAULT NULL COMMENT '外部版本号',
6 `version_code` varchar(64) DEFAULT NULL COMMENT '内部版本号',
7 `upgrade_log` text COMMENT '更新日志',
8 `download_url` varchar(128) DEFAULT NULL COMMENT '版本路径',
9 `app_md5` varchar(32) DEFAULT NULL COMMENT '文件MD5',
10 `app_status` int(11) DEFAULT NULL COMMENT '版本状态(0-关闭,1-启用)',
11 `release_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间',
12 `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
13 `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
14 `forced_upgrade` int(11) DEFAULT '0' COMMENT '是否强制升级(0-否,1-是)',
15 `app_type` int(11) DEFAULT NULL COMMENT '应用类型(1-Android,2-iOS)',
16 PRIMARY KEY (`id`)
17 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='App版本管理';
View Code
6、safari通过访问路径之后的路由处理(展开以显示代码)
1 /**
2 * 进入App下载安装页面
3 * Captain&D
4 * https://www.cnblogs.com/captainad/
5 */
6 @AuthorityVerify
7 @RequestMapping("ios")
8 public String toDownloadIosAppPage(HttpServletRequest request) {
9 String version = request.getParameter("version");
10 String httpsHost = getSetCacheService.getConfigValue("file_cloud_visit_host_https");
11 String plistUrl = httpsHost.concat("/captainad/app/plist/").concat(version).concat(".plist");
12 request.setAttribute("plist", plistUrl);
13 return "/appmng/ios_app";
View Code
7、应用下载页面的plist路由协议写法
1 <!-- Captain&D -->
2 <!-- https://www.cnblogs.com/captainad/ -->
3 <!-- 下载安装in-house应用关键代码 -->
4 <div class="wrapper wrapper-content">
5 <div class="row">
6 <div class="col-sm-12">
7 <div class="middle-box text-center animated fadeInRightBig" style="margin-top: 90%;">
8 <!--<h3 class="font-bold">这里是页面内容</h3>-->
10 <div class="install-btn">
11 <br/><a href="itms-services://?action=download-manifest&url=${plist}" class="btn btn-success btn-lg m-t">
12 <i class="fa fa-apple"></i> Install Tesla app for iOS</a>
13 </div>
14 </div>
15 </div>
16 </div>
17 </div>
1、应用列表
2、应用详情
3、扫描安装图示(项目暂时无法截图,故参考自网络,打码处理,侵删)
4、信任应用(项目暂时无法截图,故参考自网络,打码处理,侵删)
遇到问题及解决思路和方法
1、Safari点击之后出现无法连接到xxx.xx.com现象。
检查下发的plist文件能否访问。
询问Https证书是否是有效的并且受信任的。
检查访问的plist文件的链接是否是https协议的。
检查下发的plist文件xml格式是否正常,可以在线格式化下,看是否报错。
2、能够连接但是无法下载安装。
检查plist文件中链接的ipa文件是否可达。
检查文件格式是否为ipa,检查ipa文件名与plist文件名是否一致。
1、https://www.jianshu.com/p/89d22b430330
2、https://www.cnblogs.com/star91/p/5018995.html
Captain&D所发布的博文均为原创,概不任意转载,如有参考必定给出原文链接。
本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。