From 352b633a5c8f529f5928e135aeae02a1ebd75c19 Mon Sep 17 00:00:00 2001 From: Dev Date: Sat, 10 Oct 2026 15:11:14 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BD=AC=E8=B4=A6=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/service/EasyWeChatService.php | 2 +- .../service/TransferAuthorizationService.php | 75 +++++++++++-------- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/backend/application/common/service/EasyWeChatService.php b/backend/application/common/service/EasyWeChatService.php index 02359d1..cebf3cb 100644 --- a/backend/application/common/service/EasyWeChatService.php +++ b/backend/application/common/service/EasyWeChatService.php @@ -303,7 +303,7 @@ class EasyWeChatService { try { $response = $this->payApp->getClient()->get( - 'v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/' . $out_authorization_no + 'v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no/' . rawurlencode($out_authorization_no) ); $res = $response->toArray(false); Log::info('查询免确认收款授权-res' . json_encode($res, JSON_UNESCAPED_UNICODE)); diff --git a/backend/application/common/service/TransferAuthorizationService.php b/backend/application/common/service/TransferAuthorizationService.php index daf8d91..3d59484 100644 --- a/backend/application/common/service/TransferAuthorizationService.php +++ b/backend/application/common/service/TransferAuthorizationService.php @@ -24,9 +24,10 @@ class TransferAuthorizationService /** @var TransferAuthorizations */ public static $Model = TransferAuthorizations::class; - // 授权状态 + // 授权状态(与微信查询授权单接口 state 取值一致) const STATE_WAIT_USER_CONFIRM = 'WAIT_USER_CONFIRM'; //已受理,待用户确认 const STATE_TAKING_EFFECT = 'TAKING_EFFECT'; //已授权,生效中(长期有效,直至主动解除) + const STATE_CLOSED = 'CLOSED'; //已关闭,无法转账至该授权对应的用户 /** * 生成商户侧授权单号:一个用户固定一个单号。 @@ -39,6 +40,18 @@ class TransferAuthorizationService return 'TXAUTH' . str_pad((string)intval($userId), 10, '0', STR_PAD_LEFT); } + /** + * 生成新的商户侧授权单号:授权已被关闭/解除时使用。 + * 微信仅明确“未确认的授权单 30 天后单号可复用”,已确认后又被关闭的单号复用存在失败风险, + * 因此关闭后重新授权使用全新单号(原单号 ≤22 位,仍满足 32 位、字母数字限制) + * @param int $userId + * @return string + */ + public static function generateAuthNoUnique($userId): string + { + return self::generateAuthNo($userId) . strtoupper(substr(md5(uniqid('', true)), 0, 6)); + } + /** * 获取用户的授权记录 * @param int $userId @@ -46,7 +59,10 @@ class TransferAuthorizationService */ public static function getByUserId($userId) { - return self::getOne(['user_id' => $userId]); + //优先取生效中的授权记录(历史被关闭的记录仍保留,故按状态+时间取最新的) + $record = self::getOne(['user_id' => $userId, 'state' => self::STATE_TAKING_EFFECT], [], ['id', 'desc']); + + return $record ?: self::getOne(['user_id' => $userId], [], ['id', 'desc']); } /** @@ -76,44 +92,35 @@ class TransferAuthorizationService throw new \Exception('无法获取用户微信身份,请先登录小程序'); } - //已授权的用户直接返回,不再重复发起 - $record = self::getByUserId($userId); - if ($record && self::isAuthorized($userId)) { - return [ - 'authorized' => true, - 'state' => $record->state, - ]; - } - $authNo = self::generateAuthNo($userId); $wechat = new EasyWeChatService(); - //本地状态可能与微信侧不一致(如授权回调未收到):先查单同步,微信侧已生效则直接放行 + //每次提现前都回查微信侧最新授权状态并同步到本地: + //授权生效后长期有效(TAKING_EFFECT),但用户可能在微信端主动解除授权, + //只看本地记录会导致解除后仍按已授权放行,直到后台转账失败才暴露 $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, - ]; + $queryState = (string)($query['state'] ?? ''); + if ($queryState !== '') { + self::saveAuthorization($userId, $authNo, $queryState, $query['authorization_id'] ?? ''); + if ($queryState === self::STATE_TAKING_EFFECT) { + return [ + 'authorized' => true, + 'state' => $queryState, + ]; + } } - //发起授权申请(授权单号固定,复用同一单号可重新发起) + //未生效:发起授权申请(未确认的单号可复用重新发起) + //CLOSED(用户或商户已关闭) 时复用原单号存在发起失败风险,改用全新单号并新建记录 + $forceCreate = ($queryState === self::STATE_CLOSED); + if ($forceCreate) { + $authNo = self::generateAuthNoUnique($userId); + } $displayName = mb_substr($displayName ?: ($user->nickname ?: ('用户' . $userId)), 0, 32); $res = $wechat->applyTransferAuthorization($authNo, $user->openid, $displayName); if (empty($res['package_info'])) { - //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, - ]; - } + //package 仅在授权状态为 WAIT_USER_CONFIRM 时返回,拿不到时透出微信返回的错误信息 throw new \Exception($res['message'] ?? ($query['message'] ?? '发起免确认收款授权失败')); } @@ -123,7 +130,8 @@ class TransferAuthorizationService $authNo, $res['state'] ?? self::STATE_WAIT_USER_CONFIRM, '', - $res['package_info'] + $res['package_info'], + $forceCreate ); return [ @@ -143,11 +151,12 @@ class TransferAuthorizationService * @param string $state 授权状态 * @param string $authorizationId 微信侧授权单号(可为空) * @param string $packageInfo 拉起授权页的 package(可为空) + * @param bool $forceCreate 是否强制新建记录(授权单已关闭换新单号时使用) * @return void */ - private static function saveAuthorization($userId, string $authNo, string $state, string $authorizationId = '', string $packageInfo = '') + private static function saveAuthorization($userId, string $authNo, string $state, string $authorizationId = '', string $packageInfo = '', bool $forceCreate = false) { - $record = self::getByUserId($userId); + $record = $forceCreate ? null : self::getByUserId($userId); $data = ['state' => $state]; if ($authorizationId !== '') { $data['authorization_id'] = $authorizationId;