第一篇,想想就有点小激动呢~,就拿最近做了4天的微信支付开始吧。
作为一个iOS开发的搬砖农民工,感受到微信支付的小坑实在太多。所以,首先得感谢大神们在网上分享微信支付的各种经验,我才能照葫芦画瓢做出来。
第一步当然是看微信支付官网文档,其中有一张业务流程图,结合官网demo把支付的流程看懂:
【微信支付】公众号支付开发者文档
我下载的官方demo中,调起微信接口payReq的参数都是在服务器端生成(微信本身是鼓励客户APP把签名算法放到服务器上面,这样信息就不容易被破解),而我需要在本地完成这些参数的设定,客户端进行2次签名验证,主要是为了获取到prePayId(统一下单号)。主要参照了以下文章:
a.iOS微信支付开发
b.iOS-关于微信支付-IOS-第七城市
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("")
for key in keys {
reqPars.appendFormat("<%@>%@%@>", key, String(packageParams[key]!), key)
}
reqPars.appendFormat("%@ ", 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-第七城市末尾有写到。