chore: 入库生产基线(2026-10-26 与 shop.douba.top 一致)

- 11 个已跟踪文件的在途改动此前只存在于工作区,生产已在运行
- 补入两个从未入库的文件:InternalController.php(app_03/box_app 内部接口)、
  config/internal.php(内部通信 key,值从 .env 读取,不含硬编码密钥)
- 逐文件 md5 比对过生产,内容一致

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-26 17:08:55 +08:00
parent c7f85ffbda
commit fc353899a9
13 changed files with 331 additions and 37 deletions

View File

@@ -20,6 +20,7 @@ use crmeb\services\wechat\config\WorkConfig;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\facade\Log;
/**
* 客户管理
@@ -83,11 +84,29 @@ class Client extends AuthController
*/
public function synch()
{
if (!sys_config('wechat_work_corpid', '')) {
$corpId = sys_config('wechat_work_corpid', '');
if (!$corpId) {
return $this->fail('请先配置企微信息');
}
try {
$beforeCount = $this->services->count([]);
Log::info('work client synch start ' . json_encode([
'corp_id' => $corpId,
'before_count' => $beforeCount
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
} catch (\Throwable $e) {
Log::error('work client synch precheck error ' . json_encode([
'corp_id' => $corpId,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
$this->services->delete([["id", "<>", 0]]);
$this->services->authGetExternalcontact();
Log::info('work client synch dispatched ' . json_encode([
'corp_id' => $corpId
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return $this->success('已加入消息队列,请稍后查看');
}

View File

@@ -0,0 +1,134 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\controller\api\v1;
use app\Request;
use app\services\user\UserServices;
use app\services\wechat\RoutineServices;
use app\services\wechat\WechatUserServices;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\exception\ValidateException;
use think\Response;
/**
* 内部服务接口(不经过用户认证)
* 用于 H5 / app_03 后端通过 openid 自动补齐用户信息
*/
class InternalController
{
/**
* 验证内部 API Key
* @param Request $request
* @return bool true=通过,false=拒绝
*/
private function checkInternalKey(Request $request): bool
{
$apiKey = $request->header('X-Internal-Key');
// 注意:在 Swoole 沙箱里 env()/getenv() 经常读不到 .env
// 用 config() 兜底,确保两种部署(PHP-FPM / Swoole)都生效
$configKey = (string)(config('internal.api_key') ?: env('INTERNAL_API_KEY', ''));
return $apiKey && $configKey && $apiKey === $configKey;
}
/**
* 根据 openid 获取用户信息
* @param Request $request
* @param WechatUserServices $wechatUserServices
* @return Response
*/
public function userInfo(Request $request, WechatUserServices $wechatUserServices): Response
{
if (!$this->checkInternalKey($request)) {
return Response::create(['code' => 403, 'msg' => 'Forbidden'], 'json');
}
$openid = $request->param('openid', '');
if (!$openid) {
return Response::create(['code' => 400, 'msg' => 'openid is required'], 'json');
}
try {
$wechatUser = $wechatUserServices->getWechatUserInfo(['openid' => $openid]);
} catch (DataNotFoundException | ModelNotFoundException | DbException $e) {
return Response::create(['code' => 500, 'msg' => '查询失败'], 'json');
}
if (!$wechatUser) {
return Response::create(['code' => 404, 'msg' => 'user not found'], 'json');
}
$uid = (int)$wechatUser['uid'];
$userServices = app()->make(UserServices::class);
try {
$user = $userServices->getUserInfo($uid);
} catch (DataNotFoundException | ModelNotFoundException | DbException $e) {
$user = null;
}
return Response::create([
'code' => 200,
'data' => [
'uid' => $uid,
'nickname' => $user['nickname'] ?? $wechatUser['nickname'] ?? '',
'phone' => $user['phone'] ?? $wechatUser['phone'] ?? '',
'avatar' => $user['avatar'] ?? $wechatUser['headimgurl'] ?? '',
],
], 'json');
}
/**
* 通过微信 code 换 openid(绕过 store_user_mobile 限制)
*
* 设计目的:H5/app_03 在新用户场景下(CRMEB 强制绑手机号=true),
* 原 silenceAuth 返回 {auth_login:1, key:...},前端拿不到 openid 会被
* 引导到授权页。本端点只调微信 code2session 拿 openid/unionid,不查
* store_user_mobile、不建用户、不发 token,前端拿到 openid 后走自己的
* openid-login 即可自助登录。
*
* @param Request $request
* @param RoutineServices $routineServices
* @return Response
*/
public function getOpenidByCode(Request $request, RoutineServices $routineServices): Response
{
if (!$this->checkInternalKey($request)) {
return Response::create(['code' => 403, 'msg' => 'Forbidden'], 'json');
}
$code = $request->param('code', '');
if (!$code) {
return Response::create(['code' => 400, 'msg' => 'code is required'], 'json');
}
try {
$wechatInfo = $routineServices->getUserInfoByCode((string)$code);
} catch (ValidateException $e) {
return Response::create(['code' => 400, 'msg' => $e->getMessage()], 'json');
} catch (\Throwable $e) {
return Response::create(['code' => 500, 'msg' => 'code2session 失败: ' . $e->getMessage()], 'json');
}
$openid = $wechatInfo['openid'] ?? '';
if (!$openid) {
return Response::create(['code' => 500, 'msg' => 'openid 缺失'], 'json');
}
return Response::create([
'code' => 200,
'data' => [
'openid' => $openid,
'unionid' => $wechatInfo['unionid'] ?? '',
],
], 'json');
}
}

View File

@@ -151,7 +151,13 @@ class Routine
{
$token = $this->services->silenceAuth($code, (int)$spread_code, (int)$spread_spid, true);
if ($token && isset($token['auth_login'])) {
return app('json')->success('授权成功');
// 透传 openid/unionid 给小程序端,用于向 H5 注入身份参数
return app('json')->success('授权成功', [
'auth_login' => $token['auth_login'],
'key' => $token['key'],
'openid' => $token['openid'] ?? '',
'unionid' => $token['unionid'] ?? '',
]);
} else if ($token) {
return app('json')->success('登录成功', ['token' => $token['token'], 'userInfo' => $token['userInfo'], 'expires_time' => $token['params']['exp'], 'store_user_avatar' => $token['store_user_avatar'] ?? 0]);
} else

View File

@@ -85,7 +85,7 @@ abstract class HaichaoBridge
$options['http'] = [
'timeout' => 60,
'method' => 'POST',
'header' => 'Content-Type: application/json;charset=utf-8',
'header' => 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
'content' => http_build_query($baseBody)
];
$raw = file_get_contents("{$this->api['api']}$api", false, stream_context_create($options));
@@ -174,12 +174,24 @@ abstract class HaichaoBridge
private function sign(array $data): string
{
$computerStr = "";
foreach ($data as $k => $v) {
$computerStr .= $k . $v;
}
$computerStr = $this->api['appSecret'] . $computerStr;
return md5($computerStr);
if (!$data) {
return '';
}
ksort($data);
$str = '';
foreach ($data as $k => $v) {
if ($k === null || $k === '' || $k === 'sign' || $v === '' || $v === null) {
continue;
}
if (is_array($v) || is_object($v)) {
$v = json_encode($v, JSON_UNESCAPED_UNICODE);
}
$str .= $k . $v;
}
$str = $this->api['appSecret'] . $str;
return bin2hex(md5($str, true));
}
private function isTokenError(array $rt): bool

View File

@@ -18,6 +18,7 @@ use crmeb\traits\QueueTrait;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\facade\Log;
/**
* 同步客户
@@ -39,9 +40,17 @@ class WorkClientJob extends BaseJobs
*/
public function authClient($page, $cursor)
{
Log::info('work client job authClient start ' . json_encode([
'page' => (int)$page,
'cursor_len' => strlen((string)$cursor),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
/** @var WorkClientServices $make */
$make = app()->make(WorkClientServices::class);
$make->authGetExternalcontact($page, $cursor);
Log::info('work client job authClient done ' . json_encode([
'page' => (int)$page,
'cursor_len' => strlen((string)$cursor),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return true;
}

View File

@@ -60,13 +60,15 @@ abstract class BaseServices
* 创建token
* @param int $id
* @param $type
* @param string $pwd
* @param array $extras 额外字段,例如 ['phone' => '138...', 'openid' => 'o...']
* @return array
*/
public function createToken(int $id, $type, string $pwd = '')
public function createToken(int $id, $type, string $pwd = '', array $extras = [])
{
/** @var JwtAuth $jwtAuth */
$jwtAuth = app()->make(JwtAuth::class);
return $jwtAuth->createToken($id, $type, ['auth' => md5($pwd)]);
return $jwtAuth->createToken($id, $type, array_merge(['auth' => md5($pwd)], $extras));
}
/**

View File

@@ -51,7 +51,7 @@ class LoginServices extends BaseServices
*/
public function login($account, $password, $spread_uid)
{
$user = $this->dao->getOne(['phone' => $account], 'uid,pwd,status');
$user = $this->dao->getOne(['phone' => $account], 'uid,pwd,status,phone');
if ($user) {
if ($user->pwd !== md5((string)$password))
throw new ValidateException('账号或密码错误');
@@ -65,7 +65,7 @@ class LoginServices extends BaseServices
$uid = (int)$user['uid'];
//更新用户信息
$token = $this->createToken($uid, 'api', $user->pwd);
$token = $this->createToken($uid, 'api', $user->pwd, ['phone' => $user->phone ?? '']);
if ($token) {
$this->updateUserInfo(['spread_uid' => $spread_uid], $user);
// 用户登录成功事件
@@ -268,7 +268,7 @@ class LoginServices extends BaseServices
$this->updateUserInfo(['spread_uid' => $spread_uid], $user);
$uid = (int)$user['uid'];
$token = $this->createToken($uid, 'api', $user->pwd);
$token = $this->createToken($uid, 'api', $user->pwd, ['phone' => $user->phone ?? '']);
if ($token) {
// 用户登录成功事件
event('user.login', [$uid, app('request')->ip()]);
@@ -305,7 +305,7 @@ class LoginServices extends BaseServices
throw new ValidateException('修改新用户登录类型出错');
}
$uid = (int)$switch_user['uid'];
$token = $this->createToken($uid, 'api', $switch_user['pwd']);
$token = $this->createToken($uid, 'api', $switch_user['pwd'], ['phone' => $switch_user->phone ?? '']);
if ($token) {
// 用户登录成功事件
event('user.login', [$uid, app('request')->ip()]);
@@ -336,7 +336,10 @@ class LoginServices extends BaseServices
$wechatUser = app()->make(WechatUserServices::class);
//更新用户信息
$user = $wechatUser->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
$token = $this->createToken((int)$user['uid'], $userType, $user['pwd']);
$token = $this->createToken((int)$user['uid'], $userType, $user['pwd'], [
'phone' => $user['phone'] ?? $phone,
'openid' => $openid,
]);
if ($token) {
return [
'token' => $token['token'],

View File

@@ -98,7 +98,7 @@ class RoutineServices extends BaseServices
$wechatUserServices = app()->make(WechatUserServices::class);
//写入用户信息
$user = $wechatUserServices->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'routine', $openid);
}
/**
@@ -133,7 +133,7 @@ class RoutineServices extends BaseServices
$wechatUserServices = app()->make(WechatUserServices::class);
//写入用户信息
$user = $wechatUserServices->wechatOauthAfter($createData);
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'routine', $openid);
}
/**
@@ -289,15 +289,22 @@ class RoutineServices extends BaseServices
* 获取返回信息
* @param $user
* @param string $userType
* @param string $openid
* @return array
*/
public function getReturnInfo($user, string $userType = 'routine')
public function getReturnInfo($user, string $userType = 'routine', string $openid = '')
{
if (!$user || !isset($user['uid']) || !$user['uid']) {
throw new ValidateException('获取用户信息失败');
}
$uid = (int)$user['uid'];
$token = $this->createToken($uid, $userType, $user['pwd']);
// 查询 unionid(从 wechat_user 表)
$unionid = $this->dao->value(['uid' => $uid, 'user_type' => $userType, 'is_del' => 0], 'unionid') ?? '';
$token = $this->createToken($uid, $userType, $user["pwd"] ?? "", [
"phone" => $user["phone"] ?? "",
"openid" => $openid,
"unionid" => $unionid,
]);
if (!$token) {
throw new ValidateException('登录失败!');
}
@@ -347,7 +354,7 @@ class RoutineServices extends BaseServices
//更新用户信息
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
}
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, $userType, $openid);
}
/**
@@ -387,7 +394,7 @@ class RoutineServices extends BaseServices
//更新用户信息
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
}
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, $userType, $openid);
}
/**
@@ -416,7 +423,13 @@ class RoutineServices extends BaseServices
if ($notLogin || $storeUserMobile) {
$userInfoKey = md5($openid . '_' . time() . '_routine');
CacheService::setTokenBucket($userInfoKey, $createData, 7200);
return ['auth_login' => 1, 'key' => $userInfoKey];
// notLogin=true 时也透传 openid/unionid,让前端(H5/小程序)能拿到稳定身份标识
return [
'auth_login' => 1,
'key' => $userInfoKey,
'openid' => $openid,
'unionid' => $routineInfo['unionid'] ?? '',
];
} else {
//写入用户信息
$user = $wechatUserServices->wechatOauthAfter($createData);
@@ -425,7 +438,7 @@ class RoutineServices extends BaseServices
//更新用户信息
$wechatUserServices->wechatUpdata([$user['uid'], ['spread_uid' => $spread_uid]]);
}
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'routine', $openid);
}
@@ -452,7 +465,7 @@ class RoutineServices extends BaseServices
$wechatUserServices = app()->make(WechatUserServices::class);
//写入用户信息
$user = $wechatUserServices->wechatOauthAfter($createData);
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'routine', $openid);
}
/**
@@ -493,7 +506,7 @@ class RoutineServices extends BaseServices
$wechatUserServices = app()->make(WechatUserServices::class);
//写入用户信息
$user = $wechatUserServices->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'routine', $openid);
}
/**

View File

@@ -116,15 +116,19 @@ class WechatServices extends BaseServices
* 获取返回信息
* @param $user
* @param string $userType
* @param string $openid
* @return array
*/
public function getReturnInfo($user, string $userType = 'wechat')
public function getReturnInfo($user, string $userType = 'wechat', string $openid = '')
{
if (!$user || !isset($user['uid']) || !$user['uid']) {
throw new ValidateException('获取用户信息失败');
}
$uid = (int)$user['uid'];
$token = $this->createToken($uid, $userType, $user['pwd'] ?? '');
$token = $this->createToken($uid, $userType, $user['pwd'] ?? '', [
'phone' => $user['phone'] ?? '',
'openid' => $openid,
]);
if (!$token) {
throw new ValidateException('登录失败!');
}
@@ -179,7 +183,7 @@ class WechatServices extends BaseServices
} else {
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
}
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'wechat', $openid);
}
/**
@@ -218,7 +222,7 @@ class WechatServices extends BaseServices
//更新用户信息
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
}
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'wechat', $openid);
}
/**
@@ -260,7 +264,7 @@ class WechatServices extends BaseServices
//更新用户信息
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
}
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'wechat', $openid);
}
/**
@@ -332,7 +336,7 @@ class WechatServices extends BaseServices
//更新用户信息
$wechatUserServices->wechatUpdata([$user['uid'], ['spread_uid' => $spread_uid]]);
}
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, 'wechat', $openid);
}
/**
@@ -357,7 +361,7 @@ class WechatServices extends BaseServices
$wechatUser = app()->make(WechatUserServices::class);
//更新用户信息
$user = $wechatUser->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
return $this->getReturnInfo($user);
return $this->getReturnInfo($user, $userType, $openid);
}
/**
@@ -410,7 +414,7 @@ class WechatServices extends BaseServices
$wechatUser = app()->make(WechatUserServices::class);
//更新用户信息
$user = $wechatUser->wechatOauthAfter([$openid, $userInfo, $spread_uid, $login_type, $userType]);
$token = $this->getReturnInfo($user);
$token = $this->getReturnInfo($user, $userType, $openid);
$token['isbind'] = false;
return $token;
}

View File

@@ -104,6 +104,10 @@ class WorkClientServices extends BaseServices
$config = app()->make(WorkConfig::class);
$corpId = $config->corpId;
if (!$corpId) {
Log::info('work client authGetExternalcontact skip (empty corp_id) ' . json_encode([
'page' => $page,
'cursor_len' => strlen($cursor),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return true;
}
/** @var WorkMemberServices $memberService */
@@ -111,6 +115,22 @@ class WorkClientServices extends BaseServices
$menmberList = $memberService->getDataList(['corp_id' => $corpId], ['userid'], $page, 1);
//没有数据就返回成功
if (!$menmberList) {
$memberCount = 0;
try {
$memberCount = $memberService->count(['corp_id' => $corpId]);
} catch (\Throwable $e) {
Log::error('work client authGetExternalcontact member count error ' . json_encode([
'corp_id' => $corpId,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
Log::info('work client authGetExternalcontact done (no member) ' . json_encode([
'corp_id' => $corpId,
'page' => $page,
'member_count' => $memberCount,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
return true;
}
@@ -123,18 +143,43 @@ class WorkClientServices extends BaseServices
}
$userids = array_column($menmberList, 'userid');
Log::info('work client authGetExternalcontact fetch ' . json_encode([
'corp_id' => $corpId,
'page' => $page,
'cursor_len' => strlen($cursor),
'userids_count' => count($userids),
'tag_group_count' => count($tagRes['tag_group'] ?? []),
'tag_map_count' => count($tagMap),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
try {
$response = Work::getBatchClientList($userids, $cursor);
$externalContactList = $response['external_contact_list'] ?? [];
}catch (\Throwable $e){
Log::error('work client authGetExternalcontact batchGet exception ' . json_encode([
'corp_id' => $corpId,
'page' => $page,
'cursor_len' => strlen($cursor),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
WorkClientJob::dispatchDo('authClient', [$page + 1, '']);
return true;
}
$stats = [
'external_contact_count' => count($externalContactList),
'client_insert' => 0,
'client_update' => 0,
'follow_insert' => 0,
'follow_update' => 0,
'tag_save' => 0,
'missing_client_map' => 0,
];
$external = [];
$followUser = [];//内部人员跟踪
$externalUserids = [];//客户信息
$this->transaction(function () use ($externalContactList, $corpId, $externalUserids, $followUser, $external, $tagMap) {
$followUser = [];
$externalUserids = [];
$this->transaction(function () use ($externalContactList, $corpId, $tagMap, &$stats, &$externalUserids, &$followUser, &$external) {
/** @var WechatUserServices $wechatUserService */
$wechatUserService = app()->make(WechatUserServices::class);
foreach ($externalContactList as $item) {
@@ -196,10 +241,12 @@ class WorkClientServices extends BaseServices
if ($this->dao->count(['external_userid' => $externalUserid, 'corp_id' => $corpId])) {
unset($externalContact['external_userid']);
$this->dao->update(['external_userid' => $externalUserid], $externalContact);
$stats['client_update']++;
} else {
$externalContact['create_time'] = $item['follow_info']['createtime'] ??time();
$externalContact['update_time'] = time();
$external[] = $externalContact;
$stats['client_insert']++;
}
}
if ($external) {
@@ -214,6 +261,7 @@ class WorkClientServices extends BaseServices
$tagService = app()->make(WorkClientFollowTagsServices::class);
foreach ($followUser as $userid => $items) {
if(!isset($clientList[$userid])){
$stats['missing_client_map']++;
continue;
}
$items['client_id'] = $clientList[$userid];
@@ -227,9 +275,11 @@ class WorkClientServices extends BaseServices
'add_way' => $items['add_way'],
'oper_userid' => $items['oper_userid'],
]);
$stats['follow_update']++;
} else {
$res = $followService->save($items);
$id = $res->id;
$stats['follow_insert']++;
}
if (!empty($items['tags'])) {
$tagService->delete(['follow_id' => $id]);
@@ -237,14 +287,32 @@ class WorkClientServices extends BaseServices
$tag['follow_id'] = $id;
}
$tagService->saveAll($items['tags']);
$stats['tag_save'] += count($items['tags']);
}
}
}
});
Log::info('work client authGetExternalcontact processed ' . json_encode([
'corp_id' => $corpId,
'page' => $page,
'cursor_len' => strlen($cursor),
'next_cursor_len' => strlen((string)($response['next_cursor'] ?? '')),
'stats' => $stats,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
if (isset($response['next_cursor']) && $response['next_cursor']) {
Log::info('work client authGetExternalcontact dispatch next_cursor ' . json_encode([
'corp_id' => $corpId,
'page' => $page,
'next_cursor_len' => strlen((string)$response['next_cursor']),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
WorkClientJob::dispatchDo('authClient', [$page, $response['next_cursor']]);
} else if (empty($response['next_cursor'])) {
Log::info('work client authGetExternalcontact dispatch next_page ' . json_encode([
'corp_id' => $corpId,
'page' => $page + 1,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
WorkClientJob::dispatchDo('authClient', [$page + 1, '']);
}