产品介绍 服务商分账,主要用于服务商帮助特约商户完成订单收单成功后的资金分配。
使用场景举例 1、服务商抽成 在各个行业中,服务商为特约商户提供增值服务,服务商与特约商户协商,可以从特约商户的交易流水中抽取一定的手续费。
2、员工奖励 零售、餐饮等行业中,当销售人员销售完成后,达到可奖励的条件,可以通过分账,将销售奖励分给员工。
3、管理资金到账时间 在酒店行业中,利用分账功能中的“冻结/解冻”能力,当用户预订/入住酒店时,交易资金先冻结在酒店的账户中,当用户确认消费离店后,再利用“分账”功能中的“分账完结”解冻资金到酒店的账户中。这样可以避免用户退款时商户账户资金不足的情况。
4、分润给合作伙伴 在与他方合作的情况下,可以用“分账”功能,将交易资金分给合作伙伴,例如物流合作商。
下面就让我们一起写一套微信小程序支付和服务商分账
首先创建逻辑方法
public function member_pay()
{
$type = input('type');
$id = input('id');
if (!$type) {
return json(['code' => 2, 'msg' => '参数错误']);
}
$member = Member::where(['id' => $this->uid])->find();
if ($type == 2) {
$code = input('code');
if (!$code) {
return json(['code' => 2, 'msg' => '参数错误']);
}
$list = MemberCode::where(['code' => $code])->find();
if (!$list) {
return json(['code' => 2, 'msg' => '激活码错误']);
}
if ($list['status'] != 1) {
return json(['code' => 2, 'msg' => '激活码已使用']);
}
$memberM = MemberMember::where(['uid'=>$this->uid])->find();
if (!$memberM){
$data = [
'uid' => $this->uid,
'start_time' => date('Y-m-d H:i:s'),
'end_time' => date('Y-m-d H:i:s', strtotime("+" . $list['time'] . "month")),
'l_id' => $list['id'],
'create_time' => date('Y-m-d H:i:s')
];
$res = MemberMember::insert($data);
}else{
$member_level = $list['time'];
if($memberM['end_time']<date('Y-m-d H:i:s')){
$memberData = [
'end_time'=>date("Y-m-d H:i:s", strtotime("+$member_level months")),
'l_id'=>$list['id']
];
}else{
$memberData = [
'end_time'=>date("Y-m-d",strtotime("+$member_level month",strtotime($memberM['end_time']))),
'l_id'=>$list['id']
];
}
MemberMember::where(['uid'=>$this->uid])->update($memberData);
}
if ($res) {
return json(['code' => 1, 'msg' => '激活成功']);
} else {
return json(['code' => 2, 'msg' => '激活失败']);
}
} else {
$list = MemberLevel::where(['id' => $id])->find();
$order_number = rand(1111, 9999) . date('Ymd') . $this->uid . $id;
$obj = new Wxpay($member['openid'], $order_number, '用户开通会员', $list['money'] * 100, '', '');
$pay = $obj->pay();
$order = VideoOrder::where(['vid'=>$id,'type'=>1,'uid'=>$this->uid,'status'=>2])->find();
if ($order){
$data = [
'order_num'=>$order_number,
'create_time'=>date('Y-m-d H:i:s'),
'price'=>$list['money'],
];
VideoOrder::where(['id'=>$order['id']])->update($data);
}else{
$data = [
'uid'=>$this->uid,
'vid'=>$id,
'order_num'=>$order_number,
'create_time'=>date('Y-m-d H:i:s'),
'price'=>$list['money'],
'status'=>2,
'type'=>1
];
VideoOrder::insert($data);
}
$info['code'] = 1;
$info['data'] = $pay;
return json(['code' => 1, 'data' => $info]);
}
}
新建微信统一下单类
<?php
namespace app\api\home;
class Wxpay
{
protected $appid;
protected $mch_id;
protected $key;
protected $openid;
protected $out_trade_no;
protected $body;
protected $total_fee;
function __construct($openid, $out_trade_no, $body, $total_fee, $profit_sharing = 'N', $shops = [])
{
$this->appid = config('appid');
$this->openid = $openid;
$this->mch_id = config('mch_id');
$this->key = config('pay_key');
$this->out_trade_no = $out_trade_no;
$this->body = $body;
$this->total_fee = $total_fee;
$this->profit_sharing = $profit_sharing;
$this->shops = $shops;
}
public function pay()
{
$return = $this->weixinapp();
return $return;
}
private function unifiedorder()
{
$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
$shops = $this->shops;
if ($this->profit_sharing == 'Y') {
$this->key = $shops['sub_key'];
$parameters = array(
'appid' => 'wx04060xxxxxx',
'mch_id' => '1604xxxxxx',
'sub_appid' => $shops['appid'],
'sub_mch_id' => $shops['sub_mch_id'],
'nonce_str' => $this->createNoncestr(),
'body' => $this->body,
'out_trade_no' => $this->out_trade_no,
'total_fee' => $this->total_fee,
'spbill_create_ip' => $_SERVER['REMOTE_ADDR'],
'notify_url' => '你的url/api/index/pay_success',
'openid' => $this->openid,
'trade_type' => 'JSAPI',
'profit_sharing' => 'Y'
);
}else {
$parameters = array(
'appid' => $this->appid,
'mch_id' => $this->mch_id,
'nonce_str' => $this->createNoncestr(),
'body' => $this->body,
'out_trade_no' => $this->out_trade_no,
'total_fee' => $this->total_fee,
'spbill_create_ip' => $_SERVER['REMOTE_ADDR'],
'notify_url' => 'https://shoptemplate.rztnhy.com/api/index/pay_success',
'openid' => $this->openid,
'trade_type' => 'JSAPI'
);
}
$parameters['sign'] = $this->getSign($parameters);
$xmlData = $this->arrayToXml($parameters);
$return = $this->xmlToArray($this->postXmlCurl($xmlData, $url, 60));
return $return;
}
public function refund($transaction_id, $out_refund_no, $total_fee, $refund_fee)
{
$refundorder = array(
'appid' => $this->appid,
'mch_id' => $this->mch_id,
'nonce_str' => $this->createNoncestr(),
'transaction_id' => $transaction_id,
'out_refund_no' => $out_refund_no,
'total_fee' => $total_fee * 100,
'refund_fee' => $refund_fee * 100
);
$refundorder['sign'] = $this->getSign($refundorder);
$xmldata = $this->arrayToXml($refundorder);
$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
$res = $this->postXmlCurl($xmldata, $url, 30, true);
if (!$res) {
return array('status' => 0, 'msg' => "Can't connect the server");
}
$content = $this->xmlToArray($res);
if (strval($content['result_code']) == FAIL') {
return array('status' => 0, 'msg' => strval($content['err_code']) . ':' . strval($content['err_code_des']));
}
if (strval($content['return_code']) == 'FAIL') {
return array('status' => 0, 'msg' => strval($content['return_msg']));
}
return array('status' => 1, 'transaction_id' => strval($content['transaction_id']));
}
private static function postXmlCurl($xml, $url, $second = 60, $useCert = false)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
if ($useCert == true) {
$sslCertPath = "./static/cert/apiclient_cert.pem";
$sslKeyPath = "./static/cert/apiclient_key.pem";
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERT, $sslCertPath);
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLKEY, $sslKeyPath);
}
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($ch, CURLOPT_TIMEOUT, 40);
set_time_limit(0);
$data = curl_exec($ch);
if ($data) {
curl_close($ch);
return $data;
} else {
$error = curl_errno($ch);
curl_close($ch);
throw new WxPayException("curl出错,错误码:$error");
}
}
private function arrayToXml($arr)
{
$xml = "<root>";
foreach ($arr as $key => $val) {
if (is_array($val)) {
$xml .= "<" . $key . ">" . $this->arrayToXml($val) . "</" . $key . ">";
} else {
$xml .= "<" . $key . ">" . $val . "</" . $key . ">";
}
}
$xml .= "</root>";
return $xml;
}
public function xmlToArray($xml)
{
libxml_disable_entity_loader(true);
$xmlstring = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
$val = json_decode(json_encode($xmlstring), true);
return $val;
}
private function weixinapp()
{
$unifiedorder = $this->unifiedorder();
$parameters = array(
'appId' => $this->appid,
'timeStamp' => '' . time() . '',
'nonceStr' => $this->createNoncestr(),
'package' => 'prepay_id=' . $unifiedorder['prepay_id'],
'signType' => 'MD5'
);
$parameters['paySign'] = $this->getSign($parameters);
return $parameters;
}
private function createNoncestr($length = 32)
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
private function getSign($Obj)
{
foreach ($Obj as $k => $v) {
$Parameters[$k] = $v;
}
ksort($Parameters);
$String = $this->formatBizQueryParaMap($Parameters, false);
$String = $String . "&key=" . $this->key;
$String = md5($String);
$result_ = strtoupper($String);
return $result_;
}
private function formatBizQueryParaMap($paraMap, $urlencode)
{
$buff = "";
ksort($paraMap);
foreach ($paraMap as $k => $v) {
if ($urlencode) {
$v = urlencode($v);
}
$buff .= $k . "=" . $v . "&";
}
$reqPar = '';
if (strlen($buff) > 0) {
$reqPar = substr($buff, 0, strlen($buff) - 1);
}
return $reqPar;
}
}
支付成功后在回调方法中进行分账
每个项目的回调逻辑不一样。在这我就不写回调逻辑了 一般项目客户在确定收货的时候才触发分账 所以直接在确定收货的接口里使用分账方法。
public function change_state(){
$this->profitsharing($shops['appid'],$shops['sub_mch_id'],$order['transaction_id']);
}
public function profitsharing($sub_appid,$sub_mch_id,$transaction_id,$total_price){
$tmp = array(
'type'=>'MERCHANT_ID',
'account'=>'16046XXXXX',
'amount'=>$total_price*30,
'description'=>'测试使用',
);
$nonce_str = '3a93c91ff4fd56a610ba9c47146ed8ec';
$receivers = json_encode($tmp,JSON_UNESCAPED_UNICODE);
$postArr = array(
'appid' => 'wx5acb4562dxxxxx',
'mch_id' => '16046xxxxx',
'sub_appid'=>$sub_appid,
'sub_mch_id' => $sub_mch_id,
'nonce_str'=>$nonce_str,
'transaction_id'=>$transaction_id,
'out_order_no'=>rand(1,100).rand(1000,9999),
'receivers'=>$receivers
);
$sign = $this->getSign($postArr, 'HMAC-SHA256','密钥');
$postArr['sign'] = $sign;
$url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharing';
$postXML = $this->toXml($postArr);
$curl_res = $this->post($url,$postXML);
return $curl_res;
}
public function getSign(array $param, $signType = 'MD5', $md5Key)
{
$values = $this->paraFilter($param);
$values = $this->arraySort($values);
$signStr = $this->createLinkstring($values);
$signStr .= '&key=' . $md5Key;
switch ($signType)
{
case 'MD5':
$sign = md5($signStr);
break;
case 'HMAC-SHA256':
$sign = hash_hmac('sha256', $signStr, $md5Key);
break;
default:
$sign = '';
}
return strtoupper($sign);
}
public function paraFilter($para)
{
$paraFilter = array();
while (list($key, $val) = each($para))
{
if ($val == "") {
continue;
} else {
if (! is_array($para[$key])) {
$para[$key] = is_bool($para[$key]) ? $para[$key] : trim($para[$key]);
}
$paraFilter[$key] = $para[$key];
}
}
return $paraFilter;
}
public function arraySort(array $param)
{
ksort($param);
reset($param);
return $param;
}
public function createLinkString($para)
{
if (! is_array($para)) {
throw new \Exception('必须传入数组参数');
}
reset($para);
$arg = "";
while (list($key, $val) = each($para)) {
if (is_array($val)) {
continue;
}
$arg.=$key."=".urldecode($val)."&";
}
$arg = substr($arg, 0, strlen($arg) - 2);
if (get_magic_quotes_gpc()) {
$arg = stripslashes($arg);
}
return $arg;
}
public function toXml($values)
{
if (!is_array($values) || count($values) <= 0) {
return false;
}
$xml = "<xml>";
foreach ($values as $key => $val) {
if (is_numeric($val)) {
$xml.="<".$key.">".$val."</".$key.">";
} else {
$xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
}
}
$xml.="</xml>";
return $xml;
}
public function setOption($option) {
$this->option = $option + $this->option;
return $this;
}
public function post($url, $data, $timeoutMs = 3000) {
return $this->request( $data, $url,$timeoutMs);
}
public function request($xml, $url, $useCert = false, $second = 30)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,2);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')) {
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
}
if($useCert == true){
$cert_dir = '服务商证书目录';
if(
!file_exists($cert_dir."apiclient_cert.pem") ||
!file_exists($cert_dir."apiclient_key.pem")
){
return "2";
}
curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLCERT, $cert_dir."apiclient_cert.pem");
curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLKEY, $cert_dir."apiclient_key.pem");
}
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
$data = curl_exec($ch);
var_dump($data);
die;
if($data){
curl_close($ch);
return $data;
} else {
$error = curl_errno($ch);
curl_close($ch);
return "";
}
}
注意事项
1,需要在微信服务商后台绑定特约商户 和绑定小程序 特约商户申请可以在小程序内部实现。可以自己写接口 后期咱们在写 2,服务商有单独的appid和secret和小程序的appid和secret不一致 3,特约商户有自己的商户号和密钥 与服务商的商户号和密钥不一致 4,在统一下单的时候只有参数’profit_sharing’=>'Y’的时候才会是服务商模式支付。否则就是普通小程序支付 5,服务商需要设置分账比例最低百分之30 6,上述代码已上线,能够正常使用。如果在使用过程中报错,或者返回值错误请检查服务商appid、secret、特约商户商户号和密钥、小程序appid、secret等参数是否正确没有用错 7,服务商分账时使用的证书为服务商证书和微信支付的证书不一致
|