snsapi_base vs snsapi_privateinfo 对比 ┌────────────────────┬──────────────┬──────────────────────────────────────┐ │ │ snsapi_base │ snsapi_privateinfo │ ├────────────────────┼──────────────┼──────────────────────────────────────┤ │ 授权方式 │ 静默无弹窗 │ 手动弹出授权页 │ ├────────────────────┼──────────────┼──────────────────────────────────────┤ │ 获取字段 │ UserId │ UserIduser_ticket │ ├────────────────────┼──────────────┼──────────────────────────────────────┤ │ 详细信息 │ 无 │ 需用 user_ticket 再调/getuserdetail │ ├────────────────────┼──────────────┼──────────────────────────────────────┤ │ 详细信息内容 │ — │ 姓名、头像、性别、邮箱、手机、部门 │ ├────────────────────┼──────────────┼──────────────────────────────────────┤ │ user_ticket 有效期 │ — │1800秒 │ └────────────────────┴──────────────┴──────────────────────────────────────┘---安装 composer require overtrue/wechat:^6.0composer require firebase/php-jwt---1.配置// config/autoload/wechat.phpreturn[work[default[corp_idenv(WECHAT_WORK_CORP_ID),corp_secretenv(WECHAT_WORK_CORP_SECRET),agent_idenv(WECHAT_WORK_AGENT_ID),tokenenv(WECHAT_WORK_TOKEN),aes_keyenv(WECHAT_WORK_AES_KEY),oauth[scopes[snsapi_privateinfo],// 手动授权callbackenv(WECHAT_OAUTH_CALLBACK),],],],jwt[secretenv(JWT_SECRET,change-me),ttl(int)env(JWT_TTL,7200),algoHS256,],];---2.users 表迁移含详细字段?php// migrations/2024_01_01_create_users_table.phpuse Hyperf\Database\Schema\Schema;use Hyperf\Database\Schema\Blueprint;use Hyperf\Database\Migrations\Migration;classCreateUsersTableextends Migration{publicfunctionup():void{Schema::create(users,function(Blueprint $table){$table-bigIncrements(id);$table-string(wechat_userid)-unique()-comment(企业微信 userId);$table-string(name)-default()-comment(姓名);$table-string(avatar)-default()-comment(头像 URL);$table-string(gender)-default()-comment(性别: male/female);$table-string(email)-default()-comment(企业邮箱);$table-string(mobile)-default()-comment(手机号);$table-json(departments)-nullable()-comment(所在部门 ID 列表);$table-timestamps();});}publicfunctiondown():void{Schema::dropIfExists(users);}}---3.UserProfile DTO?php// app/DTO/WechatUserProfile.phpnamespaceApp\DTO;classWechatUserProfile{publicfunction__construct(publicreadonly string $userId,publicreadonly string $name,publicreadonly string $avatar,publicreadonly string $gender,publicreadonly string $email,publicreadonly string $mobile,publicreadonly array $departments,){}publicstaticfunctionfromArray(string $userId,array $data):self{$genderMap[1male,2female];returnnewself(userId:$userId,name:$data[name]??,avatar:$data[avatar]??,gender:$genderMap[$data[gender]??0]??,email:$data[email]??,mobile:$data[mobile]??,departments:$data[department]??[],);}}---4.OAuth 服务核心两步换取详情?php// app/Service/WechatOAuthService.phpnamespaceApp\Service;use App\DTO\WechatUserProfile;use EasyWeChat\Work\Application;use RuntimeException;classWechatOAuthService{publicfunction__construct(privatereadonly Application $app,){}/** * Step 1构造 snsapi_privateinfo 授权 URL */publicfunctionbuildAuthUrl(string $callbackUrl,string $state):string{return$this-app-getOAuth()-scopes([snsapi_privateinfo])-redirect($callbackUrl,$state);}/** * Step 2code 换取 userId user_ticket * * 调用 /cgi-bin/auth/getuserinfo * snsapi_privateinfo 会额外返回 user_ticket有效期 1800s * * return array{userId: string, userTicket: string} */publicfunctiongetIdentityByCode(string $code):array{try{$user$this-app-getOAuth()-userFromCode($code);}catch(\Throwable $e){thrownewRuntimeException(code 换取身份失败: .$e-getMessage());}$userId$user-getId();$userTicket$user-getRaw()[user_ticket]??;if(empty($userId)){thrownewRuntimeException(无法获取 UserId该用户可能不是企业成员);}if(empty($userTicket)){thrownewRuntimeException(未获取到 user_ticket请确认应用 scope 已设置为 snsapi_privateinfo.且用户已手动同意授权);}return[userId$userId,userTicket$userTicket];}/** * Step 3user_ticket 换取员工详细信息 * * 调用 POST /cgi-bin/auth/getuserdetail * 返回name、avatar、gender、email、mobile、department 等 */publicfunctiongetUserDetail(string $userId,string $userTicket):WechatUserProfile{try{$response$this-app-getClient()-postJson(/cgi-bin/auth/getuserdetail,[user_ticket$userTicket]);$data$response-toArray();$errcode(int)($data[errcode]??-1);if($errcode!0){thrownewRuntimeException(获取用户详情失败: .($data[errmsg]??). (errcode.$errcode.));}}catch(RuntimeException $e){throw$e;}catch(\Throwable $e){thrownewRuntimeException(调用 getuserdetail 接口失败: .$e-getMessage());}returnWechatUserProfile::fromArray($userId,$data);}}---5.用户落库服务?php// app/Service/UserService.phpnamespaceApp\Service;use App\DTO\WechatUserProfile;use Hyperf\DbConnection\Db;classUserService{/** * 根据企业微信详情 upsert 用户返回系统用户记录 */publicfunctionsyncFromWechat(WechatUserProfile $profile):array{$nowdate(Y-m-d H:i:s);$data[name$profile-name,avatar$profile-avatar,gender$profile-gender,email$profile-email,mobile$profile-mobile,departmentsjson_encode($profile-departments,JSON_UNESCAPED_UNICODE),updated_at$now,];$existingDb::table(users)-where(wechat_userid,$profile-userId)-first();if($existing){Db::table(users)-where(wechat_userid,$profile-userId)-update($data);returnarray_merge((array)$existing,$data);}$idDb::table(users)-insertGetId(array_merge($data,[wechat_userid$profile-userId,created_at$now,]));returnarray_merge([id$id,wechat_userid$profile-userId],$data);}}---6.登录控制器?php// app/Controller/AuthController.phpnamespaceApp\Controller;use App\Service\JwtService;use App\Service\UserService;use App\Service\WechatOAuthService;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\GetMapping;use Hyperf\HttpServer\Contract\RequestInterface;use Hyperf\HttpServer\Contract\ResponseInterface;use Hyperf\Logger\LoggerFactory;use Psr\Log\LoggerInterface;#[Controller(prefix:/auth)]classAuthController{privateLoggerInterface $logger;publicfunction__construct(privatereadonly WechatOAuthService $oauthService,privatereadonly UserService $userService,privatereadonly JwtService $jwtService,privatereadonly RequestInterface $request,privatereadonly ResponseInterface $response,LoggerFactory $loggerFactory,){$this-logger$loggerFactory-get(auth);}/** * Step 1发起手动授权 * GET /auth/redirect?redirect_urlhttps://your-spa.com/home */#[GetMapping(path:/redirect)]publicfunctionredirect():\Psr\Http\Message\ResponseInterface{$redirectUrl$this-request-query(redirect_url,);$statebase64_encode($redirectUrl);$callbackUrlconfig(wechat.work.default.oauth.callback);$authUrl$this-oauthService-buildAuthUrl($callbackUrl,$state);return$this-response-redirect($authUrl);}/** * Step 2企业微信回调用户手动同意后 * GET /auth/callback?codexxxstatexxx */#[GetMapping(path:/callback)]publicfunctioncallback():\Psr\Http\Message\ResponseInterface{$code$this-request-query(code,);$state$this-request-query(state,);if(empty($code)){return$this-response-json([code400,message缺少 code 参数])-withStatus(400);}try{// 1. code → userId user_ticket[userId$userId,userTicket$userTicket]$this-oauthService-getIdentityByCode($code);// 2. user_ticket → 详细信息$profile$this-oauthService-getUserDetail($userId,$userTicket);$this-logger-info(获取用户详情成功,[userId$userId,name$profile-name,depts$profile-departments,]);// 3. 写入/更新本地用户表$user$this-userService-syncFromWechat($profile);// 4. 生成 JWT$token$this-jwtService-encode($userId,[uid$user[id],name$profile-name,avatar$profile-avatar,]);// 5. 跳回前端携带 token$frontendUrlbase64_decode($state)?:/;$separatorstr_contains($frontendUrl,?)?:?;return$this-response-redirect($frontendUrl.$separator.token.$token);}catch(\Throwable $e){$this-logger-error(企业微信登录失败,[error$e-getMessage()]);return$this-response-json([code500,message$e-getMessage()])-withStatus(500);}}}---7.路由// config/routes.phpRouter::get(/auth/redirect,[App\Controller\AuthController::class,redirect]);Router::get(/auth/callback,[App\Controller\AuthController::class,callback]);---完整流程图 用户在企业微信内打开页面 │ GET/auth/redirect?redirect_url前端地址 │ └─302→ 企业微信授权页弹出xxx应用申请获取你的信息 │ 用户点击「同意」 │ GET/auth/callback?codexxxstatexxx │ ┌───────────┴────────────────┐ │getOAuth()-userFromCode()│ →/cgi-bin/auth/getuserinfo │ 返回 UserIduser_ticket │ └───────────┬────────────────┘ │ ┌───────────┴────────────────┐ │ POST/cgi-bin/auth/│ │ getuserdetail │ → 姓名/头像/部门/手机/邮箱 └───────────┬────────────────┘ │ upsert users 表 │ 生成 JWT token │302→ 前端地址?tokenxxx---getuserdetail 返回字段说明 ┌────────────┬──────────────────────────────────────────────────┐ │ 字段 │ 说明 │ ├────────────┼──────────────────────────────────────────────────┤ │ name │ 成员姓名 │ ├────────────┼──────────────────────────────────────────────────┤ │ gender │ 性别1男2女 │ ├────────────┼──────────────────────────────────────────────────┤ │ avatar │ 头像 URL有效期较短建议落库时下载到自己 CDN │ ├────────────┼──────────────────────────────────────────────────┤ │ qr_code │ 个人二维码 │ ├────────────┼──────────────────────────────────────────────────┤ │ mobile │ 手机号需应用有「手机」权限 │ ├────────────┼──────────────────────────────────────────────────┤ │ email │ 企业邮箱需应用有「邮箱」权限 │ ├────────────┼──────────────────────────────────────────────────┤ │ biz_mail │ 企业邮箱新字段 │ ├────────────┼──────────────────────────────────────────────────┤ │ department │ 所在部门 ID 数组 │ └────────────┴──────────────────────────────────────────────────┘ ▎ mobile/email 需要在企业微信后台 应用管理 → 应用权限 中开启对应字段权限否则返回空。