$userId, 'state' => self::STATE_TAKING_EFFECT], [], ['id', 'desc']); return $record ?: self::getOne(['user_id' => $userId], [], ['id', 'desc']); } /** * 用户是否已完成免确认收款授权 * @param int $userId * @return bool */ public static function isAuthorized($userId): bool { $record = self::getByUserId($userId); //授权生效后即可凭 out_authorization_no 发起免确认转账;authorization_id 仅作留存 return $record && $record->state == self::STATE_TAKING_EFFECT; } /** * 发起/获取免确认收款授权,供前端拉起授权页 * @param \app\common\model\Users $user 用户(需含 openid) * @param string $displayName 授权详情中展示的开通账号名称 * @return array {authorized: bool, state, package_info?, mch_id?, app_id?} * @throws \Exception */ public static function apply($user, string $displayName = ''): array { $userId = $user->id; if (empty($user->openid)) { throw new \Exception('无法获取用户微信身份,请先登录小程序'); } $authNo = self::generateAuthNo($userId); $wechat = new EasyWeChatService(); //每次提现前都回查微信侧最新授权状态并同步到本地: //授权生效后长期有效(TAKING_EFFECT),但用户可能在微信端主动解除授权, //只看本地记录会导致解除后仍按已授权放行,直到后台转账失败才暴露 $query = $wechat->queryTransferAuthorization($authNo); $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 时返回,拿不到时透出微信返回的错误信息 throw new \Exception($res['message'] ?? ($query['message'] ?? '发起免确认收款授权失败')); } //保存/更新授权记录(package_info 用于前端拉起授权页,state 当前为待用户确认) self::saveAuthorization( $userId, $authNo, $res['state'] ?? self::STATE_WAIT_USER_CONFIRM, '', $res['package_info'], $forceCreate ); return [ 'authorized' => false, 'state' => $res['state'] ?? self::STATE_WAIT_USER_CONFIRM, 'package_info' => $res['package_info'], //前端 wx.requestMerchantTransfer 需要:商户号 + AppID + package 'mch_id' => (string)$wechat->payApp->getMerchant()->getMerchantId(), 'app_id' => $wechat->config['app_id'], ]; } /** * 保存/更新授权记录 * @param int $userId * @param string $authNo 商户侧授权单号 * @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 = '', bool $forceCreate = false) { $record = $forceCreate ? null : 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 * @return bool */ public static function handleAuthorizationNotify(array $message): bool { $outAuthNo = trim((string)($message['out_authorization_no'] ?? '')); $authorizationId = trim((string)($message['authorization_id'] ?? '')); $state = trim((string)($message['state'] ?? '')); if (!$outAuthNo || !$authorizationId) { Log::error('免确认收款授权回调参数缺失:' . json_encode($message, JSON_UNESCAPED_UNICODE)); return false; } $record = self::getOne(['out_authorization_no' => $outAuthNo]); if (!$record) { Log::error('免确认收款授权回调找不到授权记录:' . $outAuthNo); return false; } //记录微信侧授权单号,标记授权生效(授权长期有效,直至用户或商户主动解除) $record->authorization_id = $authorizationId; $record->state = $state ?: self::STATE_TAKING_EFFECT; $record->save(); Log::info('免确认收款授权生效:' . json_encode([ 'user_id' => $record->user_id, 'state' => $record->state, ], JSON_UNESCAPED_UNICODE)); return true; } }