android 后台下载任务,断点续传

图形引擎实战:手游Android端后台下载技术分享 这个问题主要是因为下载的文件和数据库文件不在同一个目录下,中途删除下载目录的时候,没有把另一个目录下的记录文件下载进度的数据库文件给删掉,导致启动下载的时候,会首先查找文件下载进度,查找到有个文件下载了一半,然后就从一半的位置接着下载,然后就出现了文件校验不对的问题。后来,加了一个重复下载机制。采用多线程技术,一方面实现了多个文件的同时下载,另一方面,可以将几个G的大文件切割成不同片段,用不同线程来分别下载各个片段,最后实现整个文件的下载,极大提高了大文件的下载速度,通过实测,下载速度最快达到每秒十几M。 阅读详情

下载文件

/**
 * 文件下载工具
 */
object DownloadUtil {

    private fun getOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder().addInterceptor(TokenIntercepter())
            .connectTimeout(20L, TimeUnit.SECONDS)        // 连接超时
            .writeTimeout(20L, TimeUnit.SECONDS)          // 写超时
            .readTimeout(60L, TimeUnit.SECONDS)           // 读取超时
            .addNetworkInterceptor(LoggingInterceptor())          // 添加网络拦截器
            .build()
    }

    fun downloadFile(
        url: String,
        outputFile: File,
        onError: (String?) -> Unit,
        progressCallback: (bytesRead: Long, contentLength: Long, done: Boolean) -> Unit,
    ): Call {
        val client = getOkHttpClient()
        // 如果文件已存在,则获取已下载的字节数
        val downloadedLength = if (outputFile.exists()) outputFile.length() else 0L
        val requestBuilder = Request.Builder().url(url)
        if (downloadedLength > 0) {
            requestBuilder.header("Range", "bytes=$downloadedLength-")
        }
        val request = requestBuilder.build()
        val newCall = client.newCall(request)

        newCall.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                e.printStackTrace()
                onError.invoke(e.message)
            }

            override fun onResponse(call: Call, response: Response) {
                response.body?.let { body ->
                    // 判断是否为断点续传(206 状态码表示部分内容)
                    val isResuming = response.code == 206
                    LogUtils.d("DownloadUtil downloadFile $isResuming")
                    val source = body.source()
                    // 如果服务器不支持断点续传(返回 200),则重头开始下载
                    val sink = if (isResuming) {
                        // 采用追加模式写入文件
                        outputFile.appendingSink().buffer()
                    } else {
                        // 如果文件已存在且不支持续传,则删除旧文件
                        if (outputFile.exists()) {
                            outputFile.delete()
                        }
                        outputFile.sink().buffer()
                    }
                    // 计算整个文件的总大小,如果是续传则为已下载字节数加上此次响应返回的数据长度
                    val totalLength = if (isResuming) {
                        downloadedLength + body.contentLength()
                    } else {
                        body.contentLength()
                    }
                    var totalBytesRead = 0L
                    val bufferSize = 32 * 1024L
                    var bytesRead: Int

                    try {
                        val buffer = ByteArray(bufferSize.toInt())
                        while (source.read(buffer).also { bytesRead = it } != -1) {
                            sink.write(buffer, 0, bytesRead) // 逐块写入文件
                            totalBytesRead += bytesRead
                            // 当前进度为:已下载字节 + 本次读取的总字节数(续传时)
                            val currentProgress = if (isResuming) downloadedLength + totalBytesRead else totalBytesRead

                            progressCallback(
                                currentProgress, totalLength, currentProgress == totalLength
                            )
                        }
                        sink.flush()
                    } catch (e: IOException) {
                        e.printStackTrace()
                        onError.invoke(e.message)
                    } finally {
                        sink.close()
                        source.close()
                    }
                }
            }
        })
        return newCall
    }
}

后台任务

class DownloadWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {

    companion object {
        const val INPUT_DATA_URL = "url"
        const val INPUT_DATA_TARGET_FILE_PATH = "targetFile"
        const val OUTPUT_DATA_PROGRESS = "progress"
        const val OUTPUT_DATA_FILE_PATH = "filePath"
        const val UNIQUE_WORK_NAME_PRE = "download_task"

        /**
         * 启动下载任务
         * @param url 下载地址
         * @param fileName 目标文件名,下载目录存在  applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
         * @param uniqueName UNIQUE_WORK_NAME_PRE+版本号名称 作为任务的唯一标识
         */
        fun enqueueDownloadTask(
            context: Context,
            url: String,
            fileName: String,
            uniqueName: String
        ): String {
            // 构造输入数据
            val inputData = workDataOf(
                INPUT_DATA_URL to url,
                INPUT_DATA_TARGET_FILE_PATH to fileName
            )
            val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
                .setInputData(inputData)
                .setConstraints(
                    Constraints.Builder()
                        .setRequiredNetworkType(NetworkType.CONNECTED)
                        .build()
                )
                .build()

            // 使用唯一任务名称 "download_task",策略为 KEEP(已有则保持,不重新创建)
            WorkManager.getInstance(context).enqueueUniqueWork(
                uniqueName,
                ExistingWorkPolicy.REPLACE, //如果要重启的话  ExistingWorkPolicy.REPLACE,保持原来 ExistingWorkPolicy.KEEP,
                downloadRequest
            )
            // 返回任务的 UUID 字符串,便于后续观察进度
            return downloadRequest.id.toString()
        }


        fun cancelDownloadTask(context: Context, uniqueName: String) {
            WorkManager.getInstance(context).cancelUniqueWork(uniqueName)
        }

    }

    override suspend fun doWork(): Result {
        // 从输入数据获取下载地址和文件名
        val url = inputData.getString(INPUT_DATA_URL) ?: ""
        val fileName = inputData.getString(INPUT_DATA_TARGET_FILE_PATH) ?: ""

        LogUtils.v("DownloadWorker doWork fileName=$fileName,url=$url")
        if (url.isEmpty() || fileName.isEmpty()) return Result.failure()

        // 这里保存到 app 的 files 目录中,实际可根据需求修改
        val outputFile = File(fileName)

        return suspendCancellableCoroutine { continuation ->
            val call = DownloadUtil.downloadFile(url, outputFile,
                onError = { msg ->
                    continuation.resume(Result.failure()) {
                        LogUtils.e("DownloadWorker onError $msg")
                    }
                }) { bytesRead, contentLength, done ->
                val progress = (bytesRead.toFloat() / contentLength * 100).toInt()
                setProgressAsync(workDataOf(OUTPUT_DATA_PROGRESS to progress))
                LogUtils.d("DownloadWorker DownloadProgress $progress,$bytesRead,$contentLength!")
                if (done) {
                    val outputData = workDataOf(OUTPUT_DATA_FILE_PATH to outputFile.absolutePath)
                    continuation.resume(Result.success(outputData)) {
                        LogUtils.v("DownloadWorker success $fileName")
                    }

                }
            }
            continuation.invokeOnCancellation {
                LogUtils.v("DownloadWorker invokeOnCancellation ")
                call.cancel()
            }
        }
    }

}

初始化的时候,检查是否存在任务

 /**
     * 检查下载任务uniqueWorkName 是否存在
     */
    fun checkDownloadTask(
        context: Context,
        lifecycleOwner: LifecycleOwner,
        uniqueWorkName: String
    ) {
        // 观察唯一任务的状态
        checkDownloadJob?.cancel()
        checkDownloadJob = viewModelScope.launch {
            // 此处使用 getWorkInfosForUniqueWork 监听所有同名任务(通常只有一个任务)
            WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(uniqueWorkName)
                .observe(lifecycleOwner) { workInfos ->
                    LogUtils.d("InAppUpdateComposeViewModel checkDownloadTask size=${workInfos.size}")
                    if (workInfos.isNullOrEmpty()) {
                        //没有任务

                    } else {
                        // 这里取第一个任务
                        val workInfo = workInfos.first()
                        LogUtils.d("InAppUpdateComposeViewModel checkDownloadTask workInfo=${workInfo.state}")
                        when (workInfo.state) {
                            WorkInfo.State.ENQUEUED -> {
                                //排队中
                                _updateStatusFlow.value = UpdateStatus.DOWNLOADING
                            }

                            WorkInfo.State.RUNNING -> {
                                //下载中
                                _updateStatusFlow.value = UpdateStatus.DOWNLOADING
                                // 更新进度
                                val progress =
                                    workInfo.progress.getInt(DownloadWorker.OUTPUT_DATA_PROGRESS, 0)
                                _progressFlow.value = progress

                                LogUtils.d("InAppUpdateComposeViewModel checkDownloadTask progress=${progress}")
                            }

                            WorkInfo.State.SUCCEEDED -> {
                                //下载成功
                                val apkFilePath =
                                    workInfo.outputData.getString(DownloadWorker.OUTPUT_DATA_FILE_PATH)
                                val apkFile = File(apkFilePath)
                                if (apkFile.exists()) {
                                    _updateStatusFlow.value = UpdateStatus.DOWNLOAD_SUCCESS
                                    _downloadResult.value =
                                        workInfo.outputData.getString(DownloadWorker.OUTPUT_DATA_FILE_PATH)
                                } else {
                                    //任务成功但是文件不见了
                                    _updateStatusFlow.value = UpdateStatus.FILE_MISSING
                                }
                            }

                            WorkInfo.State.FAILED -> {
                                _updateStatusFlow.value = UpdateStatus.DOWNLOAD_FAILED
                            }

                            WorkInfo.State.CANCELLED -> {
                                _updateStatusFlow.value = UpdateStatus.DOWNLOAD_CANCEL
                            }

                            else -> {
                                _updateStatusFlow.value = UpdateStatus.DOWNLOADING
                            }
                        }
                    }
                }
        }
    }

   

启动下载任务

 fun startDownload(context: Context) {
        versionState.value?.apply {
            val uniqueWorkName = DownloadWorker.UNIQUE_WORK_NAME_PRE + newVersion
            val apkVersionName = "update_$newVersion.apk"
            val downloadDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
            deleteOtherVersionApk(downloadDir, apkVersionName, "update_.*\\.apk")
            DownloadWorker.enqueueDownloadTask(
                context = context,
                url = url,
                fileName = "${context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)}/$apkVersionName",
                uniqueName = uniqueWorkName
            )
        }
    }

取消任务


    fun cancelDownload(context: Context) {
        versionState.value?.apply {
            val uniqueWorkName = DownloadWorker.UNIQUE_WORK_NAME_PRE + newVersion
            DownloadWorker.cancelDownloadTask(context, uniqueWorkName)
        }
    }

Flutter下载管理插件实战:flutter_downloader跨平台文件下载解决方案 是 Flutter 生态中广泛使用的下载管理插件,封装了 Android 和 iOS 原生下载能力,支持后台下载、断点续传、任务状态监听等核心功能。其通过(Android)和(iOS)实现跨应用生命周期的可靠下载。集成时需在中添加依赖,并完成平台端配置:Android 需注册,iOS 要启用 Background Fetch 模式。该插件提供简洁的 Dart API,如enqueue发起任务、loadTasks恢复历史任务,适合构建文件更新、离线资源预加载等场景。 阅读详情

相关推荐

GE Proficy Historian8.0历史数据库部署与配置全攻略

本文提供了GE Proficy Historian 8.0历史数据库的详细部署与配置指南。内容涵盖安装前的系统环境、权限与依赖检查,服务器安装的关键步骤与类型选择,采集器的安装配置与数据接入,以及客户端工具的使用与数据分析实战。旨在帮助用户规避常见陷阱,实现一次部署成功,构建稳定高效的工业数据归档平台。

silver的专栏 768

Android 多线程下载以及断点续传

在日常开发中,我们不可避免的会接到类似这样的需求,下载一个比较大的素材文件或者安装包文件,以此实现APP的自动更新,APP内的素材替换等。这里其实我们可以记录每一次写入文件的进度,当下载任务被暂停的时候,我们就将对应的任务记录下载,记录相应的url,存储文件,当前下载的进度等基本信息,当用户再次出发的时候我们就可以从这些信息恢复进度,继续下载。获取到对应的内容,那么我们就要在文件的指定区域去写入,由于我们是多线程下载,因此文件并不是每次都是从前往后一个个字节写入的,随时可能在文件的任何一个地方写入数据。

涛的博客 3628

用Python玩转共现分析:从NLTK到热力图可视化(附完整代码)

本文详细介绍了如何使用Python进行文本共现分析,从NLTK基础文本处理到构建共现矩阵,并实现热力图可视化。通过完整的代码示例,帮助数据分析师和开发者掌握这一揭示词语间关联网络的实用NLP技术,快速从文本中提取语义关系和主题结构。

weixin_29313817的博客 350

Android 编程技巧之 ----- 断点续传原理以及实现

原文出处 : Android开发——断点续传原理以及实现 原文作者 : SEU_Calvin 感谢原文作者无私的分享 ! 1.  断点续传原理 在本地下载过程中要使用数据库实时存储到底存储到文件的哪个位置了,这样点击开始继续传递时,才能通过HTTP的GET请求中的setRequestProperty()方法可以告诉服务器,数据从哪里开始,到哪里结束。同时在本地的

Snow Yong 792

zemax双透镜公差分析

蒙特卡罗分析: 评估公差的整体影响,模拟过程中产生一系列随机的lens,满足指定的公差,然后再按照标准评估。公差分析,就是在设计了一个理想的系统后,想看看实际生产过程中如果产生公差(误差),系统会坏到什么程度,也就是光学性能受到多大影响。反灵敏度分析:分别对每个公差在性能方面的一个给定的最小允许减少量来计算公差,也就是限制了系统性能的变化,和灵敏度分析方向相反。灵敏度分析对每个公差单独求值,分别计算每个公差操作数的最大、最小、标准值(如上图)所有设计上可能的公差都列出来了。注意要固定透镜的口径。

fendoudecheng的博客 3207

R 分析裂区试验设计

裂区试验有主区部分和副区部分, 因而有主区部分误差(误差 a,简记为Ea )和副区部分误差(误差b,简记为Eb) 分别用于测验主区处理以及副区处理和主、副互作的显著性。对同一个二因素试验资料作自由度和平方和的分解,则可发现随机区组的误差项自由度和平方和分别为DFe、SSe,而裂区设计有两个误差项,其自由度和平方和分别为DFEa,DFEb, SSEa, SSEb。而区组,处理效应等各个变异项目的自由

samhuairen的专栏 8934

Android断点续传原理及实现

常见两种网络请求方式一、 HttpURLConnectionHttpURLConnection的setRequestProperty()方法,对我们要读取的字节部分进行控制,比如:···1.Range=0-100代表只读取前100个字节。2.Range=100-500代表读取从第100个字节开始,读到第500个字节为止。3.Range=100-则代表从第100个字节开始读取,一直读取到文件末尾结束。···断点续传过程。

hcgeng的专栏 1663

Allan方差这个工具,怎么把它用的更顺手?(附Allan方差、动态Allan方差代码)

本文详细说明了Allan方差和动态Allan方差的计算过程,并通过递归运算和时间序列选择两种算法提高动态Allan方差的计算效率。文末提供了快速动态Allan方差计算代码。

victoryqian528的博客 374

Android笔记--断点续传

断点续传就是在下载资源过程中,网络出现中断后,下一次下载的时候能够接着...

ljt2724960661的博客 645

华为OD机试真题-查找单入口空闲区域(Py/Java/C/C++/Js/Go)

华为OD机试(机考)双机位C卷 【查找单入口空闲区域】,提供Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解,以及在线OJ模拟做题。华为OD机试 - 查找单入口空闲区域。

qq_45776114的博客 1269

Agile Modbus使用和学习笔记

i < len;这里仅仅需要提供获取函数即可,离散输入是只读类型i++) {return 0;只读类型寄存器i++) {return 0;i < len;

Car12 169

Android 后台下载功能

Android 实现后台下载的方式

weixin_43699349的博客 3476

YOLOv8【第一章:基础入门篇·第1节】一文搞定,核心架构深度解析与环境配置!

🏆 本文收录于 《YOLOv8实战:从入门到深度优化》 专栏,该专栏专注于分享我在YOLOv8目标检测模型中的实战经验,涵盖从入门基础到深度优化的全方位解决方案。无论你是计算机视觉的新手,还是具有多年深度学习经验的专家,本专栏都将为你提供高效、可复现的技术方案,助你掌握YOLOv8的核心技术,提升目标检测性能,突破瓶颈,优化模型,并加速项目开发🚀!

**My Coding Family** 2106

Cimoc漫画下载功能详解:离线阅读完整教程

想要随时随地畅享漫画阅读体验,不受网络限制?Cimoc漫画下载功能是你的最佳选择!作为一款强大的Android在线漫画阅读器,Cimoc不仅提供丰富的漫画资源,还内置了完善的下载系统,让你轻松将喜欢的漫画保存到本地,实现真正的离线阅读自由。🎯 ## 🔍 Cimoc漫画下载功能核心优势 Cimoc的下载功能设计得非常人性化,具有以下突出特点: - **智能多任务下载**:支持同时下载多个章

gitblog_00943的博客 659

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Android 多线程断点下载文件

效果图 首先先上一下效果图 如果有需要的朋友可以往下看我的具体实现: 整个下载的过程都在后台服务中进行,因此实现了一个activity将任务加入下载 在另一个activity中显示进度并能控制暂停,同时结合了sqlite进行缓存,将任务列表缓存到本地,下次打开app任务依然存在并能继续 实现的思路 1.创建一个sqlite的表 将任务列表中的 例如文件名 url 等信息进行缓存 2. 封装下载工...

louyulin的博客 2424

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

基于AsyncTask的下载任务

本文章介绍使用AsyncTask异步下载,为了保证任务不被回收,使用service后台服务去执行下载任务,并用通知的方式查看下载进进度。 1.下载任务: package com.example.mydownload; import android.os.AsyncTask; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.IOExceptio...

qq_35698774的博客 636

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

解决Unity后台下载痛点:BackgroundDownload插件高级使用技巧

在移动游戏开发中,后台下载功能是提升用户体验的关键环节。无论是资源更新、DLC安装还是大型文件传输,Unity的BackgroundDownload插件都能提供稳定可靠的后台下载解决方案,让玩家在游戏切换到后台时仍能继续完成下载任务。 ## 一、插件核心优势解析 BackgroundDownload插件作为Unity生态中的专业后台下载工具,具备三大核心优势: ### 1.1 跨平台兼容性

gitblog_00511的博客 688

2GB资源下载不丢进度的跨平台实战方案

通过使用后台服务、持久化下载状态、处理系统限制和网络变化,可以实现游戏切后台后继续下载的能力。具体实现会因平台而异,需要根据目标平台的特性进行调整。好的,继续深入探讨如何在不同平台上实现游戏切后台后继续下载的能力。

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 3167

告别复杂下载逻辑:FileDownloader让Android多任务下载像喝水一样简单

FileDownloader是一款专为Android平台设计的高效下载引擎,它以简洁易用的API解决了多任务下载、断点续传、高并发处理等复杂问题。无论是需要同时下载多个文件,还是希望在网络不稳定时能够恢复下载进度,FileDownloader都能提供稳定可靠的解决方案,让开发者专注于业务逻辑而非下载细节。 ## 🚀 核心优势:让下载变得前所未有的简单 FileDownloader的设计理念是

gitblog_00898的博客 839

Android下载管理终极指南:使用Download-Manager-Pro实现高效并行下载

想要为你的Android应用添加强大的下载管理功能吗?Android-Download-Manager-Pro是一个专业的Android/Java下载管理库,能够帮助你以并行分块机制高效下载文件。这个开源库让开发者能够轻松实现多任务下载、断点续传和队列管理,是构建现代下载应用的完美选择。🚀 ## 📊 下载状态流转机制 每个下载任务在其生命周期中会经历6个核心状态,确保下载过程的稳定性和可控

gitblog_00963的博客 1039

解决90%下载难题:OkGo断点续传的速度控制与后台保活方案

你是否遇到过这些下载痛点?大文件下载到99%突然失败需要重下、应用退到后台下载就中断、下载时抢占带宽导致网页打不开?作为基于OkHttp的高效网络框架,OkGo的断点下载功能通过三大核心能力解决这些问题:**分片续传**、**速度限制**和**后台保活**。本文将通过实例代码和界面展示,教你如何在项目中快速集成这些高级特性。 ## 核心功能架构 OkGo的下载管理模块采用分层设计,由[OkDo

gitblog_00957的博客 500

uniapp实战:三步搞定文件下载、保存与预览(附完整代码)

本文详细介绍了Uniapp中文件下载、保存与预览的全链路解决方案,涵盖`uni.downloadFile`、`uni.saveFile`和`uni.openDocument`等核心API的使用技巧。通过实战代码示例和跨平台兼容性分析,帮助开发者高效实现文件操作功能,并解决安卓与iOS的权限差异、临时文件管理等常见问题。

weixin_30823001的博客 772

如何使用PRDownloader:Android文件下载的终极解决方案

PRDownloader是一款专为Android平台设计的强大文件下载库,提供暂停和恢复功能,让开发者轻松实现稳定高效的下载体验。无论是大型文件还是多个并行下载任务,PRDownloader都能提供可靠的支持,是Android开发中处理文件下载的理想选择。 ## PRDownloader简介:为什么它是Android开发者的必备工具 PRDownloader作为一款专注于Android平台的文

gitblog_00156的博客 844

Aria:让Android下载体验焕然一新的解决方案

### 什么是Aria?它解决了哪些传统下载方式的痛点? Aria是一款专为Android平台设计的**高性能下载框架**,它彻底改变了传统下载方式中存在的速度慢、稳定性差、代码冗余等问题。通过轻量级架构设计(核心包体积<500KB)和模块化API,Aria让开发者能够用最少的代码实现专业级下载功能,同时提供**多线程加速**、**断点续传**(网络中断后无需重新下载的技术机制)和**智能任务调

gitblog_01043的博客 353

突破Android下载瓶颈:FileDownloader多线程架构与实战指南

你是否还在为Android应用中的下载功能头疼?文件太大导致下载失败、多任务下载时应用卡顿、后台下载被系统杀死——这些问题不仅影响用户体验,更是开发中的常见痛点。本文将深入剖析FileDownloader的底层架构,带你掌握多线程下载、断点续传的核心原理,并通过实战案例展示如何在项目中快速集成这一强大引擎。读完本文,你将获得构建稳定、高效下载系统的完整方案,轻松应对各种复杂下载场景。 ## 一、...

gitblog_00061的博客 411

实战Fetch:构建企业级Android下载应用的终极教程

想要为你的Android应用添加专业的文件下载功能吗?🤔 今天我将为你介绍一个强大的Android文件下载库——Fetch,它能够帮助你快速构建企业级的下载应用!📱 无论是简单的图片下载还是复杂的多文件并发下载,Fetch都能轻松应对。作为Android平台上最优秀的文件下载管理器库,Fetch提供了简单易用的API、后台持续下载、并发下载支持、暂停恢复功能等企业级特性。 ## 🚀 为什么

gitblog_00749的博客 1047
上一篇: Android studio取消代理
下一篇: AlarmManager添加定时任务
lynn8570_blog
博客等级 码龄10年 254粉丝 87原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值