移动端文件直传到阿里云oss流程包括代码

移动端文件直传到阿里云oss流程包括代码

这里使用sts的鉴权授权机制进行上传(当然也可以使用明文的上传机制、自签名模式、最安全的就是sts的授权上传机制,可以最大限度的保护阿里云的账户安全)
开发的整个流程如下,此处是以java,android为例子(其他的语言版本可以根据流程直接从阿里云官方文档上获取相应的版本示例代码。)

一、首先获取移动端上传的ststooken

说明:ststooken是应用服务端用来直接上传到阿里云oss的密钥,包含临时的AccessKeyId、AccessKeySecret、失效时间Expiration、SecurityToken(以上是移动端最需要的参数)及其他请求返回参数。
注:ststooken的使用不会在第二次请求后第一次的ststooken失效,两次的都可以进行授权上传文件。
ststooken获取的前提是创建子RAM账户使用子RAM账户的AccessKeyId、AccessKeySecret来进行获取。
创建子RAM账户需要在阿里云控制台进行创建,请参考官方文档访问控制模块RAM账户的创建:https://help.aliyun.com/product/28625.html?spm=5176.doc28627.3.1.LXoJux

package com.aliyun.demo;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.aliyuncs.sts.model.v20150401.AssumeRoleRequest;
import com.aliyuncs.sts.model.v20150401.AssumeRoleResponse;


import net.sf.json.JSONObject;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(asyncSupported = true)
public class AppTokenServer extends HttpServlet{
    /**
     * 
     */
    private static final long serialVersionUID = 5522372203700422672L;
    // 目前只有"cn-hangzhou"这个region可用, 不要使用填写其他region的值
    public static final String REGION_CN_HANGZHOU = "cn-hangzhou";
    public static final String STS_API_VERSION = "2015-04-01";
    protected AssumeRoleResponse assumeRole(String accessKeyId, String accessKeySecret, String roleArn,
            String roleSessionName, String policy, ProtocolType protocolType, long durationSeconds) throws ClientException 
    {
        try {
            // 创建一个 Aliyun Acs Client, 用于发起 OpenAPI 请求
            IClientProfile profile = DefaultProfile.getProfile(REGION_CN_HANGZHOU, accessKeyId, accessKeySecret);
            DefaultAcsClient client = new DefaultAcsClient(profile);

            // 创建一个 AssumeRoleRequest 并设置请求参数
            final AssumeRoleRequest request = new AssumeRoleRequest();
            request.setVersion(STS_API_VERSION);
            request.setMethod(MethodType.POST);
            request.setProtocol(protocolType);

            request.setRoleArn(roleArn);
            request.setRoleSessionName(roleSessionName);
            request.setPolicy(policy);
            request.setDurationSeconds(durationSeconds);

            // 发起请求,并得到response
            final AssumeRoleResponse response = client.getAcsResponse(request);

            return response;
        } catch (ClientException e) {
            throw e;
        }
    }

    public static String ReadJson(String path){
        //从给定位置获取文件
        File file = new File(path);
        BufferedReader reader = null;
        //返回值,使用StringBuffer
        StringBuffer data = new StringBuffer();
        //
        try {
            reader = new BufferedReader(new FileReader(file));
            //每次读取文件的缓存
            String temp = null;
            while((temp = reader.readLine()) != null){
                data.append(temp);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //关闭文件流
            if (reader != null){
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data.toString();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {


        String data = ReadJson("./config.json");
        System.out.println("用户输入url:" + data);
        if (data.equals(""))
        {
            response(request, response, "./config.json is empty or not found");
            return;
        }
        System.out.println(data);
        JSONObject jsonObj  = JSONObject.fromObject(data);


        // 只有 RAM用户(子账号)才能调用 AssumeRole 接口
        // 阿里云主账号的AccessKeys不能用于发起AssumeRole请求
        // 请首先在RAM控制台创建一个RAM用户,并为这个用户创建AccessKeys
        String accessKeyId = jsonObj.getString("AccessKeyID");
        String accessKeySecret = jsonObj.getString("AccessKeySecret");

        // RoleArn 需要在 RAM 控制台上获取
        String roleArn = jsonObj.getString("RoleArn");
        long durationSeconds = jsonObj.getLong("TokenExpireTime");
        String policy = ReadJson(jsonObj.getString("PolicyFile"));
        // RoleSessionName 是临时Token的会话名称,自己指定用于标识你的用户,主要用于审计,或者用于区分Token颁发给谁
        // 但是注意RoleSessionName的长度和规则,不要有空格,只能有'-' '_' 字母和数字等字符
        // 具体规则请参考API文档中的格式要求
        String roleSessionName = "alice-001";

        // 此处必须为 HTTPS
        ProtocolType protocolType = ProtocolType.HTTPS;

        try {
            final AssumeRoleResponse stsResponse = assumeRole(accessKeyId, accessKeySecret, roleArn, roleSessionName,
                    policy, protocolType, durationSeconds);

            Map<String, String> respMap = new LinkedHashMap<String, String>();
            respMap.put("status", "200");
            respMap.put("AccessKeyId", stsResponse.getCredentials().getAccessKeyId());
            respMap.put("AccessKeySecret", stsResponse.getCredentials().getAccessKeySecret());
            respMap.put("SecurityToken", stsResponse.getCredentials().getSecurityToken());
            respMap.put("Expiration", stsResponse.getCredentials().getExpiration());

            JSONObject ja1 = JSONObject.fromObject(respMap);
            response(request, response, ja1.toString());

        } catch (ClientException e) {

            Map<String, String> respMap = new LinkedHashMap<String, String>();
            respMap.put("status", e.getErrCode());
            respMap.put("AccessKeyId", "");
            respMap.put("AccessKeySecret", "");
            respMap.put("SecurityToken", "");
            respMap.put("Expiration", "");         
            JSONObject ja1 = JSONObject.fromObject(respMap);
            response(request, response, ja1.toString());
        }

    }


    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
        doGet(request, response);
    }

    private void response(HttpServletRequest request, HttpServletResponse response, String results) throws IOException {
        String callbackFunName = request.getParameter("callback");
        if (callbackFunName==null || callbackFunName.equalsIgnoreCase(""))
            response.getWriter().println(results);
        else
            response.getWriter().println(callbackFunName + "( "+results+" )");
        response.setStatus(HttpServletResponse.SC_OK);
        response.flushBuffer();
    }
}

官方的demo及描述地址:https://help.aliyun.com/document_detail/31922.html?spm=5176.product31815.6.610.DE2LHD

二、移动端直接上传文件到oss

说明:通过步骤一,与应用服务端交互获取的返回参数ststooken直接进行创建参数请求上传文件。
注:此处只是简单的上传,例如相关的–文件md5校验、文件断点续传、文件上传回调进度等等请参考官方sdk根据自己的需要进行添加。
官方sdk地址:https://help.aliyun.com/document_detail/32047.html?spm=5176.doc32046.6.687.9fgdiw

android实例代码如下:

String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";

//第一步获取的sts的相关临时请求参数
OSSCredentialProvider credentialProvider = new OSSStsTokenCredentialProvider("<StsToken.AccessKeyId>", "<StsToken.SecretKeyId>", "<StsToken.SecurityToken>");

//创建上传请求对象
OSS oss = new OSSClient(getApplicationContext(), endpoint, credentialProvider);

//调用上传请求方法
try {
    PutObjectResult putResult = oss.putObject(put);
    Log.d("PutObject", "UploadSuccess");
    Log.d("ETag", putResult.getETag());
    Log.d("RequestId", putResult.getRequestId());
} catch (ClientException e) {
    // 本地异常如网络异常等
    e.printStackTrace();
} catch (ServiceException e) {
    // 服务异常
    Log.e("RequestId", e.getRequestId());
    Log.e("ErrorCode", e.getErrorCode());
    Log.e("HostId", e.getHostId());
    Log.e("RawMessage", e.getRawMessage());
}

三、文件上传设置阿里云回调应用服务器

说明:进行移动端直传oss的时候,如果有需要是要将每次移动端的上传进行本地以用服务器的记录,则需要阿里云oss移动端直传回调机制。


回调的流程:
这里写图片描述
图3.1


注:在进行这个功能的时候一定要确认oss中创建的bucket是否开启了回调机制,若未开启的话,即使移动端设置了回调参数,默认也是直接由阿里云oss进行返回上传状态200(即只走图3.1中的步骤1、4),不调业务服务器(即本地应用服务器)。
示例代码:

package com.demo.demo;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
///import com.aliyun.oss.OSSClient;
//import com.aliyun.oss.common.utils.BinaryUtil;
import java.net.URI;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import com.aliyun.oss.common.utils.BinaryUtil;


//import com.aliyun.oss.common.utils.BinaryUtil;

@SuppressWarnings("deprecation")
@WebServlet(asyncSupported = true)
public class CallbackServer extends HttpServlet {
    /**
     * 
     */
    private static final long serialVersionUID = 5522372203700422672L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println("用户输入url:" + request.getRequestURI());
        response(request, response, "input get ", 200);

    }

    @SuppressWarnings({ "finally" })
    public String executeGet(String url) {
        BufferedReader in = null;

        String content = null;
        try {
            // 定义HttpClient
            @SuppressWarnings("resource")
            DefaultHttpClient client = new DefaultHttpClient();
            // 实例化HTTP方法
            HttpGet request = new HttpGet();
            request.setURI(new URI(url));
            HttpResponse response = client.execute(request);

            in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            StringBuffer sb = new StringBuffer("");
            String line = "";
            String NL = System.getProperty("line.separator");
            while ((line = in.readLine()) != null) {
                sb.append(line + NL);
            }
            in.close();
            content = sb.toString();
        } catch (Exception e) {
        } finally {
            if (in != null) {
                try {
                    in.close();// 最后要关闭BufferedReader
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return content;
        }
    }

    public String GetPostBody(InputStream is, int contentLen) {
        if (contentLen > 0) {
            int readLen = 0;
            int readLengthThisTime = 0;
            byte[] message = new byte[contentLen];
            try {
                while (readLen != contentLen) {
                    readLengthThisTime = is.read(message, readLen, contentLen - readLen);
                    if (readLengthThisTime == -1) {// Should not happen.
                        break;
                    }
                    readLen += readLengthThisTime;
                }
                return new String(message);
            } catch (IOException e) {
            }
        }
        return "";
    }


    protected boolean VerifyOSSCallbackRequest(HttpServletRequest request, String ossCallbackBody) throws NumberFormatException, IOException
    {
        boolean ret = false;    
        String autorizationInput = new String(request.getHeader("Authorization"));
        String pubKeyInput = request.getHeader("x-oss-pub-key-url");
        byte[] authorization = BinaryUtil.fromBase64String(autorizationInput);
        byte[] pubKey = BinaryUtil.fromBase64String(pubKeyInput);
        String pubKeyAddr = new String(pubKey);
        if (!pubKeyAddr.startsWith("http://gosspublic.alicdn.com/") && !pubKeyAddr.startsWith("https://gosspublic.alicdn.com/"))
        {
            System.out.println("pub key addr must be oss addrss");
            return false;
        }
        String retString = executeGet(pubKeyAddr);
        retString = retString.replace("-----BEGIN PUBLIC KEY-----", "");
        retString = retString.replace("-----END PUBLIC KEY-----", "");
        String queryString = request.getQueryString();
        String uri = request.getRequestURI();
        String decodeUri = java.net.URLDecoder.decode(uri, "UTF-8");
        String authStr = decodeUri;
        if (queryString != null && !queryString.equals("")) {
            authStr += "?" + queryString;
        }
        authStr += "\n" + ossCallbackBody;
        ret = doCheck(authStr, authorization, retString);
        return ret;
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        //回调请求的方法地址
        /**
         * @"callbackUrl": @"http://abc.com/callback.php"
         */
        String ossCallbackBody = GetPostBody(request.getInputStream(), Integer.parseInt(request.getHeader("content-length")));
        boolean ret = VerifyOSSCallbackRequest(request, ossCallbackBody);
        System.out.println("verify result:" + ret);
        System.out.println("OSS Callback Body:" + ossCallbackBody);

        //此处进上传回调后上传的文件进行存储在本地数据库中
        //首先回调前得确认是否为阿里云的正确数据回调请求
        if (ret)
        {
            //如果为阿里云正确的回调请求则进行回调数据的本地记录存储
            response(request, response, "{\"Status\":\"OK\"}", HttpServletResponse.SC_OK);
        }
        else
        {
            //回调请求校验不成功返回返回回调不成功的response
            response(request, response, "{\"Status\":\"verdify not ok\"}", HttpServletResponse.SC_BAD_REQUEST);
        }
    }

    public static boolean doCheck(String content, byte[] sign, String publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            byte[] encodedKey = BinaryUtil.fromBase64String(publicKey);
            PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
            java.security.Signature signature = java.security.Signature.getInstance("MD5withRSA");
            signature.initVerify(pubKey);
            signature.update(content.getBytes());
            boolean bverify = signature.verify(sign);
            return bverify;

        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    private void response(HttpServletRequest request, HttpServletResponse response, String results, int status) throws IOException {
        String callbackFunName = request.getParameter("callback");
        response.addHeader("Content-Length", String.valueOf(results.length()));
        if (callbackFunName == null || callbackFunName.equalsIgnoreCase(""))
            response.getWriter().println(results);
        else
            response.getWriter().println(callbackFunName + "( " + results + " )");
        response.setStatus(status);
        response.flushBuffer();
    }
}

官方示例demo地址:https://help.aliyun.com/document_detail/31922.html?spm=5176.doc31853.2.5.9bu4mT


至此文件上传就结束了!!!!!!!!

当然还少了一步,上传的文件,web端还是需要访问的。

四、oss文件的浏览

说明:上传的文件浏览
一是可以直接在阿里云控制台访问相应的bucket进行文件的访问;

二是可以借助第三方云市场中的oss工具进行访问下载等相关功能(直接云市场中搜索oss即可);

三是自己写代码浏览。
此处可以参考阿里云官方文档的防盗链的功能模块
官方地址:https://help.aliyun.com/document_detail/31937.html
自己写代码浏览:首先要确认所要浏览的bucket的读写属性设置,
①若为“公共”权限,则可以直接使用图片地址就可以进行访问。
http:// + your bucket name + edpoint + /bucket下object的key值
例如:http://referer-test.oss-cn-hangzhou.aliyuncs.com/aliyun-logo.png
②若为“私有”权限,则直接用地址就无法进行访问,需要使用签名URL进行访问,而且签名的url是有访问的时效性,可以很好的起到防盗链的作用。
注:这种自签名的形式如果在文件更改了的情况下,必须根据key重新获取签名的url,即使文件更改前后key值相同,原先未失效的签名url将是访问的未更改前的文档。

package com.xu;

import java.net.URL;
import java.util.Date;

import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.GeneratePresignedUrlRequest;
import com.n22.ehero.base.tool.LogTool;
import com.talife.tnt.server.uploadfiles.model.OssBucketUrlTemp;

public class GetOssUrlImpl{

    /**
     * 服务端文件回显url拼接(访问阿里云OSS的文件)
     * 
     * @param bucketName
     * @param key
     * @author xu
     */
    public static OssBucketUrlTemp getOssBucketUrl(String bucketName,String key) {
        // Generate a presigned URL
        OssBucketUrlTemp ossBucketUrlTemp = new OssBucketUrlTemp();
        try {
            // endpoint以杭州为例,其它region请按实际情况填写
            String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
            // accessKey请登录https://ak-console.aliyun.com/#/查看
            String accessKeyId = "";
            String accessKeySecret = "";
            // 创建OSSClient实例
            OSSClient client = new OSSClient(endpoint, accessKeyId, accessKeySecret);
            Date expires = new Date (new Date().getTime() + 1000 * 60 * 30); // 目前为30minute to expire

            GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, key);

            generatePresignedUrlRequest.setExpiration(expires);

            URL url = client.generatePresignedUrl(generatePresignedUrlRequest);

            LogTool.inf(OssBucketUrlTemp.class,"临时oss资源访url"+url.toString());
            String query = "";
            String expiration = "";
            if (null!=url) {
                query = url.getQuery();
                expiration = query.substring(8, 18);
                ossBucketUrlTemp.setUrl(url.getProtocol()+"://"+url.getHost()+url.getFile());
                ossBucketUrlTemp.setExpiration(expiration);
                ossBucketUrlTemp.setUrlU(url);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ossBucketUrlTemp;
    }

}

另附所需要的jar包

–注意几个jar包的版本一定要统一。(官方各版本sdk及源码地址:https://oss.sonatype.org/?spm=5176.7926450.195949.1.A6eeeH#nexus-search;gav~com.aliyun~aliyun-java-sdk-*~~~)

  • aliyun-java-sdk-core-2.1.7.jar
  • aliyun-java-sdk-sts-2.1.6.jar
  • aliyun-sdk-oss-2.0.6.jar
  • ezmorph-1.0.6.jar
  • json-lib-2.4-jdk15.jar

此版本需要对应使用的http的jar包版本为

  • httpclient-4.4.jar
  • httpcore-4.4.jar

有了上传怎么能少了文件下载呢!!!!!!!!

五、oss文件流式下载(java版本)

说明:文件的下载方式很多,此处使用流式单文件下载及多文件打包下载。其他的下载可以参考官方不同语言的sdk。

    /**
     * 
     * <p>Title: 下载单个文件oss</p>
     * <p>Description: TODO</p>
     * @author xu 
     */
    public void downloadFile() {
        try {
            request.setCharacterEncoding("UTF-8");
            // 获取参数
            String fileUrl = request.getParameter("filepath");
            String filename = request.getParameter("filename");
            String fileSuffix = request.getParameter("fileSuffix");

            // 创建OSSClient实例
            OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
            OSSObject ossObject = ossClient.getObject(bucket_name, filename);
            DataInputStream fis = new DataInputStream(ossObject.getObjectContent());
            byte[] buffer = new byte[1024];
            // 清空response
            response.reset();
            // 设置response的Header
            response.addHeader("Content-Disposition",
                    "attachment;filename=" + new String(fileSuffix.getBytes("gb2312"), "ISO8859-1"));
            response.addHeader("Content-Length", "" + buffer);

            OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
            response.setContentType("application/octet-stream");

            //网络资源流式下载
            int bytesum = 0;
            int byteread = 0;
            while ((byteread = fis.read(buffer)) != -1) {
                bytesum += byteread;
//                  System.out.println(bytesum);
                toClient.write(buffer, 0, byteread);
            }

            toClient.flush();
            toClient.close();
            fis.close();
            // 关闭client
            ossClient.shutdown();

        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 
     * <p>Title: 下载全部文件oss</p>
     * <p>Description: TODO</p>
     * @throws Exception
     * @author xu
     */
    public void downloadAllFile() {
        try {
            request.setCharacterEncoding("UTF-8");

            // 获取参数
            String policyNo = request.getParameter("policyNo");
            String fileUrl = request.getParameter("pinjiefilepath");
            String[] urls = fileUrl.split(","); 
            byte[] buffer = new byte[1024];
            // 生成的ZIP文件
            String strZipName = File.separator + "file" + File.separator + "img" + File.separator +new Date().getTime()+File.separator + policyNo + ".zip";
            String filepath = request.getSession().getServletContext().getRealPath(strZipName);
            //创建文件
            File temp = new File(filepath.substring(0, filepath.lastIndexOf(File.separator)));
            if (!temp.exists()) {
                temp.mkdirs();
            }
            ZipOutputStream out = new ZipOutputStream(new FileOutputStream(filepath));

            //将文件打包
            for (int i = 0; i < urls.length; i++) {
                // 创建OSSClient实例
                OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
                OSSObject ossObject = ossClient.getObject(bucket_name, urls[i].substring(0, urls[i].length() - 4));
                DataInputStream fis = new DataInputStream(ossObject.getObjectContent());
                out.putNextEntry(new ZipEntry(urls[i]));
                System.out.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXX----文件                                  "+i+1);
                int bytesum = 0;
                int byteread = 0;
                while ((byteread = fis.read(buffer)) != -1) {
                    bytesum += byteread;
//                      System.out.println(bytesum);
                    out.write(buffer, 0, byteread);
                }
                out.closeEntry();
                fis.close();
                ossClient.shutdown();
            }

            out.close();
            System.out.println("生成"+policyNo+".zip成功");

            // 取得zip文件名。
            File zipFile = new File(filepath);
            String filename = zipFile.getName();
            // 以流的形式下载zip文件。
            InputStream fis = new BufferedInputStream(new FileInputStream(filepath));
            byte[] mbuffer = new byte[fis.available()];
            fis.read(mbuffer);
            fis.close();
            // 清空response
            response.reset();
            // 设置response的Header
            response.addHeader("Content-Disposition",
                    "attachment;filename=" + new String(filename.getBytes("gb2312"), "ISO8859-1"));
            OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
            response.setContentType("application/octet-stream");
            response.addHeader("Content-Length", "" + zipFile.length());

            toClient.write(mbuffer);
            toClient.flush();
            toClient.close();

            //删除服务器临时文件
            deleteFile(temp);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 
     * <p>Title: 删除文件</p>
     * <p>Description: TODO</p>
     * @param file
     * @author xu
     */
    private void deleteFile(File file) {
        if(file.isFile()) {
            file.delete();
        } else {
            File[] listFiles = file.listFiles();
            for (File mFile : listFiles) {
                deleteFile(mFile);
            }
        }

         if(file.exists()) {        
             file.delete();  
         }
    }
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值