PHP微信支付开发(1)-下单与支付

一、概述

本系列博客将讨论基于微信支付的项目开发中,涉及到的下单与支付、退款、以及订单查询的后端代码实现。在本系列博客中,将以代码片段作为示例,来讨论ThinkPHP 后端接口实现的过程。

在本系列的接口示例中,返回的状态码标识如下:

0: 业务成功
-1: 业务失败

开发环境如果:

  • ThinkPHP 6 或者 ThinkPHP 5 / 5.1
  • PHP 7 运行环境

本文是第一篇,我们先讨论下单与支付。

二、定义数据库

在项目中,我们通常需要在业务数据库中生成订单数据,同时需要在微信中台生成对应的订单。并且业务订单状态需要与微信订单状态一致。

-- 建立表结构
CREATE TABLE `orders`
(
    `id`              INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '表ID',
    `fee`             DECIMAL(15, 2)   NOT NULL DEFAULT 0.00 COMMENT '订单费用',
    `status`          TINYINT(1)       NOT NULL DEFAULT 0 COMMENT '状态:0-未支付,1-已支付,2-退款中,3-已退款',
    `time_start`      int(11) UNSIGNED          DEFAULT NULL COMMENT '交易起始时间',
    `time_expire`     int(11) UNSIGNED          DEFAULT NULL COMMENT '交易结束时间',
    `order_time`      int(11) UNSIGNED          DEFAULT NULL COMMENT '支付完成时间',
    `out_trade_no`    varchar(32)      NOT NULL COMMENT '商户订单号',
    `out_refund_no`   varchar(32)               DEFAULT NULL COMMENT '商户退款订单号',
    `transaction_id`  varchar(32)               DEFAULT NULL COMMENT '微信支付流水号',
    `refund_desc`     varchar(64)               DEFAULT NULL COMMENT '退款原因',
    `refund_fee`      DECIMAL(15, 2)   NOT NULL DEFAULT 0.00 COMMENT '退款金额',
    `refund_time`     int(11) UNSIGNED          DEFAULT NULL COMMENT '退款完成时间',
    `create_time`     INT(11) UNSIGNED          DEFAULT NULL COMMENT '创建时间',
    `update_time`     INT(11) UNSIGNED          DEFAULT NULL COMMENT '更新时间',
    `delete_time`     INT(11) UNSIGNED          DEFAULT NULL COMMENT '删除时间',
    CONSTRAINT `pk_id_orders` PRIMARY KEY (`id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 100
  DEFAULT CHARSET = utf8mb4
  COLLATE utf8mb4_unicode_ci COMMENT ='订单信息表';

在本例中我们只考虑基本数据字段,但在实际项目开发中你应当考虑实际业务需求。本例订单状态只考虑四种:未支付、已支付、退款中、已退款。数据库中,订单费用单位记录为元

三、下单:

$order = new Order();
$order['fee'] = 0.1;
$order['out_trade_no'] = date('YmdHis').mt_rand(1000, 9999);
$order['time_start'] = time();
$order['time_expire'] = strtotime('+10 minutes');
$order->save();

在上面代码中,Order类代表订单对应的模型。我们将订单费用固定设置为0.1元(实际项目中应当根据商品费用、折扣、运费等来计算),订单号我们根据时间生成,并拼接上一个随机数。要注意的是,在实际开发中,订单号要避免重复。接下来我们在common.php文件中定义一个请求微信下单接口的方法

function order_request($url, $data = null)
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    if (!empty($data)) {
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    }
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    $output = curl_exec($curl);
    curl_close($curl);
    return $output;
}

接着,定义下单的参数

$params = [
    'appid' => config('wx.app_id'), //APP ID
    'mch_id' => config('wx.mch_id'), //商户号
    'nonce_str' => md5(time()), //随机字符串
    'sign_type' => 'MD5', //加密方式
    'body' => '订单费用', //商品描述
    'attach' => '微信小程序支付', //附加信息
    'out_trade_no' => $order['out_trade_no'], //商户订单号
    'total_fee' => $order['fee'] * 100, //标价金额,单位分
    'spbill_create_ip' => request()->ip(), //终端IP
    'time_start' => date('YmdHis', $order['time_start']), //交易起始时间
    'time_expire' => date('YmdHis', $order['time_expire']), //交易结束时间
    'notify_url' => 'https://test.com/order/callback', //通知地址
    'trade_type' => 'JSAPI', //交易类型
    'openid' => $user['openid'], //用户openid
];

以上代码中,参数列表是最基本的,实际项目中你可以根据业务需求设置更多的参数。此外,我们把APP ID和商户号写在配置文件中。同事要注意的是,微信下单接口中,接收的金额单位为分。

微信订单接口只支持xml,接着我们从数组参数中构造xml字符串

$stringA = '';
// 根据键名对参数进行字典排序
ksort($params);
// 创建xml
$xml = '';
$xml .= '<xml>';
// 遍历参数
foreach ($params as $key => $value) {
    //拼装参数
    $stringA .= $key . '=' . $value . '&';
    //拼装xml
    $xml .= '<' . $key . '>' . $value . '</' . $key . '>';
}
//与商户API秘钥进行拼接
$signTmp = $stringA . 'key=' . config('wx.mch_key');
$sign = strtoupper(md5($signTmp));//签名后的32位字符

//将签名添加到请求参数中
$xml .= '<sign>' . $sign . '</sign>';
$xml .= '</xml>';

下单成功后,将返回结果结构好支付参数,返回给前端调起微信支付

$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'; //下单接口地址
$res_xml = order_request($url, $xml);//下单
$simpleXMLElement = simplexml_load_string($res_xml, 'SimpleXMLElement', LIBXML_NOCDATA);
//将返回的xml转为对象
$jsonStr = json_encode($simpleXMLElement);//将对象转为json字符串
$jsonArray = json_decode($jsonStr, true);//将json字符串转为数组
if ($jsonArray['return_code'] != 'SUCCESS') {
    return json(['code'=>-1,'msg'=>'下单失败']);
}
//构造支付参数
$data['appId'] = $jsonArray['appid'];
$data['timeStamp'] = strval(time());
$data['nonceStr'] = md5(time());
$data['package'] = 'prepay_id=' . $jsonArray['prepay_id'];
$data['signType'] = 'MD5';
ksort($data);

//构造支付签名
$data['key'] = config('wx.mch_key');//商户API秘钥
$stringTmp = '';
foreach ($data as $key => $value) {
    $stringTmp .= $key . '=' . $value . '&';
}
$stringTmp = rtrim($stringTmp, '&');
$data['paySign'] = strtoupper(md5($stringTmp));
unset($data['key']);

//将支付参数返回给前端
return json($data);

在以上代码中,我们把商户API秘钥写在配置文件中。

四、支付回调

当前端用户支付成功之后,微信会将支付结果发送一个GET请求回调到业务服务器,我们需要设置好对应的路由并匹配上回调方法,回调地址就是下单时设置的通知地址,通知内容是XML。在回调方法中,我们先获取参数。

$xml = file_get_contents('php://input');
if (!$xml) {
    exit(0);//xml数据为空,终止程序
}
//将xml解析为对象
$simpleXMLElement = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
//将对象转为数组
$jsonStr = json_encode($simpleXMLElement);
$jsonArray = json_decode($jsonStr, true);

接下来验证签名

$stringA = '';
foreach ($jsonArray as $key => $value) {
    if ($key == 'sign') {
        continue;
    }
    //拼装参数
    $stringA .= $key . '=' . $value . '&';
}
$signTmp = $stringA . 'key=' . config('wx.mch_key');//与商户API秘钥进行拼接
$sign = strtoupper(md5($signTmp));// 签名后的32位字符
if ($sign != $jsonArray['sign']) {
    exit(0);//签名错误,终止程序
}

验证签名通过之后,更新订单状态,并且将结果返回给微信服务器即可

$order = Order::where('out_trade_no',$jsonArray['out_trade_no'])->find();
if (!$order) {
    exit(0);//查询不到订单,终止程序
}
//更新订单状态
$order['status'] = 1;
$order['order_time'] = time();
$order['transaction_id'] = $jsonArray['transaction_id'];
$order->save();
// 给微信返回数据
$xml = '<xml>';
$xml .= '<return_code>' . '<![CDATA[SUCCESS]]>' . '</return_code>';
$xml .= '<return_msg>' . '<![CDATA[OK]]>' . '</return_msg>';
$xml .= '</xml>';
return response($xml, 200, [], 'xml');
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266