第一篇簡書,想想就有點小激動呢~,就拿最近做了4天的微信支付開始吧。
作為一個iOS開發的搬磚農民工,感受到微信支付的小坑實在太多。所以,首先得感謝大神們在網上分享微信支付的各種經驗,我才能照葫蘆畫瓢做出來。
第一步當然是看微信支付官網文檔,其中有一張業務流程圖,結合官網demo把支付的流程看懂:
我下載的官方demo中,調起微信接口payReq的參數都是在服務器端生成(微信本身是鼓勵客戶APP把簽名算法放到服務器上面,這樣信息就不容易被破解),而我需要在本地完成這些參數的設定,客戶端進行2次簽名驗證,主要是為了獲取到prePayId(統一下單號)。主要參照了以下文章:
c.iOS客戶端的微信支付接入 - iPhone手機開發技術文章 - 紅黑聯盟
這三篇結合起來看應該講的很詳細。聲明一下:我的代碼完全是照著c篇,把OC翻譯成swift寫出來的。如下:
'
import Foundation
class WXPayManager: NSObject, WXApiDelegate {
let WX_PAY_APP_ID = "wxbaf100d54*******" //公眾賬號ID
let WX_PAY_COMPANY_ID = "12769*****" //商戶號
let WX_PAY_API_KEY = "chuxiaolan******"
let WX_PAY_PREPAY_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder" //預支付相關url
let WX_PAY_NOTIFY_URL = "http://218.244.***.***:8080/payment_server/ops/payForWechat.do"
var debugInfo = NSMutableString() //debug信息
var lastErrCode = Int() //返回的錯誤碼
var orderBean:OrderBean?
class func defaultManager()->WXPayManager {
struct Singleton {
static var predicate:dispatch_once_t = 0
static var instance:WXPayManager? = nil
}
dispatch_once(&Singleton.predicate) { () -> Void in
Singleton.instance = WXPayManager()
}
return Singleton.instance!
}
//MARK:- WXApiDelegate
//微信支付的回調方法
func onResp(resp:BaseResp) {
print(resp.errCode)
if resp is PayResp {
switch (resp.errCode) {
case 0:
NSNotificationCenter.defaultCenter().postNotificationName("NOTIFICATION_WX_PAY", object: true)
default:
NSNotificationCenter.defaultCenter().postNotificationName("NOTIFICATION_WX_PAY", object: false)
break
}
}
}
//創建package簽名
func createMd5Sign(dict:NSMutableDictionary)->String {
let contentString = NSMutableString()
let keys0 = dict.allKeys as! [String]
//按字母排序
let keys = keys0.sort()
//拼接字符串
for key in keys {
if !(dict[key] == nil) && !(key == "sign") && !(key == "key") {
contentString.appendFormat("%@=%@&", key, String(dict[key]!))
}
}
//添加key字段
contentString.appendFormat("key=%@", WX_PAY_API_KEY)
//得到MD5 sign簽名
let md5Sign = WXUtil.md5(contentString as String)
//輸出Debug Info
debugInfo.appendFormat("MD5簽名字符串:%@", contentString)
return md5Sign
}
//獲取package帶參數的簽名包
func genPackage(packageParams:NSMutableDictionary)->String {
//生成簽名
let sign = self.createMd5Sign(packageParams)
//生成xml的package
let reqPars = NSMutableString()
let keys = packageParams.allKeys as! [String]
reqPars.appendString("<xml>")
for key in keys {
reqPars.appendFormat("<%@>%@</%@>", key, String(packageParams[key]!), key)
}
reqPars.appendFormat("<sign>%@</sign></xml>", sign)
return reqPars as String
}
//提交預支付
func sendPrepay(prePayParams:NSMutableDictionary)->String? {
var prepayid:String?
//獲取提交支付
let send = self.genPackage(prePayParams)
debugInfo.appendFormat("API鏈接:%@", WX_PAY_PREPAY_URL)
debugInfo.appendFormat("發送的xml:%@", send)
//發送請求的post xml數據
let res:NSData = WXUtil.httpSend(WX_PAY_PREPAY_URL, method: "POST", data: send)
//輸出Debug Info
debugInfo.appendFormat("服務器返回:%@", String(data: res, encoding: NSUTF8StringEncoding)!)
let xml = XMLHelper()
//開始解析
xml.startParse(res)
var resParams = xml.getDict()
//判斷返回
var return_code = resParams["return_code"] as? String
var result_code = resParams["result_code"] as? String
if return_code == "SUCCESS" {
//生成返回數據的簽名
let sign = self.createMd5Sign(resParams)
let send_sign = resParams["sign"] as? String
//驗證簽名正確性
if sign == send_sign {
if result_code == "SUCCESS" {
//驗證業務處理狀態
prepayid = resParams["prepay_id"] as! String
return_code = ""
debugInfo.appendString("獲取預支付交易標示成功!")
}
} else {
self.lastErrCode = 1
debugInfo.appendFormat("gen_sign=%@, send_sign=%@", sign, send_sign!)
debugInfo.appendString("服務器返回簽名驗證錯誤!")
}
} else {
self.lastErrCode = 2
debugInfo.appendString("接口返回錯誤!")
}
return prepayid
}
//生成預支付訂單
func getPrepayOrder()->NSMutableDictionary? {
let appid = self.WX_PAY_APP_ID
let mch_id = self.WX_PAY_COMPANY_ID
let nonce_str = self.getRandom_32()
let trade_type = "APP"
let body = "眾菜-訂單號\(orderBean!.id!)"
let notify_url = self.WX_PAY_NOTIFY_URL
//商戶支付的訂單號由商戶自定義生成,微信支付要求商戶訂單號保持唯一性(建議根據當前系統時間加隨機序列來生成訂單號)。重新發起一筆支付要使用原訂單號,避免重復支付;已支付過或已調用關單、撤銷(請見后文的API列表)的訂單號不能重新發起支付。
var out_trade_no = String(orderBean!.id!)
out_trade_no = out_trade_no.stringByAppendingString("_")
out_trade_no = out_trade_no.stringByAppendingString(String(Int(NSDate().timeIntervalSince1970)))
let spbill_create_ip = "127.0.0.1"
// “*100”,因為微信支付的下單金額 以分為單位!
let total_fee0 = Int((orderBean?.placedPrice)!*100)
let total_fee = String(total_fee0)
// print(total_fee0, total_fee)
//預付單參數訂單設置
var packageParams = NSMutableDictionary()
packageParams["appid"] = appid //開放平臺appid
packageParams["mch_id"] = mch_id //商戶號
// packageParams["device_info"] = //支付設備號或門店號
packageParams["nonce_str"] = nonce_str //隨機串
packageParams["trade_type"] = trade_type //支付類型,固定為APP
packageParams["body"] = body //訂單描述,展示給用戶
packageParams["notify_url"] = notify_url //支付結果異步通知
packageParams["out_trade_no"] = out_trade_no //商戶訂單號
packageParams["spbill_create_ip"] = spbill_create_ip //發器支付的機器ip
packageParams["total_fee"] = total_fee //訂單金額
//獲取prepayId (預支付會話標識)
let prePayid:String? = self.sendPrepay(packageParams)
if prePayid == nil {
debugInfo.appendString("獲取prepayid失敗!")
return nil
}
//獲取到prepayid后進行二次簽名
//網上有人說:第二次簽名時的nonce_str需要是第一次的nonce_str。 但我試了下,好像不需要啊
let package = "Sign=WXPay"
//第二次簽名參數列表
//這里有個大坑!sign簽名時的key,一定要和文檔上對應的key一樣,如appid,noncestr;千萬不能寫成發請求的那種req.appId。
let signParams = NSMutableDictionary()
signParams["appid"] = appid
signParams["partnerid"] = mch_id
signParams["noncestr"] = getRandom_32()
signParams["package"] = package
let timeStamp = Int(NSDate().timeIntervalSince1970)
signParams["timestamp"] = String(timeStamp)
signParams["prepayid"] = prePayid
//生成簽名
let sign = self.createMd5Sign(signParams)
//添加簽名
signParams["sign"] = sign
debugInfo.appendFormat("第二步簽名成功,sign=%@", sign)
return signParams
}
//調用支付接口, 喚起微信支付界面
func WXPay() {
if WXApi.isWXAppInstalled() {
let dict = self.getPrepayOrder()
if dict == nil {
//錯誤提示
let debug = debugInfo
print("WXPay failed...")
print(debugInfo)
return
}
let timeStamp = Int(dict!["timestamp"] as! String)
let req = PayReq()
req.partnerId = dict!["partnerid"] as! String
req.prepayId = dict!["prepayid"] as! String
req.nonceStr = dict!["noncestr"] as! String
req.timeStamp = UInt32(timeStamp!)
req.package = dict!["package"] as! String
req.sign = dict!["sign"] as! String
print(dict!)
WXApi.sendReq(req)
} else {
print("請安裝微信")
}
}
//隨機生成32位的字母加數字混合的字符串
func getRandom_32()->String {
var str = String()
for var i = 0; i < 32; i++ {
let number = arc4random() % 36
if number < 10 {
let figure = arc4random() % 10;
str = str.stringByAppendingFormat("%d", figure)
} else {
let figure = (arc4random() % 26) + 97
let char = Character(UnicodeScalar(figure))
str = str.stringByAppendingString(String(char))
}
}
print(str)
return str
}
}'
接下來說一說我遇到的那些坑:
1.除了WXApi.h,WXApiObject.h,libWeChatSDK等之外,還要導入WXUtil.h,WXUtil.m(用于簽名md5),ApiXml.h,ApiXml.m(用于Xml解析),否則寫代碼時找不到這兩個類。
2.遵守WXApiDelegate,才能調用onResp方法。
3.配置完URL Schemes后,需要在plist里加上以下兩個屬性,Allow Arbitrary Loads傳輸協議什么鬼的(我也不懂);LSApplicationQueriesSchemes添加weixin:因為蘋果公司iOS 9系統策略更新,限制了http協議的訪問,此外應用需要在“Info.plist”中將要使用的URL Schemes列為白名單,才可正常檢查其他應用是否安裝。
4.微信的price單位是分,所以total_fee要注意*100。
5.所有的參數和參數類型都一定不要寫錯!appid一定不能錯。
6.除了最后發送payReq.timeStamp(時間戳)是Int類型,其他地方的參數字典,不管用于簽名還是xml解析 應該都是String類型吧(total_fee我也是寫的String)
7.進行sign簽名的時候:對簽名的key應該像文檔中定義的這樣寫如:appid,mch_id...(注意大小寫),而不是payReq中appId,mch_Id這樣。否則,我出現的錯誤是:喚起了微信,界面卻是空白只有一個確定按鈕,點擊就返回到原app中,支付失敗,反復檢查參數也都是正確的,找了大半天,這也是我喚起支付界面的最后一個bug,所以說,這對我是個大坑!(如果跳出空白的確定頁面,也許是參數錯了)
8.網上還有一些其他的坑,也有很多有效的解決辦法,多上網找找,然后對著代碼仔細找找。像友盟分享已經導入了微信的SDK,微信有沖突,這在b篇iOS-關于微信支付-IOS-第七城市末尾有寫到。