hyperf方案 对接企业微信实现一个 HyperF 命令行任务,将企业微信全量通讯录(部门 + 员工)同步到本地数据库,使用 upsert 方式处理新增和更新,并输出同步统计(新增/更新/跳过

张开发
2026/4/15 3:33:11 15 分钟阅读

分享文章

hyperf方案 对接企业微信实现一个 HyperF 命令行任务,将企业微信全量通讯录(部门 + 员工)同步到本地数据库,使用 upsert 方式处理新增和更新,并输出同步统计(新增/更新/跳过
设计思路 ─ ─────────────────────────── 请求进入 │ ├─ 检查 tokenHeader:Authorization/Cookie/Query │ ├─ 有效 → 注入用户信息到 Context放行 │ └─ 无效/缺失 │ ├─ Accept:application/jsonAPI 请求→ 返回401JSON │ └─ 浏览器请求 → 记录原始 URL → 跳转企业微信授权 │ 授权回调/auth/callback │ └─ 换取 userId → 生成 token →302跳回原始 URL携带 token---1.用户上下文协程安全?php// app/Context/UserContext.phpnamespaceApp\Context;use Hyperf\Context\Context;classUserContext{privateconstKEYauth.user;publicstaticfunctionset(array $user):void{Context::set(self::KEY,$user);}publicstaticfunctionget():?array{returnContext::get(self::KEY);}publicstaticfunctionuserId():string{returnself::get()[wechat_userid]??;}publicstaticfunctionuid():int{return(int)(self::get()[uid]??0);}}---2.Token 提取工具?php// app/Support/TokenExtractor.phpnamespaceApp\Support;use Psr\Http\Message\ServerRequestInterface;classTokenExtractor{/** * 按优先级从请求中提取 token * 1. Authorization: Bearer xxx * 2. Cookie: tokenxxx * 3. Query: ?tokenxxx */publicstaticfunctionextract(ServerRequestInterface $request):string{// 1. Header$authorization$request-getHeaderLine(Authorization);if(str_starts_with($authorization,Bearer )){returnsubstr($authorization,7);}// 2. Cookie$cookies$request-getCookieParams();if(!empty($cookies[token])){return$cookies[token];}// 3. Query string$params$request-getQueryParams();if(!empty($params[token])){return$params[token];}return;}/** * 判断是否为 API 请求期望 JSON 响应 */publicstaticfunctionisApiRequest(ServerRequestInterface $request):bool{$accept$request-getHeaderLine(Accept);$contentType$request-getHeaderLine(Content-Type);returnstr_contains($accept,application/json)||str_contains($contentType,application/json)||str_starts_with($request-getUri()-getPath(),/api/);}}---3.认证中间件?php// app/Middleware/WechatAuthMiddleware.phpnamespaceApp\Middleware;use App\Context\UserContext;use App\Service\JwtService;use App\Service\WechatOAuthService;use App\Support\TokenExtractor;use Hyperf\Contract\ConfigInterface;use Hyperf\HttpMessage\Stream\SwooleStream;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;use Psr\Http\Server\MiddlewareInterface;use Psr\Http\Server\RequestHandlerInterface;use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;use Psr\Log\LoggerInterface;use Hyperf\Logger\LoggerFactory;classWechatAuthMiddlewareimplements MiddlewareInterface{privateLoggerInterface $logger;publicfunction__construct(privatereadonly JwtService $jwtService,privatereadonly WechatOAuthService $oauthService,privatereadonly HttpResponse $response,privatereadonly ConfigInterface $config,LoggerFactory $loggerFactory,){$this-logger$loggerFactory-get(auth);}publicfunctionprocess(ServerRequestInterface $request,RequestHandlerInterface $handler):ResponseInterface{$tokenTokenExtractor::extract($request);// ── token 存在验证并注入上下文 ──────────────────────────if(!empty($token)){try{$payload$this-jwtService-decode($token);UserContext::set([uid$payload-uid,wechat_userid$payload-sub,name$payload-name??,avatar$payload-avatar??,]);return$handler-handle($request);}catch(\Throwable $e){$this-logger-warning(token 验证失败,[error$e-getMessage()]);// token 无效继续走未授权逻辑}}// ── 未授权处理 ─────────────────────────────────────────────returnTokenExtractor::isApiRequest($request)?$this-unauthorizedJson():$this-redirectToOAuth($request);}/** * API 请求返回 401 JSON */privatefunctionunauthorizedJson():ResponseInterface{$bodyjson_encode([code401,message未授权请先登录,],JSON_UNESCAPED_UNICODE);return$this-response-withStatus(401)-withHeader(Content-Type,application/json; charsetutf-8)-withBody(newSwooleStream($body));}/** * 浏览器请求记录原始 URL跳转企业微信授权 */privatefunctionredirectToOAuth(ServerRequestInterface $request):ResponseInterface{$uri$request-getUri();$originalUrl(string)$uri;// 完整原始 URL$statebase64_encode($originalUrl);// 透传给回调$callbackUrl$this-config-get(wechat.work.default.oauth.callback);$authUrl$this-oauthService-buildAuthUrl($callbackUrl,$state);$this-logger-info(未授权跳转企业微信授权,[original$originalUrl]);return$this-response-redirect($authUrl);}}---4.授权回调控制器处理 state 还原原始 URL?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);}/** * 企业微信回调 * GET /auth/callback?codexxxstatebase64(原始URL) */#[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 → userIdsnsapi_base或 userId user_ticketsnsapi_privateinfo [userId$userId,userTicket$userTicket]$this-oauthService-getIdentityByCode($code);// 2. 获取详情并落库有 user_ticket 时if(!empty($userTicket)){$profile$this-oauthService-getUserDetail($userId,$userTicket);$user$this-userService-syncFromWechat($profile);}else{$user$this-userService-findOrCreate($userId);}// 3. 生成 JWT$token$this-jwtService-encode($userId,[uid$user[id],name$user[name]??,avatar$user[avatar]??,]);$this-logger-info(登录成功,[userId$userId]);// 4. 还原原始 URL将 token 写入 Cookie 并跳回$originalUrlbase64_decode($state)?:/;$ttl(int)config(wechat.jwt.ttl,7200);return$this-response-withCookie($this-buildCookie(token,$token,$ttl))-redirect($originalUrl);}catch(\Throwable $e){$this-logger-error(登录失败,[error$e-getMessage()]);return$this-response-json([code500,message$e-getMessage()])-withStatus(500);}}privatefunctionbuildCookie(string $name,string $value,int$ttl):\Hyperf\HttpMessage\Cookie\Cookie{returnnew\Hyperf\HttpMessage\Cookie\Cookie(name:$name,value:$value,expire:time()$ttl,path:/,httpOnly:true,// 防 XSSsecure:true,// 仅 HTTPSsameSite:Lax,);}}---5.注册中间件 全局注册所有路由生效// config/autoload/middlewares.phpreturn[http[App\Middleware\WechatAuthMiddleware::class,],];路由组注册推荐只保护需要登录的路由// config/routes.phpuse App\Middleware\WechatAuthMiddleware;// 公开路由不需要登录Router::get(/auth/callback,[App\Controller\AuthController::class,callback]);Router::get(/health,fn()ok);// 需要登录的路由组Router::addGroup(,function(){Router::get(/dashboard,[App\Controller\DashboardController::class,index]);Router::addGroup(/api,function(){Router::get(/user/me,[App\Controller\UserController::class,me]);Router::post(/message,[App\Controller\MessageController::class,send]);});},[middleware[WechatAuthMiddleware::class]]);---6.控制器中获取当前用户?php// app/Controller/UserController.phpnamespaceApp\Controller;use App\Context\UserContext;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\GetMapping;use Hyperf\HttpServer\Contract\ResponseInterface;#[Controller(prefix:/api/user)]classUserController{publicfunction__construct(privatereadonly ResponseInterface $response,){}#[GetMapping(path:/me)]publicfunctionme():\Psr\Http\Message\ResponseInterface{return$this-response-json([code0,data[uidUserContext::uid(),user_idUserContext::userId(),nameUserContext::get()[name]??,avatarUserContext::get()[avatar]??,],]);}}---完整请求流程 浏览器访问/dashboard无 token │ WechatAuthMiddleware │ ├─ 无 token →redirectToOAuth()│ └─ statebase64(/dashboard)│ └─302→ 企业微信授权页 │ 用户同意授权 │ GET/auth/callback?codexxxstatebase64(/dashboard)│ ├─ code → userId → 生成 JWT ├─ Set-Cookie:tokenxxx;HttpOnly;Secure └─302→/dashboard原始 URL │ 浏览器再次访问/dashboard携带 Cookie token │ WechatAuthMiddleware │ ├─ 解析 Cookie token → 验证通过 ├─UserContext::set(用户信息)└─ 放行 → 控制器处理---token 传递方式对比 ┌─────────────────────────────┬──────────────────────┬──────────────────┐ │ 方式 │ 适用场景 │ 安全性 │ ├─────────────────────────────┼──────────────────────┼──────────────────┤ │ CookieHttpOnlySecure │ 浏览器页面 │ 高防 XSS │ ├─────────────────────────────┼──────────────────────┼──────────────────┤ │ Authorization Header │ API/SPA │ 需前端手动管理 │ ├─────────────────────────────┼──────────────────────┼──────────────────┤ │ URL Query?token│ 回调跳转时一次性传递 │ 低不要长期使用 │ └─────────────────────────────┴──────────────────────┴──────────────────┘ ▎ 中间件按 Header → Cookie → Query 优先级提取回调时通过 Cookie ▎ 写入后续请求自动携带无需前端额外处理。

更多文章