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

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

概要

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


分析文档

先搞清楚官方接口能干嘛,不能干嘛。

一、能干(的功能)

1. 角色

  1. 服务商
  2. 子商户(特约商户)

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 文件的路径。

三、逻辑处理

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

  1. 获取需要分账的商户的配置,并关联查询已添加的分账接收方信息 => $configs;
foreach ($configs as $config) {
    $this->toShareForOneSeller($config);
}
  1. 为单个商户的设定时间段内的订单发起分账
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 定时执行上述逻辑方法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值