uniapp实现Android apk自动检测更新强制下载安装

概要
本篇文章主要讲诉uniapp 打包的APK如何实现在线升级功能,实现强制升级更新、可选升级更新、下载进度百分比显示、下载完成自动跳转安装功能,自己项目已经测试没有问题,可以结合自己的项目进行引入使用。

需求分析
1、需要进入app进行检查版本,进行判断是否需要更新

2、显示升级更新弹窗

3、是强制升级更新或者可选升级更新

4、对APP下载监听,进行进度条的展示(plus.downloader.createDownload(url,options,completedCallback)(下载))

5、下载完成apk后,直接跳转安装 (plus.runtime.install(安装APP))

技术实现梳理
1.是否更新判断:

        通过接口(自己实现接口)获取线上最新版本号(默认规定版本号为正整数)与本地APP版本号进行比较大小,当线上最新版本号大于本地版本号就需要更新。本地App版本可在每次发版时候在manifest.json-基础配置-应用版本号进行设置

   2.升级弹窗的展示

       升级弹窗实现有2种方案,一种直接在首页里嵌套弹窗组件,另一种是把弹窗放置在独立的页面,并把页面窗口设置透明,当需要升级的时候直接从首页进入,从视觉效果上看就相当于在首页上的悬浮窗口。考虑到后续有强制更新页面不能返回等操作,便于维护本案例将采用第二种方案

 3.根据升级类型限制操作

   当升级类型为强制升级意味着页面不能做除了升级的任何操作,包括返回功能,关闭弹窗功能,禁止返回可通过onBackPress生命周期函数处理,弹窗关闭入口动态控制,包括关闭按钮,遮罩层点击关闭功能等

 4.下载APP监听下载进度

  通过H5+方式下载 :plus.downloader.createDownload,生成下载任务对象(downloadTask),通过downloadTask.addEventListener("statechanged",(task,status)=>{})监听下载进度

 5.下载完自动安装

  通过H5+ plus.runtime.install实现自动安装,该api只能监听是否打开安装页面,无法监测到apk是否安装成功,还需要调用安卓原生注册广播事件,监听apk安装成功回调

核心API讲解
1.plus.downloader.createDownload(url,options,completedCallback)(下载)

     说明:请求下载管理创建新的下载任务,创建成功则返回Download对象,用于管理下载任务   

     参数:url: ( String ) 必选 要下载文件资源地址

要下载文件的url地址,仅支持网络资源地址,支持http或https协议。 允许创建多个相同url地址的下载任务。 注意:如果url地址中包含中文或空格等,需要进行urlencode转换。

options: ( DownloadOptions ) 可选 下载任务的参数

可通过此参数设置下载任务属性,如保存文件路径、下载优先级等。

completedCallback: ( DownloadCompletedCallback ) 可选 下载任务完成回调函数

当下载任务下载完成时触发,成功或失败都会触发。

返回值:Download:新建的下载任务对象

 2.plus.runtime.install(filePath,options,installSuccessCB,installErrorCB)(安装APP)

说明:支持以下类型安装包:1. 应用资源安装包(wgt),扩展名为'.wgt'; 2. 应用资源差量升级包(wgtu),扩展名为'.wgtu'; 3. 系统程序安装包(apk),要求使用当前平台支持的安装包格式。       注意:仅支持本地地址,调用此方法前需把安装包从网络地址或其他位置放置到运行时环境可以访问的本地目录。

参数:

filePath: ( String ) 必选 要安装的文件路径

支持应用资源安装包(wgt)、应用资源差量升级包(wgtu)、系统程序包(apk)。

options: ( WidgetOptions ) 可选

应用安装设置的参数

installSuccessCB: ( InstallSuccessCallback ) 可选

正确安装后的回调

installErrorCB: ( InstallErrorCallback ) 可选

安装失败的回调

返回值:void : 无

代码实现
    新增升级弹窗页面路由,设置窗口透明

    项目目录:

 pages.json: 
 

{   
	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
    {
        "path": "pages/index/index", //首页
        "style": {
            "navigationBarTitleText": ""
        }
    },{
        "path": "pages/index/upgrade", //升级窗口页面
        "style": {
            "navigationBarTitleText": "",
            "navigationStyle": "custom", //导航栏自定义
            "app-plus": {
                "bounce": "none",
                "animationType": "none", //取消窗口动画
                "background": "transparent" // 设置背景透明
            }
        }
    }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "uni-app",
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8"
    },
    "uniIdRouter": {}
}
index.vue:(首页)

从接口获取最新版本号,跟本地对比,判断是否进入升级弹窗页

checkVersionPage(){
				var postData = {
					app: getApp().globalData.APP,
					version: getApp().globalData.versionName
				}
				getAppVersion(postData).then(res=>{//这里是自己的接口进行判断,获取本地版本名称和最新版本apk版本名称做的对比,服务端做的对比
					if(res.data.code == '0000'){
						if(res.data.data.is_update == 2){
							var versionData = res.data.data
							uni.navigateTo({
								url: '/pages/upgrade/upgrade?versionName=' + res.data.data.version + '&isForce=' + res.data.data.is_force,
								events: {
									// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
									acceptDataFromOpenedPage: function(data) {
									},
									someEvent: function(data) {
									}
								},
								success: function(res) {
									// 通过eventChannel向被打开页面传送数据
									res.eventChannel.emit('acceptDataFromOpenerPage', versionData)
								},
								fail: function(error) {
								}
							})
						}
					}
				})
			},
upgrade.vue(升级弹窗页):

布局样式可根据实际调整,顶部背景图upgrade_bg.png自行放入static

<template>
	<view class="upgrade-popup">
		<image class="header-bg" src='../../static/versionBac.jpg' mode="widthFix"></image>
		<view class="main">
			<view class="versionPopupTitleClass">发现新版本</view>
			<view class="versionPopupNameClass">v{{versionName}}</view>
			<view class="content">
				<text class="updataContentTitleClass">更新内容</text>
				<view class="updataContentClass" v-html="versionDesc"></view>
			</view>
			<!--下载状态-进度条显示 -->
			<view class="footer" v-if="isStartDownload">
				<view class="progress-view" :class="{'active':!hasProgress}" @click="handleInstallApp">
					<!-- 进度条 -->
					<view v-if="hasProgress" style="height: 100%;">
						<view class="txt">{{percentText}}</view>
						<view class="progress" :style="setProStyle"></view>
					</view>
					<view v-else>
						<view class="btn upgrade force">
							{{ isDownloadFinish  ? '立即安装' :'下载中...'}}
						</view>
					</view>
				</view>
			</view>
			<!-- 强制更新 -->
			<view class="footer" v-else-if="isForceUpdate">
				<view class="btn upgrade force" @click="handleUpgrade">立即体验</view>
			</view>
			<!-- 可选择更新 -->
			<view class="footer" v-else>
				<view class="btn close" @click="handleClose">暂不更新</view>
				<view class="btn upgrade" @click="handleUpgrade">立即体验</view>
			</view>
		</view>
	</view>
</template>
 
<script>
	import {
		downloadApp,
		installApp
	} from '../../utils/upgrade.js'
	export default {
 
		data() {
			return {
				isForceUpdate: false, //是否强制更新
				versionName: '', //版本名称
				versionDesc: '', //更新说明
				downloadUrl: '', //APP下载链接
				isDownloadFinish: false, //是否下载完成
				hasProgress: false, //是否能显示进度条
				currentPercent: 0, //当前下载百分比
				isStartDownload: false, //是否开始下载
				fileName: '', //下载后app本地路径名称
			}
		},
 
		computed: {
			//设置进度条样式,实时更新进度位置
			setProStyle() {
				return {
					width: (510 * this.currentPercent / 100) + 'rpx' //510:按钮进度条宽度
				}
			},
 
			//百分比文字
			percentText() {
				let percent = this.currentPercent;
				if (typeof percent !== 'number' || isNaN(percent)) return '下载中...'
				if (percent < 100) return `下载中${percent}%`
				return '立即安装'
			}
 
		},
 
		onLoad(option) {
			if(option.versionName){
				this.versionName = option.versionName
			}
			if(option.isForce == 2){
				this.isForceUpdate = true
			}else{
				this.isForceUpdate = false
			}
			var that = this
			let eventChannel = this.getOpenerEventChannel();
			try {
				// 监听versionUpdateData事件,获取上一页面通过eventChannel传送到当前页面的数据
				eventChannel.on('acceptDataFromOpenerPage', function(data) {
					that.$utils.log("打印传递过来的数据  " + JSON.stringify(data))
					that.init(data)
				})
			} catch (e) {
				//TODO handle the exception
			}
		},
 
		onBackPress(options) {
			// 禁用返回
			if (options.from == 'backbutton') {
				return true;
			}
		},
 
		methods: {
			//初始化获取最新APP版本信息
			init(data) {
				if (data.is_force == 2) {
					this.isForceUpdate = true
				} else {
					this.isForceUpdate = false
				}
				if (data.version) {
					this.versionName = data.version
				}
				if (data.version_desc) {
					this.versionDesc = data.version_desc
				}
				this.downloadUrl = data.url ? data.url : "xx.apk"
			},
 
			//更新
			handleUpgrade() {
				if (this.downloadUrl) {
					this.isStartDownload = true
					//开始下载App
					downloadApp(this.downloadUrl, current => {
						//下载进度监听
						this.hasProgress = true
						this.currentPercent = current
					}).then(fileName => {
						//下载完成
						this.isDownloadFinish = true
						this.fileName = fileName
						if (fileName) {
							//自动安装App
							this.handleInstallApp()
						}
					}).catch(e => {
						console.log(e, 'e')
					})
				} else {
					uni.showToast({
						title: '下载链接不存在',
						icon: 'none'
					})
				}
			},
 
			//安装app
 
			handleInstallApp() {
				//下载完成才能安装,防止下载过程中点击
				if (this.isDownloadFinish && this.fileName) {
					installApp(this.fileName, () => {
						//安装成功,关闭升级弹窗
						uni.navigateBack()
					})
				}
			},
 
			//关闭返回
			handleClose() {
				uni.setStorageSync('upDateTime',Math.floor(new Date().getTime() / 1000))
				uni.setStorageSync('isUpdate',true)
				uni.navigateBack()
			},
		}
	}
</script>
 
<style>
	page {
		background: rgba(0, 0, 0, 0.5);
		/**设置窗口背景半透明*/
	}
</style>
 
<style lang="scss" scoped>
	.upgrade-popup {
		width: 610rpx;
		height: 654.14rpx;
		position: fixed;
		top: 50%;
		left: 52%;
		transform: translate(-50%, -50%);
		border-radius: 20rpx;
		box-sizing: border-box;
	}
 
	.header-bg {
		width: 100%;
	}
	.main {
		position: absolute;
		top: 69rpx;
		padding-left: 35rpx;
		box-sizing: border-box;
		
 
		.footer {
			width: 100%;
			display: flex;
			justify-content: center;
			align-items: center;
			position: relative;
			flex-shrink: 0;
			margin-top: 100rpx;
 
			.btn {
				width: 246rpx;
				display: flex;
				justify-content: center;
				align-items: center;
				position: relative;
				z-index: 999;
				height: 96rpx;
				box-sizing: border-box;
				font-size: 38rpx;
				font-family: Source Han Sans-Regular;
				border-radius: 10rpx;
				letter-spacing: 2rpx;
				border-radius: 50rpx 50rpx 50rpx 50rpx;
				&.force {
					width: 500rpx;
				}
 
				&.close {
					margin-right: 25rpx;
					background: #E1E1E1;
					font-weight: 400;
					font-size: 34rpx;
					color: #3D3D3D;
				}
 
				&.upgrade {
					background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
					color: white;
				}
 
			}
 
			.progress-view {
				width: 500rpx;
				height: 90rpx;
				display: flex;
				position: relative;
				align-items: center;
				border-radius: 50rpx;
				background-color: #FFCEBE;
				display: flex;
				justify-content: flex-start;
				padding: 0px;
				box-sizing: border-box;
				border: none;
				overflow: hidden;
				font-size: 38rpx;
				font-family: Source Han Sans-Regular;
				&.active {
					// background-color: #FFCEBE;
					background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
					border-radius: 50rpx 50rpx 50rpx 50rpx;
				}
				.progress {
					height: 100%;
					// background-color: #FFCEBE;
					background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
					padding: 0px;
					box-sizing: border-box;
					border: none;
					border-radius: 50rpx;
				}
 
				.txt {
					
					position: absolute;
					top: 50%;
					left: 50%;
					transform: translate(-50%, -50%);
					color: #fff;
 
				}
 
			}
 
		}
 
	}
	.content {
		margin-top: 60rpx;
	}
	.versionPopupTitleClass{
		height: 53rpx;
		font-family: Alibaba PuHuiTi;
		font-weight: 700;
		font-size: 38rpx;
		color: #3D3D3D;
		line-height: 53rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 41.35rpx;
	}
	.versionPopupNameClass{
		width: 71rpx;
		height: 38rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 350;
		font-size: 26rpx;
		color: #ADADAD;
		line-height: 38rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 8rpx;
	}
	.updataContentTitleClass{
		height: 38rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 26rpx;
		color: #3D3D3D;
		line-height: 38rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 120rpx;
	}
	.updataContentClass{
		height: 32rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 23rpx;
		color: #727272;
		line-height: 32rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 16.92rpx;
	}
	.zanbugengxinClass{
		width: 252rpx;
		height: 92rpx;
		background: #E1E1E1;
		border-radius: 50rpx 50rpx 50rpx 50rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 34rpx;
		color: #3D3D3D;
		text-align: center;
		font-style: normal;
		text-transform: none;
	}
	.lijitiyanClass{
		width: 252rpx;
		height: 92rpx;
		background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
		border-radius: 50rpx 50rpx 50rpx 50rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 34rpx;
		color: #FFFFFF;
		text-align: center;
		font-style: normal;
		text-transform: none;
	}
</style>
upgrade.js(下载、安装工具类):
/**
 * @description H5+下载App
 * @param downloadUrl:App下载链接
 * @param progressCallBack:下载进度回调
 */
export const downloadApp = (downloadUrl, progressCallBack = () => {}, ) => {
	return new Promise((resolve, reject) => {
		//创建下载任务
		const downloadTask = plus.downloader.createDownload(downloadUrl, {
			method: "GET"
		}, (task, status) => {
			console.log(status, 'status')
			if (status == 200) { //下载成功
				resolve(task.filename)
			} else {
				reject('fail')
				uni.showToast({
					title: '下载失败',
					duration: 1500,
					icon: "none"
				});
			}
		})
		//监听下载过程
		downloadTask.addEventListener("statechanged", (task, status) => {
			switch (task.state) {
				case 1: // 开始  
					break;
				case 2: //已连接到服务器  
					break;
				case 3: // 已接收到数据  
					let hasProgress = task.totalSize && task.totalSize > 0 //是否能获取到App大小
					if (hasProgress) {
						let current = parseInt(100 * task.downloadedSize / task
						.totalSize); //获取下载进度百分比
						progressCallBack(current)
					}
					break;
				case 4: // 下载完成       
					break;
			}
		});
 
		//开始执行下载
		downloadTask.start();
	})
}
 
/**
 * @description H5+安装APP
 * @param fileName:app文件名
 * @param callBack:安装成功回调
 */
export const installApp = (fileName, callBack = () => {}) => {
	//注册广播监听app安装情况
	onInstallListening(callBack);
	//开始安装
	plus.runtime.install(plus.io.convertLocalFileSystemURL(fileName), {}, () => {
		//成功跳转到安装界面
	}, function(error) {
		uni.showToast({
			title: '安装失败',
			duration: 1500,
			icon: "none"
		});
	})
}
 
/**
 * @description 注册广播监听APP是否安装成功
 * @param callBack:安装成功回调函数
 */
const onInstallListening = (callBack = () => {}) => {
	let mainActivity = plus.android.runtimeMainActivity(); //获取activity
	//生成广播接收器
	let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
		onReceive: (context, intent) => { //接收广播回调  
			plus.android.importClass(intent);
			mainActivity.unregisterReceiver(receiver); //取消监听
			callBack()
		}
	});
	let IntentFilter = plus.android.importClass('android.content.IntentFilter');
	let Intent = plus.android.importClass('android.content.Intent');
	let filter = new IntentFilter();
	filter.addAction(Intent.ACTION_PACKAGE_ADDED); //监听APP安装     
	filter.addDataScheme("package");
	mainActivity.registerReceiver(receiver, filter); //注册广播
}

注意事项:
1.无法弹出安装APP界面 

manifest.json-APP常用其他设置-targetSdkVersion必须设置26以上

2.无法安装APP

 manifest.json-APP权限设置需勾选:

"<uses-permission android:name=\"android.permission.INSTALL_PACKAGES\"/>",

"<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>"

3.获取的版本号和设置的不一致

通过uni.getSystemInfoSync().appVersionCode获取的本地应用版本号和manifest.json-应用版本号设置不一致,需要云打包或者自定义基座里面才能生效

4.无法获取下载进度

app下载请求回复体头部需要返回content-length字段,才能正常获取到app总大小,需要下载接口开启支持,本演示例子已做显示的兼容处理。

<think>好的,我现在需要帮助用户了解如何在UniApp小程序中使用云打包生成APK,并在用户打开时自动检查更新。首先,我得确认用户的需求是什么。用户可能已经开发了一个UniApp的小程序,现在想要生成APK文件,并且希望应用在启动时能自动检查是否有新版本,提示用户更新。 接下来,我需要回顾UniApp的相关文档,确保信息的准确性。UniApp支持云打包,这通常通过HBuilderX进行。云打包生成APK的过程相对直接,但自动更新机制可能需要一些配置和代码实现。 用户可能对云打包的具体步骤不太熟悉,所以需要分步骤解释。首先,云打包需要在HBuilderX中登录开发者账号,配置项目的manifest.json文件,包括应用名称、版本号等基本信息。然后选择云打包选项,选择Android平台,配置证书(如果有的话),最后提交打包请求。打包完成后,下载APK文件进行发布。 然后是自动检查更新的部分。UniApp本身是否有内置的更新机制?根据之前的知识,UniApp提供了热更新的能力,但可能需要结合uni-upgrade-center插件或者自己实现更新检查逻辑。需要确认这一点是否正确。如果使用uni-upgrade-center,可以方便地管理版本和更新,但需要后端支持。或者,开发者可以自行实现一个API来检查最新版本号,与当前应用的版本号对比,如果有新版本则提示用户下载安装。 还需要考虑Android的版本号(versionCode)和版本名称(versionName)的设置,因为应用商店和更新机制依赖versionCode来判断是否需要更新。每次更新时,必须递增versionCode,而versionName可以按语义化版本设置,方便用户理解。 用户可能遇到的问题包括:如何设置版本号,如何配置更新检查的API,如何处理用户下载更新后的安装。特别是Android 8.0以上版本需要允许安装未知来源的应用,这可能需要引导用户开启权限。 另外,需要注意云打包可能需要付费,尤其是如果需要多次打包或使用某些原生插件。用户可能需要了解相关的费用或限制。 总结下来,回答的结构应该包括云打包生成APK的步骤,配置版本号,实现自动更新检查的方法(使用uni-upgrade-center或自定义API),以及处理安装更新的注意事项。需要确保每一步都清晰,并且指出可能遇到的常见问题和解决方法。</think>在 UniApp实现**云打包生成 APK 并自动检查更新**,需结合云打包流程和版本更新逻辑。以下是具体实现方案: --- ### 一、云打包生成 APK 1. **配置 `manifest.json`** -UniApp 项目中,打开 `manifest.json`,配置以下关键信息: ```json { "appid": "你的应用ID", "versionName": "1.0.0", // 用户可见版本(如 1.2.3) "versionCode": "100", // 整数,必须递增(如 101, 102) "android": { "packageName": "com.example.app", // 应用包名(唯一标识) "permissions": ["INTERNET"] // 需网络权限检查更新 } } ``` 2. **执行云打包** - 在 HBuilderX 中: - 顶部菜单选择 **发行 → 原生App-云打包** - 选择 **Android 平台**,配置证书(无证书可勾选“使用公共测试证书”)。 - 提交后等待云端生成 APK,下载至本地即可发布。 --- ### 二、实现自动检查更新 #### 方案 1:使用 UniApp 官方插件 **`uni-upgrade-center`**(推荐) 1. **安装插件** - 在 HBuilderX 插件市场搜索 `uni-upgrade-center`,导入到项目中。 2. **配置更新服务器** - 在 `uni-upgrade-center` 后台配置版本信息: - 上传新 APK 文件,填写 `versionCode` 和 `versionName`。 - 设置更新类型(强制更新/提示更新)。 3. **客户端触发检查更新** - 在 `App.vue` 的 `onLaunch` 生命周期中调用检查更新: ```javascript export default { onLaunch() { // 初始化升级中心 uni.$upgradeCenter.init({ appid: '你的应用ID', platform: 'android' }); // 自动检查更新 uni.$upgradeCenter.checkUpdate(); } } ``` #### 方案 2:自定义更新逻辑(适合简单需求) 1. **创建版本检查接口** - 后端提供一个 API,返回最新版本信息,例如: ```json { "versionCode": 101, "versionName": "1.0.1", "downloadUrl": "https://example.com/app.apk", "forceUpdate": false } ``` 2. **客户端对比版本号** - 在应用启动时调用接口,对比本地 `versionCode` 与服务器最新值: ```javascript // App.vue 中 onLaunch() { uni.request({ url: 'https://api.example.com/check-update', success: (res) => { const currentCode = parseInt(plus.runtime.versionCode); if (res.data.versionCode > currentCode) { this.showUpdateDialog(res.data); // 弹窗提示更新 } } }); } ``` 3. **弹窗提示并下载安装** ```javascript methods: { showUpdateDialog(data) { uni.showModal({ title: '发现新版本', content: `最新版本:${data.versionName}`, confirmText: '立即更新', success: (result) => { if (result.confirm) { // 下载 APK uni.downloadFile({ url: data.downloadUrl, success: (downloadRes) => { if (downloadRes.statusCode === 200) { // 安装 APKAndroid) plus.runtime.install(downloadRes.tempFilePath); } } }); } } }); } } ``` --- ### 三、注意事项 1. **Android 安装权限** - 若用户首次安装非商店应用,需引导开启 **“允许安装未知来源应用”** 权限。 2. **版本号规则** - 每次更新必须递增 `versionCode`(云打包时配置),否则系统会拒绝安装。 3. **热更新限制** - 涉及原生代码修改(如新增原生插件)必须重新云打包,无法通过热更新覆盖。 --- ### 四、流程图解 ```plaintext 用户打开 APP │ ├─ 启动时触发检查更新 │ ├─ 方案1:通过 uni-upgrade-center 自动检测 │ └─ 方案2:调用自定义 API 对比版本号 │ └─ 发现新版本 ├─ 弹窗提示用户 ├─ 用户确认后下载 APK └─ 自动跳转安装 ``` 通过上述方案,可实现 UniApp 小程序的云打包和自动更新功能。**推荐使用 `uni-upgrade-center`**,节省开发成本并直接管理版本发布。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值