Merge branch 'feature/mzl_dev' into feature/crmAdmin

# Conflicts:
#	app/functions.php
#	config/constants/Constants.php
#	config/middleware.php
This commit is contained in:
2025-07-10 11:14:30 +08:00
23 changed files with 1341 additions and 954 deletions
+30 -3
View File
@@ -8,11 +8,38 @@ use support\Request;
class AuthController extends BaseController
{
/**
* 不需要头信息的方法
*/
protected $noNeedHeader = ['feishuAuth'];
// 调试
public function feishuAuth(Request $request)
{
try {
$code = $request->all();
return $this->success($code);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function feishuScope(Request $request)
{
try {
$result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code);
return $this->success($result);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function login(Request $request)
{
try {
$data = $this->validate(new UserValidate(), $request->post(), 'upwd_login');
$data = $this->validate(new UserValidate(), $this->requestData($request), $request->login_type.'_login');
$result = (new AuthService())->login($data);
return $this->success($result, '登录成功');
} catch (\Throwable $e) {
@@ -23,11 +50,11 @@ class AuthController extends BaseController
public function register(Request $request)
{
try {
$data = $this->validate(new UserValidate(), $request->post(), 'upwd_register');
$data = $this->validate(new UserValidate(), $this->requestData($request), $request->login_type.'_register');
$result = (new AuthService())->register($data);
return $this->success($result, '注册成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage().'|'.$e->getFile().'|'.$e->getLine());
return $this->error($e->getMessage());
}
}
+9 -2
View File
@@ -2,11 +2,10 @@
namespace app\common\controller;
use support\Request;
use support\Response;
use support\exception\BusinessException;
use app\common\traits\ResponseTrait;
use think\Validate;
use support\Request;
abstract class BaseController
{
@@ -37,4 +36,12 @@ abstract class BaseController
// 验证成功,返回原始数据
return $data;
}
/**
* 参数处理
*/
public function requestData(Request $request)
{
return $request->all() + $request->basic_data;
}
}
+219 -228
View File
@@ -2,10 +2,14 @@
namespace app\common\logic;
use app\common\model\Corp;
use app\common\model\CorpFeishuApp;
use app\common\model\UserTokenLog;
use app\common\model\Systems;
use app\common\model\User;
use app\common\model\UserFeishuApp;
use app\common\model\UserSn;
use app\common\utils\Feishu;
use app\common\utils\Jwt;
use support\Redis;
use support\exception\BusinessException;
@@ -19,47 +23,27 @@ class AuthLogic
public function uPwdLogin(array $data): array
{
$user = $this->validateUser($data['account'], $data['password']);
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $data['system_code']);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
return $this->generateTokensAndReturn($user, $system, $data);
}
/**
* 飞书登录
* 如果用户不存在则新增用户信息
*/
public function feishuLogin(array $data): array
{
$feishuUserInfo = $this->getFeishuUserInfo($data['feishu_code']);
$feishuUserInfo = $this->getFeishuUserInfo($data);
$user = $this->getOrCreateUser($feishuUserInfo);
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
if ($user->isLogin) {
$this->validateSystemPermission($user, $system);
}
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $data['system_code']);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
return $this->generateTokensAndReturn($user, $system, $data);
}
/**
@@ -69,25 +53,15 @@ class AuthLogic
{
$userId = $this->validateQrCodeToken($data['qr_code_token']);
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new BusinessException('用户无效', 403);
}
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $data['system_code']);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
return $this->generateTokensAndReturn($user, $system, $data);
}
/**
@@ -102,29 +76,143 @@ class AuthLogic
// 对密码进行哈希处理
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
// 获取企业ID
$corpInfo = $this->validateCorp($data['corp_abbr']);
$data['corp_id'] = $corpInfo->id;
// 处理账户
$data = $data + normalizeAccount($data['account']);
// 创建用户记录
$user = $this->createUser($data);
// 更新用户编号的用户 ID
$this->updateUserId($user->id, $sn->user_sn);
// 注册后如果直接登录(返回token)
$system = $this->validateSystem($data['system_code']);
return $this->generateTokensAndReturn($user, $system, $data);
}
/** ----------- 内部方法 ------------- **/
/**
* 查找或创建飞书用户
*/
private function getOrCreateUser(array $feishuUserInfo)
{
$userFeishuApp = UserFeishuApp::firstOrCreate([
'name' => $feishuUserInfo['en_name'],
'mobile' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''),
'union_id' => $feishuUserInfo['union_id'],
'user_id' => $feishuUserInfo['user_id'],
'open_id' => $feishuUserInfo['open_id'],
'tenant_key' => $feishuUserInfo['tenant_key']
]);
if ($userFeishuApp->uid) {
$user = User::find($userFeishuApp->uid);
$user->isLogin = true;
return $user;
} else {
$user = User::where('phone', $userFeishuApp->mobile)->first();
$user->isLogin = true;
if (!$user) {
$user = $this->createUserByFeishu($feishuUserInfo, $userFeishuApp);
$user->isLogin = false;
}
$userFeishuApp->uid = $user->id;
$userFeishuApp->corp_id = $feishuUserInfo['corp_id'];
if (!$userFeishuApp->save()) {
throw new BusinessException('绑定用户飞书记录失败');
}
return $user;
}
}
/**
* 过滤和格式化手机号
*/
private function sanitizePhone($mobile)
{
return isset($mobile) ? str_replace('+86', '', $mobile) : '';
}
/**
* 创建飞书用户
*/
private function createUserByFeishu(array $feishuUserInfo, UserFeishuApp $userFeishuApp)
{
// 1. 获取适用的用户编号
$userSn = UserSn::where('user_id', '=', 0)->inRandomOrder()->first();
if (!$userSn) {
throw new BusinessException('未找到适用编号');
}
// 2. 创建用户
$user = new User();
$user->sn = $userSn->user_sn;
$user->name = $feishuUserInfo['en_name'];
$user->avatar = $feishuUserInfo['avatar_middle'] ?? '';
$user->password = password_hash($userSn->user_sn, PASSWORD_DEFAULT);
$user->phone = $userFeishuApp->mobile;
$user->email = '';
$user->system_code = $feishuUserInfo['system_code'];
$user->corp_id = $feishuUserInfo['corp_id'];
if (!$user->save()) {
throw new BusinessException('用户创建失败');
}
// 更新使用状态
$this->updateUserId($user->id, $userSn->user_sn);
return $user;
}
/**
* 生成并记录token,返回包含token的响应数据
*/
private function generateTokensAndReturn(User $user, Systems $system, array $data): array
{
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $data['system_code']);
return [
'user' => $user,
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
'system_code' => $data['system_code'],
'corp_abbr' => $data['corp_abbr'],
'login_type' => $data['login_type']
// 'payload' => $payload,
];
}
public function updateUserId($userId, $userSn)
/**
* 获取飞书用户信息
*/
private function getFeishuUserInfo(array $data)
{
$certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']);
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']);
$userInfo = $feishu->userInfo($userAccessToken['access_token']);
if ($userInfo['code'] != 0) {
throw new BusinessException('授权失败,请重试');
}
return $userInfo['data'] + $data + $certificate;
}
/**
* 更新用户编号
*/
private function updateUserId($userId, $userSn)
{
$updated = UserSn::where('user_sn', $userSn)
->update(['user_id' => $userId]);
@@ -136,81 +224,6 @@ class AuthLogic
return $updated;
}
/**
* 获取可用的用户编号
*
* @return \app\common\model\UserSn 用户编号模型实例
* @throws BusinessException 当未找到适用编号时抛出
*/
private function getAvailableUserSn(): \app\common\model\UserSn
{
$userSnModel = new UserSn();
$sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first();
if (!$sn) {
Log::error('用户注册时未找到适用编号');
throw new BusinessException('未找到适用编号');
}
return $sn;
}
/**
* 创建用户记录
*
* @param array $data 包含用户信息的数组
* @return \app\common\model\User 用户模型实例
*/
private function createUser(array $data): \app\common\model\User
{
try {
return User::create($data);
} catch (\Throwable $e) {
Log::error('用户注册时创建用户记录失败', [
'data' => $data,
'error' => $e->getMessage()
]);
throw new BusinessException('用户注册失败,请稍后重试', 500);
}
}
/**
* 刷新token
* @param string $refreshToken
* @param string|null $systemCode 可选,指定系统
* @return array
*/
public function refreshToken(string $refreshToken, ?string $systemCode = null): array
{
if (!$systemCode) throw new BusinessException('缺少system_code', 422);
// 校验refresh_token
$userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}");
if (!$userId) {
throw new BusinessException('refresh_token无效或已过期', 401);
}
$validToken = Redis::get("refresh_token:{$userId}:{$systemCode}");
if ($validToken !== $refreshToken) {
throw new BusinessException('refresh_token无效或已过期', 401);
}
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new BusinessException('用户无效', 403);
}
$system = $this->validateSystem($systemCode);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$this->recordUserToken($user->id, $token, $systemCode);
return [
'access_token' => $token,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 记录用户token到 Redis 和 MySQL
*/
@@ -254,7 +267,6 @@ class AuthLogic
}
}
/**
* 校验token有效性
*/
@@ -278,78 +290,25 @@ class AuthLogic
}
/**
* 登出/失效token
* 获取飞书应用信息
*/
public function logout(?string $token, ?string $systemCode = null)
public function feishuAppInfo(string $corpAbbr, string $systemCode)
{
if ($token) {
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
$payload = null;
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
$userId = $payload['uid'] ?? null;
$aud = $payload['aud'] ?? null;
$sc = $systemCode ?? $aud;
if ($userId && $sc) {
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
}
Redis::del("user_token_meta:{$token}");
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
}
$corpInfo = $this->validateCorp($corpAbbr);
$fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemCode);
return [
'app_id' => $fsAppInfo->app_id,
'app_secret' => $fsAppInfo->app_secret,
'r_url' => $fsAppInfo->r_url,
'scope' => $fsAppInfo->scopes,
'state' => $fsAppInfo->state,
'abbr' => $corpAbbr,
'corp_id' => $corpInfo->id
];
}
/**
* 强制拉黑token
*/
public function blockToken(string $token, ?string $systemCode = null)
{
if (!$token) {
throw new BusinessException('token不能为空', 422);
}
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
$payload = null;
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
$userId = $payload['uid'] ?? null;
$aud = $payload['aud'] ?? null;
$sc = $systemCode ?? $aud;
if ($userId && $sc) {
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
}
Redis::del("user_token_meta:{$token}");
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
}
/**
* 获取用户所有活跃token及元信息(按system_code)
*/
public function getUserTokens(int $userId, string $systemCode): array
{
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
$result = [];
foreach ($tokens as $token) {
$meta = Redis::hGetAll("user_token_meta:{$token}");
$result[] = [
'token' => $token,
'meta' => $meta,
];
}
return $result;
}
/**
* 强制某用户所有token全部下线
*/
public function blockAllTokensOfUser(int $userId, string $systemCode)
{
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
foreach ($tokens as $token) {
$this->blockToken($token, $systemCode);
}
}
/** ----------- 内部方法 ------------- **/
/**
* 用户验证
* 验证用户信息
*/
private function validateUser(string $account, string $password)
{
@@ -364,7 +323,7 @@ class AuthLogic
}
/**
* 系统验证
* 验证系统信息
*/
private function validateSystem(string $systemCode)
{
@@ -376,7 +335,31 @@ class AuthLogic
}
/**
* 用户系统权限校验
* 验证公司信息
*/
private function validateCorp(string $corpAbbr)
{
$corp = Corp::where('abbr', $corpAbbr)->first();
if (!$corp) {
throw new BusinessException('公司不存在', 403);
}
return $corp;
}
/**
* 验证飞书应用信息
*/
private function validateFeishuApp(int $corpId, string $systemCode)
{
$feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_code', $systemCode)->first();
if (!$feishuAppInfo) {
throw new BusinessException('飞书应用不存在', 403);
}
return $feishuAppInfo;
}
/**
* 验证用户系统权限
*/
private function validateSystemPermission(User $user, Systems $system)
{
@@ -388,6 +371,54 @@ class AuthLogic
}
}
/**
* 验证扫码令牌
*/
private function validateQrCodeToken(string $qrCodeToken)
{
$userId = Redis::get("qr_code_token:{$qrCodeToken}");
if (!$userId) {
throw new BusinessException('扫码令牌无效或已过期', 401);
}
return $userId;
}
/**
* 获取可用的用户编号
*
* @return \app\common\model\UserSn 用户编号模型实例
* @throws BusinessException 当未找到适用编号时抛出
*/
private function getAvailableUserSn(): \app\common\model\UserSn
{
$userSnModel = new UserSn();
$sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first();
if (!$sn) {
Log::error('用户注册时未找到适用编号');
throw new BusinessException('未找到适用编号');
}
return $sn;
}
/**
* 创建用户记录
*
* @param array $data 包含用户信息的数组
* @return \app\common\model\User 用户模型实例
*/
private function createUser(array $data): \app\common\model\User
{
try {
return User::create($data);
} catch (\Throwable $e) {
Log::error('用户注册时创建用户记录失败', [
'data' => $data,
'error' => $e->getMessage()
]);
throw new BusinessException('用户注册失败,请稍后重试', 500);
}
}
/**
* JWT payload 构建
* 可扩展 payload 字段
@@ -432,44 +463,4 @@ class AuthLogic
Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration'));
return $refreshToken;
}
/**
* 验证扫码令牌
*/
private function validateQrCodeToken(string $qrCodeToken)
{
$userId = Redis::get("qr_code_token:{$qrCodeToken}");
if (!$userId) {
throw new BusinessException('扫码令牌无效或已过期', 401);
}
return $userId;
}
/**
* 调用飞书API获取用户信息(示例)
*/
private function getFeishuUserInfo(string $code)
{
// 实际实现需要调用飞书 API
return [
'user_id' => 'feishu_user_123',
'name' => '飞书用户',
];
}
/**
* 查找或创建飞书用户(示例)
*/
private function getOrCreateUser(array $feishuUserInfo)
{
$user = User::where('feishu_user_id', $feishuUserInfo['user_id'])->first();
if (!$user) {
$user = new User();
$user->feishu_user_id = $feishuUserInfo['user_id'];
$user->name = $feishuUserInfo['name'];
$user->status = 1;
$user->save();
}
return $user;
}
}
+451 -216
View File
@@ -2,357 +2,592 @@
namespace app\common\logic;
use app\common\model\Corp;
use app\common\model\CorpFeishuApp;
use app\common\model\UserTokenLog;
use app\common\model\Systems;
use app\common\model\User;
use app\common\model\UserFeishuApp;
use app\common\model\UserSn;
use app\common\utils\Feishu;
use app\common\utils\Jwt;
use support\Cache;
use support\Redis;
use support\exception\BusinessException;
use support\Log;
class AuthLogic_bak
{
/**
* 账号密码登录方法
*
* 该方法接收包含账号、密码和系统编码的数据数组,验证用户信息和系统信息,
* 检查用户对系统的权限,生成访问令牌和刷新令牌并返回。
*
* @param array $data 包含登录信息的数组,键名为 'account'(账号)、'password'(密码)、'system_code'(系统编码)
* @return array 包含访问令牌、刷新令牌和令牌过期时间的数组
* 账号密码登录
*/
public function uPwdLogin(array $data): array
{
// 验证用户账号和密码,返回用户对象
$user = $this->validateUser($data['account'], $data['password']);
// 验证系统编码,返回系统对象
$system = $this->validateSystem($data['system_code']);
// 验证用户对该系统是否有访问权限
$this->validateSystemPermission($user, $system);
// 准备 JWT 令牌的负载数据
$payload = $this->preparePayload($user, $system);
// 根据负载数据生成访问令牌
$token = $this->generateAccessToken($payload);
// 根据用户 ID 生成刷新令牌
$refreshToken = $this->generateRefreshToken($user->id);
$user = $this->validateUser($data['account'], $data['password']);
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $data['system_code']);
// 返回包含访问令牌、刷新令牌和令牌过期时间的数组
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 飞书登录
*
* @param array $data
* @return array
* 如果用户不存在则新增用户信息
*/
public function feishuLogin(array $data): array
public function feishuLogin(array $data, bool $isLogin = true): array
{
$feishuUserInfo = $this->getFeishuUserInfo($data['feishu_code']);
$feishuUserInfo = $this->getFeishuUserInfo($data);
$user = $this->getOrCreateUser($feishuUserInfo);
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->preparePayload($user, $system);
if ($user->isLogin === true) {
$this->validateSystemPermission($user, $system);
}
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $data['system_code']);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 扫码登录
*
* @param array $data
* @return array
*/
public function qrCodeLogin(array $data): array
{
$userId = $this->validateQrCodeToken($data['qr_code_token']);
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new \Exception('用户无效', 403);
throw new BusinessException('用户无效', 403);
}
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->preparePayload($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token, $data['system_code']);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 验证用户信息
* 注册
*/
public function register(array $data)
{
// 获取可用用户编号
$sn = $this->getAvailableUserSn();
$data['sn'] = $sn->user_sn;
// 对密码进行哈希处理
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
// 获取企业ID
$corpInfo = $this->validateCorp($data['corp_abbr']);
$data['corp_id'] = $corpInfo->id;
// 创建用户记录
$user = $this->createUser($data);
// 更新用户编号的用户 ID
$this->updateUserId($user->id, $sn->user_sn);
// 注册后如果直接登录(返回token)
$system = $this->validateSystem($data['system_code']);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
$this->recordUserToken($user->id, $token, $data['system_code']);
return [
'user' => $user,
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
public function updateUserId($userId, $userSn)
{
$updated = UserSn::where('user_sn', $userSn)
->update(['user_id' => $userId]);
if ($updated === 0) {
throw new BusinessException('用户编号不存在或更新失败');
}
return $updated;
}
/**
* 获取可用的用户编号
*
* @param string $account
* @param string $password
* @return User|null
* @return \app\common\model\UserSn 用户编号模型实例
* @throws BusinessException 当未找到适用编号时抛出
*/
private function getAvailableUserSn(): \app\common\model\UserSn
{
$userSnModel = new UserSn();
$sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first();
if (!$sn) {
Log::error('用户注册时未找到适用编号');
throw new BusinessException('未找到适用编号');
}
return $sn;
}
/**
* 创建用户记录
*
* @param array $data 包含用户信息的数组
* @return \app\common\model\User 用户模型实例
*/
private function createUser(array $data): \app\common\model\User
{
try {
return User::create($data);
} catch (\Throwable $e) {
Log::error('用户注册时创建用户记录失败', [
'data' => $data,
'error' => $e->getMessage()
]);
throw new BusinessException('用户注册失败,请稍后重试', 500);
}
}
/**
* 刷新token
* @param string $refreshToken
* @param string|null $systemCode 可选,指定系统
* @return array
*/
public function refreshToken(string $refreshToken, ?string $systemCode = null): array
{
if (!$systemCode) throw new BusinessException('缺少system_code', 422);
// 校验refresh_token
$userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}");
if (!$userId) {
throw new BusinessException('refresh_token无效或已过期', 401);
}
$validToken = Redis::get("refresh_token:{$userId}:{$systemCode}");
if ($validToken !== $refreshToken) {
throw new BusinessException('refresh_token无效或已过期', 401);
}
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new BusinessException('用户无效', 403);
}
$system = $this->validateSystem($systemCode);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$this->recordUserToken($user->id, $token, $systemCode);
return [
'access_token' => $token,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 记录用户token到 Redis 和 MySQL
*/
protected function recordUserToken(int $userId, string $token, string $systemCode)
{
$request = request();
$ip = $request ? $request->getRealIp() : '';
$ua = $request ? $request->header('user-agent', '') : '';
$now = time();
$expireAt = $now + config('jwt.jwt_expiration');
// Redis Set 用户token集合
Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token);
Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration'));
// Redis Hash token元信息
$meta = [
'system_code' => $systemCode,
'login_ip' => $ip,
'user_agent' => $ua,
'login_time' => $now,
'expired_at' => $expireAt,
];
Redis::hMSet("user_token_meta:{$token}", $meta);
Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration'));
// 可选:MySQL 持久化
try {
UserTokenLog::create([
'user_id' => $userId,
'token' => $token,
'system_code'=> $systemCode,
'login_ip' => $ip,
'user_agent' => $ua,
'login_time' => $now,
'expired_at' => $expireAt,
'is_valid' => 1,
]);
} catch (\Throwable $e) {
Log::error('Token log write failed: '.$e->getMessage());
}
}
/**
* 校验token有效性
*/
public function verifyToken(string $token, string $systemCode): array
{
if (!$systemCode) throw new BusinessException('缺少system_code', 422);
$payload = (new Jwt())->verifyToken($token);
// 校验黑名单
if (Redis::has("token_blacklist:{$token}")) {
throw new BusinessException('token已失效', 403);
}
// 强制校验aud(系统隔离)
if ($systemCode && $payload['aud'] != $systemCode && $payload['aud'] != ($payload['system_code'] ?? '')) {
throw new BusinessException('token不属于当前系统', 403);
}
return (array)$payload;
}
/**
* 登出/失效token
*/
public function logout(?string $token, ?string $systemCode = null)
{
if ($token) {
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
$payload = null;
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
$userId = $payload['uid'] ?? null;
$aud = $payload['aud'] ?? null;
$sc = $systemCode ?? $aud;
if ($userId && $sc) {
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
}
Redis::del("user_token_meta:{$token}");
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
}
}
/**
* 强制拉黑token
*/
public function blockToken(string $token, ?string $systemCode = null)
{
if (!$token) {
throw new BusinessException('token不能为空', 422);
}
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
$payload = null;
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
$userId = $payload['uid'] ?? null;
$aud = $payload['aud'] ?? null;
$sc = $systemCode ?? $aud;
if ($userId && $sc) {
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
}
Redis::del("user_token_meta:{$token}");
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
}
/**
* 获取用户所有活跃token及元信息(按system_code)
*/
public function getUserTokens(int $userId, string $systemCode): array
{
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
$result = [];
foreach ($tokens as $token) {
$meta = Redis::hGetAll("user_token_meta:{$token}");
$result[] = [
'token' => $token,
'meta' => $meta,
];
}
return $result;
}
/**
* 强制某用户所有token全部下线
*/
public function blockAllTokensOfUser(int $userId, string $systemCode)
{
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
foreach ($tokens as $token) {
$this->blockToken($token, $systemCode);
}
}
/** ----------- 内部方法 ------------- **/
/**
* 用户验证
*/
private function validateUser(string $account, string $password)
{
$user = User::byAccount($account)->first();
if (!$user || !password_verify($password, $user->password)) {
throw new \Exception('账号或密码错误', 401);
throw new BusinessException('账号或密码错误', 401);
}
if ($user->status != 1) {
throw new \Exception('账号已禁用', 403);
throw new BusinessException('账号已禁用', 403);
}
return $user;
}
/**
* 验证系统信息
*
* @param string $systemCode
* @return Systems|null
* 系统验证
*/
private function validateSystem(string $systemCode)
{
$system = Systems::where('code', $systemCode)->first();
if (!$system) {
throw new \Exception('系统不存在', 403);
throw new BusinessException('系统不存在', 403);
}
return $system;
}
/**
* 验证用户系统权限
*
* @param User $user
* @param Systems $system
* 公司验证
*/
private function validateCorp(string $corpAbbr)
{
$corp = Corp::where('abbr', $corpAbbr)->first();
if (!$corp) {
throw new BusinessException('公司不存在', 403);
}
return $corp;
}
/**
* 飞书应用信息
*/
private function validateFeishuApp(int $corpId, string $systemCode)
{
$feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_code', $systemCode)->first();
if (!$feishuAppInfo) {
throw new BusinessException('飞书应用不存在', 403);
}
return $feishuAppInfo;
}
/**
* 用户系统权限校验
*/
private function validateSystemPermission(User $user, Systems $system)
{
if ($system->is_role) {
$systemIds = $user->systems->pluck('id')->toArray();
if (!in_array($system->id, $systemIds)) {
throw new \Exception('无该系统权限', 403);
throw new BusinessException('无该系统权限', 403);
}
}
}
/**
* 准备 JWT 负载
*
* @param User $user
* @param Systems $system
* @return array
* JWT payload 构建
* 可扩展 payload 字段
*/
private function preparePayload(User $user, Systems $system)
private function buildJwtPayload(User $user, ?Systems $system = null): array
{
$payload = [
'uid' => $user->id,
'sub' => 'user_'.$user->id,
'aud' => $system->domain_url,
'role_ids' => [],
'system_ids' => []
$now = time();
$exp = $now + config('jwt.jwt_expiration');
$system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : [];
$role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : [];
return [
'sub' => 'user_' . $user->id,
'aud' => $system->code,
'iat' => $now,
'nbf' => $now,
'exp' => $exp,
'typ' => 'access',
'uid' => $user->id,
'role_ids' => $role_ids,
'system_ids' => $system_ids
];
if ($system->is_role) {
$systemIds = $user->systems->pluck('id')->toArray();
$payload['role_ids'] = $user->roles->pluck('id')->toArray();
$payload['system_ids'] = $systemIds;
}
return $payload;
}
/**
* 生成访问令牌
*
* @param array $payload
* @return string
* JWT签发
*/
private function generateAccessToken(array $payload)
{
return (new Jwt())->generateToken($payload);
}
// private function verifyRefreshToken(string $token)
// {
// return (new Jwt())->verifyToken($token);
// }
/**
* 生成刷新令牌
*
* @param int $userId
* @return string
* 刷新令牌生成
*/
private function generateRefreshToken(int $userId)
private function generateRefreshToken(int $userId, string $systemCode)
{
$refreshToken = bin2hex(random_bytes(32));
Cache::set("refresh_token:{$refreshToken}", $userId, config('jwt.jwt_refresh_token_expiration'));
// user_id+system_code只允许一个refresh_token生效
Redis::set("refresh_token:{$userId}:{$systemCode}", $refreshToken, config('jwt.jwt_refresh_token_expiration'));
// 建立refresh_token到user_id+system_code映射,便于校验
Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration'));
return $refreshToken;
}
/**
* 调用飞书 API 获取用户信息
*
* @param string $code
* @return array
*/
private function getFeishuUserInfo(string $code)
{
// 实际实现需要调用飞书 API,这里只是示例
return [
'user_id' => 'feishu_user_123',
'name' => '飞书用户',
];
}
/**
* 根据飞书用户信息查找或创建本地用户
*
* @param array $feishuUserInfo
* @return User
*/
private function getOrCreateUser(array $feishuUserInfo)
{
// 实际实现需要根据飞书用户信息查找或创建本地用户,这里只是示例
$user = User::where('feishu_user_id', $feishuUserInfo['user_id'])->first();
if (!$user) {
$user = new User();
$user->feishu_user_id = $feishuUserInfo['user_id'];
$user->name = $feishuUserInfo['name'];
$user->status = 1;
$user->save();
}
return $user;
}
/**
* 验证扫码令牌
*
* @param string $qrCodeToken
* @return int
*/
private function validateQrCodeToken(string $qrCodeToken)
{
// 实际实现需要验证扫码令牌,这里只是示例
$userId = Cache::get("qr_code_token:{$qrCodeToken}");
$userId = Redis::get("qr_code_token:{$qrCodeToken}");
if (!$userId) {
throw new \Exception('扫码令牌无效或已过期', 401);
throw new BusinessException('扫码令牌无效或已过期', 401);
}
return $userId;
}
/**
* 处理用户注册逻辑
*
* @param array $data
* @return User
* 获取飞书用户信息
*/
public function register(array $data)
private function getFeishuUserInfo(array $data)
{
$userSnModel = new UserSn();
$sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first();
if (!$sn) {
// 获取凭证
$certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']);
// 获取token
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']);
// 获取用户信息
$userInfo = $feishu->userInfo($userAccessToken['access_token']);
if ($userInfo['code'] != 0) {
throw new BusinessException('授权失败,请重试');
}
return $userInfo['data'] + $data + $certificate;
}
/**
* 查找或创建飞书用户
*/
private function getOrCreateUser(array $feishuUserInfo)
{
$userFeishuApp = UserFeishuApp::firstOrCreate([
'name' => $feishuUserInfo['en_name'],
'mobile' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''),
'union_id' => $feishuUserInfo['union_id'],
'user_id' => $feishuUserInfo['user_id'],
'open_id' => $feishuUserInfo['open_id'],
'tenant_key' => $feishuUserInfo['tenant_key']
]);
if ($userFeishuApp->uid) {
$user = User::find($userFeishuApp->uid);
$user->isLogin = true;
return $user;
} else {
$user = User::where('phone', $userFeishuApp->mobile)->first();
$user->isLogin = true;
if (!$user) {
$user = $this->createUserByFeishu($feishuUserInfo, $userFeishuApp);
$user->isLogin = false;
}
$userFeishuApp->uid = $user->id;
$userFeishuApp->corp_id = $feishuUserInfo['corp_id'];
if (!$userFeishuApp->save()) {
throw new BusinessException('绑定用户飞书记录失败');
}
return $user;
}
}
/**
* 过滤和格式化手机号
*/
private function sanitizePhone($mobile)
{
return isset($mobile) ? str_replace('+86', '', $mobile) : '';
}
/**
* 创建飞书用户
*/
private function createUserByFeishu(array $feishuUserInfo, UserFeishuApp $userFeishuApp)
{
// 1. 获取适用的用户编号
$userSn = UserSn::where('user_id', '=', 0)->inRandomOrder()->first();
if (!$userSn) {
throw new BusinessException('未找到适用编号');
}
$data['sn'] = $sn->user_sn;
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
$user = User::create($data);
return $this->updateUserId($user->id, $sn->user_sn);
}
// 2. 创建用户
$user = new User();
$user->sn = $userSn->user_sn;
$user->name = $feishuUserInfo['en_name'];
$user->avatar = $feishuUserInfo['avatar_middle'] ?? '';
$user->password = password_hash($userSn->user_sn, PASSWORD_DEFAULT);
$user->phone = $userFeishuApp->mobile;
$user->email = '';
$user->system_code = $feishuUserInfo['system_code'];
$user->corp_id = $feishuUserInfo['corp_id'];
public function updateUserId($userId, $userSn)
{
$userSn = User::where('user_sn', $userSn)->first();
if (!$userSn) {
throw new BusinessException('用户编号不存在');
if (!$user->save()) {
throw new BusinessException('用户创建失败');
}
return $userSn->update([
'user_id' => $userId
]);
// 更新使用状态
$this->updateUserId($user->id, $userSn->user_sn);
return $user;
}
/**
* 校验token
* 飞书应用信息
*/
public function verifyToken(string $token): array
public function feishuAppInfo(string $corpAbbr, string $systemCode)
{
$payload = (new Jwt())->verifyToken($token);
if (Cache::has("token_blacklist:{$token}")) {
throw new \Exception('token已失效', 403);
}
return (array)$payload;
}
/**
* 刷新token
* @param string $refreshToken
* @return array
*/
public function refreshToken(string $refreshToken): array
{
$userId = Cache::get("refresh_token:{$refreshToken}");
if (!$userId) {
throw new \Exception('refresh_token无效或已过期', 401);
}
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new \Exception('用户无效', 403);
}
// 获取角色ID、系统ID(和你登录时保持一致)
$role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : [];
$system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : [];
$payload = [
'uid' => $user->id,
'sub' => 'user_' . $user->id,
'role_ids' => $role_ids,
'system_ids' => $system_ids,
];
// 可补充aud、exp等字段,保持和登录生成payload一致
$token = (new Jwt())->generateToken($payload);
$corpInfo = $this->validateCorp($corpAbbr);
$fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemCode);
return [
'access_token' => $token,
'expires_in' => config('jwt.jwt_expiration'),
'app_id' => $fsAppInfo->app_id,
'app_secret' => $fsAppInfo->app_secret,
'r_url' => $fsAppInfo->r_url,
'scope' => $fsAppInfo->scopes,
'state' => $fsAppInfo->state,
'abbr' => $corpAbbr,
'corp_id' => $corpInfo->id
];
}
/**
* 登出,将token加入黑名单
*/
public function logout(?string $token)
{
if ($token) {
Cache::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
}
}
public function blockToken(string $token)
{
if (!$token) {
throw new \Exception('token不能为空', 422);
}
// 加入黑名单
Cache::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
}
}
-416
View File
@@ -1,416 +0,0 @@
<?php
namespace app\common\logic;
use app\common\model\UserTokenLog;
use app\common\model\Systems;
use app\common\model\User;
use app\common\model\UserSn;
use app\common\utils\Jwt;
use support\Cache;
use support\exception\BusinessException;
use support\Log;
class AuthLogic_bak2
{
/**
* 账号密码登录
*/
public function uPwdLogin(array $data): array
{
$user = $this->validateUser($data['account'], $data['password']);
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id);
// 记录 token 到 Redis 和 MySQL
$this->recordUserToken($user->id, $token);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload, // 方便前端调试,可移除
];
}
/**
* 飞书登录
*/
public function feishuLogin(array $data): array
{
$feishuUserInfo = $this->getFeishuUserInfo($data['feishu_code']);
$user = $this->getOrCreateUser($feishuUserInfo);
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 扫码登录
*/
public function qrCodeLogin(array $data): array
{
$userId = $this->validateQrCodeToken($data['qr_code_token']);
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new BusinessException('用户无效', 403);
}
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 注册
*/
public function register(array $data)
{
$userSnModel = new UserSn();
$sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first();
if (!$sn) {
throw new BusinessException('未找到适用编号');
}
$data['sn'] = $sn->user_sn;
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
$user = User::create($data);
$this->updateUserId($user->id, $sn->user_sn);
return $user;
}
public function updateUserId($userId, $userSn)
{
$userSn = User::where('user_sn', $userSn)->first();
if (!$userSn) {
throw new BusinessException('用户编号不存在');
}
return $userSn->update([
'user_id' => $userId
]);
}
/**
* 刷新token
* @param string $refreshToken
* @param string|null $systemCode 可选,指定系统
* @return array
*/
public function refreshToken(string $refreshToken, ?string $systemCode = null): array
{
$userId = Cache::get("refresh_token:{$refreshToken}");
if (!$userId) {
throw new BusinessException('refresh_token无效或已过期', 401);
}
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new BusinessException('用户无效', 403);
}
$system = null;
if ($systemCode) {
$system = Systems::where('code', $systemCode)->first();
if (!$system) {
throw new BusinessException('系统不存在', 403);
}
$this->validateSystemPermission($user, $system);
}
$payload = $this->buildJwtPayload($user, $system);
$token = $this->generateAccessToken($payload);
return [
'access_token' => $token,
'expires_in' => config('jwt.jwt_expiration'),
'payload' => $payload,
];
}
/**
* 记录用户token到 Redis 和 MySQL
*/
protected function recordUserToken(int $userId, string $token)
{
$request = request();
$ip = $request ? $request->getRealIp() : '';
$ua = $request ? $request->header('user-agent', '') : '';
$now = time();
$expireAt = $now + config('jwt.jwt_expiration');
// Redis Set 用户token集合
Cache::sAdd("user_tokens:{$userId}", $token);
Cache::expire("user_tokens:{$userId}", config('jwt.jwt_expiration'));
// Redis Hash token元信息
$meta = [
'login_ip' => $ip,
'user_agent' => $ua,
'login_time' => $now,
'expired_at' => $expireAt,
];
Cache::hMSet("user_token_meta:{$token}", $meta);
Cache::expire("user_token_meta:{$token}", config('jwt.jwt_expiration'));
// 可选:MySQL 持久化
try {
UserTokenLog::create([
'user_id' => $userId,
'token' => $token,
'login_ip' => $ip,
'user_agent' => $ua,
'login_time' => $now,
'expired_at' => $expireAt,
'is_valid' => 1,
]);
} catch (\Throwable $e) {
// 写库失败可忽略,但建议日志留存
Log::error('Token log write failed: '.$e->getMessage());
}
}
/**
* 校验token有效性
*/
public function verifyToken(string $token): array
{
$payload = (new Jwt())->verifyToken($token);
if (Cache::has("token_blacklist:{$token}")) {
throw new BusinessException('token已失效', 403);
}
return (array)$payload;
}
// /**
// * 登出/失效token
// */
// public function logout(?string $token)
// {
// if ($token) {
// Cache::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
// }
// }
// /**
// * 强制拉黑token
// */
// public function blockToken(string $token)
// {
// if (!$token) {
// throw new BusinessException('token不能为空', 422);
// }
// Cache::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
// }
public function logout(?string $token)
{
if ($token) {
Cache::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
// 1. 解析token查用户
$payload = null;
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
$userId = $payload['uid'] ?? null;
if ($userId) {
Cache::sRem("user_tokens:{$userId}", $token);
}
// 2. 删除meta
Cache::del("user_token_meta:{$token}");
// 3. 更新MySQL为失效
\app\common\model\UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
}
}
public function blockToken(string $token)
{
if (!$token) {
throw new BusinessException('token不能为空', 422);
}
Cache::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
// 1. 解析token查用户
$payload = null;
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
$userId = $payload['uid'] ?? null;
if ($userId) {
Cache::sRem("user_tokens:{$userId}", $token);
}
Cache::del("user_token_meta:{$token}");
\app\common\model\UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
}
/**
* 获取用户所有活跃token及元信息
*/
public function getUserTokens(int $userId): array
{
$tokens = Cache::sMembers("user_tokens:{$userId}") ?: [];
$result = [];
foreach ($tokens as $token) {
$meta = Cache::hGetAll("user_token_meta:{$token}");
// 可选再查 MySQL 补充日志记录等
$result[] = [
'token' => $token,
'meta' => $meta,
];
}
return $result;
}
/**
* 强制某用户所有token全部下线
*/
public function blockAllTokensOfUser(int $userId)
{
$tokens = Cache::sMembers("user_tokens:{$userId}") ?: [];
foreach ($tokens as $token) {
$this->blockToken($token);
}
}
/** ----------- 内部方法 ------------- **/
/**
* 用户验证
*/
private function validateUser(string $account, string $password)
{
$user = User::byAccount($account)->first();
if (!$user || !password_verify($password, $user->password)) {
throw new BusinessException('账号或密码错误', 401);
}
if ($user->status != 1) {
throw new BusinessException('账号已禁用', 403);
}
return $user;
}
/**
* 系统验证
*/
private function validateSystem(string $systemCode)
{
$system = Systems::where('code', $systemCode)->first();
if (!$system) {
throw new BusinessException('系统不存在', 403);
}
return $system;
}
/**
* 用户系统权限校验
*/
private function validateSystemPermission(User $user, Systems $system)
{
if ($system->is_role) {
$systemIds = $user->systems->pluck('id')->toArray();
if (!in_array($system->id, $systemIds)) {
throw new BusinessException('无该系统权限', 403);
}
}
}
/**
* JWT payload 构建
* 可扩展 payload 字段
*/
private function buildJwtPayload(User $user, ?Systems $system = null): array
{
$now = time();
$exp = $now + config('jwt.jwt_expiration');
$system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : [];
$role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : [];
return [
'sub' => 'user_' . $user->id,
'aud' => $system ? $system->domain_url : '',
'iat' => $now,
'nbf' => $now,
'exp' => $exp,
'typ' => 'access',
'uid' => $user->id,
'role_ids' => $role_ids,
'system_ids' => $system_ids
];
}
/**
* JWT签发
*/
private function generateAccessToken(array $payload)
{
return (new Jwt())->generateToken($payload);
}
/**
* 刷新令牌生成
*/
private function generateRefreshToken(int $userId)
{
$refreshToken = bin2hex(random_bytes(32));
Cache::set("refresh_token:{$refreshToken}", $userId, config('jwt.jwt_refresh_token_expiration'));
return $refreshToken;
}
/**
* 验证扫码令牌
*/
private function validateQrCodeToken(string $qrCodeToken)
{
$userId = Cache::get("qr_code_token:{$qrCodeToken}");
if (!$userId) {
throw new BusinessException('扫码令牌无效或已过期', 401);
}
return $userId;
}
/**
* 调用飞书API获取用户信息(示例)
*/
private function getFeishuUserInfo(string $code)
{
// 实际实现需要调用飞书 API
return [
'user_id' => 'feishu_user_123',
'name' => '飞书用户',
];
}
/**
* 查找或创建飞书用户(示例)
*/
private function getOrCreateUser(array $feishuUserInfo)
{
$user = User::where('feishu_user_id', $feishuUserInfo['user_id'])->first();
if (!$user) {
$user = new User();
$user->feishu_user_id = $feishuUserInfo['user_id'];
$user->name = $feishuUserInfo['name'];
$user->status = 1;
$user->save();
}
return $user;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace app\common\model;
use support\Model;
/**
* corp 企业-信息
* @property integer $id ID(主键)
* @property string $name 名称
* @property string $abbr 缩写
* @property string $tenant_key 飞书企业标识
* @property integer $sort 排序
* @property integer $status 状态
* @property integer $created_at 创建时间
* @property integer $updated_at 更新时间
*/
class Corp extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'corp';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace app\common\model;
use support\Model;
/**
* corp_feishu_app 企业-飞书应用
* @property integer $id ID(主键)
* @property integer $corp_id 公司ID
* @property string $name 名称
* @property string $tenant_key 企业标识
* @property string $app_id 应用ID
* @property string $app_secret 应用秘钥
* @property string $base_uri 基础域
* @property string $r_url 授权地址
* @property string $scopes 授权范围
* @property string $state 携带参数
* @property string $system_code 绑定系统码
* @property integer $sort 排序
* @property integer $status 状态
* @property integer $created_at 创建时间
* @property integer $updated_at 更新时间
*/
class CorpFeishuApp extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'corp_feishu_app';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
}
+2 -1
View File
@@ -58,7 +58,8 @@ class User extends Model
'phone',
'email',
'status',
'system_code'
'system_code',
'corp_id'
];
// 用户所有角色
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace app\common\model;
use support\Model;
/**
* user_feishu_app 用户-飞书应用
* @property integer $id ID(主键)
* @property integer $uid 用户ID
* @property integer $corp_id 公司ID
* @property string $name 名称
* @property string $mobile 手机
* @property string $union_id ISV的唯一标识
* @property string $user_id 用户 user_id
* @property string $open_id 应用内的唯一标识
* @property string $tenant_key 当前企业标识
* @property integer $sort 排序
* @property integer $status 状态
* @property integer $created_at 创建时间
* @property integer $updated_at 更新时间
*/
class UserFeishuApp extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'user_feishu_app';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
protected $fillable = [
'uid',
'corp_id',
'name',
'mobile',
'union_id',
'user_id',
'open_id',
'tenant_key',
'sort',
'status'
];
}
+12
View File
@@ -4,11 +4,23 @@ namespace app\common\service;
use app\common\logic\AuthLogic;
use app\common\model\User;
use app\common\utils\Feishu;
use support\Db;
class AuthService
{
/**
* 飞书授权
*/
public function feishuAuthorize(string $corpAbbr, string $systemCcode)
{
$logic = new AuthLogic();
$certificate = $logic->feishuAppInfo($corpAbbr, $systemCcode);
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
return ['auth_url' => $feishu->authorize($certificate)];
}
/**
* 登录,支持多类型
*/
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace app\common\service;
use app\common\model\Corp;
use app\common\model\Systems;
use support\Redis;
use support\exception\BusinessException;
class BasicDataService
{
// 定义Redis键名前缀常量
const LOGIN_TYPE_KEY = 'login_types';
const SYSTEM_CODE_KEY = 'system_codes';
const CORP_ABBR_KEY = 'corp_abbrs';
/**
* 检查并缓存独立的基础参数
*/
public function checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)
{
if (!$loginType && !$systemCode && !$corpAbbr) {
throw new BusinessException(htmlpage());
}
// 检查并缓存登录类型
if (!$this->checkParamInCache(self::LOGIN_TYPE_KEY, $loginType)) {
$this->cacheLoginTypes();
if (!$this->checkParamInCache(self::LOGIN_TYPE_KEY, $loginType)) {
throw new BusinessException('无效的登录方式');
}
}
// 检查并缓存系统码
if (!$this->checkParamInCache(self::SYSTEM_CODE_KEY, $systemCode)) {
$this->cacheSystemCodes();
if (!$this->checkParamInCache(self::SYSTEM_CODE_KEY, $systemCode)) {
throw new BusinessException('无效的系统码');
}
}
// 检查并缓存公司标识
if (!$this->checkParamInCache(self::CORP_ABBR_KEY, $corpAbbr)) {
$this->cacheCorpAbbrs();
if (!$this->checkParamInCache(self::CORP_ABBR_KEY, $corpAbbr)) {
throw new BusinessException('无效的公司标识');
}
}
return true;
}
/**
* 检查参数是否在Redis集合中
*/
private function checkParamInCache($cacheKey, $param)
{
return Redis::sIsMember($cacheKey, $param);
}
/**
* 缓存所有登录类型到集合
*/
private function cacheLoginTypes()
{
$cacheKey = self::LOGIN_TYPE_KEY;
// 如果集合已存在,直接返回
if (Redis::exists($cacheKey)) {
return;
}
$loginTypes = $this->getAllLoginTypes();
if (empty($loginTypes)) {
throw new BusinessException('未获取到登录类型数据');
}
// 批量添加值到集合
Redis::sAdd($cacheKey, ...$loginTypes);
Redis::expire($cacheKey, 3600); // 1小时有效期
}
/**
* 缓存所有系统码到集合
*/
private function cacheSystemCodes()
{
$cacheKey = self::SYSTEM_CODE_KEY;
// 如果集合已存在,直接返回
if (Redis::exists($cacheKey)) {
return;
}
$systemCodes = $this->getAllSystemCodes();
if (empty($systemCodes)) {
throw new BusinessException('未获取到系统码数据');
}
// 批量添加值到集合
Redis::sAdd($cacheKey, ...$systemCodes);
Redis::expire($cacheKey, 3600); // 1小时有效期
}
/**
* 缓存所有公司标识到集合
*/
private function cacheCorpAbbrs()
{
$cacheKey = self::CORP_ABBR_KEY;
// 如果集合已存在,直接返回
if (Redis::exists($cacheKey)) {
return;
}
$corpAbbrs = $this->getAllCorpAbbrs();
if (empty($corpAbbrs)) {
throw new BusinessException('未获取到公司标识数据');
}
// 批量添加值到集合
Redis::sAdd($cacheKey, ...$corpAbbrs);
Redis::expire($cacheKey, 3600); // 1小时有效期
}
/**
* 获取所有有效的登录类型
*/
private function getAllLoginTypes()
{
return ['upwd', 'feishu', 'qrcode']; // 示例数据
}
/**
* 获取所有有效的系统码
*/
private function getAllSystemCodes()
{
return Systems::pluck('code')->toArray();
}
/**
* 获取所有有效的公司标识
*/
private function getAllCorpAbbrs()
{
return Corp::pluck('abbr')->toArray();
}
}
+4 -4
View File
@@ -19,7 +19,7 @@ trait ResponseTrait
public function success(
$data = [],
string $message = '请求成功',
int $code = 0,
int $code = 200,
int $httpStatus = 200,
array $header = ['Content-Type' => 'application/json']
): Response {
@@ -38,18 +38,18 @@ trait ResponseTrait
*/
public function error(
string $message = '请求失败',
int $code = 1,
int $code = 400,
int $httpStatus = 400,
$data = [],
array $header = ['Content-Type' => 'application/json']
): Response {
return $this->result('error', $code, $httpStatus, $message, $data, $header);
return $this->result('fail', $code, $httpStatus, $message, $data, $header);
}
/**
* 统一响应格式方法
*
* @param string $status 响应状态,例如 "success" 或 "error"
* @param string $status 响应状态,例如 "success" 或 "fail"
* @param int $code 内部状态码
* @param int $httpStatus HTTP 状态码
* @param string $message 响应消息
+8 -8
View File
@@ -16,15 +16,15 @@ class Feishu
protected $client = null;
protected $options = [];
protected $error = '';
protected $gs = 'hesu';
protected $abbr = 'hesu';
/**
* 初始化
*/
public function __construct(string $appId = '', string $appSecret = '', string $gs = 'hesu')
public function __construct(string $appId = '', string $appSecret = '', string $abbr = 'hesu')
{
if ($gs === 'mbaf') {
if ($abbr === 'mbaf') {
$this->baseUri = 'https://open.larksuite.com/open-apis/';
} else {
$this->baseUri = 'https://open.feishu.cn/open-apis/';
@@ -32,7 +32,7 @@ class Feishu
$this->appId = $appId;
$this->appSecret = $appSecret;
$this->gs = $gs;
$this->abbr = $abbr;
$this->client = new Client([
'base_uri' => $this->baseUri
@@ -94,8 +94,8 @@ class Feishu
public function authorize($certificate, $url = 'authen/v1/authorize')
{
$arr = [
'app_id' => $certificate['appId'],
'redirect_uri' => $certificate['rUrl'],
'app_id' => $certificate['app_id'],
'redirect_uri' => $certificate['r_url'],
'scope' => $certificate['scope'],
'state' => $certificate['state']
];
@@ -108,7 +108,7 @@ class Feishu
*/
public function tenantAccessToken()
{
$tenantAccessToken = Cache::get($this->gs.'-tenantAccessToken');
$tenantAccessToken = Cache::get($this->abbr.'-tenantAccessToken');
if (!$tenantAccessToken) {
$this->options["json"] = [
"app_id" => $this->appId,
@@ -117,7 +117,7 @@ class Feishu
$rst = $this->requestResult('auth/v3/tenant_access_token/internal', 'POST');
if ($rst) {
$tenantAccessToken = $rst['tenant_access_token'];
Cache::set($this->gs.'-tenantAccessToken', $tenantAccessToken, $rst['expire']);
Cache::set($this->abbr.'-tenantAccessToken', $tenantAccessToken, $rst['expire']);
} else {
throw new BusinessException('tenant_access_token 获取失败');
}
+22 -4
View File
@@ -2,6 +2,7 @@
namespace app\common\validate;
use app\common\model\Corp;
use app\common\model\Systems;
use app\common\model\User;
use think\Validate;
@@ -17,7 +18,9 @@ class UserValidate extends Validate
'phone' => 'checkMobile|checkPhoneUnique',
'email' => 'email|checkEmailUnique',
'login_type' => 'require|in:upwd,feishu,qrcode',
'system_code' => 'require|checkSystemCode'
'system_code' => 'require|checkSystemCode',
'corp_abbr' => 'require|checkCorpAbbr',
'feishu_code' => 'require|string'
];
protected $message = [
@@ -40,12 +43,19 @@ class UserValidate extends Validate
'login_type.require' => '请选择登录方式',
'login_type.in' => '登录方式不支持',
'system_code.require' => '系统码不能为空',
'system_code.checkSystemCode' => '系统码不存在或不合法'
'system_code.checkSystemCode' => '系统码不存在或不合法',
'corp_abbr.require' => '公司标识不能为空',
'corp_abbr.checkCorpAbbr' => '公司标识不存在或不合法',
'feishu_code.require' => '飞书code不能为空',
'feishu_code.string' => '飞书code必须是字符串'
];
protected $scene = [
'upwd_register' => ['name', 'account', 'password', 'confirm_password', 'login_type', 'system_code'],
'upwd_login' => ['account', 'password', 'login_type', 'system_code'],
'upwd_register' => ['name', 'account', 'password', 'confirm_password'],
'feishu_register' => ['feishu_code'],
'upwd_login' => ['account' => 'require|checkAccount', 'password'],
'feishu_auth' => ['corp_abbr', 'system_code', 'login_type'],
'feishu_login' => ['feishu_code']
];
/**
@@ -129,4 +139,12 @@ class UserValidate extends Validate
{
return Systems::where('code', $value)->exists();
}
/**
* 验证 corp_abbr 企业标识是否存在
*/
protected function checkCorpAbbr($value, $rule, $data, $field)
{
return Corp::where('abbr', $value)->exists();
}
}
+4 -68
View File
@@ -6,79 +6,15 @@ use support\Request;
class IndexController
{
public function index(Request $request)
{
return '
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Code</title>
<style>
body {
margin: 0;
padding: 0;
position: relative;
font-family: Arial, sans-serif;
}
.watermark-container {
pointer-events: none;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.watermark {
position: absolute;
color: rgba(0, 0, 0, 0.08);
font-size: 20px;
transform: rotate(-30deg);
white-space: nowrap;
}
</style>
</head>
<body>
<div class="watermark-container" id="watermark-container"></div>
<div style="padding: 100px;">
<p>🌞 hi~</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;愿你代码如诗,逻辑清晰,Bug 自动远离!</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;记得适当休息,喝杯好茶或咖啡,为灵感续航 ☕</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;加油,永远相信美好的事情即将发生。 ✨</p>
</div>
<script>
const container = document.getElementById("watermark-container");
const text = "fdesk 总台系统";
const xCount = 8; // 横向水印数量
const yCount = 6; // 纵向水印数量
const xSpacing = window.innerWidth / xCount;
const ySpacing = window.innerHeight / yCount;
for (let i = 0; i < yCount; i++) {
for (let j = 0; j < xCount; j++) {
const div = document.createElement("div");
div.className = "watermark";
div.textContent = text;
div.style.left = `${j * xSpacing}px`;
div.style.top = `${i * ySpacing}px`;
container.appendChild(div);
}
}
</script>
</body>
</html>
';
return htmlpage();
}
public function view(Request $request)
{
return view('index/view', ['name' => 'webman']);
return view('index/qrcode', ['name' => 'webman']);
}
public function json(Request $request)
@@ -86,4 +22,4 @@ class IndexController
return json(['code' => 0, 'msg' => 'ok']);
}
}
}
+38
View File
@@ -4,6 +4,44 @@
*/
use support\exception\BusinessException;
/**
* 标准化账号参数
*
* @param string $account 用户输入的账号(可能是手机号或邮箱)
* @return array 返回 ['phone' => $account] 或 ['email' => $account]
*/
function normalizeAccount(string $account): array
{
// 使用FILTER_VALIDATE_EMAIL验证邮箱格式
if (filter_var($account, FILTER_VALIDATE_EMAIL)) {
return ['email' => $account];
}
// 清理手机号中的非数字字符
$cleanPhone = preg_replace('/[^0-9]/', '', $account);
// 验证手机号格式(简单版:11位数字)
if (preg_match('/^1[3-9]\d{9}$/', $cleanPhone)) {
return ['phone' => $cleanPhone];
}
// 如果既不是邮箱也不是手机号,根据业务需求处理
// 这里默认按手机号处理(可根据实际需求调整)
return ['phone' => $cleanPhone];
}
/**
* 公共html页面
*/
function htmlpage()
{
static $html;
if (!$html) {
$html = file_get_contents(public_path('mzl.html'));
}
return $html;
}
/**
* 获取配置
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace app\middleware;
use app\common\utils\Jwt;
use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class AuthCheck implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$controller = new ReflectionClass($request->controller);
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
if (in_array($request->action, $noNeedLogin)) {
// 白名单
return $handler($request);
} else {
try {
$authHeader = $request->header('Authorization');
if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
$body = [
'status' => 'fail',
'code' => 401,
'message' => '未授权',
'data' => [],
];
return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
}
$token = $matches[1];
$request->user = (new Jwt())->verifyToken($token);
} catch (\Throwable $e) {
$body = [
'status' => 'fail',
'code' => 401,
'message' => $e->getMessage(),
'data' => [],
];
return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
}
return $handler($request);
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
use app\common\service\BasicDataService;
use ReflectionClass;
class BasicData implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
$controller = new ReflectionClass($request->controller);
$noNeedHeader = $controller->getDefaultProperties()['noNeedHeader'] ?? [];
if (in_array($request->action, $noNeedHeader) || in_array($request->path(), config('whitelist.basi_cdata'))) {
// 白名单
return $handler($request);
} else {
try {
$corpAbbr = $request->header('Fdesk-Corp-Abbr');
$systemCode = $request->header('Fdesk-System-Code');
$loginType = $request->header('Fdesk-Login-Type');
$basicDataService = new BasicDataService();
if ($basicDataService->checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)) {
$request->login_type = $loginType;
$request->system_code = $systemCode;
$request->corp_abbr = $corpAbbr;
$request->basic_data = [
'login_type' => $loginType,
'system_code' => $systemCode,
'corp_abbr' => $corpAbbr
];
}
return $handler($request);
} catch (\Throwable $th) {
throw $th;
}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>飞书扫码登录示例</title>
<!-- 引入二维码 SDK -->
<script src="https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js"></script>
</head>
<body>
<!-- 二维码展示容器 -->
<div id="login_container" style="width:500px;height:600px;"></div>
<script>
// 1. 配置你的参数
// const CLIENT_ID = '你的_APP_ID';
// const REDIRECT_URI = encodeURIComponent('https://example.com/api/oauth/callback/feishu');
// const STATE = '随机串_防CSRF';
// // 用于跳转获取 code 的 URL
const GOTO = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=cli_a73e1e6406fa501c&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&response_type=code&state=MZL`;
// const GOTO = `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=cli_a73e1e6406fa501c&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&scope=contact:user.base:readonly contact:contact.base:readonly contact:user.employee_id:readonly im:message:send_as_bot&state=xinyouhe`;
// 2. 创建二维码实例
const QRLoginObj = QRLogin({
id: 'login_container',
goto: GOTO,
width: '500',
height:'500'
});
// 3. 监听扫码并授权后的消息
function handleMessage(event) {
// 校验来源和数据合法性
if (QRLoginObj.matchOrigin(event.origin) && QRLoginObj.matchData(event.data)) {
console.log(event.data);
// return;
const tmpCode = event.data.tmp_code;
// 带上 tmp_code 重定向到后端回调接口
window.location.href = `${GOTO}&tmp_code=${tmpCode}`;
}
}
window.addEventListener
? window.addEventListener('message', handleMessage, false)
: window.attachEvent('onmessage', handleMessage);
</script>
</body>
</html>
+3 -2
View File
@@ -14,6 +14,7 @@
return [
'' => [
app\middleware\AccessControl::class
],
\app\middleware\AccessControl::class,
\app\middleware\BasicData::class
]
];
+8 -2
View File
@@ -28,8 +28,14 @@ use Webman\Route;
Route::group('/common', function () {
Route::group('/auth', function () {
Route::post('/login', [app\common\controller\AuthController::class, 'login']);
Route::post('/register', [app\common\controller\AuthController::class, 'register']);
Route::any('/login', [app\common\controller\AuthController::class, 'login']);
Route::any('/register', [app\common\controller\AuthController::class, 'register']);
Route::any('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']);
// 调试
Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']);
});
});
+9
View File
@@ -0,0 +1,9 @@
<?php
return [
'basi_cdata' => [
'/',
'/index/index',
// '/index/view'
]
];
+64
View File
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Code</title>
<style>
body {
margin: 0;
padding: 0;
position: relative;
font-family: Arial, sans-serif;
}
.watermark-container {
pointer-events: none;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.watermark {
position: absolute;
color: rgba(0, 0, 0, 0.08);
font-size: 20px;
transform: rotate(-30deg);
white-space: nowrap;
}
</style>
</head>
<body>
<div class="watermark-container" id="watermark-container"></div>
<div style="padding: 100px;">
<p>🌞 hi~</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;愿你代码如诗,逻辑清晰,Bug 自动远离!</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;记得适当休息,喝杯好茶或咖啡,为灵感续航 ☕</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;加油,永远相信美好的事情即将发生。 ✨</p>
</div>
<script>
const container = document.getElementById("watermark-container");
const text = "fdesk 总台系统";
const xCount = 8; // 横向水印数量
const yCount = 6; // 纵向水印数量
const xSpacing = window.innerWidth / xCount;
const ySpacing = window.innerHeight / yCount;
for (let i = 0; i < yCount; i++) {
for (let j = 0; j < xCount; j++) {
const div = document.createElement("div");
div.className = "watermark";
div.textContent = text;
div.style.left = `${j * xSpacing}px`;
div.style.top = `${i * ySpacing}px`;
container.appendChild(div);
}
}
</script>
</body>
</html>