恩...ALLPAY的串接上面其實挺簡單的。就是在加密那塊很麻煩
主要是用HTTP POS的方式來實作,DELPHI中我們就直接用TIDHTTP即可完成
因為URL 是HTTPS的 所以必須再帶入SSL的兩個必須DLL
開發過程中遇到了幾個問題我以下列表 並附上程式 有興趣的可以參考看看
1. TIDHTTP ...在Windows8的版本
HTTP.ReadTimeout := 30
必須填入此值。不可用預設的0 在W7 W10 不會有錯 因為0在DELPHI 寫0會有一個預設TIMEOUT時間 (應該也是30秒吧) .....
2. SHA256 加密
{ TODO : SHA256 }
LoadOpenSSLLibrary;
SHA256 := TIdHashSHA256.Create;
try
Result := SHA256.HashStringAsHex(mCMVstr) ;
finally
SHA256.free;
end;
LoadOpenSSLlibary 一定要先啟用 否則 TidSHA256 ..跑出來的不會有值
3. AES 加密
網路範例7成大概找到都是ECB的加密。ALLPAY使用的是CBC的加密
利用ElAES.pas 來做, 這個PAS到處都抓得著
{ TODO : xStr 明文, xKey 加密金鑰 xIV 加密向量 }
function EncryAES(xStr, xKey, xIV: AnsiString): String;
var
Source: TStringStream;
Dest: TStringStream;
{ TODO : 引用ELAES.PAS就會認識了ˊ }
mKey: TAESKey128;
mIV: TAESBuffer;
strRemainder,i:Integer;
mstr,aKey,aIV:Ansistring;
begin
try
{ TODO : 不要覺得奇怪,不這樣重給,值就是會不對 }
{ TODO : ※我外部傳入其實已經是ANSISTING了不知道為什麼直接使用上面的xStr xKey 這載入的值就會有異常。因此從新給一次※ }
mstr:= xStr;
aKey:= xKey;
aIV:=xIV;
{ TODO : pksc5 PHP 是用pksc5 但是DELPHI & JAVA 就是要用pksc5才會對 }
{ TODO : ※補位的主要原因是在 EncryptAESStreamCBC 會檢核你輸入的數值是否有達到128K 192K 248K 因此來源值需要補位※ }
strRemainder :=Length(mstr) mod 16;
strRemainder := 16 - strRemainder;
for i:= 1 to strRemainder do
begin
mstr := mstr + Chr(strRemainder);
end;
{ TODO : KEY & IV 將KEY轉成Array of Byte }
FillChar( mKey, SizeOf(mKey), #0 );
Move( PAnsiChar(aKey)^, mKey, 16);
FillChar( mIV, SizeOf(mIV), #0 );
Move( PAnsiChar(aIV)^, mIV, 16);
{ TODO : Input & output 並賦予值 }
Source := TStringStream.Create(mstr);
Dest := TStringStream.Create;
{ TODO : 加密 }
EncryptAESStreamCBC( Source, 0, mKey, mIV, Dest );
{ TODO : BASE64 千萬不要對Dest做任何的變化,保持BYTE來做加解密}
Result := EncodeBase64(Dest.Bytes, Dest.size);
finally
Source.Free;
Dest.Free;
end;
end;
4. 解密
{ TODO : 再回傳上有一組資訊是被加密過得因此我們必須解密才能得到裡面的值做紀錄 }
{ TODO : xStr的值會是ALLPAY回應給我們的加密電文 , 因此要按照當初加密的方式反向解密 }
function DecryptAES(xStr, xKey, xIV: AnsiString): String;
var
Source: TStringStream;
Dest: TStringStream;
mKey: TAESKey128;
mIV: TAESBuffer;
strRemainder,i:Integer;
mstr,aKey,aIV:Ansistring;
mBytes, mReturnByte :TBytes;
begin
try
{ TODO : Input & output }
Dest := TStringStream.Create;
Source := TStringStream.Create;
{ TODO : 不要覺得奇怪,不這樣重給,值就是會不對 }
mstr:= xStr;
aKey:= xKey;
aIV:=xIV;
{ TODO-1 : 反解密 urlDecode..這邊要強調的是我們的TIDHTTP其實有自動幫我們做過一次 URLEncode }
{ TODO : 因此當我們收到回傳是需要解析的時候呢,要做一次解,實際上是看需要....以allpay來說 這組urlDecode 是解了第二次了..總而言之你不能在看到你字串內還有%這個符號了}
mstr := urlDecode(mstr) ;
{ TODO-2 : 反解密 .DecodeBase64 }
mBytes := DecodeBase64(mstr);
{ TODO : 將資料已Byte方式輸入...千千萬萬不要.....拿字串輸入哦 }
{ TODO : 不可以這樣寫 Source.WriteBuffer(mBytes, mBytes.size); 寫進去的值根本不對。我不知道為什麼@@ }
Source.Position := 0;
for i := 0 to length(mBytes) -1 do
Source.WriteBuffer(mBytes[i], 1);
{ TODO : 不用補位 }
{ TODO : KEY & IV 將KEY轉成Array of Byte }
FillChar( mKey, SizeOf(mKey), #0 );
Move( PAnsiChar(aKey)^, mKey, 16);
FillChar( mIV, SizeOf(mIV), #0 );
Move( PAnsiChar(aIV)^, mIV, 16);
{ TODO-3 : 解密 }
DecryptAESStreamCBC( Source, 0, mKey, mIV, Dest );
{ TODO : 濾掉byte < 32 --小於32的都是特殊字元對於解析無意義}
Dest.Position := 0;
SetLength(mReturnByte, Dest.size);
for i:= 0 to Dest.size do
begin
if ( Dest.Bytes[i] <= 32 ) then
continue;
Dest.Read(mReturnByte[i], 1);
end;
{ TODO : 轉字串 }
Result := stringof(mReturnByte);
finally
Source.Free;
Dest.Free;
end;
end;
5. 解析參數
URLAPI參數回復一班都會是 A=123&B=安安你好&C=還不錯
我原先也想直接用INDY的元件解這些資料但好像沒有 那就自己做吧 提供給大家
利用一組Tstringlist 來將收到的回傳 依照& 寫入LIST內
procedure DismantlResponse(xSource: AnsiString;
var xST: TstringList);
begin
xST.StrictDelimiter := true; // 忽略空白,不將空白當作是分隔基準
xST.Delimiter := '&'; // &在HTML事件 這個東西是不會被加密成其他的
xST.DelimitedText:= xSource;
end;
再利用你想找的字串去取得=之後的值
function GetResponseString(xName: AnsiString; const xST: TstringList): AnsiString;
var
i, r, j:Integer;
begin
Result:='';
i := xST.Count;
for r := 0 to i-1 do
begin
j:= pos(xName, xST[r]);
if j > 0 then
begin
Result := copy( xST[r], Length(xName)+2, length(xST[r])-(Length(xName)+1));
break;
end;
end;
end;
6. 扣款 最後付上一個付款動作 有需要的人在修改成自己想要的樣貌即可
變數x開頭代表外部傳入 F為全域變數 PlatformID MerchantID 固定值自行套入
begin
HTTP := TIdHTTP.Create(nil);
HTTP.HandleRedirects := True;
HTTP.ReadTimeout := 30;
try
try
{ TODO : 這邊會這樣寫 因為ALLPAY的文件上沒有說要排序但實際上你要幫他排序。因此利用STRINGGRID.sort 讓他由字母大小排序 }
mData := now; //為了保持時間一致 因為你如果每次都NOW FORMAT...組到好會有落差ㄎㄎ...
mSL := TStringList.Create;
mSL.Add('PlatformID=' + PlatformID);
mSL.Add('MerchantID=' + MerchantID);
mSL.Add('MerchantTradeNo=' + xorderId);
mSL.Add('StoreID=' + 'S01');
mSL.Add('PosID=' + '01');
mSL.Add('MerchantTradeDate=' + FormatDateTime('YYYYMMDD', mData));
mSL.Add('MerchantTradeTime=' + FormatDateTime('HHNNSS', mData));
mSL.Add('Barcode=' + xbuyerID);
mSL.Add('TradeAmount=' + '100');
mSL.Add('TradeDesc=' + '');
mSL.Add('CheckOutType=' + '1');
mSL.Add('Remark=' + '');
mSL.Sort;
{ TODO : 1. 將CheckMacValue的資訊組起 前面的排序不可刪除 很重要 、我會貼在下面、大家再自行參考}
mCMVstr := ComposeStr(mSL);
{ TODO : 先將Data組起來 uses SuperObject 注意這個mData 建議}
JsonData := SO;
JsonData.s['PlatformID'] := PlatformID; // 開發商代號
JsonData.s['MerchantID'] := MerchantID; // 個特店的代號
JsonData.s['MerchantTradeNo'] := xorderId; // 交易序號
JsonData.s['StoreID'] := 'S01'; // 店號
JsonData.s['PosID'] := '01'; // 機號
JsonData.s['MerchantTradeDate'] := FormatDateTime('YYYYMMDD', mData);
JsonData.s['MerchantTradeTime'] := FormatDateTime('HHNNSS', mData);
JsonData.s['Barcode'] := xbuyerID; // 使用者條碼
JsonData.i['TradeAmount'] := 100; // 交易金額
JsonData.s['TradeDesc'] := ''; // 商品描述
JsonData.i['CheckOutType'] := 1; // 1. 直接結帳 2 需使用結帳
JsonData.s['Remark'] := ''; // 備注 沒需要
JsonData.s['CheckMacValue'] := mCMVstr; // 檢核碼
mCMVstr := EncryAES(JsonData.AsString, HashKEY, HashIV );
mSL.Clear;
mSL.Add('PlatformID=' + URLEncode(PlatformID));
mSL.Add('MerchantID=' + URLEncode(MerchantID));
mSL.Add('Encryption=1');
mSL.Add('Format=2');
mSL.Add('Data=' + URLEncode(mCMVstr));
try
HTTP.IOHandler := FHandlerSSL;;
mCMVstr := HTTP.Post( 'https://newpayment-stage.allpay.com.tw/POS/Payment', mSL);
mCMVstr := urlDecode(mCMVstr); //前面有講到我已經先解過一次了哦 到解AES又要再一次
//抓取回傳資訊
slRespon := TStringList.Create;
try
{ TODO : 先解成區塊 }
DismantlResponse(mCMVstr, slRespon);
mRtnCode := GetResponseString('RtnCode', slRespon);
{ TODO : 回傳=1代表完成 其他都代表有問題,ALLPAY文件上會寫 }
if not ( mRtnCode = '1') then
begin
Result := False;
Showmessage(AllpayResponsMapping(strtoint(GetResponseString('RtnCode', slRespon))));
exit;
end ;
if FSuccesePay or ( mRtnCode = '1') then
begin
{ TODO : 取得DATA資料 }
mTempStr := GetResponseString('Data', slRespon);
JsonData := SO(mTempStr);
gEZPay.BusyerID := BusyID;
gEZPay.WalletOrderNo := JsonData['AllpayTradeNo'].AsString;
gEZPay.WalletData := JsonData['AllpayTradeDate'].AsString;
gEZPay.Wallettime := JsonData['AllpayTradeTime'].AsString;
gEZPay.PayMoney := JsonData['TradeAmount'].AsString;
gEZPay.BuyerWallet := JsonData['PaymentType'].AsString;
{ TODO : 記得要印單哦 系統當機你就找不到退的依據了 }
MobilePayLog('Pay:', '狀態:交易成功', UsesName, '');
MobilePayLog('Pay:', '卡號:'+gEZPay.BusyerID, UsesName, '');
MobilePayLog('Pay:', '銀行交易序號' + gEZPay.BankOrderNo, UsesName, '');
MobilePayLog('Pay:', '支付交易序號' + gEZPay.WalletOrderNo, UsesName, '');
MobilePayLog('Pay:', '支付機構種類' + gEZPay.BuyerWallet, UsesName, '');
Result := True;
{ TODO : 在下結帳 }
自行補腦了.....基本上結帳成功其他的問題你已不存在,
end;
finally
slRespon.Free;
end;
HTTP.Disconnect;
finally
FreeandNil(mHandler);
FreeandNil(mSL);
FreeandNil(HTTP);
end;
except
on E: EIdHTTPProtocolException do
Showmessage('Indy raised a protocol error!' + sLineBreak + 'HTTP status code: ' + IntToStr(E.ErrorCode) + sLineBreak + 'Error message' + E.Message);
on E: EIdConnClosedGracefully do
Showmessage ('Indy reports, that connection was closed gracefully!');
on E: EIdSocketError do
Showmessage('Indy raised a socket error!' + sLineBreak + 'Error code: ' + IntToStr(E.LastError) + sLineBreak + 'Error message' + E.Message);
on E: EIdException do
Showmessage('Indy raised an exception!' + sLineBreak + 'Exception class: ' + E.ClassName + sLineBreak + 'Error message: ' + E.Message);
on E: Exception do
Showmessage('A non-Indy related exception has been raised!');
end;
finally
end;
end;
OK 這些大概就是過程了
目前ALLPAY的測試環境呢 在某些錯誤回復上會被誤導。參數送錯或者是BARCODE錯她會回復HTTP 500/1的錯誤。屆時你們在與ALLPAY人員溝通即可。
下次再分享 支付寶toDelphi 微信toDelphi
留言列表