概要
基於官方文檔:服務商分賬接口文檔 ,根據我們自身的需求開發功能。此文為開發后的總結和思考。
分析文檔
先搞清楚官方接口能干嘛,不能干嘛。
一、能干(的功能)
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證書 ↩︎
————————————————