Java開發常用工具類

jopen 12年前發布 | 97K 次閱讀 Java Java開發

package com.cucpay.tradeportal.util;

import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID;

import org.apache.commons.lang.StringUtils; import org.apache.mina.core.buffer.IoBuffer;

/**

  • 交易前置系統專用工具類
  • @create Aug 15, 2012 12:16:49 PM
  • @update Sep 27, 2012 3:07:09 PM
  • @author 玄玉<http://blog.csdn/net/jadyer&gt;
  • @version v2.0
  • @history v1.7.2-->新增<code>getHexSign()</code>通過指定算法簽名字符串方法
  • @history v1.7.2-->新增<code>getString()</code>字節數組轉為字符串方法
  • @history v1.7.3-->修改<code>getSysJournalNo()</code>實現細節為<code>java.util.UUID.randomUUID()</code>
  • @history v1.7.4-->新增<code>getHexSign()</code>根據指定的簽名密鑰和算法簽名Map<String,String>
  • @history v1.7.5-->新增<code>getStringSimple()</code>獲取一個字符串的簡明效果,返回的字符串格式類似于"abcd***hijk"
  • @history v2.0-->局部的StringBuffer一律StringBuilder之(本思路提示自坦克<captmjc@gmail.com>) */ public class TradePortalUtil { private TradePortalUtil(){}

    /**

    • 獲取系統流水號
    • @see 若欲指定返回值的長度or是否全由數字組成等,you can call {@link TradePortalUtil#getSysJournalNo(int, boolean)}
    • @return 長度為20的全數字 */ public static String getSysJournalNo(){ return getSysJournalNo(20, true); }
/**
 * 獲取系統流水號
 * @param length   指定流水號長度
 * @param toNumber 指定流水號是否全由數字組成
 */
public static String getSysJournalNo(int length, boolean isNumber){
    //replaceAll()之后返回的是一個由十六進制形式組成的且長度為32的字符串
    String uuid = UUID.randomUUID().toString().replaceAll("-", "");
    if(uuid.length() > length){
        uuid = uuid.substring(0, length);
    }else if(uuid.length() < length){
        for(int i=0; i<length-uuid.length(); i++){
            uuid = uuid + Math.round(Math.random()*9);
        }
    }
    if(isNumber){
        return uuid.replaceAll("a", "1").replaceAll("b", "2").replaceAll("c", "3").replaceAll("d", "4").replaceAll("e", "5").replaceAll("f", "6");
    }else{
        return uuid;
    }
}


/**
 * 判斷輸入的字符串參數是否為空
 * @return boolean 空則返回true,非空則flase
 */
public static boolean isEmpty(String input) {
    return null==input || 0==input.length() || 0==input.replaceAll("\\s", "").length();
}


/**
 * 判斷輸入的字節數組是否為空
 * @return boolean 空則返回true,非空則flase
 */
public static boolean isEmpty(byte[] bytes){
    return null==bytes || 0==bytes.length;
}


/**
 * 從org.apache.mina.core.buffer.IoBuffer中讀取字符串
 * @see 該方法默認以GBK解碼
 * @see 若想自己指定字符集,可以使用<code>getStringFromIoBuffer(IoBuffer buffer, int size, String charset)</code>方法
 * @param size 所要讀取的字節數
 */
public static String getStringFromIoBuffer(IoBuffer buffer, int size){
    return getStringFromIoBuffer(buffer, size, "GBK");
}


/**
 * 從org.apache.mina.core.buffer.IoBuffer中讀取字符串
 * @param size    所要讀取的字節數
 * @param charset 解碼的字符集
 */
public static String getStringFromIoBuffer(IoBuffer buffer, int size, String charset){
    String result = null;
    try {
        result = buffer.getString(size, Charset.forName(charset).newDecoder());
    } catch (CharacterCodingException e) {
        LogUtil.getLogger().error("字符解碼異常,自動切換第二種解碼方式,本次的堆棧信息如下", e);
        try {
            result = new String(buffer.array(), charset);
        } catch (UnsupportedEncodingException ee) {
            LogUtil.getLogger().error("字符解碼異常,系統不支持該字符集[" + charset + "],本次的堆棧信息如下", ee);
        }
    }
    return result;
}


/**
 * 獲取實體類中的屬性
 * @see 本方法用到了反射,其適用于所有的屬性類型均為byte[]的JavaBean
 * @return String field11=value11 field22=value22 field33=value33
 */
public static String getStringFromObjectForByte(Object obj){
    StringBuilder sb = new StringBuilder(); //局部的StringBuffer一律StringBuilder之
    sb.append(obj.getClass().getName()).append("@").append(obj.hashCode()).append("[");
    for(Field field : obj.getClass().getDeclaredFields()){
        String methodName = "get" + StringUtils.capitalize(field.getName()); //構造getter方法
        Object fieldValue = null;
        try{
            fieldValue = obj.getClass().getDeclaredMethod(methodName).invoke(obj); //執行getter方法,獲取其返回值
        }catch(Exception e){
            //一旦發生異常,便將屬性值置為UnKnown,故此處沒必要一一捕獲所有異常
            sb.append("\n").append(field.getName()).append("=UnKnown");
            continue;
        }
        if(fieldValue == null){
            sb.append("\n").append(field.getName()).append("=null");
        }else{
            sb.append("\n").append(field.getName()).append("=").append(new String((byte[])fieldValue));
        }
    }
    return sb.append("\n]").toString();
}


/**
 * 獲取Map中的屬性
 * @see 由于Map.toString()打印出來的參數值對,是橫著一排的...參數多的時候,不便于查看各參數值
 * @see 故此仿照commons-lang.jar中的ReflectionToStringBuilder.toString()編寫了本方法
 * @return String key11=value11 \n key22=value22 \n key33=value33 \n......
 */
public static String getStringFromMap(Map<String, String> map){
    StringBuilder sb = new StringBuilder();
    sb.append(map.getClass().getName()).append("@").append(map.hashCode()).append("[");
    for(Map.Entry<String,String> entry : map.entrySet()){
        sb.append("\n").append(entry.getKey()).append("=").append(entry.getValue());
    }
    return sb.append("\n]").toString();
}


/**
 * 獲取Map中的屬性
 * @see 該方法的參數適用于打印Map<String, byte[]>的情況
 * @return String key11=value11 \n key22=value22 \n key33=value33 \n......
 */
public static String getStringFromMapForByte(Map<String, byte[]> map){
    StringBuilder sb = new StringBuilder();
    sb.append(map.getClass().getName()).append("@").append(map.hashCode()).append("[");
    for(Map.Entry<String,byte[]> entry : map.entrySet()){
        sb.append("\n").append(entry.getKey()).append("=").append(new String(entry.getValue()));
    }
    return sb.append("\n]").toString();
}


/**
 * 獲取Map中的屬性
 * @see 該方法的參數適用于打印Map<String, Object>的情況
 * @return String key11=value11 \n key22=value22 \n key33=value33 \n......
 */
public static String getStringFromMapForObject(Map<String, Object> map){
    StringBuilder sb = new StringBuilder();
    sb.append(map.getClass().getName()).append("@").append(map.hashCode()).append("[");
    for(Map.Entry<String,Object> entry : map.entrySet()){
        sb.append("\n").append(entry.getKey()).append("=").append(entry.getValue().toString());
    }
    return sb.append("\n]").toString();
}


/**
 * 金額元轉分
 * @see 注意:該方法可處理貳仟萬以內的金額,且若有小數位,則不限小數位的長度
 * @see 注意:如果你的金額達到了貳仟萬以上,則不推薦使用該方法,否則計算出來的結果會令人大吃一驚
 * @param amount  金額的元進制字符串
 * @return String 金額的分進制字符串
 */
public static String moneyYuanToFen(String amount){
    if(isEmpty(amount)){
        return amount;
    }
    //傳入的金額字符串代表的是一個整數
    if(-1 == amount.indexOf(".")){
        return Integer.parseInt(amount) * 100 + "";
    }
    //傳入的金額字符串里面含小數點-->取小數點前面的字符串,并將之轉換成單位為分的整數表示
    int money_fen = Integer.parseInt(amount.substring(0, amount.indexOf("."))) * 100;
    //取到小數點后面的字符串
    String pointBehind = (amount.substring(amount.indexOf(".") + 1));
    //amount=12.3
    if(pointBehind.length() == 1){
        return money_fen + Integer.parseInt(pointBehind)*10 + "";
    }
    //小數點后面的第一位字符串的整數表示
    int pointString_1 = Integer.parseInt(pointBehind.substring(0, 1));
    //小數點后面的第二位字符串的整數表示
    int pointString_2 = Integer.parseInt(pointBehind.substring(1, 2));
    //amount==12.03,amount=12.00,amount=12.30
    if(pointString_1 == 0){
        return money_fen + pointString_2 + "";
    }else{
        return money_fen + pointString_1*10 + pointString_2 + "";
    }
}


/**
 * 金額元轉分
 * @see 該方法會將金額中小數點后面的數值,四舍五入后只保留兩位....如12.345-->12.35
 * @see 注意:該方法可處理貳仟萬以內的金額
 * @see 注意:如果你的金額達到了貳仟萬以上,則非常不建議使用該方法,否則計算出來的結果會令人大吃一驚
 * @param amount  金額的元進制字符串
 * @return String 金額的分進制字符串
 */
public static String moneyYuanToFenByRound(String amount){
    if(isEmpty(amount)){
        return amount;
    }
    if(-1 == amount.indexOf(".")){
        return Integer.parseInt(amount) * 100 + "";
    }
    int money_fen = Integer.parseInt(amount.substring(0, amount.indexOf("."))) * 100;
    String pointBehind = (amount.substring(amount.indexOf(".") + 1));
    if(pointBehind.length() == 1){
        return money_fen + Integer.parseInt(pointBehind)*10 + "";
    }
    int pointString_1 = Integer.parseInt(pointBehind.substring(0, 1));
    int pointString_2 = Integer.parseInt(pointBehind.substring(1, 2));
    //下面這種方式用于處理pointBehind=245,286,295,298,995,998等需要四舍五入的情況
    if(pointBehind.length() > 2){
        int pointString_3 = Integer.parseInt(pointBehind.substring(2, 3));
        if(pointString_3 >= 5){
            if(pointString_2 == 9){
                if(pointString_1 == 9){
                    money_fen = money_fen + 100;
                    pointString_1 = 0;
                    pointString_2 = 0;
                }else{
                    pointString_1 = pointString_1 + 1;
                    pointString_2 = 0;
                }
            }else{
                pointString_2 = pointString_2 + 1;
            }
        }
    }
    if(pointString_1 == 0){
        return money_fen + pointString_2 + "";
    }else{
        return money_fen + pointString_1*10 + pointString_2 + "";
    }
}


/**
 * 金額分轉元
 * @see 注意:如果傳入的參數中含小數點,則直接原樣返回
 * @see 該方法返回的金額字符串格式為<code>00.00</code>,其整數位有且至少有一個,小數位有且長度固定為2
 * @param amount  金額的分進制字符串
 * @return String 金額的元進制字符串
 */
public static String moneyFenToYuan(String amount){
    if(isEmpty(amount)){
        return amount;
    }
    if(amount.indexOf(".") > -1){
        return amount;
    }
    if(amount.length() == 1){
        return "0.0" + amount;
    }else if(amount.length() == 2){
        return "0." + amount;
    }else{
        return amount.substring(0, amount.length()-2) + "." + amount.substring(amount.length()-2);
    }
}


/**
 * 字節數組轉為字符串
 * @see 該方法默認以ISO-8859-1轉碼
 * @see 若想自己指定字符集,可以使用<code>getString(byte[] data, String charset)</code>方法
 */
public static String getString(byte[] data){
    return getString(data, "ISO-8859-1");
}


/**
 * 字節數組轉為字符串
 * @see 如果系統不支持所傳入的<code>charset</code>字符集,則按照系統默認字符集進行轉換
 */
public static String getString(byte[] data, String charset){
    if(isEmpty(data)){
        return "";
    }
    if(isEmpty(charset)){
        return new String(data);
    }
    try {
        return new String(data, charset);
    } catch (UnsupportedEncodingException e) {
        LogUtil.getLogger().error("將byte數組[" + data + "]轉為String時發生異常:系統不支持該字符集[" + charset + "]");
        return new String(data);
    }
}


/**
 * 獲取一個字符串的簡明效果
 * @return String 返回的字符串格式類似于"abcd***hijk"
 */
public static String getStringSimple(String data){
    return data.substring(0,4) + "***" + data.substring(data.length()-4);
}


/**
 * 字符串轉為字節數組
 * @see 該方法默認以ISO-8859-1轉碼
 * @see 若想自己指定字符集,可以使用<code>getBytes(String str, String charset)</code>方法
 */
public static byte[] getBytes(String data){
    return getBytes(data, "ISO-8859-1");
}


/**
 * 字符串轉為字節數組
 * @see 如果系統不支持所傳入的<code>charset</code>字符集,則按照系統默認字符集進行轉換
 */
public static byte[] getBytes(String data, String charset){
    data = (data==null ? "" : data);
    if(isEmpty(charset)){
        return data.getBytes();
    }
    try {
        return data.getBytes(charset);
    } catch (UnsupportedEncodingException e) {
        LogUtil.getLogger().error("將字符串[" + data + "]轉為byte[]時發生異常:系統不支持該字符集[" + charset + "]");
        return data.getBytes();
    }
}


/**
 * 根據指定的簽名密鑰和算法簽名Map<String,String>
 * @see 方法內部首先會過濾Map<String,String>參數中的部分鍵值對
 * @see 過濾規則:移除鍵名為"cert","hmac","signMsg"或者鍵值為null或者鍵值長度為零的鍵值對
 * @see 過濾結果:過濾完Map<String,String>后會產生一個字符串,其格式為[key11=value11|key22=value22|key33=value33]
 * @see And the calls {@link TradePortalUtil#getHexSign(String,String,String,boolean)}進行簽名
 * @param param     待簽名的Map<String,String>
 * @param charset   簽名時轉碼用到的字符集
 * @param algorithm 簽名時所使用的算法,從業務上看,目前其可傳入兩個值:MD5,SHA-1
 * @param signKey   簽名用到的密鑰
 * @return String algorithm digest as a lowerCase hex string
 */
public static String getHexSign(Map<String, String> param, String charset, String algorithm, String signKey){
    StringBuilder sb = new StringBuilder();
    List<String> keys = new ArrayList<String>(param.keySet());
    Collections.sort(keys);
    for(int i=0; i<keys.size(); i++){
        String key = keys.get(i);
        String value = param.get(key);
        if(key.equalsIgnoreCase("cert") || key.equalsIgnoreCase("hmac") || key.equalsIgnoreCase("signMsg") || value==null || value.length()==0){
            continue;
        }
        sb.append(key).append("=").append(value).append("|");
    }
    sb.append("key=").append(signKey);
    return getHexSign(sb.toString(), charset, algorithm, true);
}


/**
 * 通過指定算法簽名字符串
 * @see Calculates the algorithm digest and returns the value as a hex string
 * @see If system dosen't support this <code>algorithm</code>, return "" not null
 * @see It will Calls {@link TradePortalUtil#getBytes(String str, String charset)}
 * @see 若系統不支持<code>charset</code>字符集,則按照系統默認字符集進行轉換
 * @see 若系統不支持<code>algorithm</code>算法,則直接返回""空字符串
 * @see 另外,commons-codec.jar提供的DigestUtils.md5Hex(String data)與本方法getHexSign(data, "UTF-8", "MD5", false)效果相同
 * @param data        Data to digest
 * @param charset     字符串轉碼為byte[]時使用的字符集
 * @param algorithm   目前其有效值為<code>MD5,SHA,SHA1,SHA-1,SHA-256,SHA-384,SHA-512</code>
 * @param toLowerCase 指定是否返回小寫形式的十六進制字符串
 * @return String algorithm digest as a lowerCase hex string
 */
public static String getHexSign(String data, String charset, String algorithm, boolean toLowerCase){
    char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    //Used to build output as Hex
    char[] DIGITS = toLowerCase ? DIGITS_LOWER : DIGITS_UPPER;
    //get byte[] from {@link TradePortalUtil#getBytes(String, String)}
    byte[] dataBytes = getBytes(data, charset);
    byte[] algorithmData = null;
    try {
        //get an algorithm digest instance
        algorithmData = MessageDigest.getInstance(algorithm).digest(dataBytes);
    } catch (NoSuchAlgorithmException e) {
        LogUtil.getLogger().error("簽名字符串[" + data + "]時發生異常:System doesn't support this algorithm[" + algorithm + "]");
        return "";
    }
    char[] respData = new char[algorithmData.length << 1];
    //two characters form the hex value
    for(int i=0,j=0; i<algorithmData.length; i++){
        respData[j++] = DIGITS[(0xF0 & algorithmData[i]) >>> 4];
        respData[j++] = DIGITS[0x0F & algorithmData[i]];
    }
    return new String(respData);
}


/**
 * 字符編碼
 * @see 該方法默認會以UTF-8編碼字符串
 * @see 若想自己指定字符集,可以使用<code>encode(String chinese, String charset)</code>方法
 */
public static String encode(String chinese){
    return encode(chinese, "UTF-8");
}


/**
 * 字符編碼
 * @see 該方法通常用于對中文進行編碼
 * @see 若系統不支持指定的編碼字符集,則直接將<code>chinese</code>原樣返回
 */
public static String encode(String chinese, String charset){
    chinese = (chinese==null ? "" : chinese);
    try {
        return URLEncoder.encode(chinese, charset);
    } catch (UnsupportedEncodingException e) {
        LogUtil.getLogger().error("編碼字符串[" + chinese + "]時發生異常:系統不支持該字符集[" + charset + "]");
        return chinese;
    }
}


/**
 * 字符解碼
 * @see 該方法默認會以UTF-8解碼字符串
 * @see 若想自己指定字符集,可以使用<code>decode(String chinese, String charset)</code>方法
 */
public static String decode(String chinese){
    return decode(chinese, "UTF-8");
}


/**
 * 字符解碼
 * @see 該方法通常用于對中文進行解碼
 * @see 若系統不支持指定的解碼字符集,則直接將<code>chinese</code>原樣返回
 */
public static String decode(String chinese, String charset){
    chinese = (chinese==null ? "" : chinese);
    try {
        return URLDecoder.decode(chinese, charset);
    } catch (UnsupportedEncodingException e) {
        LogUtil.getLogger().error("解碼字符串[" + chinese + "]時發生異常:系統不支持該字符集[" + charset + "]");
        return chinese;
    }
}


/**
 * 字符串右補空格
 * @see 該方法默認采用空格(其ASCII碼為32)來右補字符
 * @see 若想自己指定所補字符,可以使用<code>rightPadForByte(String str, int size, int padStrByASCII)</code>方法
 */
public static String rightPadForByte(String str, int size){
    return rightPadForByte(str, size, 32);
}


/**
 * 字符串右補字符
 * @see 若str對應的byte[]長度不小于size,則按照size截取str對應的byte[],而非原樣返回str
 * @see 所以size參數很關鍵..事實上之所以這么處理,是由于支付處理系統接口文檔規定了字段的最大長度
 * @see 若對普通字符串進行右補字符,建議org.apache.commons.lang.StringUtils.rightPad(...)
 * @param size          該參數指的不是字符串長度,而是字符串所對應的byte[]長度
 * @param padStrByASCII 該值為所補字符的ASCII碼,如32表示空格,48表示0,64表示@等
 */
public static String rightPadForByte(String str, int size, int padStrByASCII){
    byte[] srcByte = str.getBytes();
    byte[] destByte = null;
    if(srcByte.length >= size){
        //由于size不大于原數組長度,故該方法此時會按照size自動截取,它會在數組右側填充'(byte)0'以使其具有指定的長度
        destByte = Arrays.copyOf(srcByte, size);
    }else{
        destByte = Arrays.copyOf(srcByte, size);
        Arrays.fill(destByte, srcByte.length, size, (byte)padStrByASCII);
    }
    return new String(destByte);
}


/**
 * 字符串左補空格
 * @see 該方法默認采用空格(其ASCII碼為32)來左補字符
 * @see 若想自己指定所補字符,可以使用<code>leftPadForByte(String str, int size, int padStrByASCII)</code>方法
 */
public static String leftPadForByte(String str, int size){
    return leftPadForByte(str, size, 32);
}


/**
 * 字符串左補字符
 * @see 若str對應的byte[]長度不小于size,則按照size截取str對應的byte[],而非原樣返回str
 * @see 所以size參數很關鍵..事實上之所以這么處理,是由于支付處理系統接口文檔規定了字段的最大長度
 * @param padStrByASCII 該值為所補字符的ASCII碼,如32表示空格,48表示0,64表示@等
 */
public static String leftPadForByte(String str, int size, int padStrByASCII){
    byte[] srcByte = str.getBytes();
    byte[] destByte = new byte[size];
    Arrays.fill(destByte, (byte)padStrByASCII);
    if(srcByte.length >= size){
        System.arraycopy(srcByte, 0, destByte, 0, size);
    }else{
        System.arraycopy(srcByte, 0, destByte, size-srcByte.length, srcByte.length);
    }
    return new String(destByte);
}


/**
 * 獲取前一天日期yyyyMMdd
 * @see 經測試,針對閏年02月份或跨年等情況,該代碼仍有效。測試代碼如下
 * @see calendar.set(Calendar.YEAR, 2013);
 * @see calendar.set(Calendar.MONTH, 0);
 * @see calendar.set(Calendar.DATE, 1);
 * @see 測試時,將其放到<code>calendar.add(Calendar.DATE, -1);</code>前面即可
 * @return 返回的日期格式為yyyyMMdd
 */
public static String getYestoday(){
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.DATE, -1);
    return new SimpleDateFormat("yyyyMMdd").format(calendar.getTime());
}


/**
 * 獲取當前的日期yyyyMMdd
 */
public static String getCurrentDate(){
    return new SimpleDateFormat("yyyyMMdd").format(new Date());
}


/**
 * 獲取當前的時間yyyyMMddHHmmss
 */
public static String getCurrentTime(){
    return new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
}


/**
 * HTML字符轉義
 * @see 對輸入參數中的敏感字符進行過濾替換,防止用戶利用JavaScript等方式輸入惡意代碼
 * @see String input = <img src='http://t1.baidu.com/it/fm=0&gp=0.jpg'/>
 * @see HtmlUtils.htmlEscape(input);         //from spring.jar
 * @see StringEscapeUtils.escapeHtml(input); //from commons-lang.jar
 * @see 盡管Spring和Apache都提供了字符轉義的方法,但Apache的StringEscapeUtils功能要更強大一些
 * @see StringEscapeUtils提供了對HTML,Java,JavaScript,SQL,XML等字符的轉義和反轉義
 * @see 但二者在轉義HTML字符時,都不會對單引號和空格進行轉義,而本方法則提供了對它們的轉義
 * @return String 過濾后的字符串
 */
public static String htmlEscape(String input) {
    if(isEmpty(input)){
        return input;
    }
    input = input.replaceAll("&", "&amp;");
    input = input.replaceAll("<", "&lt;");
    input = input.replaceAll(">", "&gt;");
    input = input.replaceAll(" ", "&nbsp;");
    input = input.replaceAll("'", "&#39;");   //IE暫不支持單引號的實體名稱,而支持單引號的實體編號,故單引號轉義成實體編號,其它字符轉義成實體名稱
    input = input.replaceAll("\"", "&quot;"); //雙引號也需要轉義,所以加一個斜線對其進行轉義
    input = input.replaceAll("\n", "<br/>");  //不能把\n的過濾放在前面,因為還要對<和>過濾,這樣就會導致<br/>失效了
    return input;
}

}</pre>來自:http://blog.csdn.net/jadyer/article/details/7960811

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!