java通过ip获取用户所在国家

什么是GeoIP ?
  所谓GeoIP,就是通过来访者的IP, 定位他的经纬度,国家/地区,省市,甚至街道等位置信息的一个数据库。GeoIP有两个版本,一个免费版,一个收费版本。收费版本的准确率和数据更好一些。
  GeoIP如何使用?

  GeoIP支持多种语言调用,这里我们以java为例。

执行下面的命令:

        wget -N -q http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz

        gzip -d GeoIP.dat.gz

        cp GeoIP.dat   /usr/local/share/GeoIP/GeoIP.dat

  具体执行代码:

package com.test.intl.commons.ip;

/**
 * 这个类封装了对国家的数据。 类Country.java的实现描述:TODO 类实现描述
 *
 */
public class Country {

    private String code;
    private String name;

    /**
     * Creates a new Country.
     *
     * @param code the country code.
     * @param name the country name.
     */
    public Country(String code, String name) {
        this.code = code;
        this.name = name;
    }

    /**
     * Returns the ISO two-letter country code of this country.
     *
     * @return the country code.
     */
    public String getCode() {
        return code;
    }

    /**
     * Returns the name of this country.
     *
     * @return the country name.
     */
    public String getName() {
        return name;
    }
}


package com.test.intl.commons.ip;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;

import org.apache.log4j.Logger;

import sun.net.util.IPAddressUtil;

import com.maxmind.geoip.DatabaseInfo;

public class CountryLookupService {

    /**
     * 这个是数据文件的。
     */
    private File                                  databaseFile              = null;

    /**
     * buffer最大的数据。
     */
    private int                                   maxBuffer                 = 0;

    /**
     * 判断是否进行了初始化工作。
     */
    private boolean                               isInit                    = false;

    //
    private static Logger                         log                       = Logger.getLogger(CountryLookupService.class);

    /**
     * 每次进行check的时间间隔,默认是1分钟一次。
     */
    private int                                   checkfreshInvertal        = 60 * 1000;
    /**
     * 最后一次做check的时间间隔。
     */
    private long                                  lastcheckfreshTime        = 0;

    /**
     * Database file.
     */
    private SafeBufferRandomAccessFile            file                      = null;

    /**
     * Information about the database.
     */
    // private DatabaseInfo databaseInfo = null;
    /**
     * The database type. Default is the country edition.
     */
    byte                                          databaseType              = DatabaseInfo.COUNTRY_EDITION;

    int                                           databaseSegments[];
    int                                           recordLength;

    String                                        licenseKey;
    int                                           dnsService                = 0;

    private final static int                      COUNTRY_BEGIN             = 16776960;
    private final static int                      STATE_BEGIN               = 16700000;
    private final static int                      STRUCTURE_INFO_MAX_SIZE   = 20;
    // private final static int DATABASE_INFO_MAX_SIZE = 100;

    private final static int                      SEGMENT_RECORD_LENGTH     = 3;
    private final static int                      STANDARD_RECORD_LENGTH    = 3;
    private final static int                      ORG_RECORD_LENGTH         = 4;
    private final static int                      MAX_RECORD_LENGTH         = 4;

    // private final static int MAX_ORG_RECORD_LENGTH = 300;
    // private final static int FULL_RECORD_LENGTH = 50;

    public static final Country                   UNKNOWN_COUNTRY           = new Country("--", "N/A");

    private final static HashMap<String, Integer> hashmapcountryCodetoindex = new HashMap<String, Integer>(512);
    private final static HashMap<String, Integer> hashmapcountryNametoindex = new HashMap<String, Integer>(512);
    private final static String[]                 countryCode               = { "--", "AP", "EU", "AD", "AE", "AF",
            "AG", "AI", "AL", "AM", "AN", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AZ", "BA", "BB", "BD", "BE", "BF",
            "BG", "BH", "BI", "BJ", "BM", "BN", "BO", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF",
            "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM",
            "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "FX", "GA", "GB",
            "GD", "GE", "GF", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM",
            "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IO", "IQ", "IR", "IS", "IT", "JM", "JO", "JP", "KE", "KG",
            "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU",
            "LV", "LY", "MA", "MC", "MD", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU",
            "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM",
            "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RU",
            "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "ST", "SV",
            "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TM", "TN", "TO", "TP", "TR", "TT", "TV", "TW", "TZ",
            "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "YU",
            "ZA", "ZM", "ZR", "ZW", "A1", "A2", "O1"                       };

    private final static String[]                 countryName               = { "N/A", "Asia/Pacific Region", "Europe",
            "Andorra", "United Arab Emirates", "Afghanistan", "Antigua and Barbuda", "Anguilla", "Albania", "Armenia",
            "Netherlands Antilles", "Angola", "Antarctica", "Argentina", "American Samoa", "Austria", "Australia",
            "Aruba", "Azerbaijan", "Bosnia and Herzegovina", "Barbados", "Bangladesh", "Belgium", "Burkina Faso",
            "Bulgaria", "Bahrain", "Burundi", "Benin", "Bermuda", "Brunei Darussalam", "Bolivia", "Brazil", "Bahamas",
            "Bhutan", "Bouvet Island", "Botswana", "Belarus", "Belize", "Canada", "Cocos (Keeling) Islands",
            "Congo, The Democratic Republic of the", "Central African Republic", "Congo", "Switzerland",
            "Cote D'Ivoire", "Cook Islands", "Chile", "Cameroon", "China", "Colombia", "Costa Rica", "Cuba",
            "Cape Verde", "Christmas Island", "Cyprus", "Czech Republic", "Germany", "Djibouti", "Denmark", "Dominica",
            "Dominican Republic", "Algeria", "Ecuador", "Estonia", "Egypt", "Western Sahara", "Eritrea", "Spain",
            "Ethiopia", "Finland", "Fiji", "Falkland Islands (Malvinas)", "Micronesia, Federated States of",
            "Faroe Islands", "France", "France, Metropolitan", "Gabon", "United Kingdom", "Grenada", "Georgia",
            "French Guiana", "Ghana", "Gibraltar", "Greenland", "Gambia", "Guinea", "Guadeloupe", "Equatorial Guinea",
            "Greece", "South Georgia and the South Sandwich Islands", "Guatemala", "Guam", "Guinea-Bissau", "Guyana",
            "Hong Kong", "Heard Island and McDonald Islands", "Honduras", "Croatia", "Haiti", "Hungary", "Indonesia",
            "Ireland", "Israel", "India", "British Indian Ocean Territory", "Iraq", "Iran, Islamic Republic of",
            "Iceland", "Italy", "Jamaica", "Jordan", "Japan", "Kenya", "Kyrgyzstan", "Cambodia", "Kiribati", "Comoros",
            "Saint Kitts and Nevis", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait",
            "Cayman Islands", "Kazakstan", "Lao People's Democratic Republic", "Lebanon", "Saint Lucia",
            "Liechtenstein", "Sri Lanka", "Liberia", "Lesotho", "Lithuania", "Luxembourg", "Latvia",
            "Libyan Arab Jamahiriya", "Morocco", "Monaco", "Moldova, Republic of", "Madagascar", "Marshall Islands",
            "Macedonia, the Former Yugoslav Republic of", "Mali", "Myanmar", "Mongolia", "Macau",
            "Northern Mariana Islands", "Martinique", "Mauritania", "Montserrat", "Malta", "Mauritius", "Maldives",
            "Malawi", "Mexico", "Malaysia", "Mozambique", "Namibia", "New Caledonia", "Niger", "Norfolk Island",
            "Nigeria", "Nicaragua", "Netherlands", "Norway", "Nepal", "Nauru", "Niue", "New Zealand", "Oman", "Panama",
            "Peru", "French Polynesia", "Papua New Guinea", "Philippines", "Pakistan", "Poland",
            "Saint Pierre and Miquelon", "Pitcairn", "Puerto Rico", "" + "Palestinian Territory, Occupied", "Portugal",
            "Palau", "Paraguay", "Qatar", "Reunion", "Romania", "Russian Federation", "Rwanda", "Saudi Arabia",
            "Solomon Islands", "Seychelles", "Sudan", "Sweden", "Singapore", "Saint Helena", "Slovenia",
            "Svalbard and Jan Mayen", "Slovakia", "Sierra Leone", "San Marino", "Senegal", "Somalia", "Suriname",
            "Sao Tome and Principe", "El Salvador", "Syrian Arab Republic", "Swaziland", "Turks and Caicos Islands",
            "Chad", "French Southern Territories", "Togo", "Thailand", "Tajikistan", "Tokelau", "Turkmenistan",
            "Tunisia", "Tonga", "East Timor", "Turkey", "Trinidad and Tobago", "Tuvalu", "Taiwan",
            "Tanzania, United Republic of", "Ukraine", "Uganda", "United States Minor Outlying Islands",
            "United States", "Uruguay", "Uzbekistan", "Holy See (Vatican City State)",
            "Saint Vincent and the Grenadines", "Venezuela", "Virgin Islands, British", "Virgin Islands, U.S.",
            "Vietnam", "Vanuatu", "Wallis and Futuna", "Samoa", "Yemen", "Mayotte", "Yugoslavia", "South Africa",
            "Zambia", "Zaire", "Zimbabwe", "Anonymous Proxy", "Satellite Provider", "Other" };

    /**
     * Create a new lookup service using the specified database file.
     *
     * @param databaseFile String representation of the database file.
     * @throws java.io.IOException if an error occured creating the lookup service from the database file.
     */
    public CountryLookupService(String databaseFile, int maxBuffer) {
        this(new File(databaseFile), maxBuffer);
    }

    /**
     * Create a new lookup service using the specified database file.
     *
     * @param databaseFile the database file.
     * @throws java.io.IOException if an error occured creating the lookup service from the database file.
     */
    public CountryLookupService(File databaseFile, int maxBuffer) {
        lastcheckfreshTime = System.currentTimeMillis();
        this.databaseFile = databaseFile;
        this.maxBuffer = maxBuffer;
        // 这里可能出现文件不存在的情况,这样的情况,主要考虑到一些应用不会使用到这个东西,所以我们认为是合理的。
        try {
            initDataFile();
        } catch (Exception ex) {
            log.error(ex);
        }

    }

    private void initDataFile() throws IOException {
        this.file = new SafeBufferRandomAccessFile(this.databaseFile, this.maxBuffer);
        this.file.start();
        init();
        this.file.end();
        isInit = true;
    }

    /**
     * 这个值,每隔1分钟做一次check,如果该数据没有做初始化的工作 那么在check满足一份钟的前提下,我们会做一次初始化的工作。 进行初始化的工作,我们认为是正确的。
     *
     * @return
     */
    public boolean checkNeedfresh() {
        if (System.currentTimeMillis() - lastcheckfreshTime > checkfreshInvertal) {
            lastcheckfreshTime = System.currentTimeMillis();
            // 如果没有做初始化工作,那么要先做初始化的工作,然后在做处理。
            if (!isInit || this.file == null) {
                if (this.databaseFile.exists()) {
                    synchronized (this) {
                        if (!isInit || this.file == null) {
                            try {
                                initDataFile();
                            } catch (Exception e) {
                                log.error(e);
                            }
                        }
                    }
                }
                return false;
            }
            return this.file.needfresh();
        } else {
            return false;
        }
    }

    /**
     * Reads meta-data from the database file.
     *
     * @throws java.io.IOException if an error occurs reading from the database file.
     */
    private void init() throws IOException {
        int i, j;
        byte[] delim = new byte[3];
        byte[] buf = new byte[SEGMENT_RECORD_LENGTH];

        for (i = 0; i < 233; i++) {
            hashmapcountryCodetoindex.put(countryCode[i], new Integer(i));
            hashmapcountryNametoindex.put(countryName[i], new Integer(i));
        }
        if (file == null) {
            // distributed service only
            return;
        }
        file.seek(file.length() - 3);
        for (i = 0; i < STRUCTURE_INFO_MAX_SIZE; i++) {
            file.read(delim);
            if (delim[0] == -1 && delim[1] == -1 && delim[2] == -1) {
                databaseType = file.readByte();
                if (databaseType >= 106) {
                    // Backward compatibility with databases from April 2003 and earlier
                    databaseType -= 105;
                }
                // Determine the database type.
                if (databaseType == DatabaseInfo.REGION_EDITION) {
                    databaseSegments = new int[1];
                    databaseSegments[0] = STATE_BEGIN;
                    recordLength = STANDARD_RECORD_LENGTH;
                } else if (databaseType == DatabaseInfo.CITY_EDITION_REV0
                           || databaseType == DatabaseInfo.CITY_EDITION_REV1
                           || databaseType == DatabaseInfo.ORG_EDITION || databaseType == DatabaseInfo.ISP_EDITION) {
                    databaseSegments = new int[1];
                    databaseSegments[0] = 0;
                    if (databaseType == DatabaseInfo.CITY_EDITION_REV0
                        || databaseType == DatabaseInfo.CITY_EDITION_REV1) {
                        recordLength = STANDARD_RECORD_LENGTH;
                    } else {
                        recordLength = ORG_RECORD_LENGTH;
                    }
                    file.read(buf);
                    for (j = 0; j < SEGMENT_RECORD_LENGTH; j++) {
                        databaseSegments[0] += (unsignedByteToInt(buf[j]) << (j * 8));
                    }
                }
                break;
            } else {
                file.seek(file.getFilePointer() - 4);
            }
        }
        if (databaseType == DatabaseInfo.COUNTRY_EDITION) {
            databaseSegments = new int[1];
            databaseSegments[0] = COUNTRY_BEGIN;
            recordLength = STANDARD_RECORD_LENGTH;
        }
    }

    /**
     * Closes the lookup service.
     */
    public void close() {
        try {
            file.close();
            file = null;
        } catch (Exception e) {
            log.error(e);
        }
    }

    /**
     * Returns the country the IP address is in.
     *
     * @param ipAddress String version of an IP address, i.e. "127.0.0.1"
     * @return the country the IP address is from.
     */
    public Country getCountry(String ipAddress) {
        // 如果不能正常的初始化,那么我们不会出错,但是返回一个未识别的国家
        if (this.file == null) {
            return UNKNOWN_COUNTRY;
        }
        if (IPAddressUtil.isIPv4LiteralAddress(ipAddress) || IPAddressUtil.isIPv6LiteralAddress(ipAddress)) {
            try {
                return getCountry(bytesToLong(InetAddress.getByName(ipAddress).getAddress()));
            } catch (Throwable e) {
                log.error(e);
            }
        }

        //
        return UNKNOWN_COUNTRY;
    }

    /**
     * Returns the country the IP address is in.
     *
     * @param ipAddress the IP address.
     * @return the country the IP address is from.
     */
    public Country getCountry(InetAddress ipAddress) {
        return getCountry(bytesToLong(ipAddress.getAddress()));
    }

    /**
     * Returns the country the IP address is in.
     *
     * @param ipAddress the IP address in long format.
     * @return the country the IP address is from.
     */
    public Country getCountry(long ipAddress) {
        if (file == null) {
            throw new IllegalStateException("Database has been closed.");
        }
        int ret = warpSeekCountry(ipAddress) - COUNTRY_BEGIN;
        if (ret == 0) {
            return UNKNOWN_COUNTRY;
        } else {
            return new Country(countryCode[ret], countryName[ret]);
        }
    }

    protected int warpSeekCountry(long ipAddress) {
        this.file.start();
        try {
            return this.seekCountry(ipAddress);
        } finally {
            this.file.end();
        }

    }

    /**
     * Finds the country index value given an IP address.
     *
     * @param ipAddress the ip address to find in long format.
     * @return the country index.
     */
    private int seekCountry(long ipAddress) {
        byte[] buf = new byte[2 * MAX_RECORD_LENGTH];
        int[] x = new int[2];
        int offset = 0;
        for (int depth = 31; depth >= 0; depth--) {
            try {
                file.seek(2 * recordLength * offset);
                file.read(buf);
            } catch (IOException e) {
                log.error(e);
            }
            for (int i = 0; i < 2; i++) {
                x[i] = 0;
                for (int j = 0; j < recordLength; j++) {
                    int y = buf[i * recordLength + j];
                    if (y < 0) {
                        y += 256;
                    }
                    x[i] += (y << (j * 8));
                }
            }

            if ((ipAddress & (1 << depth)) > 0) {
                if (x[1] >= databaseSegments[0]) {
                    return x[1];
                }
                offset = x[1];
            } else {
                if (x[0] >= databaseSegments[0]) {
                    return x[0];
                }
                offset = x[0];
            }
        }

        // shouldn't reach here
        log.error("Error seeking country while seeking " + ipAddress);

        return 0;
    }

    /**
     * Returns the long version of an IP address given an InetAddress object.
     *
     * @param address the InetAddress.
     * @return the long form of the IP address.
     */
    private static long bytesToLong(byte[] address) {
        long ipnum = 0;
        for (int i = 0; i < 4; ++i) {
            long y = address[i];
            if (y < 0) {
                y += 256;
            }
            ipnum += y << ((3 - i) * 8);
        }
        return ipnum;
    }

    private static int unsignedByteToInt(byte b) {
        return (int) b & 0xFF;
    }
}

package com.test.intl.commons.ip;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 这个是用于实现线程安全的结构。 类SafeBufferRandomAccessFile.java的实现描述:TODO 类实现描述
 *
 */
public class SafeBufferRandomAccessFile {

    private File                      f                = null;

    private RandomAccessFile          file;

    /**
     * buffer可以支持的最大的长度,超过这个长度就报错误。
     */
    private int                       maxSize          = 0;

    /**
     * 缓存的数据,这个只有一次的机会
     */
    private byte[]                    buffer;
    /**
     * 用于缓存当前线程的状态,这个做法虽然慢一点,但是可以不破坏原来的功能,而且检索锁竞争,应该足够了。
     */
    private static ThreadLocal<Point> pointLocal       = new ThreadLocal<Point>();
    /**
     * 文件的长度,这个一定是int的,这个不是为了大文件服务的。
     */
    private int                       fileSize         = 0;

    private long                      lastmodifiedTime = 0;

    public SafeBufferRandomAccessFile(File f, int maxSize) throws IOException, FileNotFoundException {
        this.f = f;
        file = new RandomAccessFile(f, "r");
        this.maxSize = maxSize;
        readToBuffer();
        lastmodifiedTime = f.lastModified();
    }

    public void readToBuffer() throws IOException {
        long size;
        if ((size = file.length()) > maxSize) {
            throw new SaftBufferOverflowException("file path = [" + f.getAbsolutePath() + "] length=[" + size + "]");
        }
        buffer = new byte[(int) size];
        file.read(buffer, 0, buffer.length);
        fileSize = buffer.length;
    }

    /**
     * 开始一系列的操作。 启动后,所有的操作都是线程安全的。 每次启动的时候,会重置所有的内容。
     */
    public void start() {
        pointLocal.set(new Point(1));
    }

    /**
     * 关闭一次请求, 关闭后,所有的操作就不再是线程安全的。
     */
    public void end() {
        pointLocal.set(null);
    }

    /**
     * 表示该文件是否需要刷新
     *
     * @return
     */
    public boolean needfresh() {
        if (lastmodifiedTime == f.lastModified()) {
            return false;
        }
        return true;
    }

    /**
     * 关闭当前文件并进行释放所有的资源。
     *
     * @throws IOException
     */
    public void close() throws IOException {
        this.buffer = null;
        this.file.close();
    }

    public long getFilePointer() throws IOException {
        return pointLocal.get().curPos;
    }

    public long length() throws IOException {
        return this.fileSize;
    }

    private int innerLength() {
        return this.fileSize;
    }

    public int read() throws IOException {
        byte[] b = new byte[1];
        int i = read(b, 0, b.length);
        if (i == -1) {
            return -1;
        }
        return (int) b[0];
    }

    // public boolean checkNeedfresh(){
    //
    // }

    public int read(byte[] b, int off, int len) throws IOException {
        // 由于b是从0开始的,所以这里的偏移量要小一位。所以这里需要等于号。
        if (off + len > b.length) {
            return 0;
        }
        int cur = pointLocal.get().curPos;
        if (cur == -1) return -1;

        int readSize = 0;
        // 如果剩余的字段比len要长的话,那么直接读取len长度的内容。
        if (innerLength() - cur >= len) {
            System.arraycopy(buffer, cur, b, off, len);
            cur = cur + len;
            readSize = len;
            // 超出末尾了。
        } else {
            System.arraycopy(buffer, cur, b, off, innerLength() - cur);
            cur = -1;
            readSize = -1;
        }
        pointLocal.get().curPos = cur;
        return readSize;
    }

    public int read(byte[] b) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * 指的是从文件起始位置跳过制定的pos的位置。 pos小于0,什么都不做。 当seek不会超出,只会设置到最末尾。 这个语意stream读取是一致的,但是写入的不一致,所以也是正确的。
     *
     * @param pos
     * @throws IOException
     */
    public void seek(long pos) throws IOException {
        if (pos < 0) return;
        pointLocal.get().curPos = (int) (pos > length() ? length() : pos);
    }

    /**
     * 跳过指定n的数量的byte,并丢弃。
     *
     * @param n
     * @return 实际跳过的byte数量。
     * @throws IOException
     */
    public int skipBytes(int n) throws IOException {
        if (n <= 0) {
            return 0;
        }
        long pos = getFilePointer();
        long len = length();

        long newpos = pos + n;
        if (newpos > len) {
            newpos = len;
        }

        seek(newpos);

        return (int) (newpos - pos);
    }

    public byte readByte() throws IOException {
        return (byte) this.read();
    }

    private class Point {

        public Point(int curPos) {
            this.curPos = curPos;
        }

        private int curPos;
    }
}

package com.test.intl.commons.ip;

/**
 * buffer的内容溢出的异常。 类SaftBufferOverflowException.java的实现描述:TODO 类实现描述
 *
 */
public class SaftBufferOverflowException extends RuntimeException {

    private static final long serialVersionUID = 6433209191770907106L;

    public SaftBufferOverflowException() {
        super();
    }

    public SaftBufferOverflowException(String message) {
        super(message);
    }

    public SaftBufferOverflowException(String message, Throwable cause) {
        super(message, cause);
    }

    public SaftBufferOverflowException(Throwable cause) {
        super(cause);
    }

}

测试代码:

package com.test.intl.commons.ip;


public class TestCountry {

    private static final String geoidFile  = "/usr/local/share/GeoIP/GeoIP.dat";
    /** max buffer size for the GeoIP file, 3M */
    private static final int    MAX_BUFFER = 1024 * 1024 * 3;
    public static void main(String[] args) {
        Country country = new CountryLookupService(geoidFile, MAX_BUFFER).getCountry("2001:0db8:85a3:08d3:1319:8a2e:0370:7344");
        System.out.println(country.getCode() + ";" + country.getName());
    }
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值