email)) { return Doo::translate($value); } return $value; } return Base::formatName($this->email); } /** * 头像地址 * @param $value * @return string */ public function getUserimgAttribute($value) { return self::getAvatar($this->userid, $value, $this->email, $this->nickname); } /** * 身份权限 * @param $value * @return array */ public function getIdentityAttribute($value) { if (empty($value)) { return []; } return array_filter(is_array($value) ? $value : explode(",", trim($value, ","))); } /** * 部门 * @param $value * @return array */ public function getDepartmentAttribute($value) { if (empty($value)) { return []; } return array_filter(is_array($value) ? $value : Base::explodeInt($value)); } /** * 获取所属部门名称 * @return string */ public function getDepartmentName() { if (empty($this->department)) { return ""; } $key = "UserDepartment::" . md5(Cache::get("UserDepartment::rand") . '-' . implode(',' , $this->department)); $list = Cache::remember($key, now()->addMonth(), function() { $list = UserDepartment::select(['id', 'owner_userid', 'name'])->whereIn('id', $this->department)->take(10)->get(); return $list->toArray(); }); $array = []; foreach ($list as $item) { $array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? ' (M)' : ''); } return implode(', ', $array); } /** * 判断是否为部门负责人 */ public function isDepartmentOwner() { return UserDepartment::where('owner_userid', $this->userid)->exists(); } /** * 获取机器人所有者 * @return int */ public function getBotOwner() { if (!$this->bot) { return 0; } $key = "userBotOwner::" . $this->userid; return intval(Cache::remember($key, now()->addMonth(), function() { return intval(UserBot::whereBotId($this->userid)->value('userid')) ?: $this->userid; })); } /** * 是否在线 * @return bool */ public function getOnlineStatus() { $online = $this->bot || OnlineData::live($this->userid) > 0; if ($online) { return true; } return WebSocket::whereUserid($this->userid)->exists(); } /** * 返回是否LDAP用户 * @return bool */ public function isLdap() { return in_array('ldap', $this->identity); } /** * 返回是否临时帐号 * @return bool */ public function isTemp() { return in_array('temp', $this->identity); } /** * 返回是否禁用帐号(离职) * @param bool $incAt 是否包含禁用时间 * @return bool */ public function isDisable($incAt = false) { if ($incAt) { return in_array('disable', $this->identity) || $this->disable_at; } return in_array('disable', $this->identity); } /** * 返回是否管理员 * @return bool */ public function isAdmin() { return in_array('admin', $this->identity); } /** * 返回是否AI机器人 * @return bool */ public function isAiBot(&$aiName = '') { if (preg_match('/^ai-(.*?)@bot\.system$/', $this->email, $matches)) { $aiName = $matches[1]; return true; } return false; } /** * 返回是否用户机器人 * @return bool */ public function isUserBot() { if (preg_match('/^user-(.*?)@bot\.system$/', $this->email)) { return true; } return false; } /** * 判断是否管理员 */ public function checkAdmin() { $this->identity('admin'); } /** * 判断用户权限(身份) * @param $identity */ public function identity($identity) { if (!in_array($identity, $this->identity)) { throw new ApiException('权限不足'); } } /** * 检查环境是否允许 * @param null $onlyUserid 仅指定会员 */ public function checkSystem($onlyUserid = null) { if ($onlyUserid && $onlyUserid != $this->userid) { return; } if (env("PASSWORD_ADMIN") == 'disabled') { if ($this->userid == 1) { throw new ApiException('当前环境禁止此操作'); } } if (env("PASSWORD_OWNER") == 'disabled') { throw new ApiException('当前环境禁止此操作'); } } /** * 删除会员 * @param $reason * @return bool|null */ public function deleteUser($reason) { $ret = AbstractModel::transaction(function () use ($reason) { // 删除原因 $userDelete = UserDelete::createInstance([ 'operator' => User::userid(), 'userid' => $this->userid, 'email' => $this->email, 'reason' => $reason, 'cache' => array_merge($this->getRawOriginal(), [ 'department_name' => $this->getDepartmentName() ]) ]); $userDelete->save(); // 删除未读 WebSocketDialogMsgRead::whereUserid($this->userid)->delete(); // 删除待办 WebSocketDialogMsgTodo::whereUserid($this->userid)->delete(); // 删除邮箱验证记录 UserEmailVerification::whereEmail($this->email)->delete(); // return $this->delete(); }); return $ret; } /** * 检查发送聊天内容前必须设置昵称、电话 * @return void */ public function checkChatInformation() { if ($this->bot) { return; } $chatInformation = Base::settingFind('system', 'chat_information'); if ($chatInformation == 'required') { if (empty($this->getRawOriginal('nickname'))) { throw new ApiException('请设置昵称', [], -2); } if (empty($this->getRawOriginal('tel'))) { throw new ApiException('请设置联系电话', [], -3); } } } /** ***************************************************************************************** */ /** ***************************************************************************************** */ /** ***************************************************************************************** */ /** * 注册会员 * @param $email * @param $password * @param array $other * @return self */ public static function reg($email, $password, $other = []) { // 邮箱 if (!Base::isEmail($email)) { throw new ApiException('请输入正确的邮箱地址'); } $user = self::whereEmail($email)->first(); if ($user) { $isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open'; if ($isRegVerify && $user->email_verity === 0) { UserEmailVerification::userEmailSend($user); throw new ApiException('您的帐号已注册过,请验证邮箱', ['code' => 'email']); } throw new ApiException('邮箱地址已存在'); } // 密码 self::passwordPolicy($password); // 开始注册 $user = Doo::userCreate($email, $password); if ($other) { $user->updateInstance($other); } $user->az = Base::getFirstCharter($user->nickname); $user->pinyin = Base::cn2pinyin($user->nickname); $user->created_ip = Base::getIp(); if ($user->save()) { $setting = Base::setting('system'); $reg_identity = $setting['reg_identity'] ?: 'normal'; $all_group_autoin = $setting['all_group_autoin'] ?: 'yes'; // 注册临时身份 if ($reg_identity === 'temp') { $user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['temp']), ['temp'])); $user->save(); } // 加入全员群组 if ($all_group_autoin === 'yes') { $dialog = WebSocketDialog::whereGroupType('all')->orderByDesc('id')->first(); $dialog?->joinGroup($user->userid, 0); } } $createdUser = $user->find($user->userid); if (!$createdUser->bot) { // Manticore 索引同步 AbstractObserver::taskDeliver(new ManticoreSyncTask('user_sync', $createdUser->toArray())); // 触发 user_onboard hook Apps::dispatchUserHook($createdUser, 'user_onboard', 'onboard'); } return $createdUser; } /** * 管理员创建员工账号(复用注册逻辑,强制正式身份,可选首登改密 / 部门 / 职位) * @param string $email * @param string $password * @param string $nickname * @param array $options changePass(bool,默认true) / emailVerity(bool,默认false,标记邮箱已认证) / department(int[]) / profession(string) * @return self * @throws ApiException */ public static function createByAdmin(string $email, $password, string $nickname, array $options = []): self { $nickname = trim($nickname); if (mb_strlen($nickname) < 2 || mb_strlen($nickname) > 20) { throw new ApiException('昵称需为2-20个字'); } $changePass = ($options['changePass'] ?? true) ? 1 : 0; $emailVerity = ($options['emailVerity'] ?? false) ? 1 : 0; $profession = trim((string)($options['profession'] ?? '')); // 校验前置(reg 之前快速失败,且可在无 Swoole 环境单测) self::assertValidProfession($profession); $departmentIds = self::assertValidDepartments($options['department'] ?? []); // 复用 reg:邮箱校验/查重、passwordPolicy、Doo::userCreate、az/pinyin、全员群、索引同步、user_onboard hook $user = self::reg($email, $password, ['nickname' => $nickname]); // 管理员显式创建的账号视为正式员工,去除系统 reg_identity 可能带上的 temp if (in_array('temp', $user->identity)) { $user->identity = Base::arrayImplode(array_diff($user->identity, ['temp'])); } $user->changepass = $changePass; // 复用现有首登强制改密机制 $user->email_verity = $emailVerity; // 管理员可在创建时直接标记邮箱认证状态 if ($profession !== '') { $user->profession = $profession; } if ($departmentIds) { $user->department = Base::arrayImplode($departmentIds); } $user->save(); // 设置了部门 → 加入对应部门群(复刻 operation 的 type=department 入群逻辑) if ($departmentIds) { $departments = UserDepartment::whereIn('id', $departmentIds)->get(); foreach ($departments as $department) { try { if ($department->dialog_id > 0 && $dialog = WebSocketDialog::find($department->dialog_id)) { $dialog->joinGroup([$user->userid], 0, true); $dialog->pushMsg("groupJoin", null, [$user->userid]); } } catch (\Throwable $e) { // 部门入群为尽力投递:单个部门失败不影响账号创建与其他部门 \Log::warning('createByAdmin: 部门入群失败', [ 'userid' => $user->userid, 'department_id' => $department->id, 'error' => $e->getMessage(), ]); } } } return $user; } /** * 将上传表格(Excel::toArray 的二维数组)归一化为导入行 * @param array $sheet * @return array [{line, email, nickname, password}] */ public static function parseImportRows(array $sheet): array { $rows = []; foreach ($sheet as $index => $cells) { if ($index === 0) { continue; // 表头 } $email = trim((string)($cells[0] ?? '')); $nickname = trim((string)($cells[1] ?? '')); $password = trim((string)($cells[2] ?? '')); $profession = trim((string)($cells[3] ?? '')); if ($email === '' && $nickname === '' && $password === '') { continue; // 空行(仅职位有值也视为空行跳过) } $rows[] = [ 'line' => $index + 1, // 电子表格行号(从 1 开始) 'email' => $email, 'nickname' => $nickname, 'password' => $password, 'profession' => $profession, ]; } return $rows; } /** * 校验单条导入行 * @param array $row ['email'=>,'nickname'=>,'password'=>,'profession'=>(选填)] * @return string|null 错误文案;null 表示通过 */ public static function validateImportRow(array $row): ?string { $email = trim((string)($row['email'] ?? '')); $nickname = trim((string)($row['nickname'] ?? '')); $password = trim((string)($row['password'] ?? '')); if ($email === '' || $nickname === '' || $password === '') { return '邮箱、昵称、初始密码均为必填'; } if (!Base::isEmail($email)) { return '邮箱格式不正确'; } if (mb_strlen($nickname) < 2 || mb_strlen($nickname) > 20) { return '昵称需为2-20个字'; } try { self::passwordPolicy($password); } catch (ApiException $e) { return $e->getMessage(); } // 职位/职称选填,填写则校验 2-20 字 try { self::assertValidProfession((string)($row['profession'] ?? '')); } catch (ApiException $e) { return $e->getMessage(); } return null; } /** * 校验职位/职称:非空时必须 2-20 字(复用 operation 的现有文案) * @param string $profession * @return void * @throws ApiException */ public static function assertValidProfession(string $profession): void { $profession = trim($profession); if ($profession === '') { return; } if (mb_strlen($profession) < 2) { throw new ApiException('职位/职称不可以少于2个字'); } if (mb_strlen($profession) > 20) { throw new ApiException('职位/职称最多只能设置20个字'); } } /** * 规整并校验部门 ID 列表:转正整数去重、最多 10 个、且每个必须存在 * @param mixed $ids * @return int[] * @throws ApiException */ public static function assertValidDepartments($ids): array { if (!is_array($ids)) { $ids = []; } $ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0))); if (count($ids) > 10) { throw new ApiException('最多只可加入10个部门'); } if ($ids) { $existing = UserDepartment::whereIn('id', $ids)->pluck('id')->map(fn($v) => (int)$v)->all(); if (count($existing) < count($ids)) { throw new ApiException('修改部门不存在'); } } return $ids; } /** * 批量导入用户(部门/职位逐行:department 来自前端逐行设置,profession 来自 Excel 行) * @param array $rows 每行含 email/nickname/password/profession,可选 department(int[]) * @param bool $changePass 是否要求首登改密(对本批所有账号生效) * @return array ['total'=>int, 'success'=>int, 'failed'=>[['line','email','reason']]] * @throws ApiException 行数超限 */ public static function importUsers(array $rows, bool $changePass = true): array { if (count($rows) > self::IMPORT_MAX) { throw new ApiException('单次最多导入' . self::IMPORT_MAX . '条'); } $success = 0; $failed = []; $seen = []; foreach ($rows as $row) { $error = self::validateImportRow($row); if ($error === null) { $emailLower = strtolower(trim((string)$row['email'])); if (isset($seen[$emailLower])) { $error = '文件内邮箱重复'; } else { $seen[$emailLower] = true; } } if ($error === null) { try { self::createByAdmin($row['email'], $row['password'], $row['nickname'], [ 'changePass' => $changePass, 'emailVerity' => !empty($row['email_verity']), 'department' => $row['department'] ?? [], 'profession' => $row['profession'] ?? '', ]); $success++; continue; } catch (ApiException $e) { $error = $e->getMessage(); } } $failed[] = [ 'line' => $row['line'] ?? 0, 'email' => $row['email'] ?? '', 'reason' => $error, ]; } return [ 'total' => count($rows), 'success' => $success, 'failed' => $failed, ]; } /** * 批量导入预览(只解析+校验,不创建任何账号) * 逐行判定 ok/error:必填/邮箱格式/昵称长度/密码策略、文件内邮箱重复、系统中邮箱已存在 * @param array $rows parseImportRows 的输出 * @return array ['total'=>int,'valid'=>int,'invalid'=>int,'rows'=>[['line','email','nickname','password','status','reason']]] */ public static function importPreview(array $rows): array { if (count($rows) > self::IMPORT_MAX) { throw new ApiException('单次最多导入' . self::IMPORT_MAX . '条'); } // 预查系统中已存在的邮箱(小写比较) $emails = []; foreach ($rows as $row) { $e = strtolower(trim((string)($row['email'] ?? ''))); if ($e !== '') { $emails[$e] = true; } } $existing = []; if ($emails) { foreach (self::whereIn('email', array_keys($emails))->pluck('email') as $em) { $existing[strtolower($em)] = true; } } $seen = []; $valid = 0; $list = []; foreach ($rows as $row) { $reason = self::validateImportRow($row); $emailLower = strtolower(trim((string)($row['email'] ?? ''))); if ($reason === null) { if (isset($seen[$emailLower])) { $reason = '文件内邮箱重复'; } else { $seen[$emailLower] = true; if (isset($existing[$emailLower])) { $reason = '邮箱地址已存在'; } } } $ok = $reason === null; if ($ok) { $valid++; } $list[] = [ 'line' => $row['line'] ?? 0, 'email' => $row['email'] ?? '', 'nickname' => $row['nickname'] ?? '', 'password' => $row['password'] ?? '', 'profession' => $row['profession'] ?? '', 'email_verity' => 1, // 默认标记为已认证,前端可在预览中按行调整 'status' => $ok ? 'ok' : 'error', 'reason' => $reason ?? '', ]; } return [ 'total' => count($rows), 'valid' => $valid, 'invalid' => count($rows) - $valid, 'rows' => $list, ]; } /** * 获取我的ID * @return int */ public static function userid() { $user = self::authInfo(); if (!$user) { return 0; } return $user->userid; } /** * 获取我的昵称 * @return string */ public static function nickname() { $user = self::authInfo(); if (!$user) { return ''; } return $user->nickname; } /** * 用户身份认证(获取用户信息) * @param null $identity 判断身份 * @return self */ public static function auth($identity = null) { $user = self::authInfo(); if (!$user) { $token = Base::token(); if ($token) { UserDevice::forget($token); throw new ApiException('身份已失效,请重新登录', [], -1); } else { throw new ApiException('请登录后继续...', [], -1); } } if ($user->isDisable()) { throw new ApiException('帐号已停用...', [], -1); } if ($identity) { $user->identity($identity); } return $user; } /** * 用户身份认证(获取用户信息) * @return self|false */ private static function authInfo() { if (RequestContext::has('auth')) { // 缓存 return RequestContext::get('auth'); } if (Doo::userId() <= 0) { // 没有登录 return RequestContext::save('auth', false); } if (Doo::userExpired()) { // 登录过期 return RequestContext::save('auth', false); } if (!UserDevice::check()) { // token 不存在 return RequestContext::save('auth', false); } $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first(); if (!$user) { // 登录信息不匹配 return RequestContext::save('auth', false); } // 更新登录信息 $upArray = []; if (Base::getIp() && $user->line_ip != Base::getIp()) { $upArray['line_ip'] = Base::getIp(); } if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) { $upArray['line_at'] = Carbon::now(); } $headerLanguage = RequestContext::get('header_language'); if (empty($user->lang) || $headerLanguage) { if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) { $upArray['lang'] = $headerLanguage; } } if ($upArray) { $user->updateInstance($upArray); $user->save(); } return RequestContext::save('auth', $user); } /** * 生成 token * @param self $userinfo * @param bool $refresh 获取新的token * @return string */ public static function generateToken($userinfo, $refresh = false) { if (!$refresh) { if (Doo::userId() != $userinfo->userid || Doo::userEmail() != $userinfo->email || Doo::userEncrypt() != $userinfo->encrypt) { $refresh = true; } } if ($refresh) { $days = $userinfo->bot ? 0 : max(1, intval(Base::settingFind('system', 'token_valid_days', 30))); $token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt, $days); } else { $token = Doo::userToken(); } UserDevice::record($token); unset($userinfo->encrypt); unset($userinfo->password); return $userinfo->token = $token; } /** * 生成无设备的 token(主要用于接口调用,此 token 不检查设备是否存在) * @param self $userinfo * @param $ttl * @return mixed */ public static function generateTokenNoDevice($userinfo, $ttl) { $key = 'user_token_no_device_' . $userinfo->userid; return Cache::remember($key, $ttl, function () use ($userinfo, $ttl) { $token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt); Cache::put(UserDevice::ck(md5($token)), $userinfo->userid, $ttl); return $token; }); } /** * userid 获取 基础信息 * @param int $userid 会员ID * @return self */ public static function userid2basic($userid, $addField = []) { if (empty($userid)) { return null; } $userid = intval($userid); if (RequestContext::has("userid2basic_" . $userid)) { return RequestContext::get("userid2basic_" . $userid); } $userInfo = self::whereUserid($userid)->select(array_merge(User::$basicField, $addField))->first(); if ($userInfo) { $userInfo->online = $userInfo->getOnlineStatus(); $userInfo->department_name = $userInfo->getDepartmentName(); } return RequestContext::save("userid2basic_" . $userid, $userInfo ?: []); } /** * userid 获取 昵称 * @param $userid * @return string */ public static function userid2nickname($userid) { return self::userid2basic($userid)?->nickname ?: ''; } /** * 是否需要验证码 * @param $email * @return array */ public static function needCode($email) { $login_code = Base::settingFind('system', 'login_code'); switch ($login_code) { case 'open': return Base::retSuccess('need'); case 'close': return Base::retError('no'); default: if (Cache::get("code::" . $email) == 'need') { return Base::retSuccess('need'); } else { return Base::retError('no'); } } } /** * 临时帐号别名 * @return mixed|string */ public static function tempAccountAlias() { $alias = Base::settingFind('system', 'temp_account_alias'); return $alias ?: Doo::translate("临时帐号"); } /** * 获取头像 * @param $userid * @param $userimg * @param $email * @param $nickname * @return string */ public static function getAvatar($userid, $userimg, $email, $nickname) { // 自定义头像 if ($userimg && !str_contains($userimg, 'avatar/')) { return Base::fillUrl($userimg); } // 机器人头像 switch ($email) { case 'system-msg@bot.system': return url("images/avatar/default_system.png"); case 'task-alert@bot.system': return url("images/avatar/default_task.png"); case 'check-in@bot.system': return url("images/avatar/default_checkin.png"); case 'anon-msg@bot.system': return url("images/avatar/default_anon.png"); case 'approval-alert@bot.system': return url("images/avatar/default_approval.png"); case 'okr-alert@bot.system': return url("images/avatar/default_okr.png"); case 'ai-openai@bot.system': return url("images/avatar/default_openai.png"); case 'ai-claude@bot.system': return url("images/avatar/default_claude.png"); case 'ai-deepseek@bot.system': return url("images/avatar/default_deepseek.png"); case 'ai-gemini@bot.system': return url("images/avatar/default_gemini.png"); case 'ai-grok@bot.system': return url("images/avatar/default_grok.png"); case 'ai-ollama@bot.system': return url("images/avatar/default_ollama.png"); case 'ai-zhipu@bot.system': return url("images/avatar/default_zhipu.png"); case 'bot-manager@bot.system': return url("images/avatar/default_bot.png"); case 'meeting-alert@bot.system': return url("images/avatar/default_meeting.png"); } // 生成文字头像 if (self::$defaultAvatarMode === 'auto') { return url("avatar/" . urlencode($nickname) . ".png"); } // 系统默认头像 $name = ($userid - 1) % 21 + 1; return url("images/avatar/default_{$name}.png"); } /** * 检测密码策略是否符合 * @param $password * @return void */ public static function passwordPolicy($password) { if (strlen($password) < 6) { throw new ApiException('密码设置不能小于6位数'); } if (strlen($password) > 32) { throw new ApiException('密码最多只能设置32位数'); } // 复杂密码 $password_policy = Base::settingFind('system', 'password_policy'); if ($password_policy == 'complex') { if (preg_match("/^[0-9]+$/", $password)) { throw new ApiException('密码不能全是数字,请包含数字,字母大小写或者特殊字符'); } if (preg_match("/^[a-zA-Z]+$/", $password)) { throw new ApiException('密码不能全是字母,请包含数字,字母大小写或者特殊字符'); } if (preg_match("/^[0-9A-Z]+$/", $password)) { throw new ApiException('密码不能全是数字+大写字母,密码包含数字,字母大小写或者特殊字符'); } if (preg_match("/^[0-9a-z]+$/", $password)) { throw new ApiException('密码不能全是数字+小写字母,密码包含数字,字母大小写或者特殊字符'); } } } /** * 获取机器人或创建 * @param $key * @param $update * @param $userid * @return self|null */ public static function botGetOrCreate($key, $update = [], $userid = 0) { $email = "{$key}@bot.system"; $botUser = self::whereEmail($email)->first(); if (empty($botUser)) { $botUser = Doo::userCreate($email, Base::generatePassword(32)); if (empty($botUser)) { return null; } $botUser->updateInstance([ 'created_ip' => Base::getIp(), ]); $botUser->save(); if ($userid > 0) { UserBot::createInstance([ 'userid' => $userid, 'bot_id' => $botUser->userid, ])->save(); } // if (empty($update['nickname'])) { $update['nickname'] = UserBot::systemBotName($email); } } if ($update) { if (isset($update['nickname']) && $botUser->nickname != $update['nickname']) { $botUser->az = Base::getFirstCharter($botUser->nickname); $botUser->pinyin = Base::cn2pinyin($botUser->nickname); } $botUser->updateInstance($update); $botUser->save(); } return $botUser; } /** * 是否机器人 * @param $userid * @return bool */ public static function isBot($userid) { if (empty($userid)) { return false; } // 这个不会有变化,所以可以使用永久缓存 return (bool)Cache::rememberForever('is-bot-user-' . $userid, function () use ($userid) { return (bool)User::find($userid)?->bot; }); } /** * 按关键词搜索用户(Scope) * 支持:邮箱(含@)、用户ID(纯数字)、昵称/拼音/职业 * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $keyword 搜索关键词 * @return \Illuminate\Database\Eloquent\Builder */ public function scopeSearchByKeyword($query, string $keyword) { if (str_contains($keyword, "@")) { // 包含 @ 按邮箱搜索 return $query->where("email", "like", "%{$keyword}%"); } if (is_numeric($keyword)) { // 纯数字:匹配用户ID 或 昵称/拼音/职业 return $query->where(function ($q) use ($keyword) { $q->where("userid", intval($keyword)) ->orWhere("nickname", "like", "%{$keyword}%") ->orWhere("pinyin", "like", "%{$keyword}%") ->orWhere("profession", "like", "%{$keyword}%"); }); } // 普通文本:搜索昵称/拼音/职业 return $query->where(function ($q) use ($keyword) { $q->where("nickname", "like", "%{$keyword}%") ->orWhere("pinyin", "like", "%{$keyword}%") ->orWhere("profession", "like", "%{$keyword}%"); }); } }