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


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

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

    一、能干(的功能)
    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证书 ↩︎
    ————————————————

  • 相关阅读:
    java网络编程(UDP详解)
    java网络编程(TCP详解)
    java-单例详解
    java-接口和抽象类的联系和区别。
    java-集合小结
    Java异常(一)Java异常简介及其框架
    JAVA的接口
    深入理解Java的接口和抽象类
    linux动态库编译和使用详细剖析
    C基础 time.h 简单思路扩展
  • 原文地址:https://www.cnblogs.com/Alex80/p/13227866.html
Copyright © 2020-2023  润新知