2016年3月16日 星期三

Google Play 驗證 In-app Billing 電子收據 - 實作篇

經過上一篇介紹如何進行 Google Play 驗證 In-app Billing  如何進行設定後,本篇將解說驗證的工作流程。
Google API 使用 OAuth 2.0 認證機制,在認證之前我們需要先產生 JWT 向 Google 要求一組 Token,
然後再將 Google 給的 Token 在 Call API 時一并放在 Request 內一起送過去。
什麼是 JWT呢?簡單來說它是一個開放標準基於 json 的一種認證方式可以在不同的網域間共享資訊。
想更進一步的了解 JWT可以到 http://self-issued.info/docs/draft-ietf-oauth-jwt-bearer.html

驗證的第一步就是先向 Google 要來一組 Token,往後的 API 呼叫都需要用到它,
附帶一提 Token 有時效性(印象中最高可設定1小時)

///
/// 向 Google 索取存取API所需的 Token
/// 
public static void FetchGoogleAccessToken() {
    //JOSE Header 設定使用何種加密方式進行簽章
    var header = new { typ = "JWT", alg = "RS256" };
    var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
    var issueTime = DateTime.UtcNow;
    var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
    var exp = (int)issueTime.AddMinutes(60).Subtract(utc0).TotalSeconds;
    //ClamsSet傳送到 Google 進行 Auth 時的資訊
    var claimset = new {
        iss = /*上一篇有提到的  EMAIL ADDRESS */,
        scope = "https://www.googleapis.com/auth/androidpublisher",
        aud = "https://accounts.google.com/o/oauth2/token",
        iat,
        exp
    };

    // Encoded header
    var headerSerialized = JsonConvert.SerializeObject(header);
    var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
    //網路上找的到 Base64UrlEncode 不貼碼了
    var headerEncoded = Base64UrlEncode(headerBytes);

    // Encoded claimset
    var claimsetSerialized = JsonConvert.SerializeObject(claimset);
    var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
    //網路上找的到 Base64UrlEncode 不貼碼了
    var claimsetEncoded = Base64UrlEncode(claimsetBytes);
    //C# 6.0 的語言 可替換成 string.Format("{0}.{1}",headerEncoded,claimsetEncoded)
    var input = $"{headerEncoded}.{claimsetEncoded}";
    var inputBytes = Encoding.UTF8.GetBytes(input);

    // signiture
    var certificate = new X509Certificate2(
        Path.Combine(
            /*存放P12檔的路徑*/,
            /*P12檔的檔名*/),
        "notasecret");
    var rsa = (RSACryptoServiceProvider)certificate.PrivateKey;
    var cspParam = new CspParameters {
        KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
        KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
    };
    var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
    var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
    var signatureEncoded = Base64UrlEncode(signatureBytes);

     //JWT 需要將三個連在一起︰Header,Claims Set 和 Sign 然後用 . 隔開
    var jwt = $"{headerEncoded}.{claimsetEncoded}.{signatureEncoded}";
    var r = (HttpWebRequest)WebRequest.Create("https://accounts.google.com/o/oauth2/token");
    r.Method = "POST";
    r.ContentType = "application/x-www-form-urlencoded";
    r.UserAgent = string.Format(Section.Get.Common.Culture, " Mozilla/4.0 (compatible; Win32; {0}.{1})", 1, 0);
    ServicePointManager.ServerCertificateValidationCallback = (sender, certificates, chain, sslPolicyErrors) => true;
    var postData = $@"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={jwt}";
    var postBytes = Encoding.UTF8.GetBytes(postData);
    r.ContentLength = postBytes.Length;
    using (var postStream = r.GetRequestStream()) {
        postStream.Write(postBytes, 0, postBytes.Length);
        postStream.Close();
    }
    Task.Factory.FromAsync(r.BeginGetResponse, r.EndGetResponse, null).ContinueWith(t => {
        try {
            var result = t.Result.GetResponseStream();
            if (null == result) return;
            using (var sr = new StreamReader(result, Encoding.UTF8)) {
                var token = sr.ReadToEnd();
                //GoogleToken 這個類別只有三個欄位 string access_token、string token_type、int expires_in
                var tokenObj = JsonConvert.DeserializeObject(token);
                //tokenObj.access_token 需要保存下來
                Log.Info($"NewAccessToken={tokenObj.access_token}");
                sr.Close();
            }
        }
        catch (Exception ex) {
            Log.Error(ex.Message, ex);
        }
    }, TaskContinuationOptions.ExecuteSynchronously);
}

最後,當玩家玩成儲值流程時先別急著將道具或代幣給玩家,先向 Google 問一下這位玩家剛剛是否有真的消費
實作的方式可以參考下面的連結進行實作
https://developers.google.com/android-publisher/api-ref/purchases/products/get#request

2014年10月25日 星期六

Google Play 驗證 In-app Billing 電子收據 - 設定篇

      Android 遊戲的開發者在提供遊戲內購買的功能時都要面對的問題:如何驗證  In-app Billing  電子收據,網路上
可以找到不少的方法可以繞過 Android In-app Billing 的驗證,用 Fake IAB receipt 欺騙我們開發的 Android  遊戲。
要防治這個問題最簡單的方式就是將 IAB receipt 向 Google Play 進行驗證,下圖(臨時用 Google 文件畫的)是我們
製作的遊戲在遊戲內購買功能的流程,因採用負載平衡所以登入的動作會多二個步驟,整個流程大致如下︰
1.登入Master Server
2.Master Server回傳負載最輕的伺服器
3.連線到遊戲伺服器
4.Client 發送儲值要求 要Game Server
5.Game Server回傳我方的訂單編號給 Client
6.Client 向 Google Play 請求儲值
7.Google Play 回傳交易憑證到 Client
8.Client 要求結束儲值並將交易憑證給 Game Server
9.Game server 向 Google Play 驗證交易憑證
10.Google Play 回報交易結果
11.Game Server 通知儲值結果給 Client
App 若只做到第7個步驟時就會將玩家儲值或購買的東西給玩家時,就能被 Fake  IAB receipt 所斯騙。
要進行第8個步驟前需要先到 Google Play  的管理中心進行設定,登入 Google Play Developer Console 必須是帳戶擁有者才可以進行設定,接到到設定 -> API 存取權的頁面,接著點擊建立新專案
連著會建立好專案之後再點擊建立OAuth用戶端之後會轉到 Google Console
緊接著再點擊 Create new Client ID(看你使用的Browser的語系而定,繁中版會顯示中文)
接著選第二種 Server account
建立好之後會出現下面的資訊,過程中會下載一個 p12 憑證檔案,請保存好,此外再記好 EMAIL ADDRESS 下篇會用到。
接著點擊左方選單的 APIs 將 Google Play Android Developer API 開啟
最後一步,必需將剛建立的帳號給予管理員的權限才行。
      透過上述的設定之後,接下來我們就可以在遊戲伺服器內用剛才建立的 Server Account 向 Google Play 進行交易憑證的驗證,
防止收到 Fake IAB receipt 時仍將點數或道具交給玩家。

2014年10月16日 星期四

MSSQL 進行資料庫還原時出現 3154 的錯誤解決方法

剛剛是附加的問題,現在則是遇上了資料庫還原的問題,
 在SQL Server 2014版本中還原從SQL Server 2008備份出來的數據庫時,
會出現錯誤:「備分組包含現有的 '***' 資料庫以外的資料庫備份。RESTORE DATABASE 正在異常結束。 (Microsoft SQL Server,錯誤: 3154)」。 
久遠前學習到的在SQL Server要作資料庫還原時,
需要首先建立一個資料庫(舊的也行總之要先有一個資料庫實體),然後才能可以進行資料庫還原 
1.建立空的資料庫
2.在資料庫上按右鍵工作-〉還原-〉檔案和檔案群組
設定好選項後就出現下歹3154的錯誤

突然發現  2012、2014版上可以直接在在資料庫的ROOT層上操作還原
設定上和原有的一模一樣,只差在一個有建好資料庫,一個沒建資料庫。
總之,這個問題就這樣的被排解掉了。打個文記錄一下

MSSQL 進行資料庫附加時出現 5123 的錯誤解決方法

昨天機房內的主機的硬碟突然掛了(RAID 0),就在重灌系統後,正要復原SQL SERVER的資料庫時發了一段小插曲,因為資料庫的實體檔案沒放在系統碟內,很自然的就使用“附加"的動作來掛回去資料庫到 SQL SERVER中,但它確出現了下面的錯誤訊息。

一時間也不知道要怎處理,上GOOGLE查閱,大家也都說把只要把.mdf檔和.ldf加入 NETWORK SERVICE就可以解決這個問題,但我加了也是出現相同的錯誤。並且發現檔上有個不存在的帳號權限,推測檔案就是缺少了這個帳號的權限才會出現5123的錯誤。
有了方向就可以出發向偉大的Google尋求解答,在下了N個關鍵字找尋後,MSDN還是吃微軟飯的人的庇護所, 按造文件上的說明一步一步的操作,果然解決了資料庫檔不能附加的問題。
設定 Database Engine 對檔案系統的存取權限

2014年5月31日 星期六

AzDG可逆加密演算法 For Ruby

╮╯▂╰╭ AzDG再度登板
#/*Sava as AzDG.rb*/
#!/usr/bin/env ruby
# encoding: UTF-8
require 'digest'
require 'base64'

class AzDG
    attr_accessor :cipher
    def initialize(cipher = "Private key")
        @cipherHash = Digest::MD5.hexdigest(cipher)
    end

    def cipherEncode(inputData)   
        outData = Array.new(inputData.length)
        for i in (0..inputData.length-1) 
            outData[i] = (inputData[i] ^ @cipherHash[i%32].ord)
        end    
        return outData
    end

    def Encrypt(inputData)  
        noise = Digest::MD5.hexdigest(Time.new.strftime("%Y-%m-%d %H:%M:%S"))
        aryInputData = inputData.bytes.to_a        
        outData = Array.new(aryInputData.length * 2)
        for i in (0..aryInputData.length-1) 
            outData[(i*2)] = noise[i%32].ord     
            outData[(i*2)+1] = aryInputData[i] ^ noise[i%32].ord 
        end        
        return Base64.encode64(cipherEncode(outData).pack('c*'))
    end

    def Decrypt(inputData) 
        decodeSourceText = cipherEncode(Base64.decode64(inputData).bytes.to_a)
        outData = Array.new()
        i = 0           
        while i < decodeSourceText.length do 
            outData.push(decodeSourceText[i].ord ^ decodeSourceText[i+1].ord)    
            i += 2
        end
        return outData.pack('c*').force_encoding('UTF-8')
    end
end     

使用方式

require './AzDG'
azdg = AzDG.new
var  = azdg.Encrypt("張學友")
puts "#{var}"
puts azdg.Decrypt(var)

2014年3月14日 星期五

AzDG可逆加密演算法 For Javascript

老狗玩不出新把戲( ╮╯▂╰╭)AzDG再度上場,本次的版本是給Node.js於後端使用,請勿使用在前端頁面中,這樣的話私鑰就曝光了。
使用前請先用NPM安裝MD5的套件( MD5 ),此外也沒有提供額外的字元集支持,請使用UTF-8字元編碼。
程式中的 byte to utf-8 字串的程式碼來源在這裡
/*Sava as AzDG.js*/
(function() {
    var md5 = require('MD5');    
    var utf8 =
    {
        encode: function(str) {
            var len = str.length;
            var result = [];
            var code;
            var i;
            for (i = 0; i < len; i++) {
                code = str.charCodeAt(i);
                if (code <= 0x7f) {
                    result.push(code);
                } else if (code <= 0x7ff) { // 2 bytes                     
                    result.push(0xc0 | (code >>> 6 & 0x1f),
                        0x80 | (code & 0x3f));
                } else if (code <= 0xd700 || code >= 0xe000) { // 3 bytes
                    result.push(0xe0 | (code >>> 12 & 0x0f),
                        0x80 | (code >>> 6 & 0x3f),
                        0x80 | (code & 0x3f));
                } else { // 4 bytes, surrogate pair
                    code = (((code - 0xd800) << 10) | (str.charCodeAt(++i) - 0xdc00)) + 0x10000;
                    result.push(0xf0 | (code >>> 18 & 0x07),
                        0x80 | (code >>> 12 & 0x3f),
                        0x80 | (code >>> 6 & 0x3f),
                        0x80 | (code & 0x3f));
                }
            }
            return result;
        },
        decode: function(array) {
            var out, i, len, c;
            out = "";
            len = array.length;
            i = 0;
            while (i < len) {
                c = array[i++];
                switch (c >> 4) {
                case 0:
                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                    // 0xxxxxxx
                    out += String.fromCharCode(c);
                    break;
                case 12:
                case 13:
                    // 110x xxxx   10xx xxxx
                    out += String.fromCharCode(((c & 0x1F) << 6) | (array[i++] & 0x3F));
                    break;
                case 14:
                    // 1110 xxxx  10xx xxxx  10xx xxxx
                    out += String.fromCharCode(((c & 0x0F) << 12) | 
                      ((array[i++] & 0x3F) << 6) |
                      ((array[i++] & 0x3F) << 0));
                    break;
                }
            }
            return out;
        }
    }

    var AzDG = {
        cipher: 'Private key',
        cipherEncode: function(inputData) {
            var cipherHash = utf8.encode(md5(this.cipher));
            var outData = new Array(inputData.length);
            for (var i = 0; i < inputData.length; i++) {
                outData[i] = (inputData[i] ^ cipherHash[i % 32]);
            }
            return outData;
        },
        encrypt: function(sourceText) {
            var inputData  = utf8.encode(sourceText); 
            var noise = md5(Math.round((new Date()).getTime()).toString()); 
            var outData = new Array(inputData.length * 2);
            for (var j = 0; j < inputData.length; j++) {
                outData[(j * 2)] = noise[j % 32];
                outData[(j * 2) + 1] = inputData[j] ^ noise[j % 32];
            }           
            return new Buffer(this.cipherEncode(outData)).toString('base64')
        },
        decrypt: function(sourceText) {
            var decodeSourceText = this.cipherEncode(new Buffer(sourceText, 'base64'));
            var size = Math.ceil(decodeSourceText.length / 2);
            var outData = new Array(size);
            for (var j = 0; j < size; ++j) {
                outData[j] = (decodeSourceText[(j * 2)] ^ decodeSourceText[(j * 2) + 1]);
            }
            return utf8.decode(outData);
        }
    }
    module.exports.getInstance = function() {
        return AzDG; 
    };
})();

使用方式

var azdg =  require('./AzDG').getInstance();
encode = azdg.encrypt('她來聽我 的演唱會 在十七歲的初戀 第一次約會');
console.log('decode:' +azdg.decrypt(encode));

2013年12月27日 星期五

AzDG可逆加密演算法 For Dart

Google 2011 年時所推出的語言,據說是要取代現有的Javascript,先前已有稍為看了一下,
因為沒有專案在使用這門語言,所以就沒有進一步的了解據聞公司明年的合作伙伴採用Dart來作為專案的主要撰寫語言,
所以再度把AzDG拿來練手,和google 另一門語言 Go 一樣還滿易學的。


import 'package:crypto/crypto.dart' show MD5, CryptoUtils;
import 'package:utf/utf.dart';

class AzDG {
    String _cipher = 'Private key';
    List< int > _cipherHash;
    AzDG({String cipher}) {
        if (cipher != null) {
            _cipher = cipher;
        }
        var md5 = new MD5();
        md5.add(this._cipher.codeUnits);
        _cipherHash = CryptoUtils.bytesToHex(md5.close()).codeUnits;
    }

    List < int > _cipherEncode(List < int > inputData) {
        var outData = new List < int > (inputData.length);
        var loopCount = inputData.length;
        for (var i = 0; i & lt; loopCount; i++) {
            outData[i] = inputData[i] ^ _cipherHash[i % 32];
        }
        return outData;
    }

    String Decrypt(String sourceText) {
        var decodeSourceText = _cipherEncode(CryptoUtils.base64StringToBytes(sourceText));
        var size = (decodeSourceText.length~ / 2).toInt();
        var outData = new List < int > (size);
        for (int j = 0; j < size; ++j) {
            outData[j] = (decodeSourceText[(j * 2)] ^ decodeSourceText[(j * 2) + 1]);
        }
        return decodeUtf8(outData);
    }

    String Crypt(String sourceText) {
        var now = new DateTime.now();
        var md5 = new MD5();
        md5.add(now.toString().codeUnits);
        var noise = CryptoUtils.bytesToHex(md5.close()).codeUnits;
        var inputData = encodeUtf8(sourceText);
        var outData = new List < int > (inputData.length * 2);
        for (var j = 0; j < inputData.length; j++) {
            outData[(j * 2)] = noise[j % 32];
            outData[(j * 2) + 1] = inputData[j] ^ noise[j % 32];
        }
        return CryptoUtils.bytesToBase64(_cipherEncode(outData));
    }
}

2013年12月17日 星期二

Java 解開ZIP檔案

奮戰了幾天,雖然實作了JSP在線解開ZIP檔案的功能
但是有個致命的缺點
ZIP檔的檔案名稱不能含有非ASCII的字元(ISO-8859-1)
一遇上非ISO-8859-1編碼的字串
下面的程式就會停擺
google一下有個package可以解決這個問題
目前還在研究這個package
有收穫再貼上來分享
先看看下面簡易的在線解壓功能
老實說,效能不怎好
解相同的檔案較PHP及C#都要來的慢..


package com.jIAn.decompress;
import java.lang.*;
import java.io.*;
import java.util.zip.*;
public class zip
{
    public zip(){}
    public void DecompressFile(String arg_zipFileName, String arg_outputDirectory) throws Exception
    {
        ZipInputStream objZipInputStream = new ZipInputStream(new FileInputStream(arg_zipFileName));
        ZipEntry objZipEntry;
        InputStream objInputStream = null;
        File objFile ;
        int intStreamLength ;        
        while ((objZipEntry = objZipInputStream.getNextEntry()) != null) 
        {
            
            if (objZipEntry.isDirectory()) 
            {
                String strDirectoryName = objZipEntry.getName();
                strDirectoryName = strDirectoryName.substring(0, strDirectoryName.length() - 1);
                objFile = new File(arg_outputDirectory + File.separator + strDirectoryName);
                objFile.mkdir();
                System.out.println("建立資料夾 " + arg_outputDirectory + File.separator + strDirectoryName);
            }
            else 
            {
                objFile = new File(arg_outputDirectory + File.separator + objZipEntry.getName());
                objFile.createNewFile();
                FileOutputStream objFileOutputStream = new FileOutputStream(objFile);                
                while ((intStreamLength = objZipInputStream.read()) != -1)
                {
                    objFileOutputStream.write(intStreamLength);
                }
                objFileOutputStream.close();
            }
            System.out.println("解壓 " + objZipEntry.getName()+".......ok");
        }
        objZipInputStream.close();
    }

}
如何使用?
建立物件
zip objZip = new zip();
objZip.DecompressFile(欲解開的Zip檔,解到那一個目錄);
objZip.DecompressFile("C:/zip/123.zip","C:\\zip\\123");
還有一個要注意的地方
如果解壓到多層路徑需要先行建立多層路徑的目錄

Java DES 範例


package com.jIAn.crypt;
import javax.crypto.*;

public class DES
{
    private static String strDefaultKey = "jIAn";    //預設的金鑰
    private StringBuffer objSb             = null;
    private Cipher objCipher             = null;
    private java.security.Key objKey    = null;
    private int intStringLength            = 0;
    private int intTemp                    = 0;
    
    //預設建構子
    public DES() throws Exception
    {        
        this(strDefaultKey);
    }

    //自訂密鑰
    public DES(String arg_strKey) throws Exception 
    {
        setKey(arg_strKey.getBytes());
        objCipher = Cipher.getInstance("DES");
    }

    //從指定的字串製成密鑰,密鑰所需的字元陣列長度為8位,不足及超過都要處理
     private void setKey(byte[] arg_strPrivateKey) throws Exception 
     {         
         byte[] arrTempByteArray = new byte[8];
         // 將原始字元陣列轉換為8位
         for (int i = 0; i < arg_strPrivateKey.length && i < arrTempByteArray.length; i++)
         {
             arrTempByteArray[i] = arg_strPrivateKey[i];
         }
         // 設定密鑰
         objKey = new javax.crypto.spec.SecretKeySpec(arrTempByteArray, "DES");    
     }
     
     //將byte陣列轉換16進制值的字串,如:byte[]{1,18}轉換為:0112     
    public String byte2Hex(byte[] arg_bteArray) throws Exception 
    {
        intStringLength = arg_bteArray.length;    
        objSb = new StringBuffer(intStringLength * 2);
        for (int i = 0; i < intStringLength; i++)
        {
            intTemp = (int)arg_bteArray[i];
            //負數需要轉成正數
            if(intTemp < 0) 
            {
                intTemp = intTemp + 256;
            }
            // 小於0F需要補0
            if (intTemp < 16)
            {
                objSb.append("0");
            }
            objSb.append(Integer.toString(intTemp, 16));
        }
        return objSb.toString();
     }

    
    //將16進制值的字串轉成byte陣列        
    public byte[] hex2Byte(String arg_strHexString) throws Exception 
    {
        byte[] arrByteDAta = arg_strHexString.getBytes();
        intStringLength = arrByteDAta.length;
        byte[] aryRetuenData = new byte[intStringLength / 2];
        for (int i = 0; i < intStringLength; i = i + 2)
        {
            aryRetuenData[i / 2] =  (byte)Integer.parseInt(new String(arrByteDAta, i, 2), 16);
        }
        return aryRetuenData;
    }

    //加密字串
     public byte[] doEncrypt(byte[] arg_bteArray) throws Exception
     {
         objCipher.init(Cipher.ENCRYPT_MODE, objKey);
         return objCipher.doFinal(arg_bteArray);
     }
     
     public String encrypt(String arg_strToEncriptString) throws Exception 
     {
         return byte2Hex(doEncrypt(arg_strToEncriptString.getBytes()));
     }
     
     public byte[] doDecrypt(byte[] arg_bteArray) throws Exception 
     {
        objCipher.init(Cipher.DECRYPT_MODE, objKey);     
        return objCipher.doFinal(arg_bteArray);
     }
     //解密字串
     public String decrypt(String arg_strToDecriptString) throws Exception 
     {
         return new String(doDecrypt(hex2Byte(arg_strToDecriptString)));
     }
}
測試

package com.jIAn.crypt;
import java.util.*;
public class Test 
{
    public static void main(String[] args) 
    {
        try 
        {
            String test = "jIAn";
            DES des = new DES("jIAn");//自定義密鑰
            System.out.println("加密前的字符:"+test);
            System.out.println("加密後的字符:"+des.encrypt(test));
            System.out.println("解密後的字符:"+des.decrypt(des.encrypt(test)));            
        } 
        catch (Exception e)
        {
            
            e.printStackTrace();
        }
        
    }
}

C# String To Unicode 編碼 (UTF-16BE)


public String text2uni2(String arg_Source)
{
       int enCode;
       char[] charArray = arg_Source.ToCharArray();
       StringBuilder uniText = new StringBuilder(arg_Source.Length * 2);
       for (int i = 0; i < charArray.Length; i++)
       {
              char a = charArray[i];
              uniText.Append("\\u");
              enCode = (a >> 8);
              string hexCode = enCode.ToString("X");
              if (hexCode.ToString().Length == 1)
              {
                     uniText.Append("0");
              }
              uniText.Append(hexCode);
              enCode = (a & 0xFF);
              hexCode = enCode.ToString("X");
              if (hexCode.ToString().Length == 1)
              {
                     uniText.Append("0");
              }
              uniText.Append(hexCode);
       }
       return (uniText.ToString());
}

例如
ドラマも大人気「のだめカンタービレ」の世界
轉換後
\u30c9\u30e9\u30de\u3082\u5927\u4eba\u6c17\u300c\u306e\u3060\u3081
\u30ab\u30f3\u30bf\u30fc\u30d3\u30ec\u300d\u306e\u4e16\u754c

La new打進冠軍決賽
轉換後
\u004c\u0061\u0020\u006e\u0065\u0077\u6253\u9032\u51a0\u8ecd\u6c7a\u8cfd