Merge branch 'develop'

This commit is contained in:
mzl
2025-07-11 18:01:23 +08:00
parent 642c25503d
commit 925c5501f7
83 changed files with 4869 additions and 1162 deletions
+74 -35
View File
@@ -8,11 +8,40 @@ use support\Request;
class AuthController extends BaseController
{
/**
* 不需要头信息的方法
*/
protected $noNeedHeader = ['feishuAuth'];
protected $noNeedLogin = ['feishuScope', 'login', 'register', '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,54 +52,64 @@ 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());
}
}
public function verify(Request $request)
{
try {
$token = $request->post('access_token') ?: $request->header('authorization');
$systemCode = $request->post('system_code');
if (!$token || !$systemCode) return $this->error('参数缺失', 422);
$payload = (new AuthService())->verifyToken($token, $systemCode);
return $this->success($payload, '验证成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage() ?: '无效或过期的token', 401);
return $this->error($e->getMessage());
}
}
public function refresh(Request $request)
{
try {
$refresh_token = $request->post('refresh_token');
$systemCode = $request->post('system_code');
if (!$refresh_token || !$systemCode) return $this->error('参数缺失', 422);
$result = (new AuthService())->refreshToken($refresh_token, $systemCode);
$data = $this->requestData($request);
if (!$data['refresh_token'] || !$data['system_code']) return $this->error('参数缺失', 422);
$result = (new AuthService())->refreshToken($data['refresh_token'], $data['system_code']);
return $this->success($result, '刷新成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage(), 401);
}
}
public function logout(Request $request)
{
$token = $request->post('access_token') ?: $request->header('Authorization');
$systemCode = $request->post('system_code') ?: $request->header('system_code');
(new AuthService())->logout($token, $systemCode);
return $this->success([], '已登出');
}
public function blockToken(Request $request)
{
$token = $request->post('token');
$systemCode = $request->post('system_code');
if (!$token || !$systemCode) return $this->error('参数缺失', 422);
(new AuthService())->blockToken($token, $systemCode);
return $this->success([], '已强制停用该token');
}
// public function perm(Request $request)
// {
// try {
// $data = $this->requestData($request);
// if (!$data['access_token'] || !$data['system_code']) return $this->error('参数缺失', 422);
// $result = (new AuthService())->logout($data['access_token'], $data['system_code']);
// return $this->success($result, '已登出');
// } catch (\Throwable $e) {}
// }
// public function verify(Request $request)
// {
// try {
// $token = $request->post('access_token') ?: $request->header('authorization');
// $systemCode = $request->post('system_code');
// if (!$token || !$systemCode) return $this->error('参数缺失', 422);
// $payload = (new AuthService())->verifyToken($token, $systemCode);
// return $this->success($payload, '验证成功');
// } catch (\Throwable $e) {
// return $this->error($e->getMessage() ?: '无效或过期的token', 401);
// }
// }
// public function logout(Request $request)
// {
// $token = $request->post('access_token') ?: $request->header('Authorization');
// $systemCode = $request->post('system_code') ?: $request->header('system_code');
// (new AuthService())->logout($token, $systemCode);
// return $this->success([], '已登出');
// }
// public function blockToken(Request $request)
// {
// $token = $request->post('token');
// $systemCode = $request->post('system_code');
// if (!$token || !$systemCode) return $this->error('参数缺失', 422);
// (new AuthService())->blockToken($token, $systemCode);
// return $this->success([], '已强制停用该token');
// }
}
+12 -4
View File
@@ -2,15 +2,15 @@
namespace app\common\controller;
use support\Request;
use support\Response;
use support\exception\BusinessException;
use app\common\traits\ResponseTrait;
use app\common\traits\DataTransformer;
use think\Validate;
use support\Request;
abstract class BaseController
class BaseController
{
use ResponseTrait;
use ResponseTrait, DataTransformer;
protected mixed $validator = null;
@@ -37,4 +37,12 @@ abstract class BaseController
// 验证成功,返回原始数据
return $data;
}
/**
* 参数处理
*/
public function requestData(Request $request)
{
return $request->all() + $request->basic_data;
}
}
@@ -0,0 +1,36 @@
<?php
namespace app\common\controller;
use support\Request;
use app\common\service\CorpService;
use app\common\service\CorpFeishuAppService;
class PresetController extends BaseController
{
protected $noNeedLogin = ['corps', 'feishuApp'];
public function corps()
{
try {
$result = (new CorpService())->getList();
return $this->success($result, '获取公司列表');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function feishuApp(Request $request)
{
try {
$corpId = $request->get('corp_id');
$result = (new CorpFeishuAppService())->getFeishuApp($corpId);
return $this->success($result, '获取飞书应用信息');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace app\common\controller;
use app\common\service\OssService;
use app\common\traits\ResponseTrait;
use support\Request;
use Shopwwi\WebmanFilesystem\Facade\Storage;
class UploadController
{
use ResponseTrait;
public function upload(Request $request)
{
try {
$scene = $request->input('scene', 'image');
$file = $request->file('file');
if ($file && $file->isValid()) {
$pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.md5($file->getUploadName() . time()).'.'.$file->getUploadExtension();
$fullPathUrl = public_path() . $pathUrl;
$file->move($fullPathUrl);
$data = [
'domain' => getenv('MAIN_DOMAIN'),
'file_url' => $pathUrl,
'full_url' => getenv('MAIN_DOMAIN') . $pathUrl,
'scene' => $scene,
];
return $this->success($data, 'upload success');
}
return $this->error('file not found');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
// 上传单文件附件
public function uploadFile(Request $request)
{
try {
$file = $request->file('file');
if (!$file || !$file->isValid()) {
return $this->error('上传失败');
}
$originalName = $file->getUploadName();
$result = Storage::adapter('oss')
->path('vehicle_album/'.date('Ymd'))
->size(1024*1024*5)
->upload($file);
$ossservice = new OssService();
$data = [
'original_name' => $originalName,
'file_name' => $result->file_name,
'file_url' => $ossservice->generatePrivateUrl($result->file_name)
];
return $this->success($data);
}catch (\Throwable $e){
return $this->error($e->getMessage());
}
}
// 上传单文件图片
public function uploadImage(Request $request)
{
try {
$file = $request->file('image');
if (!$file || !$file->isValid()) {
return $this->error('上传失败');
}
$result = Storage::adapter('oss')
->path('vehicle_album/'.date('Ymd'))
->size(1024*1024*5)
->extYes(['image/jpeg','image/png'])
->extNo(['image/gif'])
->upload($file);
$ossservice = new OssService();
$data = [
'href' => $result->file_name,
'alt' => '',
'url' => $ossservice->generatePrivateUrl($result->file_name)
];
return json(['errno' => 0, 'data' => $data]);
// return $this->success($data);
} catch (\Throwable $e) {
return json(['errno' => 1, 'message' => $e->getMessage()]);
// return $this->error($e->getMessage());
}
}
public function proxy(Request $request)
{
$filePath = $request->get('file'); // 原OSS文件路径
try {
// 生成签名URL
$signedUrl = (new \app\common\service\OssService())->generatePrivateUrl($filePath);
// 获取图片数据并返回
$imageData = file_get_contents($signedUrl);
// 根据文件类型设置Content-Type
$mime = mime_content_type('data://application/octet-stream;base64,'.base64_encode($imageData));
return response($imageData, 200, [
'Content-Type' => $mime,
'Cache-Control' => 'public, max-age=600' // 客户端缓存10分钟
]);
} catch (\Exception $e) {
return response('Image not found '.$e->getMessage(), 404);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace app\common\controller;
use support\Request;
use app\common\service\UserService;
class UserController extends BaseController
{
/**
* 用户信息
*/
public function info(Request $request)
{
try {
$uid = $request->user->uid;
$result = (new UserService())->getUserInfo($uid);
return $this->success($result, '获取用户信息');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
+267 -239
View File
@@ -2,10 +2,13 @@
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\UserSn;
use app\common\utils\Feishu;
use app\common\utils\Jwt;
use support\Redis;
use support\exception\BusinessException;
@@ -19,75 +22,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']);
// 记录 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 qrCodeLogin(array $data): array
{
$userId = $this->validateQrCodeToken($data['qr_code_token']);
$user = User::find($userId);
if (!$user || $user->status != 1) {
throw new BusinessException('用户无效', 403);
if ($user->isLogin) {
$this->validateSystemPermission($user, $system);
}
$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);
}
/**
@@ -95,36 +50,132 @@ class AuthLogic
*/
public function register(array $data)
{
// 获取可用用户编号
$sn = $this->getAvailableUserSn();
$data['sn'] = $sn->user_sn;
try {
// 获取可用用户编号
$sn = $this->getAvailableUserSn();
$data['sn'] = $sn->user_sn;
// 对密码进行哈希处理
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
// 对密码进行哈希处理
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
// 创建用户记录
$user = $this->createUser($data);
// 获取企业ID
$corpInfo = $this->validateCorp($data['corp_abbr']);
$data['corp_id'] = $corpInfo->id;
// 更新用户编号的用户 ID
$this->updateUserId($user->id, $sn->user_sn);
// 处理账户
$data = $data + normalizeAccount($data['account']);
// 创建用户记录
$user = $this->createUser($data);
// 注册后如果直接登录(返回token)
$system = $this->validateSystem($data['system_code']);
// 更新用户编号的用户 ID
$this->updateUserId($user->id, $sn->user_sn);
$system = $this->validateSystem($data['system_code']);
return $this->generateTokensAndReturn($user, $system, $data);
} catch (\Throwable $e) {
throw $e;
}
}
/**
* 查找或创建飞书用户
*/
private function getOrCreateUser(array $feishuUserInfo)
{
try {
$phone = $this->sanitizePhone($feishuUserInfo['mobile'] ?? '');
$user = User::where('phone', $phone)->where('corp_id', $feishuUserInfo['corp_id'])->first();
if (!$user) {
// 获取编号
$userSn = $this->getAvailableUserSn();
// 创建用户
$user = User::create([
'name' => $feishuUserInfo['en_name'],
'avatar' => $feishuUserInfo['avatar_middle'],
'phone' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''),
'union_id' => $feishuUserInfo['union_id'],
'user_id' => $feishuUserInfo['user_id'],
'open_id' => $feishuUserInfo['open_id'],
'tenant_key' => $feishuUserInfo['tenant_key'],
'corp_id' => $feishuUserInfo['corp_id'],
'password' => password_hash($userSn->user_sn, PASSWORD_DEFAULT)
]);
// 更新使用
$this->updateUserId($user->id, $userSn->user_sn);
// 更新编号
if (!$user->update(['sn'=>$userSn->user_sn])) {
throw new BusinessException('编号绑定失败');
}
// 是否登陆
$user->isLogin = false;
} else {
$user->update([
'union_id' => $feishuUserInfo['union_id'],
'user_id' => $feishuUserInfo['user_id'],
'open_id' => $feishuUserInfo['open_id'],
'tenant_key' => $feishuUserInfo['tenant_key']
]);
}
return $user;
} catch (\Throwable $e) {
throw $e;
}
}
/**
* 过滤和格式化手机号
*/
private function sanitizePhone($mobile)
{
return isset($mobile) ? str_replace('+86', '', $mobile) : '';
}
/**
* 生成并记录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 +187,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 +230,6 @@ class AuthLogic
}
}
/**
* 校验token有效性
*/
@@ -278,78 +253,27 @@ 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);
$systemInfo = $this->validateSystem($systemCode);
$fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemInfo->id);
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,
'system_id' => $systemInfo->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 +288,7 @@ class AuthLogic
}
/**
* 系统验证
* 验证系统信息
*/
private function validateSystem(string $systemCode)
{
@@ -376,7 +300,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, int $systemId)
{
$feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_id', $systemId)->first();
if (!$feishuAppInfo) {
throw new BusinessException('飞书应用不存在', 403);
}
return $feishuAppInfo;
}
/**
* 验证用户系统权限
*/
private function validateSystemPermission(User $user, Systems $system)
{
@@ -388,6 +336,42 @@ class AuthLogic
}
}
/**
* 获取可用的用户编号
*
* @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 字段
@@ -433,43 +417,87 @@ class AuthLogic
return $refreshToken;
}
/**
* 验证扫码令牌
*/
private function validateQrCodeToken(string $qrCodeToken)
{
$userId = Redis::get("qr_code_token:{$qrCodeToken}");
if (!$userId) {
throw new BusinessException('扫码令牌无效或已过期', 401);
}
return $userId;
}
/**
* 调用飞书API获取用户信息(示例)
* 刷新token
* @param string $refreshToken
* @param string|null $systemCode 可选,指定系统
* @return array
*/
private function getFeishuUserInfo(string $code)
public function refreshToken(string $refreshToken, ?string $systemCode = null): array
{
// 实际实现需要调用飞书 API
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);
$refreshToken = $this->generateRefreshToken($user->id, $systemCode);
$this->recordUserToken($user->id, $token, $systemCode);
return [
'user_id' => 'feishu_user_123',
'name' => '飞书用户',
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
// 'payload' => $payload,
];
}
/**
* 查找或创建飞书用户(示例)
* 登出/失效token
*/
private function getOrCreateUser(array $feishuUserInfo)
public function logout(?string $token, ?string $systemCode = null)
{
$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();
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]);
}
return $user;
}
/**
* 强制拉黑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]);
}
}
+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;
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace app\common\logic;
use Illuminate\Database\Eloquent\Model;
use support\exception\BusinessException;
class BaseLogic
{
/**
* 获取列表
*/
public function getList(array $params, string $modelClass)
{
$query = $modelClass::query();
// 你可以在此处添加分页、排序等功能
if (isset($params['page']) && isset($params['per_page'])) {
$query->paginate($params['per_page'], ['*'], 'page', $params['page']);
}
return $query->get();
}
/**
* 获取单个记录
*/
public function getDetail(int $id, string $modelClass)
{
$record = $modelClass::find($id);
if (!$record) {
throw new BusinessException("记录不存在", 404);
}
return $record;
}
/**
* 创建记录
*/
public function create(array $data, string $modelClass)
{
$model = new $modelClass;
$model->fill($data); // 根据字段填充数据
try {
$model->save();
} catch (\Throwable $e) {
throw new BusinessException("创建记录失败: " . $e->getMessage(), 500);
}
return $model;
}
/**
* 更新记录
*/
public function update(array $data, string $modelClass)
{
$record = $modelClass::find($data['id']);
if (!$record) {
throw new BusinessException("记录不存在", 404);
}
$record->update($data);
return $record;
}
/**
* 删除记录
*/
public function delete(int $id, string $modelClass)
{
$record = $modelClass::find($id);
if (!$record) {
throw new BusinessException("记录不存在", 404);
}
try {
$record->delete();
} catch (\Throwable $e) {
throw new BusinessException("删除记录失败: " . $e->getMessage(), 500);
}
return true;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace app\common\logic;
use app\common\model\CorpFeishuApp;
class CorpFeishuAppLogic
{
public function getFeishuApp($corpId)
{
return CorpFeishuApp::where('corp_id', $corpId)->first(['id', 'corp_id', 'name', 'app_id', 'r_url', 'state']);
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace app\common\logic;
use app\common\model\Corp;
class CorpLogic
{
public function getList()
{
return Corp::where('status', 1)->orderBy('sort', 'asc')->get(['id', 'name', 'abbr', 'tenant_key']);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace app\common\logic;
use app\common\model\User;
class PermLogic
{
/**
* 获取用户所有资源(合并角色+直接分配)
*/
public function getUserResources(int $userId)
{
$user = User::with(['roles.resources', 'resources'])->find($userId);
if (!$user) {
return collect();
}
$all = collect();
foreach ($user->roles as $role) {
$all = $all->merge($role->resources);
}
$all = $all->merge($user->resources);
return $all->unique('id')->values();
}
/**
* 构建目录/菜单树,节点包含权限与打开方式,拼接完整路径,并对子级菜单component处理
* @param \Illuminate\Support\Collection $resources
* @param int $parentId
* @param string $parentPath
* @return array
*/
public function buildTree($resources, int $parentId = 0, string $parentPath = ''): array
{
$tree = [];
foreach ($resources as $node) {
if ($node->parent_id !== $parentId) continue;
// 拼接完整路径
$currentPath = $parentPath;
if ($currentPath && substr($currentPath, -1) === '/') {
$currentPath = rtrim($currentPath, '/');
}
if (!empty($node->url)) {
if (!$currentPath) {
$currentPath = $node->url;
} else {
$url = ltrim($node->url, '/');
$currentPath = $currentPath . '/' . $url;
}
}
// 子节点递归
$children = [];
if ($node->type === 'directory') {
$children = $this->buildTree($resources, $node->id, $currentPath);
}
// 权限
$authList = $resources
->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id)
->map(fn($r) => [
'id' => $r->id,
'title' => $r->name,
'auth_mark' => $r->code,
])->values()->all();
// 判断是否有子级菜单(非权限)
$hasChildrenMenu = collect($resources)->where('parent_id', $node->id)
->whereIn('type', ['directory', 'menu'])->isNotEmpty();
// component 字段规则:
// 1. 存在子菜单,固定 '/index/index'
// 2. 否则为当前完整路径(或其它你想要的)
$componentValue = $hasChildrenMenu ? '/index/index' : $currentPath;
$tree[] = [
'id' => $node->id,
'name' => $node->name,
'path' => $node->url,
'component' => $componentValue,
'meta' => [
'title' => $node->name,
'icon' => $node->icon,
'keepAlive' => true,
'authList' => $authList,
'openType' => $node->open_type,
],
'children' => $children,
];
}
return $tree;
}
/**
* 获取用户所有权限code
*/
public function getPermissionCodes(int $userId): array
{
$resources = $this->getUserResources($userId);
return $resources
->filter(fn($res) => $res->type === 'permission')
->pluck('code')
->unique()
->values()
->all();
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace app\common\logic;
use app\common\model\Role;
use support\exception\BusinessException;
use support\Log;
class RoleLogic
{
/**
* 获取角色列表
*/
public function getRoleList(array $params)
{
// 假设我们需要根据system_id进行过滤,其他过滤条件也可以添加
$query = Role::query();
if (isset($params['system_id'])) {
$query->where('system_id', $params['system_id']);
}
if (isset($params['status'])) {
$query->where('status', $params['status']);
}
// 添加分页
$roles = $query->paginate($params['per_page'] ?? 15);
return $roles;
}
/**
* 获取角色详情
*/
public function getRoleDetail(int $roleId)
{
$role = Role::find($roleId);
if (!$role) {
throw new BusinessException('角色不存在', 404);
}
return $role;
}
/**
* 创建角色
*/
public function createRole(array $data)
{
$role = new Role();
$role->fill($data); // 根据字段填充数据
try {
$role->save();
} catch (\Throwable $e) {
Log::error('创建角色失败:'.$e->getMessage());
throw new BusinessException('创建角色失败', 500);
}
return $role;
}
/**
* 更新角色
*/
public function updateRole(array $data)
{
$role = Role::find($data['id']);
if (!$role) {
throw new BusinessException('角色不存在', 404);
}
$role->update($data); // 更新字段
return $role;
}
/**
* 删除角色
*/
public function deleteRole(int $roleId)
{
$role = Role::find($roleId);
if (!$role) {
throw new BusinessException('角色不存在', 404);
}
try {
$role->delete();
} catch (\Throwable $e) {
Log::error('删除角色失败:'.$e->getMessage());
throw new BusinessException('删除角色失败', 500);
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace app\common\logic;
use app\common\model\User;
use support\exception\BusinessException;
class UserLogic
{
/**
* 用户信息
*/
public function userInfo(int $uid)
{
$user = User::find($uid);
if (!$user) {
throw new BusinessException('账户不存在', 404);
}
if ($user->status != 1) {
throw new BusinessException('账号已禁用', 403);
}
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
View File
@@ -41,6 +41,8 @@ class Role extends Model
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
// 角色下所有用户
public function users()
+6 -1
View File
@@ -58,7 +58,12 @@ class User extends Model
'phone',
'email',
'status',
'system_code'
'corp_id',
'user_type',
'union_id',
'user_id',
'open_id',
'tenant_key'
];
// 用户所有角色
+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_bak 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'
];
}
+22 -1
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)];
}
/**
* 登录,支持多类型
*/
@@ -19,7 +31,7 @@ class AuthService
return match ($loginType) {
'feishu' => $logic->feishuLogin($data),
'qrcode' => $logic->qrCodeLogin($data),
// 'qrcode' => $logic->qrCodeLogin($data),
default => $logic->uPwdLogin($data),
};
}
@@ -74,4 +86,13 @@ class AuthService
{
(new AuthLogic())->blockToken($token, $systemCode);
}
// public function perms()
// {
// $permService = new PermService();
// $menuTree = $permService->getMenuTree($uid);
// $permissionCodes = $permService->getPermissionCodes($uid);
// return (new PermService())->getMenuTree($this->userId)
// }
}
+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();
}
}
@@ -0,0 +1,21 @@
<?php
namespace app\common\service;
use app\common\logic\CorpFeishuAppLogic;
class CorpFeishuAppService
{
protected $logic;
public function __construct()
{
$this->logic = (new CorpFeishuAppLogic());
}
public function getFeishuApp(int $corpId)
{
return $this->logic->getFeishuApp($corpId);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace app\common\service;
use app\common\logic\CorpLogic;
class CorpService
{
protected $logic;
public function __construct()
{
$this->logic = (new CorpLogic());
}
/**
* 获取公司list
*/
public function getList()
{
return $this->logic->getList();
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace app\common\service;
use OSS\OssClient;
use OSS\Core\OssException;
use support\exception\BusinessException;
class OssService
{
private $accessKeyId;
private $accessKeySecret;
private $bucket;
private $endpoint;
private $ossClient;
public function __construct()
{
$this->accessKeyId = getenv('OSS_ACCESSID');
$this->accessKeySecret = getenv('OSS_ACCESSSECRET');
$this->bucket = getenv('OSS_BUCKET');
$this->endpoint = getenv('OSS_ENDPOINT');
$this->ossClient = new OssClient($this->accessKeyId, $this->accessKeySecret, $this->endpoint);
}
/**
* 上传图片到 OSS
* @param string $filePath 本地文件路径
* @param string $objectKey OSS上存储的对象名称
* @return string 图片的 URL
*/
public function uploadImage($filePath, $objectKey)
{
try {
// 上传文件到 OSS
$this->ossClient->uploadFile($this->bucket, $objectKey, $filePath);
// 构造图片的私有 URL
$url = $this->generatePrivateUrl($objectKey);
return $url;
} catch (OssException $e) {
// 错误处理
throw new \RuntimeException('OSS Error: ' . $e->getMessage());
} catch (\Throwable $th) {
// 错误处理
throw new BusinessException('Error: ' . $th->getMessage());
}
}
/**
* 生成图片的私有访问 URL
* @param string $objectKey
* @return string
*/
public function generatePrivateUrl($objectKey, $expireTime = 3600)
{
// 生成 OSS 私有 URL(有效期为 3600 秒)
return $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime);
}
}
+20 -64
View File
@@ -2,11 +2,17 @@
namespace app\common\service;
use app\common\model\User;
use app\common\logic\PermLogic;
use support\Cache;
class PermService
{
protected $logic;
public function __construct()
{
$this->logic = (new PermLogic());
}
// 用户资源缓存key
protected static function cacheKey(int $userId): string
@@ -17,92 +23,42 @@ class PermService
/**
* 获取用户所有资源(合并角色+直接分配),结果缓存
*/
public static function getUserResources(int $userId)
public function getUserResources(int $userId)
{
$cacheKey = self::cacheKey($userId);
$cached = Cache::get($cacheKey);
if ($cached !== null) {
return $cached;
}
$user = User::with(['roles.resources', 'resources'])->find($userId);
if (!$user) {
return collect();
}
$all = collect();
foreach ($user->roles as $role) {
$all = $all->merge($role->resources);
}
$all = $all->merge($user->resources);
$unique = $all->unique('id')->values();
Cache::set($cacheKey, $unique, 3600);
return $unique;
// 调用 Logic 层获取资源
$resources = $this->logic->getUserResources($userId);
// 缓存结果
Cache::set($cacheKey, $resources, 3600);
return $resources;
}
/**
* 构建目录/菜单树,节点包含权限与打开方式
*/
public static function getMenuTree(int $userId): array
public function getMenuTree(int $userId): array
{
$resources = self::getUserResources($userId);
$menus = $resources
->filter(fn($r) => in_array($r->type, ['directory','menu']))
->values();
return self::buildTree($menus, $resources);
}
protected static function buildTree($nodes, $allResources, int $parentId = 0): array
{
$tree = [];
foreach ($nodes as $node) {
if ($node->parent_id !== $parentId) continue;
$children = self::buildTree($nodes, $allResources, $node->id);
$authList = $allResources
->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id)
->map(fn($r) => [
'id' => $r->id,
'title' => $r->name,
'auth_mark' => $r->code,
])->values()->all();
$tree[] = [
'id' => $node->id,
'name' => $node->name,
'path' => $node->url,
'meta' => [
'title' => $node->name,
'icon' => $node->icon,
'keepAlive' => true,
'authList' => $authList,
'openType' => $node->open_type, // 'internal' | 'external' | 'iframe'
],
'children' => $children,
];
}
return $tree;
// 调用 Logic 层处理菜单构建
$resources = $this->getUserResources($userId);
return $this->logic->buildTree($resources);
}
/**
* 获取用户所有权限code
*/
public static function getPermissionCodes(int $userId): array
public function getPermissionCodes(int $userId): array
{
return self::getUserResources($userId)
->filter(fn($res) => $res->type === 'permission')
->pluck('code')
->unique()
->values()
->all();
return $this->logic->getPermissionCodes($userId);
}
/**
* 清除资源相关缓存
*/
public static function clearCache(int $userId): void
public function clearCache(int $userId): void
{
Cache::delete(self::cacheKey($userId));
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace app\common\service;
use app\common\model\User;
use support\Cache;
class PermService_bak
{
// 用户资源缓存key
protected static function cacheKey(int $userId): string
{
return "user_resources_{$userId}";
}
/**
* 获取用户所有资源(合并角色+直接分配),结果缓存
*/
public static function getUserResources(int $userId)
{
$cacheKey = self::cacheKey($userId);
$cached = Cache::get($cacheKey);
if ($cached !== null) {
return $cached;
}
$user = User::with(['roles.resources', 'resources'])->find($userId);
if (!$user) {
return collect();
}
$all = collect();
foreach ($user->roles as $role) {
$all = $all->merge($role->resources);
}
$all = $all->merge($user->resources);
$unique = $all->unique('id')->values();
Cache::set($cacheKey, $unique, 3600);
return $unique;
}
/**
* 构建目录/菜单树,节点包含权限与打开方式
*/
public static function getMenuTree(int $userId): array
{
$resources = self::getUserResources($userId);
$menus = $resources
->filter(fn($r) => in_array($r->type, ['directory','menu']))
->values();
return self::buildTree($menus, $resources);
}
protected static function buildTree($nodes, $allResources, int $parentId = 0): array
{
$tree = [];
foreach ($nodes as $node) {
if ($node->parent_id !== $parentId) continue;
$children = self::buildTree($nodes, $allResources, $node->id);
$authList = $allResources
->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id)
->map(fn($r) => [
'id' => $r->id,
'title' => $r->name,
'auth_mark' => $r->code,
])->values()->all();
$tree[] = [
'id' => $node->id,
'name' => $node->name,
'path' => $node->url,
'meta' => [
'title' => $node->name,
'icon' => $node->icon,
'keepAlive' => true,
'authList' => $authList,
'openType' => $node->open_type, // 'internal' | 'external' | 'iframe'
],
'children' => $children,
];
}
return $tree;
}
/**
* 获取用户所有权限code
*/
public static function getPermissionCodes(int $userId): array
{
return self::getUserResources($userId)
->filter(fn($res) => $res->type === 'permission')
->pluck('code')
->unique()
->values()
->all();
}
/**
* 清除资源相关缓存
*/
public static function clearCache(int $userId): void
{
Cache::delete(self::cacheKey($userId));
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace app\common\service;
use app\common\logic\RoleLogic;
class RoleService
{
protected $roleLogic;
public function __construct()
{
$this->roleLogic = (new RoleLogic());
}
/**
* 获取角色列表
*/
public function getRoleList(array $params)
{
return $this->roleLogic->getRoleList($params);
}
/**
* 获取角色详情
*/
public function getRoleDetail(int $roleId)
{
return $this->roleLogic->getRoleDetail($roleId);
}
/**
* 创建角色
*/
public function createRole(array $data)
{
return $this->roleLogic->createRole($data);
}
/**
* 更新角色
*/
public function updateRole(array $data)
{
return $this->roleLogic->updateRole($data);
}
/**
* 删除角色
*/
public function deleteRole(int $roleId)
{
return $this->roleLogic->deleteRole($roleId);
}
}
+29 -9
View File
@@ -2,19 +2,39 @@
namespace app\common\service;
use app\model\User;
use app\common\logic\UserLogic;
class UserService
{
public static function getInfo($userId)
protected $logic;
public function __construct()
{
$user = User::with('roles')->find($userId);
if (!$user) return null;
$this->logic = (new UserLogic());
}
/**
* 获取用户信息(包括菜单和权限)
*/
public function getUserInfo(int $uid)
{
$user = $this->logic->userInfo($uid);
$permService = new PermService();
$menuTree = $permService->getMenuTree($uid);
$permissionCodes = $permService->getPermissionCodes($uid);
return [
'id' => $user->id,
'username' => $user->username,
'roles' => $user->roles->pluck('name')->toArray(),
// 可扩展其他字段
'user' => [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar,
'phone' => $user->phone,
'email' => $user->email,
'status' => $user->status,
'roles' => $user->roles->pluck('name')
],
'menu' => $menuTree,
'permissions' => $permissionCodes
];
}
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
namespace app\common\traits;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
trait DataTransformer
{
/**
* 核心方法:对任意数据进行转换并返回数组
*
* @param mixed $rawData 原始数据,可为 Paginator / Model / Collection / array 等
* @param callable|null $postProcessor 可选的后置处理回调,接受处理完的数组再返回新的数组
* @param callable|null $fieldFormatter 可选的字段命名转换回调,如 snake_case
*
* @return array
*/
protected function transformData(
mixed $rawData,
?callable $postProcessor = null,
?callable $fieldFormatter = null
): array {
try {
$fieldFormatter = $fieldFormatter ?: fn($field) => $field;
if ($rawData instanceof LengthAwarePaginator) {
$formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter);
} elseif ($rawData instanceof Model) {
$formatted = $this->formatModel($rawData, $fieldFormatter);
} elseif ($rawData instanceof Collection) {
$formatted = $this->formatCollection($rawData, $fieldFormatter);
} elseif (is_array($rawData)) {
$formatted = $this->formatArray($rawData, $fieldFormatter);
} else {
$formatted = (array) $rawData;
}
if (is_callable($postProcessor)) {
$formatted = $postProcessor($formatted);
}
return $formatted;
} catch (\Throwable $e) {
return [
'error' => true,
'message' => $e->getMessage(),
'code' => $e->getCode(),
];
}
}
/**
* 格式化分页数据:将每条记录也进行 transformData 递归处理
*
* @param LengthAwarePaginator $paginator
* @param callable|null $postProcessor
* @param callable $fieldFormatter
* @return array
*/
protected function formatPaginated(
LengthAwarePaginator $paginator,
?callable $postProcessor,
callable $fieldFormatter
): array {
$list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) {
return $this->transformData($item, $postProcessor, $fieldFormatter);
})->toArray();
return [
'records' => $list,
'total' => $paginator->total(),
'current' => $paginator->currentPage(),
'size' => $paginator->perPage()
];
}
/**
* 格式化 Eloquent 模型:递归嵌套关联
*
* @param Model $model
* @param callable $fieldFormatter
* @return array
*/
protected function formatModel(Model $model, callable $fieldFormatter): array
{
$formatted = [];
// 主表字段
foreach ($model->getAttributes() as $key => $value) {
$formatted[$fieldFormatter($key)] = $value;
}
// 关联数据,递归嵌套
foreach ($model->getRelations() as $relation => $relationData) {
if ($relationData instanceof Model) {
// 单个关联模型
$formatted[$relation] = $this->formatModel($relationData, $fieldFormatter);
} elseif ($relationData instanceof Collection) {
// 关联集合
$formatted[$relation] = $relationData
->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
->toArray();
}
}
return $formatted;
}
/**
* 格式化集合
*
* @param Collection $collection
* @param callable $fieldFormatter
* @return array
*/
protected function formatCollection(Collection $collection, callable $fieldFormatter): array
{
return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
->toArray();
}
/**
* 格式化普通数组
*
* @param array $array
* @param callable $fieldFormatter
* @return array
*/
protected function formatArray(array $array, callable $fieldFormatter): array
{
return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array);
}
}
+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 获取失败');
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace app\common\validate;
use think\Validate;
class RoleValidate extends Validate
{
protected $rule = [
'id' => 'require|integer',
'system_id' => 'require|integer',
'name' => 'require|max:64',
'desc' => 'max:128',
'sort' => 'integer',
'status' => 'in:0,1',
];
protected $message = [
'id.require' => '角色ID不能为空',
'system_id.require' => '系统ID不能为空',
'name.require' => '角色名称不能为空',
'name.max' => '角色名称不能超过64个字符',
'desc.max' => '描述不能超过128个字符',
'sort.integer' => '排序为数字',
'status.in' => '状态值必须为0或1',
];
protected $scene = [
'role_create' => ['system_id', 'name', 'desc', 'sort'],
'role_update' => ['id', 'system_id', 'name', 'desc', 'sort', 'status'],
];
}
+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();
}
}