$userId]); } /** * 用户是否已完成免确认收款授权 * @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('无法获取用户微信身份,请先登录小程序'); } //已授权的用户直接返回,不再重复发起 $record = self::getByUserId($userId); if ($record && self::isAuthorized($userId)) { return [ 'authorized' => true, 'state' => $record->state, ]; } $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'])) { //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 当前为待用户确认) self::saveAuthorization( $userId, $authNo, $res['state'] ?? self::STATE_WAIT_USER_CONFIRM, '', $res['package_info'] ); 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(可为空) * @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 * @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; } }