dootask/app/Models/User.php
kuaifan 8c809bbff1 feat(team): 团队管理支持标记成员邮箱认证状态
- 成员行操作菜单新增「标记邮箱为已认证/未认证」,复用 users.email_verity
  字段与 api/users/operation 接口,新增 setverity/clearverity 操作类型
- 创建用户:邮箱下方新增「标记邮箱为已认证」复选框(默认勾选),
  「首次登录需改密」复选框移到初始密码下方
- 批量导入:预览列表邮箱右侧显示主题色已认证图标(错误行不显示),
  支持勾选行后批量标记已/未认证;部门与认证批量行加标签对齐、
  三个批量按钮样式随选中状态统一
- createByAdmin 新增 emailVerity 选项,createuser/import 透传逐行认证状态
- 新增导入预览默认认证状态单测

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 05:49:33 +00:00

1100 lines
38 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Apps;
use App\Module\Table\OnlineData;
use App\Observers\AbstractObserver;
use App\Services\RequestContext;
use App\Tasks\ManticoreSyncTask;
use Cache;
use Carbon\Carbon;
/**
* App\Models\User
*
* @property int $userid
* @property array $identity 身份
* @property array $department 所属部门
* @property string|null $az A-Z
* @property string|null $pinyin 拼音(主要用于搜索)
* @property string|null $email 邮箱
* @property string|null $tel 联系电话
* @property string $nickname 昵称
* @property string|null $profession 职位/职称
* @property string|null $birthday 生日
* @property string|null $address 地址
* @property string|null $introduction 个人简介
* @property string $userimg 头像
* @property string|null $encrypt
* @property string|null $password 登录密码
* @property int|null $changepass 登录需要修改密码
* @property int|null $login_num 累计登录次数
* @property string|null $last_ip 最后登录IP
* @property \Illuminate\Support\Carbon|null $last_at 最后登录时间
* @property string|null $line_ip 最后在线IP接口
* @property \Illuminate\Support\Carbon|null $line_at 最后在线时间(接口)
* @property int|null $task_dialog_id 最后打开的任务会话ID
* @property string|null $created_ip 注册IP
* @property \Illuminate\Support\Carbon|null $disable_at 禁用时间(离职时间)
* @property int|null $email_verity 邮箱是否已验证
* @property int|null $bot 是否机器人
* @property string|null $lang 语言首选项
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Database\Factories\UserFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|User searchByKeyword(string $keyword)
* @method static \Illuminate\Database\Eloquent\Builder|User whereAddress($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereBirthday($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereBot($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereChangepass($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedIp($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereDepartment($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereDisableAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerity($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEncrypt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereIdentity($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereIntroduction($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLang($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastIp($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLineAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLineIp($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLoginNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereNickname($value)
* @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder|User wherePinyin($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereProfession($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereTaskDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereTel($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserimg($value)
* @mixin \Eloquent
*/
class User extends AbstractModel
{
const IMPORT_MAX = 500;
protected $primaryKey = 'userid';
protected $hidden = [
'updated_at',
];
// 默认头像类型auto自动生成system系统默认
public static $defaultAvatarMode = 'auto';
// 基本信息的字段
public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at'];
/**
* 昵称
* @param $value
* @return string
*/
public function getNicknameAttribute($value)
{
if ($value) {
if (UserBot::isSystemBot($this->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}%");
});
}
}