OCR识别身份证及营业执照图片(Java版)

本文基于百度智能云平台提供的OCR识别技术,对身份证图片及营业执照图片识别处理。

可以说是全网最详尽可用的教程,希望慢慢食用!

准备工作

1.百度智能云官网:百度AI开放平台-全球领先的人工智能服务平台

需注册账号,申请开通应程序,目的是为了得到API key(注册应用获取)和Secret Key(注册应用获取)

2.百度智能云Java SDK:GitHub - Baidu-AIP/java-sdk: 百度AI开放平台 Java SDK

用到里面获取accessToken的接口方法(已可以自己写)

一些用到的SDK

<dependency>
    <groupId>com.baidu.aip</groupId>
    <artifactId>java-sdk</artifactId>
    <version>4.8.0</version>
</dependency>

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.80</version>
</dependency>

3.官网提供的一些重要提示代码中所需工具类
FileUtil,Base64Util,HttpUtil,GsonUtils请从以下连接下载:
https://ai.baidu.com/file/658A35ABAB2D404FBF903F64D47C1F72
https://ai.baidu.com/file/C8D81F3301E24D2892968F09AE1AD6E2
https://ai.baidu.com/file/544D677F5D4E4F17B4122FBD60DB82B3
https://ai.baidu.com/file/470B3ACCA3FE43788B5A963BF0B625F3

OCR核心处理

核心处理类

package xin.cosmos.basic.ocr.baidu;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baidu.aip.auth.DevAuth;
import com.baidu.aip.util.AipClientConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import xin.cosmos.basic.exception.PlatformException;
import xin.cosmos.basic.ocr.baidu.dict.IDCardKeywords;
import xin.cosmos.basic.ocr.baidu.dict.LicenseKeywords;
import xin.cosmos.basic.ocr.dto.BusinessLicense;
import xin.cosmos.basic.ocr.dto.IdCard;
import xin.cosmos.basic.util.FileUtils;

import java.io.File;
import java.net.URLEncoder;
import java.util.Map;

/**
 * 百度OCR识别操作类
 */
@Slf4j
public class BaiduOcrHandler {

    /**
     * 身份证正面OCR识别
     *
     * @param idCardFrontImage 身份证正面照片(支持:{@linkplain File}及{@linkplain MultipartFile})
     * @return IdCard
     */
    public static IdCard handleFrontIdCard(Object idCardFrontImage) throws Exception {
        String accessToken = getAccessToken();
        if (StringUtils.isEmpty(accessToken)) {
            throw new PlatformException("获取token失败");
        }
        String base64ImgParam = getImageBase64Param(idCardFrontImage);
        String param = "id_card_side=front&image=" + base64ImgParam;
        String jsonStr = HttpUtil.post(BaiduOcrConstant.ID_CARD_FRONT_URL, accessToken, param);
        JSONObject jsonObject = JSON.parseObject(jsonStr);
        log.info("身份证正面OCR识别结果=>{}", jsonObject);

        // 解析身份证正面数据
        String status = jsonObject.getString("image_status");
        if (!"normal".equals(status)) {
            throw new PlatformException("身份证图片识别失败:%s", status);
        }
        JSONObject wordsResult = jsonObject.getJSONObject("words_result");
        if (wordsResult.isEmpty()) {
            throw new PlatformException("没有解析到任何数据");
        }
        IdCard idCard = new IdCard();
        buildIdCard(idCard, wordsResult);
        return idCard;
    }

    /**
     * 身份证正反面OCR识别
     *
     * @param idCardFrontImage 身份证正面照片(支持:{@linkplain File}及{@linkplain MultipartFile})
     * @return IdCard
     */
    public static IdCard handleMultiIdCard(Object idCardFrontImage) throws Exception {
        String accessToken = getAccessToken();
        if (StringUtils.isEmpty(accessToken)) {
            throw new PlatformException("获取token失败");
        }
        String base64ImgParam = getImageBase64Param(idCardFrontImage);
        String param = "image=" + base64ImgParam;
        String jsonStr = HttpUtil.post(BaiduOcrConstant.ID_CARD_MULTI_URL, accessToken, param);
        JSONObject jsonObject = JSON.parseObject(jsonStr);
        log.info("身份证正反面OCR识别结果=>{}", jsonObject);

        // 解析正反面
        JSONArray wordsResult = jsonObject.getJSONArray("words_result");
        if (wordsResult.isEmpty()) {
            throw new PlatformException("没有解析到任何数据");
        }
        IdCard idCard = new IdCard();
        for (Object o : wordsResult) {
            JSONObject obj = (JSONObject) o;
            JSONObject cardResult = obj.getJSONObject("card_result");
            if (cardResult == null) {
                continue;
            }
            buildIdCard(idCard, cardResult);
        }
        return idCard;
    }

    /**
     * 营业执照OCR识别
     *
     * @param businessLicenseImage 营业执照照片(支持:{@linkplain File}及{@linkplain MultipartFile})
     * @return IdCard
     */
    public static BusinessLicense handleBusinessLicense(Object businessLicenseImage) throws Exception {
        String accessToken = getAccessToken();
        if (StringUtils.isEmpty(accessToken)) {
            throw new PlatformException("获取token失败");
        }
        String base64ImgParam = getImageBase64Param(businessLicenseImage);
        String param = "detect_direction=true&" + "image=" + base64ImgParam;
        String jsonStr = HttpUtil.post(BaiduOcrConstant.BUSINESS_LICENSE_URL, accessToken, param);
        JSONObject jsonObject = JSON.parseObject(jsonStr);
        log.info("营业执照OCR识别结果=>{}", jsonObject);
        if (!jsonObject.containsKey("words_result")) {
            throw new PlatformException("识别营业执照接口返回信息状态失败");
        }
        BusinessLicense license = new BusinessLicense();
        JSONObject wordsResult = jsonObject.getJSONObject("words_result");
        buildBusinessLicense(license, wordsResult);
        return license;
    }

    /**
     * 构建身份证信息实体对象
     *
     * @param idCard 身份证对象
     * @param meta   接口返回身份证元数据
     */
    private static void buildIdCard(IdCard idCard, JSONObject meta) {
        for (Map.Entry<String, Object> entry : meta.entrySet()) {
            String key = entry.getKey();
            String resultStr = entry.getValue().toString();
            JSONObject result = JSON.parseObject(resultStr);
            String value = result.getString("words");
            switch (key) {
                case IDCardKeywords.FRONT_NAME:
                    idCard.setName(value);
                    break;
                case IDCardKeywords.FRONT_SEX:
                    idCard.setSex(value);
                    break;
                case IDCardKeywords.FRONT_NATION:
                    idCard.setNation(value);
                    break;
                case IDCardKeywords.FRONT_BIRTH:
                    idCard.setBirthDate(value);
                    break;
                case IDCardKeywords.FRONT_ADDRESS:
                    idCard.setAddress(value);
                    break;
                case IDCardKeywords.FRONT_ID:
                    idCard.setCardNumber(value);
                    break;
                case IDCardKeywords.BACK_SIGN_ORG:
                    idCard.setSignOrg(value);
                    break;
                case IDCardKeywords.BACK_SIGN_DATE:
                    idCard.setSignDate(value);
                    break;
                case IDCardKeywords.BACK_EXPIRE:
                    idCard.setExpiredDate(value);
                    break;
                default:
            }
        }
    }

    /**
     * 构建营业执照信息实体对象
     *
     * @param license 营业执照对象
     * @param meta    接口返回营业执照元数据
     */
    private static void buildBusinessLicense(BusinessLicense license, JSONObject meta) {
        for (Map.Entry<String, Object> entry : meta.entrySet()) {
            String key = entry.getKey();
            String resultStr = entry.getValue().toString();
            JSONObject result = JSON.parseObject(resultStr);
            String value = result.getString("words");
            switch (key) {
                case LicenseKeywords.UNIT_NAME:
                    license.setUnitName(value);
                    break;
                case LicenseKeywords.UNIT_TYPE:
                    license.setUnitType(value);
                    break;
                case LicenseKeywords.LEGAL_PERSON:
                    license.setLegalPerson(value);
                    break;
                case LicenseKeywords.ADDRESS:
                    license.setAddress(value);
                    break;
                case LicenseKeywords.CODE:
                    license.setCode(value);
                    break;
                case LicenseKeywords.ID_NUMBER:
                    license.setIdNumber(value);
                    break;
                case LicenseKeywords.VALIDITY:
                    license.setValidity(value);
                    break;
                case LicenseKeywords.REGISTERED_CAPITAL:
                    license.setRegisteredCapital(value);
                    break;
                case LicenseKeywords.COMPANY_CREATE_DATE:
                    license.setCompanyCreateDate(value);
                    break;
                case LicenseKeywords.LAYOUT:
                    license.setLayout(value);
                    break;
                case LicenseKeywords.BUSINESS_SCOPE:
                    license.setBusinessScope(value);
                    break;
                default:
            }
        }
    }

    /**
     * 获取目标文件类型的返回结果
     *
     * @param file File或MultipartFile类型
     * @return
     * @throws Exception
     */
    private static File transferToFile(Object file) throws Exception {
        File img;
        if (file instanceof File) {
            img = (File) file;
        } else if (file instanceof MultipartFile) {
            img = FileUtils.multipartFileToFile((MultipartFile) file);
        } else {
            throw new UnsupportedOperationException("仅支持File或MultipartFile类型的参数");
        }
        return img;
    }

    /**
     * 获取接口请求参数的base64编码格式的图片参数
     *
     * @param file
     * @return
     * @throws Exception
     */
    private static String getImageBase64Param(Object file) throws Exception {
        File img;
        if (file instanceof File) {
            img = (File) file;
        } else if (file instanceof MultipartFile) {
            img = FileUtils.multipartFileToFile((MultipartFile) file);
        } else {
            throw new UnsupportedOperationException("仅支持File或MultipartFile类型的参数");
        }
        byte[] bytes = FileUtil.readFileByBytes(img);
        String encode = Base64Util.encode(bytes);
        return URLEncoder.encode(encode, BaiduOcrConstant.CHARSET);
    }

    /**
     * 获取百度云API的access_token
     * 所需参数:grant_type - 固定值:client_credentials
     * client_id - 百度智能云注册的应用程序API Key
     * client_secret - 百度智能云注册的应用程序Secret Key
     *
     * @return access_token
     */
    public static String getAccessToken() {
        try {
            org.json.JSONObject jsonObject = DevAuth.oauth(BaiduOcrConstant.CLIENT_ID, BaiduOcrConstant.CLIENT_SECRET, config());
            return jsonObject.getString("access_token");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * 请求额外参数配置
     *
     */
    private static AipClientConfiguration config() {
        AipClientConfiguration config = new AipClientConfiguration();
        config.setConnectionTimeoutMillis(60000);
        config.setSocketTimeoutMillis(60000);
        return config;
    }

}
package xin.cosmos.basic.ocr.baidu;

/**
 * 百度OCR识别配置信息
 */
public interface BaiduOcrConstant {
    /**
     * 编码
     */
    String CHARSET = "UTF-8";

    /**
     * 百度智能云注册的应用程序API Key
     */
    String CLIENT_ID = "百度智能云注册的应用程序API Key";

    /**
     * 百度智能云注册的应用程序Secret Key
     */
    String CLIENT_SECRET = "百度智能云注册的应用程序Secret Key";

    /**
     * 身份证正面识别OCR接口地址
     */
    String ID_CARD_FRONT_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard";

    /**
     * 身份证混贴识别OCR接口地址
     */
    String ID_CARD_MULTI_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/multi_idcard";

    /**
     * 营业执照识别OCR接口地址
     */
    String BUSINESS_LICENSE_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/business_license";

    /**
     * OCR accessToken获取接口地址
     */
    String ACCESS_TOKEN_URL = "https://aip.baidubce.com/oauth/2.0/token";
}

数据实体

package xin.cosmos.basic.ocr.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@ApiModel(description = "营业执照对象")
@Data
public class BusinessLicense {

    @ApiModelProperty(value = "单位名称")
    private String unitName;

    @ApiModelProperty(value = "单位类型")
    private String unitType;

    @ApiModelProperty(value = "公司法人")
    private String legalPerson;

    @ApiModelProperty(value = "地址")
    private String address;

    @ApiModelProperty(value = "有效期")
    private String validity;

    @ApiModelProperty(value = "证件编号")
    private String idNumber;

    @ApiModelProperty(value = "社会信用代码")
    private String code;

    @ApiModelProperty(value = "注册资本")
    private String registeredCapital;

    @ApiModelProperty(value = "成立日期")
    private String companyCreateDate;

    @ApiModelProperty(value = "组成形式")
    private String layout;

    @ApiModelProperty(value = "经营范围")
    private String businessScope;

}
package xin.cosmos.basic.ocr.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@ApiModel(description = "身份证对象")
@Data
public class IdCard {

    @ApiModelProperty(value = "姓名")
    private String name;

    @ApiModelProperty(value = "性别")
    private String sex;

    @ApiModelProperty(value = "民族")
    private String nation;

    @ApiModelProperty(value = "出生日期")
    private String birthDate;

    @ApiModelProperty(value = "住址")
    private String address;

    @ApiModelProperty(value = "公民身份号码")
    private String cardNumber;

    @ApiModelProperty(value = "签发机关")
    private String signOrg;

    @ApiModelProperty(value = "签发日期")
    private String signDate;

    @ApiModelProperty(value = "失效日期")
    private String expiredDate;

}

辅助工具类

文件转换

package xin.cosmos.basic.util;

import org.springframework.core.io.ClassPathResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.*;
import java.util.Objects;

/**
 * 文件处理工具
 */
public final class FileUtils {
    /**
     * 读取classpath下的文件数据
     *
     * @param fileClassPath classpath下的文件
     * @return
     */
    public static InputStream readFileStreamWithClassPath(String fileClassPath) {
        return Thread.currentThread().getContextClassLoader().getResourceAsStream(fileClassPath);
    }

    /**
     * 读取classpath下的文件数据
     *
     * @param fileClassPath classpath下的文件
     * @return
     */
    public static File readFileWithClassPath(String fileClassPath) throws IOException {
        return new ClassPathResource(fileClassPath).getFile();
    }

    /**
     * 读取classpath下的文件数据
     *
     * @param absoluteFilePath 文件绝对路径
     * @return
     */
    public static InputStream readFile(String absoluteFilePath) throws IOException {
        return Files.newInputStream(Paths.get(absoluteFilePath));
    }

    /**
     * 复制文件到目标路径
     *
     * @param sourceFilePath
     * @param targetFilePath
     * @throws IOException
     */
    public static void copy(String sourceFilePath, String targetFilePath) throws IOException {
        Files.copy(Paths.get(sourceFilePath), Paths.get(targetFilePath));
    }

    /**
     * MultipartFile转File
     * <p>
     * 选择用缓冲区来实现这个转换即使用java 创建的临时文件 使用 MultipartFile.transferto()方法 。
     *
     * @param multipartFile
     * @return
     */
    public static File transferToFile(MultipartFile multipartFile) {
        File file = null;
        try {
            String originalFilename = multipartFile.getOriginalFilename();
            String[] filename = originalFilename.split("\\.");
            file = File.createTempFile(filename[0], filename[1]);
            multipartFile.transferTo(file);
            file.deleteOnExit();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return file;
    }

    /**
     * MultipartFile 转 File
     *
     * @param file
     * @throws Exception
     */
    public static File multipartFileToFile(MultipartFile file) throws Exception {
        File toFile = null;
        InputStream ins;
        ins = file.getInputStream();
        toFile = new File(Objects.requireNonNull(file.getOriginalFilename()));
        inputStreamToFile(ins, toFile);
        ins.close();
        return toFile;
    }

    /**
     * 获取流文件
     * @param ins
     * @param file
     */
    private static void inputStreamToFile(InputStream ins, File file) {
        try {
            OutputStream os = new FileOutputStream(file);
            int bytesRead = 0;
            byte[] buffer = new byte[8192];
            while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.close();
            ins.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 删除本地临时文件
     * @param file
     */
    public static boolean deleteTempFile(File file) {
        if (file != null) {
            File del = new File(file.toURI());
            return del.delete();
        }
        return false;
    }

}

base64

package xin.cosmos.basic.ocr.baidu;

/**
 * Base64 工具类
 */
public class Base64Util {
    private static final char last2byte = (char) Integer.parseInt("00000011", 2);
    private static final char last4byte = (char) Integer.parseInt("00001111", 2);
    private static final char last6byte = (char) Integer.parseInt("00111111", 2);
    private static final char lead6byte = (char) Integer.parseInt("11111100", 2);
    private static final char lead4byte = (char) Integer.parseInt("11110000", 2);
    private static final char lead2byte = (char) Integer.parseInt("11000000", 2);
    private static final char[] encodeTable = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};

    public Base64Util() {
    }

    public static String encode(byte[] from) {
        StringBuilder to = new StringBuilder((int) ((double) from.length * 1.34D) + 3);
        int num = 0;
        char currentByte = 0;

        int i;
        for (i = 0; i < from.length; ++i) {
            for (num %= 8; num < 8; num += 6) {
                switch (num) {
                    case 0:
                        currentByte = (char) (from[i] & lead6byte);
                        currentByte = (char) (currentByte >>> 2);
                    case 1:
                    case 3:
                    case 5:
                    default:
                        break;
                    case 2:
                        currentByte = (char) (from[i] & last6byte);
                        break;
                    case 4:
                        currentByte = (char) (from[i] & last4byte);
                        currentByte = (char) (currentByte << 2);
                        if (i + 1 < from.length) {
                            currentByte = (char) (currentByte | (from[i + 1] & lead2byte) >>> 6);
                        }
                        break;
                    case 6:
                        currentByte = (char) (from[i] & last2byte);
                        currentByte = (char) (currentByte << 4);
                        if (i + 1 < from.length) {
                            currentByte = (char) (currentByte | (from[i + 1] & lead4byte) >>> 4);
                        }
                }

                to.append(encodeTable[currentByte]);
            }
        }

        if (to.length() % 4 != 0) {
            for (i = 4 - to.length() % 4; i > 0; --i) {
                to.append("=");
            }
        }

        return to.toString();
    }
}

文件File转字节bytes

package xin.cosmos.basic.ocr.baidu;

import java.io.*;

/**
 * 文件读取工具类
 */
public class FileUtil {

    /**
     * 读取文件内容,作为字符串返回
     */
    public static String readFileAsString(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new FileNotFoundException(filePath);
        }

        if (file.length() > 1024 * 1024 * 1024) {
            throw new IOException("File is too large");
        }

        StringBuilder sb = new StringBuilder((int) (file.length()));
        // 创建字节输入流  
        FileInputStream fis = new FileInputStream(filePath);
        // 创建一个长度为10240的Buffer
        byte[] bbuf = new byte[10240];
        // 用于保存实际读取的字节数  
        int hasRead = 0;
        while ((hasRead = fis.read(bbuf)) > 0) {
            sb.append(new String(bbuf, 0, hasRead));
        }
        fis.close();
        return sb.toString();
    }

    /**
     * 根据文件路径读取byte[] 数组
     */
    public static byte[] readFileByBytes(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new FileNotFoundException(filePath);
        }
        return readFileByBytes(file);
    }

    /**
     * 根据文件读取byte[] 数组
     */
    public static byte[] readFileByBytes(File file) throws IOException {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length())) {
            BufferedInputStream in = null;
            in = new BufferedInputStream(new FileInputStream(file));
            short bufSize = 1024;
            byte[] buffer = new byte[bufSize];
            int len1;
            while (-1 != (len1 = in.read(buffer, 0, bufSize))) {
                bos.write(buffer, 0, len1);
            }
            byte[] var7 = bos.toByteArray();
            return var7;
        }
    }
}

HTTP工具类

package xin.cosmos.basic.ocr.baidu;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;

/**
 * http 工具类
 */
public class HttpUtil {

    public static String post(String requestUrl, String accessToken, String params)
            throws Exception {
        String contentType = "application/x-www-form-urlencoded";
        return HttpUtil.post(requestUrl, accessToken, contentType, params);
    }

    public static String post(String requestUrl, String accessToken, String contentType, String params)
            throws Exception {
        String encoding = "UTF-8";
        if (requestUrl.contains("nlp")) {
            encoding = "GBK";
        }
        return HttpUtil.post(requestUrl, accessToken, contentType, params, encoding);
    }

    public static String post(String requestUrl, String accessToken, String contentType, String params, String encoding)
            throws Exception {
        String url = requestUrl + "?access_token=" + accessToken;
        return HttpUtil.postGeneralUrl(url, contentType, params, encoding);
    }

    public static String postGeneralUrl(String generalUrl, String contentType, String params, String encoding)
            throws Exception {
        URL url = new URL(generalUrl);
        // 打开和URL之间的连接
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        // 设置通用的请求属性
        connection.setRequestProperty("Content-Type", contentType);
        connection.setRequestProperty("Connection", "Keep-Alive");
        connection.setUseCaches(false);
        connection.setDoOutput(true);
        connection.setDoInput(true);

        // 得到请求的输出流对象
        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
        out.write(params.getBytes(encoding));
        out.flush();
        out.close();

        // 建立实际的连接
        connection.connect();
        // 获取所有响应头字段
        Map<String, List<String>> headers = connection.getHeaderFields();
        // 遍历所有的响应头字段
        for (String key : headers.keySet()) {
            System.err.println(key + "--->" + headers.get(key));
        }
        // 定义 BufferedReader输入流来读取URL的响应
        BufferedReader in = null;
        in = new BufferedReader(
                new InputStreamReader(connection.getInputStream(), encoding));
        String result = "";
        String getLine;
        while ((getLine = in.readLine()) != null) {
            result += getLine;
        }
        in.close();
        System.err.println("result:" + result);
        return result;
    }
}

至此,功能完成!

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流沙QS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值