转账功能优化
This commit is contained in:
@@ -161,13 +161,8 @@ class EasyWeChatService
|
||||
'transfer_remark' => mb_substr($desc, 0, 32), // 最多 32 字符
|
||||
//授权转账:以授权单定位收款人,不再传 openid
|
||||
'out_authorization_no' => $out_authorization_no,
|
||||
//必须传转账场景报备信息,需按商户平台申请的场景准确填写
|
||||
'transfer_scene_report_infos' => [
|
||||
[
|
||||
'info_type' => '活动名称',
|
||||
'info_content' => mb_substr($desc ?: '佣金报酬', 0, 32),
|
||||
],
|
||||
],
|
||||
//转账场景报备信息:必须与 transfer_scene_id 对应(1005=佣金报酬 需传「岗位类型」「报酬说明」两条)
|
||||
'transfer_scene_report_infos' => $this->buildTransferSceneReportInfos($desc),
|
||||
];
|
||||
//规则:转账金额 >= 2,000 元时必须传入收款人姓名(加密)
|
||||
if ($amount >= 2000 && $name !== '') {
|
||||
@@ -256,6 +251,72 @@ class EasyWeChatService
|
||||
return $server->serve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造转账场景报备信息
|
||||
* 报备信息必须与 transfer_scene_id 一一对应,且 info_type 不可重复,否则接口会报
|
||||
* 「未传入完整且对应的转账场景报备信息」:
|
||||
* 1000 现金营销 -> 活动名称 + 奖励说明
|
||||
* 1005 佣金报酬 -> 岗位类型 + 报酬说明
|
||||
* 可通过 extra/common.php 的 transfer_scene_report_infos 覆盖
|
||||
* @param string $desc 转账备注,用于 info_content 缺省时兜底
|
||||
* @return array
|
||||
*/
|
||||
private function buildTransferSceneReportInfos(string $desc = ''): array
|
||||
{
|
||||
$reportInfos = $this->config['transfer_scene_report_infos'] ?? [];
|
||||
if (!$reportInfos) {
|
||||
$sceneId = (string)($this->config['transfer_scene_id'] ?? '1005');
|
||||
$reportInfos = $sceneId === '1000'
|
||||
? [
|
||||
['info_type' => '活动名称', 'info_content' => $desc ?: '平台活动'],
|
||||
['info_type' => '奖励说明', 'info_content' => $desc ?: '活动奖励'],
|
||||
]
|
||||
: [
|
||||
['info_type' => '岗位类型', 'info_content' => '平台接单用户'],
|
||||
['info_type' => '报酬说明', 'info_content' => $desc ?: '用户提现'],
|
||||
];
|
||||
}
|
||||
|
||||
//info_type ≤15 字符、info_content ≤32 字符
|
||||
return array_map(function ($item) use ($desc) {
|
||||
$infoType = mb_substr((string)($item['info_type'] ?? ''), 0, 15);
|
||||
$infoContent = mb_substr((string)($item['info_content'] ?? ''), 0, 32);
|
||||
if ($infoContent === '') {
|
||||
$infoContent = mb_substr($desc ?: '佣金报酬', 0, 32);
|
||||
}
|
||||
|
||||
return [
|
||||
'info_type' => $infoType,
|
||||
'info_content' => $infoContent,
|
||||
];
|
||||
}, $reportInfos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询免确认收款授权单(用于同步授权状态:授权回调丢失时以微信侧状态为准)
|
||||
* 接口:GET /v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/{no}
|
||||
* 文档:https://pay.weixin.qq.com/doc/v3/merchant/4015901167
|
||||
* @param string $out_authorization_no 商户侧授权单号
|
||||
* @return array {out_authorization_no, authorization_id, state, ...};失败时含 message
|
||||
*/
|
||||
public function queryTransferAuthorization($out_authorization_no)
|
||||
{
|
||||
try {
|
||||
$response = $this->payApp->getClient()->get(
|
||||
'v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/' . $out_authorization_no
|
||||
);
|
||||
$res = $response->toArray(false);
|
||||
Log::info('查询免确认收款授权-res' . json_encode($res, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return $res;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('查询免确认收款授权异常:' . $e->getMessage());
|
||||
return [
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询新版商家转账单状态
|
||||
* @param string $out_bill_no 商户单号
|
||||
|
||||
@@ -58,9 +58,8 @@ class TransferAuthorizationService
|
||||
{
|
||||
$record = self::getByUserId($userId);
|
||||
|
||||
return $record
|
||||
&& $record->state == self::STATE_TAKING_EFFECT
|
||||
&& $record->authorization_id;
|
||||
//授权生效后即可凭 out_authorization_no 发起免确认转账;authorization_id 仅作留存
|
||||
return $record && $record->state == self::STATE_TAKING_EFFECT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,34 +85,50 @@ class TransferAuthorizationService
|
||||
];
|
||||
}
|
||||
|
||||
//发起授权申请(授权单号固定,复用同一单号可重新发起)
|
||||
$authNo = self::generateAuthNo($userId);
|
||||
$wechat = new EasyWeChatService();
|
||||
|
||||
//本地状态可能与微信侧不一致(如授权回调未收到):先查单同步,微信侧已生效则直接放行
|
||||
$query = $wechat->queryTransferAuthorization($authNo);
|
||||
if (($query['state'] ?? '') === self::STATE_TAKING_EFFECT) {
|
||||
self::saveAuthorization($userId, $authNo, self::STATE_TAKING_EFFECT, $query['authorization_id'] ?? '');
|
||||
|
||||
return [
|
||||
'authorized' => true,
|
||||
'state' => self::STATE_TAKING_EFFECT,
|
||||
];
|
||||
}
|
||||
|
||||
//发起授权申请(授权单号固定,复用同一单号可重新发起)
|
||||
$displayName = mb_substr($displayName ?: ($user->nickname ?: ('用户' . $userId)), 0, 32);
|
||||
$res = $wechat->applyTransferAuthorization($authNo, $user->openid, $displayName);
|
||||
|
||||
if (empty($res['package_info'])) {
|
||||
throw new \Exception($res['message'] ?? '发起免确认收款授权失败');
|
||||
//package 仅在授权状态为 WAIT_USER_CONFIRM 时返回;拿不到时再查单确认微信侧是否已生效
|
||||
$query = $wechat->queryTransferAuthorization($authNo);
|
||||
if (($query['state'] ?? '') === self::STATE_TAKING_EFFECT) {
|
||||
self::saveAuthorization($userId, $authNo, self::STATE_TAKING_EFFECT, $query['authorization_id'] ?? '');
|
||||
|
||||
return [
|
||||
'authorized' => true,
|
||||
'state' => self::STATE_TAKING_EFFECT,
|
||||
];
|
||||
}
|
||||
throw new \Exception($res['message'] ?? ($query['message'] ?? '发起免确认收款授权失败'));
|
||||
}
|
||||
|
||||
//保存/更新授权记录(package_info 用于前端拉起授权页,state 当前为待用户确认)
|
||||
$data = [
|
||||
'state' => $res['state'] ?? self::STATE_WAIT_USER_CONFIRM,
|
||||
'package_info' => $res['package_info'],
|
||||
];
|
||||
if ($record) {
|
||||
$record->save($data);
|
||||
} else {
|
||||
self::create(array_merge([
|
||||
'user_id' => $userId,
|
||||
'out_authorization_no' => $authNo,
|
||||
'authorization_id' => '',
|
||||
], $data));
|
||||
}
|
||||
self::saveAuthorization(
|
||||
$userId,
|
||||
$authNo,
|
||||
$res['state'] ?? self::STATE_WAIT_USER_CONFIRM,
|
||||
'',
|
||||
$res['package_info']
|
||||
);
|
||||
|
||||
return [
|
||||
'authorized' => false,
|
||||
'state' => $data['state'],
|
||||
'state' => $res['state'] ?? self::STATE_WAIT_USER_CONFIRM,
|
||||
'package_info' => $res['package_info'],
|
||||
//前端 wx.requestMerchantTransfer 需要:商户号 + AppID + package
|
||||
'mch_id' => (string)$wechat->payApp->getMerchant()->getMerchantId(),
|
||||
@@ -121,6 +136,40 @@ class TransferAuthorizationService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存/更新授权记录
|
||||
* @param int $userId
|
||||
* @param string $authNo 商户侧授权单号
|
||||
* @param string $state 授权状态
|
||||
* @param string $authorizationId 微信侧授权单号(可为空)
|
||||
* @param string $packageInfo 拉起授权页的 package(可为空)
|
||||
* @return void
|
||||
*/
|
||||
private static function saveAuthorization($userId, string $authNo, string $state, string $authorizationId = '', string $packageInfo = '')
|
||||
{
|
||||
$record = self::getByUserId($userId);
|
||||
$data = ['state' => $state];
|
||||
if ($authorizationId !== '') {
|
||||
$data['authorization_id'] = $authorizationId;
|
||||
}
|
||||
if ($packageInfo !== '') {
|
||||
$data['package_info'] = $packageInfo;
|
||||
}
|
||||
|
||||
if ($record) {
|
||||
$record->save($data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self::create(array_merge([
|
||||
'user_id' => $userId,
|
||||
'out_authorization_no' => $authNo,
|
||||
'authorization_id' => $authorizationId,
|
||||
'package_info' => $packageInfo,
|
||||
], $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理免确认收款授权结果回调(用户授权成功后微信异步通知)
|
||||
* @param array $message 解密后的授权结果,含 out_authorization_no / authorization_id / state
|
||||
|
||||
@@ -28,6 +28,12 @@ return [
|
||||
'notify_url' => Env::get('app.url') . '/api/order/payNotify',
|
||||
// 新版商家转账(用户确认模式)转账场景ID:1005=佣金报酬
|
||||
'transfer_scene_id' => Env::get('wechat.transfer_scene_id', '1005'),
|
||||
// 转账场景报备信息:必须与 transfer_scene_id 严格对应,否则报「未传入完整且对应的转账场景报备信息」
|
||||
// 1005(佣金报酬) 固定传两条:岗位类型 + 报酬说明;info_type ≤15 字符,info_content ≤32 字符,且 info_type 不可重复
|
||||
'transfer_scene_report_infos' => [
|
||||
['info_type' => '岗位类型', 'info_content' => '平台接单用户'],
|
||||
['info_type' => '报酬说明', 'info_content' => '用户提现'],
|
||||
],
|
||||
// 新版商家转账结果回调地址(公网可访问、HTTPS、不带参数)
|
||||
'transfer_notify_url' => Env::get('app.url') . '/api/account/transferNotify',
|
||||
// 免确认收款授权结果回调地址(公网可访问、HTTPS、不带参数)
|
||||
|
||||
@@ -153,41 +153,92 @@ Page({
|
||||
this.setData({ withdrawName: e.detail.value });
|
||||
},
|
||||
onConfirmWithdraw() {
|
||||
const { withdrawName, withdrawAmount } = this.data;
|
||||
//提交前提现先确保完成免确认收款授权(授权后审核通过即可免确认直接到账零钱)
|
||||
this.ensureTransferAuthorized()
|
||||
.then(() => this.submitWithdraw())
|
||||
.catch((msg) => {
|
||||
wx.showToast({ title: typeof msg === 'string' && msg ? msg : '未完成授权,已取消提现', icon: 'none' });
|
||||
const title = msg && msg.message
|
||||
? msg.message
|
||||
: (typeof msg === 'string' && msg ? msg : '未完成授权,已取消提现');
|
||||
wx.showToast({ title: title, icon: 'none', duration: 2500 });
|
||||
});
|
||||
},
|
||||
// 确保完成免确认收款授权:已授权直接通过;未授权拉起微信授权页
|
||||
ensureTransferAuthorized() {
|
||||
return fetchTransferAuthorization().then((res) => {
|
||||
if (res.code != 1) {
|
||||
throw new Error(res.msg || '获取授权状态失败');
|
||||
}
|
||||
const data = res.data || {};
|
||||
if (data.authorized) {
|
||||
return;
|
||||
}
|
||||
//package_info 仅在授权状态为 WAIT_USER_CONFIRM(待用户确认)时才返回,缺失则无法拉起
|
||||
if (!data.package_info || !data.mch_id || !data.app_id) {
|
||||
throw new Error('免确认收款授权参数缺失,请稍后重试');
|
||||
}
|
||||
//文档要求:调起前须判断微信客户端/基础库是否支持 requestMerchantTransfer
|
||||
if (!wx.canIUse('requestMerchantTransfer')) {
|
||||
throw new Error('当前微信版本过低,请更新至最新版本后再授权');
|
||||
}
|
||||
//拉起授权用的 appId 必须与发起授权接口传入的 appid 为同一个
|
||||
let appId = data.app_id;
|
||||
const miniProgram = wx.getAccountInfoSync ? (wx.getAccountInfoSync().miniProgram || {}) : {};
|
||||
if (miniProgram.appId && miniProgram.appId !== data.app_id) {
|
||||
throw new Error('授权商户AppID与当前小程序不一致,无法拉起授权');
|
||||
}
|
||||
if (miniProgram.appId) {
|
||||
appId = miniProgram.appId;
|
||||
}
|
||||
|
||||
return this.requestMerchantTransfer({
|
||||
mchId: String(data.mch_id),
|
||||
appId: appId,
|
||||
package: data.package_info,
|
||||
});
|
||||
});
|
||||
},
|
||||
// 拉起微信免确认收款授权页(requestMerchantTransfer)
|
||||
requestMerchantTransfer(params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchTransferAuthorization().then((res) => {
|
||||
if (res.code != 1) {
|
||||
reject(res.msg || '获取授权状态失败');
|
||||
return;
|
||||
}
|
||||
const data = res.data || {};
|
||||
if (data.authorized) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
wx.requestMerchantTransfer({
|
||||
mchId: data.mch_id,
|
||||
appId: data.app_id,
|
||||
package: data.package_info,
|
||||
success: () => {
|
||||
wx.showToast({ title: '授权成功', icon: 'success' });
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('requestMerchantTransfer fail', err);
|
||||
reject('需完成免确认收款授权后才能提现');
|
||||
},
|
||||
});
|
||||
}).catch(() => reject('网络异常,请稍后重试'));
|
||||
//在用户点击手势链路中调起:先二次确认,避免异步请求后直接调起被客户端拒绝
|
||||
wx.showModal({
|
||||
title: '免确认收款授权',
|
||||
content: '提现前需先开通免确认收款授权,点击「去授权」在微信授权页完成确认后即可提现。',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (modalRes) => {
|
||||
if (!modalRes.confirm) {
|
||||
reject('已取消授权,无法提现');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
wx.requestMerchantTransfer(Object.assign({}, params, {
|
||||
success: (res) => {
|
||||
//success 仅代表授权页展示成功,不代表用户已确认授权,最终以微信异步回调为准
|
||||
console.log('requestMerchantTransfer success', res);
|
||||
const errMsg = (res && (res.err_msg || res.errMsg)) || '';
|
||||
if (errMsg && errMsg !== 'requestMerchantTransfer:ok') {
|
||||
reject(errMsg === 'requestMerchantTransfer:cancel'
|
||||
? '已取消授权,无法提现'
|
||||
: '拉起授权失败:' + errMsg);
|
||||
return;
|
||||
}
|
||||
wx.showToast({ title: '请在授权页完成确认', icon: 'none', duration: 2000 });
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('requestMerchantTransfer fail', err);
|
||||
const errMsg = (err && (err.errMsg || err.err_msg)) || '';
|
||||
reject(errMsg ? '拉起授权失败:' + errMsg : '拉起授权失败,请更新微信后重试');
|
||||
},
|
||||
}));
|
||||
} catch (e) {
|
||||
reject('当前微信版本不支持免确认收款授权,请更新后重试');
|
||||
}
|
||||
},
|
||||
fail: () => reject('已取消授权,无法提现'),
|
||||
});
|
||||
});
|
||||
},
|
||||
// 原提现提交逻辑
|
||||
|
||||
Reference in New Issue
Block a user