微信服务商的分账功能总结

概要
基于官方文档:服务商分账接口文档 ,根据我们自身的需求开发功能。此文为开发后的总结和思考。

分析文档
先搞清楚官方接口能干嘛,不能干嘛。

一、能干(的功能)
1. 角色
服务商
子商户(特约商户)
2. 开通分账
(服务商联系运营开通产品白名单后,才可在产品中心看到此功能)
开通流程文档里已经很详细了。只要一步步按着操作就 ok 。
简单总结就是服务商首先有这个产品,邀请子商户授权同意。跟 服务商退款 授权流程一样。
服务商发出邀请后,子商户后台 消息中心 (其它路径我真的找不到)可看到该邀请的信息,进入输入允许分账金额的 最大比例 (例如 20%)以及可选择是否上传与服务商签订的协议。

3. API
文档提供了 6 个接口。主要是发起分账、查询分账结果、分账接收方添加/删除和完结分账几个功能。
其中的 请求单次分账 和 请求多次分账 ,我根据我们的场景选择了单次的,后面会讲到。完结分账 没有用到。所以我这次用到的只有其中 4 个。

4. 特点
增加了分账标识参数并且参数值为「Y」的订单,它们的金额会被冻结为「待分账资金」。冻结资金即暂时不可以挪为它用(比如提供给其它订单的退款金,这是不能的)。
分账之前,会在例如「20%」的资金里先扣除结算手续费之后,才是可分账的金额。
由 添加分账接收方 接口文档可知,接收方类型支持商户及个人微信。
二、不能干
发起分账请求后,没有回调通知分账的结果。类似 付款码支付API 。
必须跑的是子商户模式。
文档没有说明,发起分账请求后,多久能分账成功。根据实际经验,大概需要 1 分钟以内的时间。也因为这是时间的不确定性,我没有使用付款码支付API 那样的轮询处理查询结果的方式。这是两原因之一。
分账只能按照订单维度进行。1
分析需求
一、功能设置
每个子商户可决定是否使用分账、分账的最大金额,查看是否已达最大金额,设置隔「a」天分账一次( a < 30)2,分账的比例「b」,合同图片,分账接收人列表;
每个分账接收人的设置,类型,帐号,名称,描述,最大可得金额,是否已达最大金额,分账比例「c」(可分账金额为订单金额 * (1 - 0.006) * b% * c%)。0.6% 为结算手续费;
商户除了用分账功能给服务商付费之外,也可给其合作的其它商户或个人进行分账。只需要把其它商户添加进分账接收人里。
二、分账的执行
定时任务,每天执行一次,查询是否有需要分账(最近分账记录至今超过「a」天)的商户(未超过最大金额)。
查询「a」天内的订单,分别执行分账。
以订单维度执行分账时,获取未达到最大金额的接收人,给接收人分账。
即将超过最大金额的商家,为了防止超过最大金额的分账订单,需要增加判断。判断此次分账的金额(订单金额 * 分账比例)是否大于剩余分账金额(最大 - 已分账金额),若大于,则分账金额替换为剩余的分账金额,继续执行分账,并且分账后更新商户「是否已达最大金额」为 true。
三、分账的记录
每笔订单执行一次分账操作就新增一条「分账记录表」的记录。
每一条分账记录对应多条「分账记录详情表」记录,与接受方一对一。
实际分账成功的金额需要根据「分账记录详情表」进行统计。
还需要「分账设置表」、「分账接收方记录表」。
开发
一、数据库字段设计
1. 分账设置表
mid 「商家 id」
is_sharing 「是否开启分账,0=否」
is_max 「是否已达最大金额,0=否」
max_amount 「分账最大金额」
shared_amount 「已分账的金额」
share_interval 「隔几天分账一次」
ratio 「分账比例」
compact_img 「合同图片地址」
其它
2. 分账接收方记录表
mid 「商家 id」
type 「分账接收方类型」
account 「分账接收方帐号」
name 「商户全称或个人姓名」
description 「分账的原因描述」
is_max 「是否已达最大金额,0=否」
max_amount 「分账最大可得金额」
ratio 「分账比例」
其它
3. 分账记录表
order_id 「订单id」
mid 「商家 id」
share_no 「分账的单号」
status 「分账结果」
close_reason 「关单原因」
其它
4. 分账记录详情表
record_id 「分账记录表的id」
receiver_id 「分账接收人表的id」
amount 「分账金额」
status 「分账结果」
fail_reason 「分账失败原因」
其它
二、接口对接(PHP7、TP5.0.24)
涉及详细逻辑的均为伪代码。

1. 获取分账签名
参考了 EasyWeChat3 源码里 生成签名 的方法:

//获取签名
private function getSign($params, $key)
{
ksort($params);
$params['key'] = $key;
$sign = strtoupper(call_user_func_array('hash_hmac', ['sha256', urldecode(http_build_query($params)), $key]));
$params['sign'] = $sign;
return $params;
}

2. 生成带分账签名的参数

/**
* @param $mid int 商户的 id
* @param $moreParam array 更多的其它参数
* @param bool $isQuery
* @return array 返回带分账签名的参数
* @throws Exception
*/
private function getParamWithSign($mid, $moreParam, $isQuery = false)
{
//获取服务商及子商户配置信息
$payConfig = $this->payConfig;
//整理生成签名的参数
$params = [
'mch_id' => $payConfig['mch_id'],
'sub_mch_id' => $subAppConfig['sub_mch_id'],
'appid' => $payConfig['appid'],
'sub_appid' => $subAppConfig['sub_app_id'],
'nonce_str' => uniqid(),
'sign_type' => 'HMAC-SHA256'
];
if ($isQuery) { //「查询分账结果」无此参数,去除
unset($params['appid'], $params['sub_appid']);
}
$params = array_merge($params, $moreParam);
//获取签名
$params = $this->getSign($params, $payConfig['key']);
return $params;
}


3. 封装发送分账相关请求
private function postXml($dataArray, $url, $cert = [])
{
$dataXml = $this->arrayToXml($dataArray, false); //数组转换成 xml 的方法,网上搜的
$res = HttpClient::curl_post($url, $dataXml, $cert); //关于 cUrl 我们自己封装的方法,也可使用扩展「guzzlehttp/guzzle」
$resArray = $this->xmlToArray($res); //转换方法
//返回结果预处理
if (array_key_exists("return_code", $resArray) &&
$resArray['return_code'] == 'FAIL') throw new Exception($resArray['return_msg']);
if (!array_key_exists("return_code", $resArray)
|| !array_key_exists("result_code", $resArray)) {
throw new Exception("接口调用失败!");
}
if ($resArray['result_code'] != 'SUCCESS') {
//日志记录了 'result_code 不等于「SUCCESS」的反馈
throw new Exception($resArray['err_code_des']);
}
return $resArray;
}

剩下的就简单了。

4. 简单示例
/** 删除分账接收人
* @param $mid int 商家的 id
* @param $receiver
* @return array|bool
*/
public function removeReceiver($mid, $receiver)
{
$params['receiver'] = json_encode($receiver, JSON_UNESCAPED_UNICODE);
$dataArray = $this->getParamWithSign($mid, $params);
$url = 'https://api.mch.weixin.qq.com/pay/profitsharingremovereceiver'; // 接口 url
return $this->postXml($dataArray, $url);
}

5. 发起分账需要双向证书
可在封装的 cUrl 方法中设置证书,参考如下:

curl_setopt_array($curl, [
CURLOPT_SSLCERT => $cert['cert_path'], // 客户端证书,用于双向认证
CURLOPT_SSLCERTTYPE => $cert['cert_type'], // 证书的类型。支持的格式有"PEM" (默认值), "DER"和"ENG"。
CURLOPT_SSLKEY => $cert['key_path'], // 客户端私钥的文件路径
CURLOPT_SSLKEYTYPE => $cert['key_type'], // 客户端私钥类型,支持的私钥类型为"PEM"(默认值)、"DER"和"ENG"。
CURLOPT_KEYPASSWD => $cert['key_password'], // 客户端私钥密码,私钥在创建时可以选择加密。
]);

其中,◆ API证书调用或安装需要使用到密码,该密码的值为微信商户号(mch_id)4 里的「商户号」为服务商商户号。证书和密钥为两个 .pem 文件的路径。

三、逻辑处理
以下为根据需求开发的业务逻辑,仅为我的梳理总结。

获取需要分账的商户的配置,并关联查询已添加的分账接收方信息 => $configs;
foreach ($configs as $config) {
$this->toShareForOneSeller($config);
}
1
2
3
为单个商户的设定时间段内的订单发起分账
private function toShareForOneSeller(&$config)
{
//查询最新分账记录
$recordModel = new SellerProfitSharingRecord();
$lastRecord = $recordModel->getLastRecord($config['sid']);
//如果最新分账记录存在且时间超过设置时间,或记录不存在,则需要分账
$lastRecordDate = $lastRecord['create_time'] ?? null;
$tillNow = time() - strtotime($lastRecordDate);
if (!$lastRecord || $tillNow > $config['share_date'] * 86400) {
//查询更新并获取该商户的已分账金额
$recordModel->refreshSharedAmount($config['sid']); //用到「查询分账结果」的接口,查询后更新数据库
$sharedAmount = $recordModel->getSharedAmount($config['sid']);
//更新已分账金额
$configModel = new SellerProfitSharingConfig();
$configModel->updateSharedAmount($sharedAmount['seller'], $config['sid']);
if ($config['max_amount'] > $sharedAmount['seller']) { //未到达最大分账金额
//按时间间隔获取需要分账的订单信息
$orderModel = new Order();
$orderInfo = $orderModel->getOrderNeedShare($config);
if (!$orderInfo) return true; //因为需要使用计划任务定时执行,所以不需要抛出异常
foreach ($orderInfo as $order) {
$shareNo = 'P01' . date('ymdHis') . rand(1000, 9999); //分账单号
$canShareAmount = bcmul($order['real_amount'], $config['ratio'] / 100, 2);
//判断该商户是否已达到最大分账金额
if ($config['max_amount'] < $canShareAmount + $sharedAmount['seller']) {
$canShareAmount = bcsub($config['max_amount'], $sharedAmount['seller'], 2);
}
// 0.6% 的结算手续费
$serviceCharge = bcmul($order['real_amount'], 0.006, 2);
$canShareAmount = $canShareAmount - $serviceCharge;
//新增记录及处理分账
$res = $recordModel->addRecordWithSharing($order, $shareNo, $canShareAmount, $sharedAmount, $config);
if ($res) $sharedAmount['seller'] = bcadd($sharedAmount['seller'], $canShareAmount, 2);
}
} else {
//更新商家分账状态为已到达最大分账金额
$configModel->updateToIsMax($config['sid']);
}
}
return true;
}

四、计划任务批量处理
crontab 定时执行上述逻辑方法。

文档里的分账接口 - 常见问题 的注意事项 ↩︎

注意事项 - 分账资金的冻结期默认是30天 ↩︎

一个开源的微信非官方 SDK ↩︎

使用API证书 ↩︎
————————————————

原文地址:https://www.cnblogs.com/Alex80/p/13227866.html