├── log
├── access_token.sample.json
└── jsapi_ticket.sample.json
├── .gitignore
├── lib
├── SDKRuntimeException.php
├── Log.php
├── WxpayServerPub.php
├── WxpayClientPub.php
├── UnifiedOrderPub.php
├── JsSdk.php
└── CommonUtilPub.php
├── LICENSE
├── conf
└── WxPay.pub.config.sample.php
├── callback
└── notifyUrl.php
├── README.md
└── index.php
/log/access_token.sample.json:
--------------------------------------------------------------------------------
1 | {"access_token":"","expire_time":0}
2 |
--------------------------------------------------------------------------------
/log/jsapi_ticket.sample.json:
--------------------------------------------------------------------------------
1 | {"jsapi_ticket":"","expire_time":0}
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.swp
3 | master.sh
4 | conf/WxPay.pub.config.php
5 | log/*
6 |
--------------------------------------------------------------------------------
/lib/SDKRuntimeException.php:
--------------------------------------------------------------------------------
1 | getMessage();
7 | }
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/lib/Log.php:
--------------------------------------------------------------------------------
1 | data = $this->xmlToArray($xml);
17 | }
18 |
19 | function checkSign()
20 | {
21 | $tmpData = $this->data;
22 | unset($tmpData['sign']);
23 | $sign = $this->getSign($tmpData);//本地签名
24 | if ($this->data['sign'] == $sign) {
25 | return TRUE;
26 | }
27 | return FALSE;
28 | }
29 |
30 | /**
31 | * 获取微信的请求数据
32 | */
33 | function getData()
34 | {
35 | return $this->data;
36 | }
37 |
38 | /**
39 | * 设置返回微信的xml数据
40 | */
41 | function setReturnParameter($parameter, $parameterValue)
42 | {
43 | $this->returnParameters[$this->trimString($parameter)] = $this->trimString($parameterValue);
44 | }
45 |
46 | /**
47 | * 生成接口参数xml
48 | */
49 | function createXml()
50 | {
51 | return $this->arrayToXml($this->returnParameters);
52 | }
53 |
54 | /**
55 | * 将xml数据返回微信
56 | */
57 | function returnXml()
58 | {
59 | $returnXml = $this->createXml();
60 | return $returnXml;
61 | }
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/conf/WxPay.pub.config.sample.php:
--------------------------------------------------------------------------------
1 |
38 |
39 |
--------------------------------------------------------------------------------
/lib/WxpayClientPub.php:
--------------------------------------------------------------------------------
1 | parameters[$this->trimString($parameter)] = $this->trimString($parameterValue);
20 | }
21 |
22 | /**
23 | * 作用:设置标配的请求参数,生成签名,生成接口参数xml
24 | */
25 | function createXml()
26 | {
27 | $this->parameters["appid"] = WxPayConfPub::APPID;//公众账号ID
28 | $this->parameters["mch_id"] = WxPayConfPub::MCHID;//商户号
29 | $this->parameters["nonce_str"] = $this->createNoncestr();//随机字符串
30 | $this->parameters["sign"] = $this->getSign($this->parameters);//签名
31 | return $this->arrayToXml($this->parameters);
32 | }
33 |
34 | /**
35 | * 作用:post请求xml
36 | */
37 | function postXml()
38 | {
39 | $xml = $this->createXml();
40 | $this->response = $this->postXmlCurl($xml,$this->url,$this->curl_timeout);
41 | return $this->response;
42 | }
43 |
44 | /**
45 | * 作用:使用证书post请求xml
46 | */
47 | function postXmlSSL()
48 | {
49 | $xml = $this->createXml();
50 | $this->response = $this->postXmlSSLCurl($xml,$this->url,$this->curl_timeout);
51 | return $this->response;
52 | }
53 |
54 | /**
55 | * 作用:获取结果,默认不使用证书
56 | */
57 | function getResult()
58 | {
59 | $this->postXml();
60 | $this->result = $this->xmlToArray($this->response);
61 | return $this->result;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/callback/notifyUrl.php:
--------------------------------------------------------------------------------
1 | saveData($xml);
22 |
23 | //验证签名,并回应微信。
24 | //对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,
25 | //微信会通过一定的策略(如30分钟共8次)定期重新发起通知,
26 | //尽可能提高通知的成功率,但微信不保证通知最终能成功。
27 | if($notify->checkSign() == FALSE){
28 | $notify->setReturnParameter("return_code","FAIL");//返回状态码
29 | $notify->setReturnParameter("return_msg","签名失败");//返回信息
30 | }else{
31 | $notify->setReturnParameter("return_code","SUCCESS");//设置返回码
32 | }
33 | $returnXml = $notify->returnXml();
34 | echo $returnXml;
35 |
36 | //==商户根据实际情况设置相应的处理流程,此处仅作举例=======
37 |
38 | //以log文件形式记录回调信息
39 | $log = new Log();
40 | $logName="../log/notify_url.log";//log文件路径
41 | $log->log_result($logName,"【接收到的notify通知】:\n".$xml."\n");
42 |
43 | if($notify->checkSign() == TRUE)
44 | {
45 | if ($notify->data["return_code"] == "FAIL") {
46 | //此处应该更新一下订单状态,商户自行增删操作
47 | $log->log_result($logName,"【通信出错】:\n".$xml."\n");
48 | }
49 | elseif($notify->data["result_code"] == "FAIL"){
50 | //此处应该更新一下订单状态,商户自行增删操作
51 | $log->log_result($logName,"【业务出错】:\n".$xml."\n");
52 | }
53 | else{
54 | //此处应该更新一下订单状态,商户自行增删操作
55 | $log->log_result($logName,"【支付成功】:\n".$xml."\n");
56 | }
57 |
58 | //商户自行增加处理流程,
59 | //例如:更新订单状态
60 | //例如:数据库操作
61 | //例如:推送支付完成信息
62 | }
63 | ?>
64 |
65 |
--------------------------------------------------------------------------------
/lib/UnifiedOrderPub.php:
--------------------------------------------------------------------------------
1 | url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
12 | //设置curl超时时间
13 | $this->curl_timeout = WxPayConfPub::CURL_TIMEOUT;
14 | }
15 |
16 | /**
17 | * 生成接口参数xml
18 | */
19 | function createXml()
20 | {
21 | try
22 | {
23 | //检测必填参数
24 | if($this->parameters["out_trade_no"] == null)
25 | {
26 | throw new SDKRuntimeException("缺少统一支付接口必填参数out_trade_no!"."
");
27 | }elseif($this->parameters["body"] == null){
28 | throw new SDKRuntimeException("缺少统一支付接口必填参数body!"."
");
29 | }elseif ($this->parameters["total_fee"] == null ) {
30 | throw new SDKRuntimeException("缺少统一支付接口必填参数total_fee!"."
");
31 | }elseif ($this->parameters["notify_url"] == null) {
32 | throw new SDKRuntimeException("缺少统一支付接口必填参数notify_url!"."
");
33 | }elseif ($this->parameters["trade_type"] == null) {
34 | throw new SDKRuntimeException("缺少统一支付接口必填参数trade_type!"."
");
35 | }elseif ($this->parameters["trade_type"] == "JSAPI" &&
36 | $this->parameters["openid"] == NULL){
37 | throw new SDKRuntimeException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!"."
");
38 | }
39 | $this->parameters["appid"] = WxPayConfPub::APPID;//公众账号ID
40 | $this->parameters["mch_id"] = WxPayConfPub::MCHID;//商户号
41 | $this->parameters["spbill_create_ip"] = $_SERVER['REMOTE_ADDR'];//终端ip
42 | if (!isset($this->parameters["nonce_str"])) {
43 | $this->parameters["nonce_str"] = $this->createNoncestr();//随机字符串
44 | }
45 | $this->parameters["sign"] = $this->getSign($this->parameters);//签名
46 | return $this->arrayToXml($this->parameters);
47 | }catch (SDKRuntimeException $e)
48 | {
49 | die($e->errorMessage());
50 | }
51 | }
52 |
53 | /**
54 | * 获取prepay_id
55 | */
56 | function getPrepayId()
57 | {
58 | $this->postXml();
59 | $this->result = $this->xmlToArray($this->response);
60 | $prepay_id = $this->result["prepay_id"];
61 | return $prepay_id;
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | wxpay-php
2 | =========
3 |
4 | php项目基于微信支付JS SDK和JS API的接入开发
5 |
6 | 因为php框架繁多,而且项目开发进度比较赶,线上代码迁移出来的时间也有限,此处的代码没有经过备案域名和线上服务器测试,肯定会有很多问题,所以样例代码仅供参考
7 |
8 | ---
9 |
10 | ### 前言
11 |
12 | 因为抢红包风波,微信封杀了支付宝链接,不得不紧急加入微信支付。
13 |
14 | 微信支付的开发文档太坑,不才已被虐哭,趁现在还在坑里,记录一下留个纪念。
15 |
16 | ### 开发相关资料(排名不分先后)
17 |
18 | * [微信商户服务中心](https://mp.weixin.qq.com/paymch/readtemplate?t=mp/business/faq2_tmpl)
19 |
20 | * [商户平台开发者文档1](http://pay.weixin.qq.com/wiki/doc/api/index.html)
21 |
22 | * [商户平台开发者文档2](http://pay.weixin.qq.com/wiki/doc/api/jsapi.php)
23 |
24 | * [商户平台开发者文档3](http://pay.weixin.qq.com/wiki/doc/api/index.php?chapter=1_1)
25 |
26 | * [微信JS-SDK说明文档](http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html)
27 |
28 | * [支付开发教程(微信商户平台版)](https://mp.weixin.qq.com/paymch/readtemplate?t=mp/business/course3_tmpl)
29 |
30 | * [代公众号使用JS SDK说明](https://open.weixin.qq.com/cgi-bin/showdocument?action=doc&id=open1421823488&t=0.37369911512359977)
31 |
32 | * [JSSDK demo页面](http://demo.open.weixin.qq.com/jssdk/)
33 |
34 | * [JSSDK demo示例代码下载](http://demo.open.weixin.qq.com/jssdk/sample.zip)
35 |
36 | ### 注意点
37 |
38 | * 微信5.0后的版本才支持微信支付
39 |
40 | * 微信支付的jssdk接口只能在微信客户端里面才有效果,普通浏览器无法测试
41 |
42 | * 微信支付的相关代码必须部署在线上,域名必须备案,而且域名必须跟 <微信公众平台> 里相关设置里的域名一致
43 |
44 | * 微信大小写非常敏感,timestamp和timeStamp以及appId和appid不要弄错,要在对的接口使用对的大小写,里面是混着用的
45 |
46 | * 商户号和微信商户号要区分出来,也就是MCHID,开通微信支付后有一封邮件,里面会有相关的信息,登陆商户后台也能找到
47 |
48 | * 只有服务号才能接入微信支付,不同的服务号之前可以跨号支付,订阅号里面不能调用微信支付,会报“不允许跨号支付”的错误
49 |
50 | ### 关于签名
51 |
52 | 我开发的时候遇到三次签名生成:
53 |
54 | 1. wx.config 里面有一次 signature 的签名生成,调用的是官方sdk的方法,它用的是 SHA1 加密,生成之后的签名没有转大写
55 |
56 | 2. 获取prepay\_id的时候,中间会生成一次签名,调用的是官方sdk的方法,它用的是 MD5 加密,生成之后的签名统一转成大写
57 |
58 | 3. wx.chooseWXPay 里面有最后一次 paySign 的签名生成,我调用了步骤2里面的那个接口,最后测试成功了
59 |
60 | ### 分享代码
61 |
62 | 因为目前用的项目的框架是比较奇葩的,我之前已经把官方的代码临时整合并修改成适合项目的代码,不能直接用了,后面的日子将逐步等工作闲暇的时候,整理出直接能用的来分享给还没入坑有需要的童鞋们。
63 |
64 | ---
65 |
66 | ## 代码相关
67 |
68 | ### 开发环境
69 |
70 | 本地相关开发环境如下:
71 |
72 | * 编辑器: MacVim
73 |
74 | * php: 5.5.13,接入微信支付的相关扩展都装了,主要应该是curl扩展
75 |
76 | * nginx: 1.4.2
77 |
78 | * 操作系统: Yosemite 10.0.2
79 |
80 | ### 创建相关文件
81 |
82 | WxPay.pub.config.sample.php => WxPay.pub.config.php // 配置文件
83 | access_token.sample.json => access_token.json // 临时存储access_token
84 | jsapi_ticket.sample.json => jsapi_ticket.json // 临时存储jsapi_ticket
85 |
86 | ### 代码文件结构
87 |
88 | .
89 | ├── LICENSE
90 | ├── README.md -------------------------说明文档
91 | ├── callback --------------------------回调
92 | │ └── notifyUrl.php -----------------回调接口文件
93 | ├── conf ------------------------------配置
94 | │ ├── WxPay.pub.config.php ----------配置文件
95 | │ └── WxPay.pub.config.sample.php ---配置文件样本
96 | ├── index.php -------------------------主入口文件
97 | ├── lib -------------------------------类库
98 | │ ├── CommonUtilPub.php -------------所有接口的基类
99 | │ ├── JsSdk.php ---------------------微信支付新推出的js sdk
100 | │ ├── Log.php -----------------------日志类库
101 | │ ├── SDKRuntimeException.php -------异常类库
102 | │ ├── UnifiedOrderPub.php -----------统一支付接口类
103 | │ ├── WxpayClientPub.php ------------请求型接口的基类
104 | │ └── WxpayServerPub.php ------------响应型接口的基类
105 | ├── log -------------------------------日志
106 | │ ├── access_token.json -------------access_token临时存储文件
107 | │ ├── access_token.sample.json ------access_token临时存储文件样本
108 | │ ├── jsapi_ticket.json -------------jsapi_ticket临时存储文件
109 | │ ├── jsapi_ticket.sample.json ------jsapi_ticket临时存储文件样本
110 | │ └── notify_url.log ----------------回调接口日志文件
111 | └── master.sh -------------------------git提交shell懒人脚本
112 |
113 | 4 directories, 19 files
114 |
--------------------------------------------------------------------------------
/lib/JsSdk.php:
--------------------------------------------------------------------------------
1 | appId = $appId;
13 | $this->appSecret = $appSecret;
14 | }
15 |
16 | public function getSignPackage() {
17 | $jsapiTicket = $this->getJsApiTicket();
18 |
19 | // 注意 URL 一定要动态获取,不能 hardcode.
20 | $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
21 | $url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
22 |
23 | $timestamp = time();
24 | $nonceStr = $this->createNonceStr();
25 |
26 | // 这里参数的顺序要按照 key 值 ASCII 码升序排序
27 | $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
28 |
29 | $signature = sha1($string);
30 |
31 | $signPackage = array(
32 | "appId" => $this->appId,
33 | "nonceStr" => $nonceStr,
34 | "timestamp" => $timestamp,
35 | "url" => $url,
36 | "signature" => $signature,
37 | "rawString" => $string
38 | );
39 | return $signPackage;
40 | }
41 |
42 | private function createNonceStr($length = 16) {
43 | $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
44 | $str = "";
45 | for ($i = 0; $i < $length; $i++) {
46 | $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
47 | }
48 | return $str;
49 | }
50 |
51 | private function getJsApiTicket() {
52 | // jsapi_ticket 应该全局存储与更新,以下代码以写入到文件中做示例
53 | $data = json_decode(file_get_contents("log/jsapi_ticket.json"));
54 | if ($data->expire_time < time()) {
55 | $accessToken = $this->getAccessToken();
56 | // 如果是企业号用以下 URL 获取 ticket
57 | // $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=$accessToken";
58 | $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
59 | $res = json_decode($this->httpGet($url));
60 | $ticket = $res->ticket;
61 | if ($ticket) {
62 | $data->expire_time = time() + 7000;
63 | $data->jsapi_ticket = $ticket;
64 | $fp = fopen("log/jsapi_ticket.json", "w");
65 | fwrite($fp, json_encode($data));
66 | fclose($fp);
67 | }
68 | } else {
69 | $ticket = $data->jsapi_ticket;
70 | }
71 |
72 | return $ticket;
73 | }
74 |
75 | private function getAccessToken() {
76 | // access_token 应该全局存储与更新,以下代码以写入到文件中做示例
77 | $data = json_decode(file_get_contents("log/access_token.json"));
78 | if ($data->expire_time < time()) {
79 | // 如果是企业号用以下URL获取access_token
80 | // $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$this->appId&corpsecret=$this->appSecret";
81 | $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
82 | $res = json_decode($this->httpGet($url));
83 | $access_token = $res->access_token;
84 | if ($access_token) {
85 | $data->expire_time = time() + 7000;
86 | $data->access_token = $access_token;
87 | $fp = fopen("log/access_token.json", "w");
88 | fwrite($fp, json_encode($data));
89 | fclose($fp);
90 | }
91 | } else {
92 | $access_token = $data->access_token;
93 | }
94 | return $access_token;
95 | }
96 |
97 | private function httpGet($url) {
98 | $curl = curl_init();
99 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
100 | curl_setopt($curl, CURLOPT_TIMEOUT, 500);
101 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
102 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
103 | curl_setopt($curl, CURLOPT_URL, $url);
104 |
105 | $res = curl_exec($curl);
106 | curl_close($curl);
107 |
108 | return $res;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | GetSignPackage();
18 | $timeStamp = $signPackage['timestamp'];
19 | $nonceStr = $signPackage['nonceStr'];
20 |
21 |
22 | // 获取prepay_id
23 | // 具体参数设置可以看文档http://pay.weixin.qq.com/wiki/doc/api/index.php?chapter=9_1
24 | $unifiedOrder = new UnifiedOrderPub();
25 | $unifiedOrder->setParameter("openid",$openId);//用户openId
26 | $unifiedOrder->setParameter("body", "贡献一分钱");//商品描述,文档里写着不能超过32个字符,否则会报错,经过实际测试,临界点大概在128左右,稳妥点最好按照文档,不要超过32个字符
27 | $unifiedOrder->setParameter("out_trade_no", "123456");//商户订单号
28 | $unifiedOrder->setParameter("total_fee", "1");//总金额,单位为分
29 | $unifiedOrder->setParameter("notify_url",WxPayConfPub::NOTIFY_URL);//通知地址
30 | $unifiedOrder->setParameter("trade_type","JSAPI");//交易类型
31 | $unifiedOrder->setParameter("nonce_str", $nonceStr);//随机字符串
32 | //非必填参数,商户可根据实际情况选填
33 | //$unifiedOrder->setParameter("sub_mch_id","XXXX");//子商户号
34 | //$unifiedOrder->setParameter("device_info","XXXX");//设备号
35 | //$unifiedOrder->setParameter("attach","XXXX");//附加数据
36 | //$unifiedOrder->setParameter("time_start","XXXX");//交易起始时间
37 | //$unifiedOrder->setParameter("time_expire","XXXX");//交易结束时间
38 | //$unifiedOrder->setParameter("goods_tag","XXXX");//商品标记
39 | //$unifiedOrder->setParameter("openid","XXXX");//用户标识
40 | //$unifiedOrder->setParameter("product_id","XXXX");//商品ID
41 | $prepayId = $unifiedOrder->getPrepayId();
42 |
43 | // 计算paySign
44 | $payPackage = [
45 | "appId" => WxPayConfPub::APPID,
46 | "nonceStr" => $nonceStr,
47 | "package" => "prepay_id=" . $prepayId,
48 | "signType" => "MD5",
49 | "timeStamp" => $timeStamp
50 | ];
51 | $paySign = $unifiedOrder->getSign($payPackage);
52 | $payPackage['paySign'] = $paySign;
53 |
54 | ?>
55 |
56 |
57 |
58 |
');
207 | echo $wording."";
208 | var_dump($err);
209 | print_r('');
210 | }
211 | }
212 |
--------------------------------------------------------------------------------