first push

This commit is contained in:
ylyg5690
2025-07-01 19:39:13 +08:00
commit 5289483a8a
66 changed files with 10473 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
/runtime
/.idea
/.vscode
/vendor
*.log
.env
/tests/tmp
/tests/.phpunit.result.cache
+3
View File
@@ -0,0 +1,3 @@
User ← user_roles → Role ← role_resources → Resource (type=menu/button)
↘︎
role_permissions → Permission
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+70
View File
@@ -0,0 +1,70 @@
<div style="padding:18px;max-width: 1024px;margin:0 auto;background-color:#fff;color:#333">
<h1>webman</h1>
基于<a href="https://www.workerman.net" target="__blank">workerman</a>开发的超高性能PHP框架
<h1>学习</h1>
<ul>
<li>
<a href="https://www.workerman.net/webman" target="__blank">主页 / Home page</a>
</li>
<li>
<a href="https://webman.workerman.net" target="__blank">文档 / Document</a>
</li>
<li>
<a href="https://www.workerman.net/doc/webman/install.html" target="__blank">安装 / Install</a>
</li>
<li>
<a href="https://www.workerman.net/questions" target="__blank">问答 / Questions</a>
</li>
<li>
<a href="https://www.workerman.net/apps" target="__blank">市场 / Apps</a>
</li>
<li>
<a href="https://www.workerman.net/sponsor" target="__blank">赞助 / Sponsors</a>
</li>
<li>
<a href="https://www.workerman.net/doc/webman/thanks.html" target="__blank">致谢 / Thanks</a>
</li>
</ul>
<div style="float:left;padding-bottom:30px;">
<h1>赞助商</h1>
<h4>特别赞助</h4>
<a href="https://www.crmeb.com/?form=workerman" target="__blank">
<img src="https://www.workerman.net/img/sponsors/6429/20230719111500.svg" width="200">
</a>
<h4>铂金赞助</h4>
<a href="https://www.fadetask.com/?from=workerman" target="__blank"><img src="https://www.workerman.net/img/sponsors/1/20230719084316.png" width="200"></a>
<a href="https://www.yilianyun.net/?from=workerman" target="__blank" style="margin-left:20px;"><img src="https://www.workerman.net/img/sponsors/6218/20230720114049.png" width="200"></a>
</div>
<div style="float:left;padding-bottom:30px;clear:both">
<h1>请作者喝咖啡</h1>
<img src="https://www.workerman.net/img/wx_donate.png" width="200">
<img src="https://www.workerman.net/img/ali_donate.png" width="200">
<br>
<b>如果您觉得webman对您有所帮助,欢迎捐赠。</b>
</div>
<div style="clear: both">
<h1>LICENSE</h1>
The webman is open-sourced software licensed under the MIT.
</div>
</div>
@@ -0,0 +1,83 @@
<?php
namespace app\admin\controller;
use Psr\Http\Message\ServerRequestInterface;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\ResourceServer;
use GuzzleHttp\Psr7\Response as Psr7Response;
use app\service\AuthService;
use app\model\User;
use support\Request;
use support\Response;
class AuthController_bak
{
protected AuthorizationServer $authServer;
protected ResourceServer $resourceServer;
public function __construct(AuthorizationServer $auth, ResourceServer $res)
{
$this->authServer = $auth;
$this->resourceServer = $res;
}
// 浏览器授权页
public function authorize(ServerRequestInterface $request)
{
// 如果未登录,跳转登录页(自行实现)
// 登录后,确认授权 -> issue auth code -> 重定向回 client
// …未登录重定向到登录页,登录后调用:
// $authRequest = $this->authServer->validateAuthorizationRequest($request);
// …用户同意授权…
// return $this->authServer->completeAuthorizationRequest($authRequest, new Psr7Response());
}
// 授权码换 Token
public function token(ServerRequestInterface $request)
{
return $this->authServer->respondToAccessTokenRequest($request, new Psr7Response());
}
// 获取用户信息
public function userinfo(ServerRequestInterface $request)
{
$psr = $this->resourceServer->validateAuthenticatedRequest($request);
$userId = $psr->getAttribute('oauth_user_id');
// 根据 $userId 查询用户表返回
// return json(['id' => $userId, 'username' => 'demo']);
// 用 Webman 的 support\Response 返回 JSON 更方便
return json([
'id' => $userId,
'username' => User::find($userId)->username,
]);
}
public function info(Request $request)
{
$userId = $request->session()->get('user_id');
if (!$userId) {
return json(['code'=>401,'msg'=>'未登录']);
}
$user = User::with('roles')->find($userId);
return json([
'code' => 0,
'data' => [
'user' => [
'id' => $user->id,
'username' => $user->username,
'roles' => $user->roles->pluck('name')->all(),
],
'menus' => AuthService::getMenuTree($userId),
'permissions' => AuthService::getPermissionCodes($userId),
],
'msg' => 'ok',
]);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace app\common\controller;
use app\common\service\AuthService;
use app\common\validate\UserValidate;
use support\Request;
class AuthController extends BaseController
{
public function login(Request $request)
{
try {
$data = $this->validate(new UserValidate(), $request->post(), 'upwd_login');
$result = (new AuthService())->login($data);
return $this->success($result, '登录成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function register(Request $request)
{
try {
$data = $this->validate(new UserValidate(), $request->post(), 'upwd_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);
}
}
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);
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');
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace app\common\controller;
use support\Request;
use support\Response;
use support\exception\BusinessException;
use app\common\traits\ResponseTrait;
use think\Validate;
abstract class BaseController
{
use ResponseTrait;
protected mixed $validator = null;
/**
* 验证数据
*
* @param Validate $validator 验证器实例
* @param array $data 待验证的数据
* @param string $scene 验证场景
* @return array 验证后的数据
* @throws BusinessException 验证失败时抛出业务异常
*/
public function validate(Validate $validator, array $data, string $scene = ''): array
{
// 如果指定了验证场景,并且验证器支持场景方法,则设置验证场景
if ($scene && method_exists($validator, 'scene')) {
$validator->scene($scene);
}
// 执行数据验证
if (!$validator->check($data)) {
// 验证失败,抛出业务异常,异常信息为验证器返回的错误信息
throw new BusinessException($validator->getError());
}
// 验证成功,返回原始数据
return $data;
}
}
+475
View File
@@ -0,0 +1,475 @@
<?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\Redis;
use support\exception\BusinessException;
use support\Log;
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,
];
}
/**
* 飞书登录
*/
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, $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);
}
$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 register(array $data)
{
// 获取可用用户编号
$sn = $this->getAvailableUserSn();
$data['sn'] = $sn->user_sn;
// 对密码进行哈希处理
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
// 创建用户记录
$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;
}
/**
* 获取可用的用户编号
*
* @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 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->code,
'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, string $systemCode)
{
$refreshToken = bin2hex(random_bytes(32));
// 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;
}
/**
* 验证扫码令牌
*/
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;
}
}
+358
View File
@@ -0,0 +1,358 @@
<?php
namespace app\common\logic;
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;
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);
// 返回包含访问令牌、刷新令牌和令牌过期时间的数组
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
];
}
/**
* 飞书登录
*
* @param array $data
* @return array
*/
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->preparePayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
];
}
/**
* 扫码登录
*
* @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);
}
$system = $this->validateSystem($data['system_code']);
$this->validateSystemPermission($user, $system);
$payload = $this->preparePayload($user, $system);
$token = $this->generateAccessToken($payload);
$refreshToken = $this->generateRefreshToken($user->id);
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => config('jwt.jwt_expiration'),
];
}
/**
* 验证用户信息
*
* @param string $account
* @param string $password
* @return User|null
*/
private function validateUser(string $account, string $password)
{
$user = User::byAccount($account)->first();
if (!$user || !password_verify($password, $user->password)) {
throw new \Exception('账号或密码错误', 401);
}
if ($user->status != 1) {
throw new \Exception('账号已禁用', 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);
}
return $system;
}
/**
* 验证用户系统权限
*
* @param User $user
* @param Systems $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 \Exception('无该系统权限', 403);
}
}
}
/**
* 准备 JWT 负载
*
* @param User $user
* @param Systems $system
* @return array
*/
private function preparePayload(User $user, Systems $system)
{
$payload = [
'uid' => $user->id,
'sub' => 'user_'.$user->id,
'aud' => $system->domain_url,
'role_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
*/
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)
{
$refreshToken = bin2hex(random_bytes(32));
Cache::set("refresh_token:{$refreshToken}", $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}");
if (!$userId) {
throw new \Exception('扫码令牌无效或已过期', 401);
}
return $userId;
}
/**
* 处理用户注册逻辑
*
* @param array $data
* @return User
*/
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);
return $this->updateUserId($user->id, $sn->user_sn);
}
public function updateUserId($userId, $userSn)
{
$userSn = User::where('user_sn', $userSn)->first();
if (!$userSn) {
throw new BusinessException('用户编号不存在');
}
return $userSn->update([
'user_id' => $userId
]);
}
/**
* 校验token
*/
public function verifyToken(string $token): array
{
$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);
return [
'access_token' => $token,
'expires_in' => config('jwt.jwt_expiration'),
];
}
/**
* 登出,将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
@@ -0,0 +1,416 @@
<?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;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace app\common\model;
use support\Model;
/**
* resources 资源表:目录/菜单/权限
* @property integer $id 资源ID(主键)
* @property string $name 资源名称
* @property string $type 资源类型
* @property string $code 资源编码(权限标识)
* @property integer $parent_id 父资源ID
* @property string $url 路由 path 或 外链 URL
* @property string $icon 图标,仅目录/菜单有效
* @property string $open_type 菜单打开方式
* @property integer $sort 排序
* @property string $created_at
* @property string $updated_at
*/
class Resource extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'resources';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
// 所属系统
public function system()
{
return $this->belongsTo(Systems::class, 'system_id', 'id');
}
// 拥有该资源的角色
public function roles()
{
return $this->belongsToMany(Role::class, 'role_resources', 'resource_id', 'role_id');
}
// 拥有该资源的用户(特殊补权)
public function users()
{
return $this->belongsToMany(User::class, 'user_resources', 'resource_id', 'user_id');
}
// 递归
public function parent()
{
return $this->belongsTo(self::class, 'parent_id');
}
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace app\common\model;
use support\Model;
/**
* roles 角色表
* @property integer $id 角色ID(主键)
* @property string $name 角色名称
* @property string $desc 角色描述
* @property string $created_at
* @property string $updated_at
*/
class Role extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'roles';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
// 角色下所有用户
public function users()
{
return $this->belongsToMany(User::class, 'user_roles', 'role_id', 'user_id');
}
// 角色拥有的资源(权限点、菜单)
public function resources()
{
return $this->belongsToMany(Resource::class, 'role_resources', 'role_id', 'resource_id');
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace app\common\model;
use support\Model;
/**
* systems 系统/应用表
* @property integer $id 系统ID(主键)
* @property string $code 系统编码(唯一英文标识)
* @property string $name 系统名称
* @property string $type 系统类型
* @property string $desc 系统描述
* @property string $icon 系统图标
* @property string $domain_url 系统URL
* @property string $is_role 是否区分角色权限
* @property integer $sort 排序
* @property integer $status 状态
* @property integer $created_at 创建时间
* @property integer $updated_at 更新时间
*/
class Systems extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'systems';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
// 系统下的资源
public function resources()
{
return $this->hasMany(Resource::class, 'system_id', 'id');
}
// 拥有该系统访问权限的用户
public function users()
{
return $this->belongsToMany(User::class, 'user_systems', 'system_id', 'user_id');
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace app\common\model;
use support\Model;
/**
* users 用户表
* @property integer $id 用户ID(主键)
* @property string $sn 编号
* @property string $name 姓名
* @property string $avatar 头像
* @property string $password 密码
* @property string $phone 手机
* @property string $email 邮箱
* @property integer $status 状态
* @property string $system_code 系统码
* @property string $created_at
* @property string $updated_at
*/
class User 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';
/**
* 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 = [
'sn',
'name',
'avatar',
'password',
'phone',
'email',
'status',
'system_code'
];
// 用户所有角色
public function roles()
{
return $this->belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id');
}
// 用户直接拥有的资源(用于特殊补权)
public function resources()
{
return $this->belongsToMany(Resource::class, 'user_resources', 'user_id', 'resource_id');
}
// 用户可访问的系统
public function systems()
{
return $this->belongsToMany(Systems::class, 'user_systems', 'user_id', 'system_id');
}
// 账号查询的作用域
public function scopeByAccount($query, $account)
{
return $query->where('sn', $account)
->orWhere('phone', $account)
->orWhere('email', $account);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace app\common\model;
use support\Model;
/**
* user_sn 用户-编号
* @property integer $user_sn
* @property integer $user_id
*/
class UserSn 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_sn';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
protected $fillable = [
'user_sn',
'user_id'
];
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace app\common\model;
use support\Model;
/**
* user_token_log 用户token登录日志
* @property integer $id (主键)
* @property integer $user_id
* @property string $token
* @property string $login_ip
* @property string $user_agent
* @property integer $login_time
* @property integer $expired_at
* @property integer $is_valid 1有效 0已拉黑
*/
class UserTokenLog 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_token_log';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
protected $fillable = [
'user_id',
'token',
'login_ip',
'user_agent',
'login_time',
'expired_at',
'is_valid',
'system_code'
];
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace app\common\service;
use app\common\logic\AuthLogic;
use app\common\model\User;
use support\Db;
class AuthService
{
/**
* 登录,支持多类型
*/
public function login(array $data): array
{
$loginType = $data['login_type'] ?? 'upwd';
$logic = new AuthLogic();
return match ($loginType) {
'feishu' => $logic->feishuLogin($data),
'qrcode' => $logic->qrCodeLogin($data),
default => $logic->uPwdLogin($data),
};
}
/**
* 用户注册
*/
public function register(array $data)
{
$logic = new AuthLogic();
try {
Db::beginTransaction();
$result = $logic->register($data);
Db::commit();
return $result;
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
}
/**
* 校验token
*/
public function verifyToken(string $token, string $systemCode): array
{
return (new AuthLogic())->verifyToken($token, $systemCode);
}
/**
* 刷新token
* @param string $refreshToken
* @param string|null $systemCode
*/
public function refreshToken(string $refreshToken, string $systemCode): array
{
return (new AuthLogic())->refreshToken($refreshToken, $systemCode);
}
/**
* 登出(失效token)
*/
public function logout(?string $token, string $systemCode): void
{
(new AuthLogic())->logout($token, $systemCode);
}
/**
* 强制拉黑token
*/
public function blockToken(string $token, string $systemCode): void
{
(new AuthLogic())->blockToken($token, $systemCode);
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace app\common\service;
use app\common\logic\AuthLogic;
use app\common\model\User;
use support\Cache;
class AuthService_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(仅 type=permission)
*/
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));
}
public function login(array $data): array
{
$loginType = $data['login_type'] ?? 'upwd';
$logic = new AuthLogic();
return match ($loginType) {
'feishu' => $logic->feishuLogin($data),
'qrcode' => $logic->qrCodeLogin($data),
default => $logic->uPwdLogin($data),
};
}
/**
* 用户注册
*
* @param array $data
* @return User
*/
public function register(array $data): User
{
return (new AuthLogic())->register($data);
}
public function verifyToken(string $token): array
{
return (new AuthLogic())->verifyToken($token);
}
public function refreshToken(string $refreshToken): array
{
return (new AuthLogic())->refreshToken($refreshToken);
}
public function logout(?string $token)
{
(new AuthLogic())->logout($token);
}
public function blockToken(string $token)
{
(new AuthLogic())->blockToken($token);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace app\common\service;
use app\common\model\User;
use support\Cache;
class PermService
{
// 用户资源缓存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));
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace app\common\service;
use app\model\User;
use app\model\Role;
use app\model\Resource;
class ResourceService
{
/**
* 获取用户可见菜单树
*/
public static function getMenuTreeForUser($userId)
{
$menuList = self::getUserMenus($userId);
// 转树
return self::buildTree($menuList);
}
/**
* 获取用户所有有权限的菜单资源(type=menu)
*/
public static function getUserMenus($userId)
{
$user = User::with('roles.resources')->find($userId);
if (!$user) return [];
// 合并所有角色下的菜单资源
$allMenus = [];
foreach ($user->roles as $role) {
foreach ($role->resources as $res) {
if ($res->type === 'menu') {
$allMenus[$res->id] = $res;
}
}
}
return array_values($allMenus); // 去重后返回
}
/**
* 获取用户所有有权限的按钮/功能权限 code
*/
public static function getButtonPermissionCodesForUser($userId)
{
$user = User::with('roles.resources')->find($userId);
if (!$user) return [];
$codes = [];
foreach ($user->roles as $role) {
foreach ($role->resources as $res) {
if ($res->type === 'button' && $res->code) {
$codes[$res->code] = true;
}
}
}
return array_keys($codes); // 去重后返回权限code数组
}
/**
* 递归构建菜单树
* @param Resource[] $list
* @param int $parentId
* @return array
*/
protected static function buildTree($list, $parentId = 0)
{
$tree = [];
foreach ($list as $item) {
if ($item->parent_id == $parentId) {
$children = self::buildTree($list, $item->id);
$arr = [
'id' => $item->id,
'name' => $item->name,
'path' => $item->url,
'icon' => $item->icon ?? null,
'code' => $item->code,
'children' => $children,
];
$tree[] = $arr;
}
}
return $tree;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace app\common\service;
use app\model\User;
class UserService
{
public static function getInfo($userId)
{
$user = User::with('roles')->find($userId);
if (!$user) return null;
return [
'id' => $user->id,
'username' => $user->username,
'roles' => $user->roles->pluck('name')->toArray(),
// 可扩展其他字段
];
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace app\common\traits;
use support\Response;
trait ResponseTrait
{
/**
* 返回成功响应
*
* @param mixed $data 响应数据
* @param string $message 响应消息,默认“请求成功”
* @param int $code 内部状态码,默认 0 表示成功
* @param int $httpStatus HTTP 状态码,默认 200
* @param array $header 响应头,默认 JSON 格式
* @return Response
*/
public function success(
$data = [],
string $message = '请求成功',
int $code = 0,
int $httpStatus = 200,
array $header = ['Content-Type' => 'application/json']
): Response {
return $this->result('success', $code, $httpStatus, $message, $data, $header);
}
/**
* 返回错误响应
*
* @param string $message 响应错误消息,默认“请求失败”
* @param int $code 内部状态码,默认 1 表示错误
* @param int $httpStatus HTTP 状态码,默认 400
* @param mixed $data 可选的错误数据
* @param array $header 响应头,默认 JSON 格式
* @return Response
*/
public function error(
string $message = '请求失败',
int $code = 1,
int $httpStatus = 400,
$data = [],
array $header = ['Content-Type' => 'application/json']
): Response {
return $this->result('error', $code, $httpStatus, $message, $data, $header);
}
/**
* 统一响应格式方法
*
* @param string $status 响应状态,例如 "success" 或 "error"
* @param int $code 内部状态码
* @param int $httpStatus HTTP 状态码
* @param string $message 响应消息
* @param mixed $data 响应数据
* @param array $header 响应头
* @return Response
*/
protected function result(
string $status,
int $code,
int $httpStatus,
string $message,
$data,
array $header
): Response {
$body = [
'status' => $status,
'code' => $code,
'message' => $message,
'data' => $data,
];
return response(json_encode($body, JSON_UNESCAPED_UNICODE), $httpStatus, $header);
}
}
+228
View File
@@ -0,0 +1,228 @@
<?php
namespace app\common\utils;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use support\Cache;
use Webman\Exception\BusinessException;
class Feishu
{
protected $appId = '';
protected $appSecret = '';
protected $baseUri = '';
protected $client = null;
protected $options = [];
protected $error = '';
protected $gs = 'hesu';
/**
* 初始化
*/
public function __construct(string $appId = '', string $appSecret = '', string $gs = 'hesu')
{
if ($gs === 'mbaf') {
$this->baseUri = 'https://open.larksuite.com/open-apis/';
} else {
$this->baseUri = 'https://open.feishu.cn/open-apis/';
}
$this->appId = $appId;
$this->appSecret = $appSecret;
$this->gs = $gs;
$this->client = new Client([
'base_uri' => $this->baseUri
]);
$this->options = [
'headers' => [
'Content-Type' => 'application/json; charset=utf-8'
]
];
}
/**
* 异常
*/
public function error()
{
return $this->error;
}
/**
* 请求
*/
public function requestResult($url = "", $method = "POST")
{
try {
$res = $this->client->request($method, $this->baseUri.$url, $this->options);
if ($res->getBody()) {
$result = json_decode($res->getBody(), true);
// var_dump($result);
if ($result["code"] === 0) {
return $result;
} else {
$this->error = $result["msg"];
return false;
}
} else {
$this->error = "result error";
return false;
}
} catch (RequestException $e) {
if ($e->hasResponse()) {
var_dump('2 :'.$e->getResponse()->getBody());
throw new BusinessException($e->getResponse()->getBody());
} else {
var_dump('3 :'.$e->getMessage());
throw new BusinessException($e->getMessage());
}
} catch (\Throwable $th) {
var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage());
$this->error = $th->getFile()."|".$th->getLine()."|".$th->getMessage();
return false;
}
}
/**
* 授权
*/
public function authorize($certificate, $url = 'authen/v1/authorize')
{
$arr = [
'app_id' => $certificate['appId'],
'redirect_uri' => $certificate['rUrl'],
'scope' => $certificate['scope'],
'state' => $certificate['state']
];
$queryData = http_build_query($arr);
return $this->baseUri.$url.'?'.$queryData;
}
/**
* 自建应用获取 tenant_access_token
*/
public function tenantAccessToken()
{
$tenantAccessToken = Cache::get($this->gs.'-tenantAccessToken');
if (!$tenantAccessToken) {
$this->options["json"] = [
"app_id" => $this->appId,
"app_secret" => $this->appSecret
];
$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']);
} else {
throw new BusinessException('tenant_access_token 获取失败');
}
}
return $tenantAccessToken;
}
/**
* 通过手机号或邮箱获取用户 ID
*/
public function getUserIdByPhoneOrEmail($phone = '', $userIdType = 'open_id')
{
$this->options["headers"] = [
"Authorization" => "Bearer " . $this->tenantAccessToken(),
"Content-Type" => "application/json; charset=utf-8"
];
$this->options["json"] = [
"emails" => [],
"mobiles" => [$phone],
"include_resigned" => false
];
$rst = $this->requestResult('contact/v3/users/batch_get_id?user_id_type='.$userIdType, 'POST');
return $rst;
}
/**
* 发送消息
*/
public function sendMessages($receiveId = '', $content = '', $msgType = 'text', $receiveIdtype = 'open_id')
{
$this->options["headers"] = [
"Authorization" => "Bearer " . $this->tenantAccessToken(),
"Content-Type" => "application/json; charset=utf-8"
];
$this->options["json"] = [
"receive_id" => $receiveId,
"msg_type" => $msgType,
"content" => $content
];
$rst = $this->requestResult('im/v1/messages?receive_id_type='.$receiveIdtype, 'POST');
return $rst;
}
/**
* 编辑消息
*/
public function updateSendMessages($messageId = '', $content = '', $msgType = 'text')
{
$this->options["headers"] = [
"Authorization" => "Bearer " . $this->tenantAccessToken(),
"Content-Type" => "application/json; charset=utf-8"
];
$this->options["json"] = [
"msg_type" => $msgType,
"content" => $content
];
$rst = $this->requestResult('im/v1/messages/'.$messageId, 'PUT');
return $rst;
}
/**
* 用户 user_access_token
*/
public function userAccessToken($code = "", $redirect_uri = "")
{
$this->options["headers"] = [
"Content-Type" => "application/json; charset=utf-8"
];
$this->options["json"] = [
"grant_type" => "authorization_code",
"client_id" => $this->appId,
"client_secret" => $this->appSecret,
"code" => $code,
"redirect_uri" => $redirect_uri
];
return $this->requestResult('authen/v2/oauth/token');
}
/**
* 用户信息
*/
public function userInfo($userAccessToken = "")
{
$this->options["headers"] = [
"Authorization" => "Bearer " . $userAccessToken,
"Content-Type" => "application/json; charset=utf-8"
];
return $this->requestResult('authen/v1/user_info', "GET");
}
// public function user_access_token($code = "")
// {
// $url = "";
// $this->options["headers"] = [
// "Authorization" => "Bearer ".$this->tenantAccessToken(),
// "Content-Type" => "application/json; charset=utf-8"
// ];
// $this->options["json"] = [
// "grant_type" => "authorization_code",
// "code" => $code
// ];
// $this->options["form_params"] = [
// "app_id" => $this->app_id,
// "app_secret" => $this->app_secret
// ];
// return $this->requestResult($url);
// }
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace app\common\utils;
use Firebase\JWT\JWT as FirebaseJWT;
use Firebase\JWT\Key;
use support\exception\BusinessException;
class Jwt
{
protected $secretKey;
protected $algorithm;
protected $issuer;
protected $expiration;
public function __construct()
{
$this->secretKey = config('jwt.jwt_secret_key');
$this->algorithm = config('jwt.jwt_algorithm');
$this->issuer = config('jwt.jwt_issuer');
$this->expiration = config('jwt.jwt_expiration');
}
/**
* 生成 JWT 令牌
*
* @param array $payload
* @return string
* @throws BusinessException
*/
public function generateToken(array $payload): string
{
$defaultPayload = [
// // —— 注册 Claim ——
'iss' => $this->issuer, // 签发者
// 'sub' => 'user_1001', // 面向的用户
// 'aud' => 'api.gateway.example.com', // 接收方
// 'iat' => time(), // 签发时间
// 'nbf' => time(), // 生效时间
// 'exp' => time() + $this->expiration, // 1 小时后过期
'jti' => bin2hex(random_bytes(16)), // 唯一 ID
// // —— 自定义 Claim ——
// 'uid' => 1001, // 业务用户 ID
// 'roles' => ['admin', 'editor'], // 用户角色
// 'meta' => ['region' => 'CN', 'tier' => 5], // 任意自定义数据
];
$payload = array_merge($defaultPayload, $payload);
try {
return FirebaseJWT::encode($payload, $this->secretKey, $this->algorithm);
} catch (\Exception $e) {
throw new BusinessException("令牌生成失败:" . $e->getMessage(), 500);
}
}
/**
* 验证 JWT 令牌
*
* @param string $token
* @return object
* @throws BusinessException
*/
public function verifyToken(string $token)
{
try {
return FirebaseJWT::decode($token, new Key($this->secretKey, $this->algorithm));
} catch (\Firebase\JWT\ExpiredException $e) {
throw new BusinessException('令牌已过期', 401);
} catch (\Firebase\JWT\SignatureInvalidException $e) {
throw new BusinessException('无效的令牌签名', 401);
} catch (\Firebase\JWT\BeforeValidException $e) {
throw new BusinessException('令牌尚未生效', 401);
} catch (\Exception $e) {
throw new BusinessException('令牌验证失败:' . $e->getMessage(), 400);
}
}
public function describe()
{
/* payload
1. 注册 Claim 详解
Claim 名称 含义 类型 建议取值
iss Issuer,发行者,标识该 JWT 的签发主体 string or URI 你的服务域名或项目标识,比如 https://api.example.com
sub Subject,主题,标识该 JWT 所面向的用户 string or int 用户在你系统里的唯一 ID
aud Audience,受众,标识该 JWT 的接收方 string or array 接收该令牌的服务标识,比如 ["service-a","service-b"]
exp Expiration Time,过期时间,Unix 时间戳,到达此时间后不可用 int time() + 3600(1 小时后过期)
nbf Not Before,生效时间,Unix 时间戳,表示在此时间之前不可用 int time() 或者 time() + 60(1 分钟后生效)
iat Issued At,签发时间,Unix 时间戳 int time()
jti JWT ID,JWT 的唯一标识符,用于去重或一次性令牌 string uniqid('', true) 或者 UUID
注意:exp、nbf、iat 必须是整数(秒级时间戳),否则库会抛类型异常或验证失败。
2. 自定义 Claim
除了上面这些“注册”字段,你可以任意添加,比如:
php
$payload['uid'] = 123; // 业务用户 ID
$payload['roles'] = ['admin','user']; // 权限数组
$payload['feature'] = 'beta'; // 标记某个功能开关
这些字段在你的应用程序里可以自由取用。
*/
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace app\common\utils;
class OauthClients
{
/**
* 生成 OAuth2 client 凭证
*
* @param int $idBytes 用于生成 client_id 的随机字节长度(最终 hex 长度 = $idBytes * 2)
* @param int $secretBytes 用于生成 client_secret 的随机字节长度(最终 hex 长度 = $secretBytes * 2)
* @return array ['client_id' => string, 'client_secret' => string]
*/
public function generateClientCredentials(int $idBytes = 16, int $secretBytes = 32): array
{
// 生成随机字节,并转为十六进制字符串
$clientId = bin2hex(random_bytes($idBytes));
$clientSecret = bin2hex(random_bytes($secretBytes));
return [
'client_id' => $clientId,
'client_secret' => $clientSecret,
];
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace app\common\validate;
use app\common\model\Systems;
use app\common\model\User;
use think\Validate;
class UserValidate extends Validate
{
protected $rule = [
'name' => 'chsDash|max:60',
'avatar' => 'regex:/^(https?:\/\/\S+|\\/[\S\/]+)$/',
'password' => 'require|min:8|max:20|alphaNum',
'confirm_password' => 'require|confirm:password',
'account' => 'require|checkAccount|checkAccountUnique',
'phone' => 'checkMobile|checkPhoneUnique',
'email' => 'email|checkEmailUnique',
'login_type' => 'require|in:upwd,feishu,qrcode',
'system_code' => 'require|checkSystemCode'
];
protected $message = [
'name.chsDash' => '姓名只能是汉字、字母、数字和下划线_及破折号-',
'name.max' => '姓名最多不能超过60个字符',
'avatar.regex' => '头像必须是合法的URL或有效的文件路径',
'password.require' => '密码不能为空',
'password.min' => '密码长度不能少于8位',
'password.max' => '密码长度不能超过20位',
'password.alphaNum' => '密码只能包含字母和数字',
'confirm_password.require' => '请再次输入密码',
'confirm_password.confirm' => '两次输入的密码不一致',
'account.require' => '账号不能为空',
'account.checkAccount' => '账号必须是有效的手机号或邮箱',
'account.checkAccountUnique' => '账号已被注册',
'phone.checkMobile' => '手机号格式不正确',
'phone.checkPhoneUnique' => '手机号已被使用',
'email.email' => '邮箱格式不正确',
'email.checkEmailUnique' => '邮箱已被使用',
'login_type.require' => '请选择登录方式',
'login_type.in' => '登录方式不支持',
'system_code.require' => '系统码不能为空',
'system_code.checkSystemCode' => '系统码不存在或不合法'
];
protected $scene = [
'upwd_register' => ['name', 'account', 'password', 'confirm_password', 'login_type', 'system_code'],
'upwd_login' => ['account', 'password', 'login_type', 'system_code'],
];
/**
* 验证账号格式(手机号或邮箱)
*/
protected function checkAccount($value)
{
return $this->isMobile($value) || $this->isEmail($value);
}
/**
* 验证账号唯一性(注册场景用)
*/
protected function checkAccountUnique($value, $rule, $data, $field, $title)
{
// 登录场景不检查唯一性
if (isset($this->currentScene) && $this->currentScene === 'login') {
return true;
}
$type = $this->getAccountType($value);
$field = ($type === 'phone') ? 'phone' : 'email';
$value = ($type === 'phone') ? $this->formatPhone($value) : $value;
return !User::where($field, $value)->exists();
}
/**
* 判断是否为手机号
*/
protected function isMobile($value)
{
$mobile = preg_replace('/[\s+\-]/', '', $value);
return preg_match('/^(\+86|0086)?1[3-9]\d{9}$/', $mobile);
}
/**
* 判断是否为邮箱
*/
protected function isEmail($value)
{
return filter_var($value, FILTER_VALIDATE_EMAIL);
}
/**
* 获取账号类型(phone/email)
*/
public function getAccountType($account)
{
return $this->isMobile($account) ? 'phone' : 'email';
}
/**
* 格式化手机号(移除+86和空格)
*/
public function formatPhone($phone)
{
return preg_replace('/^(\+86|0086)|[\s\-]/', '', $phone);
}
/**
* 手机号唯一性验证
*/
protected function checkPhoneUnique($value)
{
return !User::where('phone', $this->formatPhone($value))->exists();
}
/**
* 邮箱唯一性验证
*/
protected function checkEmailUnique($value)
{
return !User::where('email', $value)->exists();
}
/**
* 验证 system_code 是否在系统表中存在
*/
protected function checkSystemCode($value, $rule, $data, $field)
{
return Systems::where('code', $value)->exists();
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace app\controller;
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 = "mzl92";
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>
';
}
public function view(Request $request)
{
return view('index/view', ['name' => 'webman']);
}
public function json(Request $request)
{
return json(['code' => 0, 'msg' => 'ok']);
}
}
+4
View File
@@ -0,0 +1,4 @@
<?php
/**
* Here is your custom functions.
*/
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
use app\service\AuthService;
class PermissionMiddleware implements MiddlewareInterface
{
protected $permission_code;
// 支持传参(如 "user:view"),可以在路由闭包里传入
public function __construct($permission_code = null)
{
$this->permission_code = $permission_code;
}
public function process(Request $request, callable $next): Response
{
// 假设有 auth() helper 方法能获取当前登录用户ID
$user_id = $request->session()->get('user_id');
if (!$user_id) {
return json(['code' => 401, 'msg' => '未登录']);
}
if ($this->permission_code && !AuthService::check($user_id, $this->permission_code)) {
return json(['code' => 403, 'msg' => '没有权限']);
}
return $next($request);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
/**
* Class StaticFile
* @package app\middleware
*/
class StaticFile implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
// Access to files beginning with. Is prohibited
if (strpos($request->path(), '/.') !== false) {
return response('<h1>403 forbidden</h1>', 403);
}
/** @var Response $response */
$response = $handler($request);
// Add cross domain HTTP header
/*$response->withHeaders([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
]);*/
return $response;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace app\process;
use Webman\App;
class Http extends App
{
}
+305
View File
@@ -0,0 +1,305 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace app\process;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use Workerman\Timer;
use Workerman\Worker;
/**
* Class FileMonitor
* @package process
*/
class Monitor
{
/**
* @var array
*/
protected array $paths = [];
/**
* @var array
*/
protected array $extensions = [];
/**
* @var array
*/
protected array $loadedFiles = [];
/**
* @var int
*/
protected int $ppid = 0;
/**
* Pause monitor
* @return void
*/
public static function pause(): void
{
file_put_contents(static::lockFile(), time());
}
/**
* Resume monitor
* @return void
*/
public static function resume(): void
{
clearstatcache();
if (is_file(static::lockFile())) {
unlink(static::lockFile());
}
}
/**
* Whether monitor is paused
* @return bool
*/
public static function isPaused(): bool
{
clearstatcache();
return file_exists(static::lockFile());
}
/**
* Lock file
* @return string
*/
protected static function lockFile(): string
{
return runtime_path('monitor.lock');
}
/**
* FileMonitor constructor.
* @param $monitorDir
* @param $monitorExtensions
* @param array $options
*/
public function __construct($monitorDir, $monitorExtensions, array $options = [])
{
$this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
static::resume();
$this->paths = (array)$monitorDir;
$this->extensions = $monitorExtensions;
foreach (get_included_files() as $index => $file) {
$this->loadedFiles[$file] = $index;
if (strpos($file, 'webman-framework/src/support/App.php')) {
break;
}
}
if (!Worker::getAllWorkers()) {
return;
}
$disableFunctions = explode(',', ini_get('disable_functions'));
if (in_array('exec', $disableFunctions, true)) {
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
} else {
if ($options['enable_file_monitor'] ?? true) {
Timer::add(1, function () {
$this->checkAllFilesChange();
});
}
}
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
}
}
/**
* @param $monitorDir
* @return bool
*/
public function checkFilesChange($monitorDir): bool
{
static $lastMtime, $tooManyFilesCheck;
if (!$lastMtime) {
$lastMtime = time();
}
clearstatcache();
if (!is_dir($monitorDir)) {
if (!is_file($monitorDir)) {
return false;
}
$iterator = [new SplFileInfo($monitorDir)];
} else {
// recursive traversal directory
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new RecursiveIteratorIterator($dirIterator);
}
$count = 0;
foreach ($iterator as $file) {
$count ++;
/** var SplFileInfo $file */
if (is_dir($file->getRealPath())) {
continue;
}
// check mtime
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
$lastMtime = $file->getMTime();
if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
continue;
}
$var = 0;
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
if ($var) {
continue;
}
// send SIGUSR1 signal to master process for reload
if (DIRECTORY_SEPARATOR === '/') {
if ($masterPid = $this->getMasterPid()) {
echo $file . " updated and reload\n";
posix_kill($masterPid, SIGUSR1);
} else {
echo "Master process has gone away and can not reload\n";
}
return true;
}
echo $file . " updated and reload\n";
return true;
}
}
if (!$tooManyFilesCheck && $count > 1000) {
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
$tooManyFilesCheck = 1;
}
return false;
}
/**
* @return int
*/
public function getMasterPid(): int
{
if ($this->ppid === 0) {
return 0;
}
if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
echo "Master process has gone away\n";
return $this->ppid = 0;
}
if (PHP_OS_FAMILY !== 'Linux') {
return $this->ppid;
}
$cmdline = "/proc/$this->ppid/cmdline";
if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
// Process not exist
$this->ppid = 0;
}
return $this->ppid;
}
/**
* @return bool
*/
public function checkAllFilesChange(): bool
{
if (static::isPaused()) {
return false;
}
foreach ($this->paths as $path) {
if ($this->checkFilesChange($path)) {
return true;
}
}
return false;
}
/**
* @param $memoryLimit
* @return void
*/
public function checkMemory($memoryLimit): void
{
if (static::isPaused() || $memoryLimit <= 0) {
return;
}
$masterPid = $this->getMasterPid();
if ($masterPid <= 0) {
echo "Master process has gone away\n";
return;
}
$childrenFile = "/proc/$masterPid/task/$masterPid/children";
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
return;
}
foreach (explode(' ', $children) as $pid) {
$pid = (int)$pid;
$statusFile = "/proc/$pid/status";
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
continue;
}
$mem = 0;
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
$mem = $match[1];
}
$mem = (int)($mem / 1024);
if ($mem >= $memoryLimit) {
posix_kill($pid, SIGINT);
}
}
}
/**
* Get memory limit
* @param $memoryLimit
* @return int
*/
protected function getMemoryLimit($memoryLimit): int
{
if ($memoryLimit === 0) {
return 0;
}
$usePhpIni = false;
if (!$memoryLimit) {
$memoryLimit = ini_get('memory_limit');
$usePhpIni = true;
}
if ($memoryLimit == -1) {
return 0;
}
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
$memoryLimit = (int)$memoryLimit;
if ($unit === 'g') {
$memoryLimit = 1024 * $memoryLimit;
} else if ($unit === 'k') {
$memoryLimit = ($memoryLimit / 1024);
} else if ($unit === 'm') {
$memoryLimit = (int)($memoryLimit);
} else if ($unit === 't') {
$memoryLimit = (1024 * 1024 * $memoryLimit);
} else {
$memoryLimit = ($memoryLimit / (1024 * 1024));
}
if ($memoryLimit < 50) {
$memoryLimit = 50;
}
if ($usePhpIni) {
$memoryLimit = (0.8 * $memoryLimit);
}
return (int)$memoryLimit;
}
}
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/favicon.ico"/>
<title>webman</title>
</head>
<body>
hello <?=htmlspecialchars($name)?>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
{
"name": "workerman/webman",
"type": "project",
"keywords": [
"high performance",
"http service"
],
"homepage": "https://www.workerman.net",
"license": "MIT",
"description": "High performance HTTP Service Framework.",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "https://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/webman/issues",
"forum": "https://wenda.workerman.net/",
"wiki": "https://workerman.net/doc/webman",
"source": "https://github.com/walkor/webman"
},
"require": {
"php": ">=8.1",
"workerman/webman-framework": "^2.1",
"monolog/monolog": "^2.0",
"webman/database": "^2.1",
"illuminate/pagination": "^12.18",
"illuminate/events": "^12.18",
"symfony/var-dumper": "^7.3",
"webman/redis": "^2.1",
"webman/cache": "^2.1",
"webman/console": "^2.1",
"vlucas/phpdotenv": "^5.6",
"firebase/php-jwt": "^6.11",
"league/oauth2-server": "^9.2",
"topthink/think-validate": "^3.0"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"": "./",
"app\\": "./app",
"App\\": "./app",
"app\\View\\Components\\": "./app/view/components"
}
},
"scripts": {
"post-package-install": [
"support\\Plugin::install"
],
"post-package-update": [
"support\\Plugin::install"
],
"pre-package-uninstall": [
"support\\Plugin::uninstall"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}
Generated
+5968
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Request;
return [
'debug' => getenv('DEBUG'),
'error_reporting' => E_ALL,
'default_timezone' => 'Asia/Shanghai',
'request_class' => Request::class,
'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
'controller_suffix' => 'Controller',
'controller_reuse' => false,
];
+21
View File
@@ -0,0 +1,21 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'files' => [
base_path() . '/app/functions.php',
base_path() . '/support/Request.php',
base_path() . '/support/Response.php',
]
];
+17
View File
@@ -0,0 +1,17 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
support\bootstrap\Session::class,
];
+31
View File
@@ -0,0 +1,31 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => 'file',
'stores' => [
'file' => [
'driver' => 'file',
'path' => runtime_path('cache')
],
'redis' => [
'driver' => 'redis',
'connection' => 'default'
],
'array' => [
'driver' => 'array'
]
]
];
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return new Webman\Container;
+29
View File
@@ -0,0 +1,29 @@
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => getenv('DB_HOST'),
'port' => getenv('DB_PORT'),
'database' => getenv('DB_NAME'),
'username' => getenv('DB_USER'),
'password' => getenv('DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'prefix' => getenv('DB_PREFIX'),
'strict' => true,
'engine' => null,
'options' => [
PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers.
],
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
],
],
];
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];
+17
View File
@@ -0,0 +1,17 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'' => support\exception\Handler::class,
];
+9
View File
@@ -0,0 +1,9 @@
<?php
return [
'jwt_secret_key' => getenv('JWT_SECRET_KEY') ?: 'default_secret_key',
'jwt_algorithm' => getenv('JWT_ALGORITHM') ?: 'HS256',
'jwt_issuer' => getenv('JWT_ISSUER') ?: 'domain.com',
'jwt_expiration' => (int)(getenv('JWT_EXPIRATION') ?: 86400),
'jwt_refresh_token_expiration' => (int)(getenv('JWT_REFRESH_TOKEN_EXPIRATION') ?: 2592000)
];
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => [
'handlers' => [
[
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/webman.log',
7, //$maxFiles
Monolog\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [null, 'Y-m-d H:i:s', true],
],
]
],
],
];
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [];
+9
View File
@@ -0,0 +1,9 @@
<?php
return [
'private_key' => __DIR__ . '/private.key',
'public_key' => __DIR__ . '/public.key',
'encrypt_key' => env('OAUTH_ENCRYPT_KEY', 'base64:xxxxxxxx'),
'access_token_expire' => 7200,
'refresh_token_expire' => 1209600,
];
+24
View File
@@ -0,0 +1,24 @@
<?php
return [
'enable' => true,
'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
'phar_filename' => 'webman.phar',
'bin_filename' => 'webman.bin',
'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
'exclude_files' => [
'.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
],
'custom_ini' => '
memory_limit = 256M
',
];
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Log;
use support\Request;
use app\process\Http;
global $argv;
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:'.getenv('PROCESS_LISTEN'),
// 'count' => cpu_count() * 4,
'count' => getenv('PROCESS_COUNT'),
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '',
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// File update detection and automatic reload
'monitor' => [
'handler' => app\process\Monitor::class,
'reloadable' => false,
'constructor' => [
// Monitor these directories
'monitorDir' => array_merge([
app_path(),
config_path(),
base_path() . '/process',
base_path() . '/support',
base_path() . '/resource',
base_path() . '/.env',
], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
// Files with these suffixes will be monitored
'monitorExtensions' => [
'php', 'html', 'htm', 'env'
],
'options' => [
'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
]
]
]
];
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => [
'password' => getenv('REDIS_PASSWORD'),
'host' => getenv('REDIS_HOST'),
'port' => getenv('REDIS_PORT'),
'database' => getenv('REDIS_DATABASE'),
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
]
];
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Webman\Route;
/**
* 当前路由规则:/{业务域}/{模块}/{操作} (可扩展:/{业务域}/{版本号}/{模块}/{操作})
*/
// 管理后台 API 样例
// GET /admin/user/{user_id}/tokens // 返回用户token+元信息
// POST /admin/user/{user_id}/kickout // body: {"tokens": ["..."]} // 踢下线
// POST /admin/user/{user_id}/kickout_all // 一键踢全部
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']);
});
});
+23
View File
@@ -0,0 +1,23 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'event_loop' => '',
'stop_timeout' => 2,
'pid_file' => runtime_path() . '/webman.pid',
'status_file' => runtime_path() . '/webman.status',
'stdout_file' => runtime_path() . '/logs/stdout.log',
'log_file' => runtime_path() . '/logs/workerman.log',
'max_package_size' => 10 * 1024 * 1024
];
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Webman\Session\FileSessionHandler;
use Webman\Session\RedisSessionHandler;
use Webman\Session\RedisClusterSessionHandler;
return [
'type' => 'file', // or redis or redis_cluster
'handler' => FileSessionHandler::class,
'config' => [
'file' => [
'save_path' => runtime_path() . '/sessions',
],
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
'auth' => '',
'timeout' => 2,
'database' => '',
'prefix' => 'redis_session_',
],
'redis_cluster' => [
'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
'timeout' => 2,
'auth' => '',
'prefix' => 'redis_session_',
]
],
'session_name' => 'PHPSID',
'auto_update_timestamp' => false,
'lifetime' => 7*24*60*60,
'cookie_lifetime' => 365*24*60*60,
'cookie_path' => '/',
'domain' => '',
'http_only' => true,
'secure' => false,
'same_site' => '',
'gc_probability' => [1, 1000],
];
+23
View File
@@ -0,0 +1,23 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* Static file settings
*/
return [
'enable' => true,
'middleware' => [ // Static file Middleware
//app\middleware\StaticFile::class,
],
];
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* Multilingual configuration
*/
return [
// Default language
'locale' => 'zh',
// Fallback language
'fallback_locale' => ['zh', 'en'],
// Folder where language files are stored
'path' => base_path() . '/resource/translations',
];
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\view\Raw;
use support\view\Twig;
use support\view\Blade;
use support\view\ThinkPHP;
return [
'handler' => Raw::class
];
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRQsAoN1+7lh7B
AEEbDNVKTu8sYHifkDjq50meFzzbfQ7NmcW3F2IOldRFZ6duETKn/qazvTA/wmRW
fXw/AVlEUppYBMO4kCIHl1ETZ5cCdDPRL5IBjxnUlH/bZpo/ox9ot4nFtGnHee76
KnVjNghakThwluDnj05gqMF0/b1CE8QvS62LygXhYL5c6e1hWke5cDjhsoarZGBA
IRYsibSj3hblesqrZhVqEZyBUeCcUx3XgylEmr81yQiWgNZ+u756chVgwX7m7lPs
dy6KeJGsn+VxXUC/C272lfSuJQVZ8lT6QoHa3M3RiKNnxsnXJXzmiNwD+Ot6WbBe
MEl62RbNAgMBAAECggEACL1W2CpXFOpBCfXjdWh0s5mwJhrRvtxGCRPnmhlfWpos
MMymZy5FudU3gqSNgUY2AH7De8gMrutrLfkn4/VZXJMHzJUT9YfZ6Mlz9eScK0D3
wkAdD9qbQHeC2+kHmF1mnB5NbzsVnEkMs/I4NkOh8RWVyI7MmtWV3bYxyjVbHtpj
VugJLzU2wt2T2rSSVN+sG0HlGqkSyPqP3R0Rdquvfry/YNA7wbSqxJaF22Mf0vOh
fo9mkkj9L9VZ+R9ksaZuQEYmtqYq18LG/OhYg4gTcZ9JBQ7fMFfBiRR3SYws7Suc
Y9Izp76Owwpkok+O+XJNpHCx1+gQ4IKh+Aj1W9tL4QKBgQDvvheKn0Hm9SxZq8cq
ALthISdlcg+8YLmhGWLxFxDHAd4xNKty4sj+Q2x4Mw8GcLWYIL7HoqBF4EtdQgMw
uTCHaxPWLAAuaU3RJZLOKxXpkNiedo9bcYWtz7kXiS2wOecVlVNEALaLEDmkHBZi
uUV11rGP0o0qWxWpQXzxFhDMHQKBgQDfc39YL1vbKZDa2XtoOoHL0usHVJuhhGhk
jSxmVfwKk/6FWXPAiuQluiICUOtMTo2Ips+5O+KZNO39B9ZItwaUoGxOOhaO31OA
2gQIw/fWuU7FDbnipSydCWHPWPZ3EngjeQG8EOtRWvHQ55Nc/KoxZhABlZcdw/YQ
O/lteU6WcQKBgGvrGPPInsTVZoJzBfNNYwvk/bP3tUeqDsWLBPPb2yyKFbf2qprL
pYh3V1OAQGo4wDXbv6Vj+luaDLLNLVx4cHT06XgGVIqP0nysOjpeQL3j+u7SFdQm
9PL5x6rlkFB8sB4SGupIm+NbiWbrUdRc6/yQiVthfzQqzkRje1eXBDatAoGBAKK0
7jBfY8qFTMYfAL5Xg/XP4nXXBPAqgv1c0sy9paZqNzYQebnlKNXT4DnBhGRu+p5b
oPwngAvBP940m7MdGryUx+h6YbytH5e+qJtdXxVKn/Ha2dUQIxlCIKekBuuTVkm2
Li4sP71gtJ5CUcqu7JrFz7fnnj79VyphEXHGmBPxAoGBANDNiLUIq84EFZJ2Z1QP
85glI5Mzbjbg0fJWuHYId8+ZkL/PpQvGMnw0vPt6rasrX9D3FU1B3I2yAmQVswhN
SoeTpu85EoNJp4rTPiRrQjvlcuMuxJqyxmksJHq981PYn25BEbhw/UtD0Qxc6LC/
T2us9BEQbZmq/K9l9k5s2pQ9
-----END PRIVATE KEY-----
+9
View File
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ULAKDdfu5YewQBBGwzV
Sk7vLGB4n5A46udJnhc8230OzZnFtxdiDpXURWenbhEyp/6ms70wP8JkVn18PwFZ
RFKaWATDuJAiB5dRE2eXAnQz0S+SAY8Z1JR/22aaP6MfaLeJxbRpx3nu+ip1YzYI
WpE4cJbg549OYKjBdP29QhPEL0uti8oF4WC+XOntYVpHuXA44bKGq2RgQCEWLIm0
o94W5XrKq2YVahGcgVHgnFMd14MpRJq/NckIloDWfru+enIVYMF+5u5T7HcuiniR
rJ/lcV1Avwtu9pX0riUFWfJU+kKB2tzN0YijZ8bJ1yV85ojcA/jrelmwXjBJetkW
zQIDAQAB
-----END PUBLIC KEY-----
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env php
<?php
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
support\App::run();
+24
View File
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Request
* @package support
*/
class Request extends \Webman\Http\Request
{
}
+24
View File
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Response
* @package support
*/
class Response extends \Webman\Http\Response
{
}
+139
View File
@@ -0,0 +1,139 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Dotenv\Dotenv;
use support\Log;
use Webman\Bootstrap;
use Webman\Config;
use Webman\Middleware;
use Webman\Route;
use Webman\Util;
use Workerman\Events\Select;
use Workerman\Worker;
$worker = $worker ?? null;
if (empty(Worker::$eventLoopClass)) {
Worker::$eventLoopClass = Select::class;
}
set_error_handler(function ($level, $message, $file = '', $line = 0) {
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
});
if ($worker) {
register_shutdown_function(function ($startTime) {
if (time() - $startTime <= 0.1) {
sleep(1);
}
}, time());
}
if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
Dotenv::createUnsafeMutable(base_path(false))->load();
} else {
Dotenv::createMutable(base_path(false))->load();
}
}
Config::clear();
support\App::loadAllConfig(['route']);
if ($timezone = config('app.default_timezone')) {
date_default_timezone_set($timezone);
}
foreach (config('autoload.files', []) as $file) {
include_once $file;
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
foreach ($projects['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
Middleware::load(config('middleware', []));
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project) || $name === 'static') {
continue;
}
Middleware::load($project['middleware'] ?? []);
}
Middleware::load($projects['middleware'] ?? [], $firm);
if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
Middleware::load(['__static__' => $staticMiddlewares], $firm);
}
}
Middleware::load(['__static__' => config('static.middleware', [])]);
foreach (config('bootstrap', []) as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['bootstrap'] ?? [] as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
foreach ($projects['bootstrap'] ?? [] as $className) {
/** @var string $className */
if (!class_exists($className)) {
$log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
$directory = base_path() . '/plugin';
$paths = [config_path()];
foreach (Util::scanDir($directory) as $path) {
if (is_dir($path = "$path/config")) {
$paths[] = $path;
}
}
Route::load($paths);
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env php
<?php
use Webman\Config;
use Webman\Console\Command;
use Webman\Console\Util;
use support\Container;
use Dotenv\Dotenv;
if (!Phar::running()) {
chdir(__DIR__);
}
require_once __DIR__ . '/vendor/autoload.php';
if (!$appConfigFile = config_path('app.php')) {
throw new RuntimeException('Config file not found: app.php');
}
if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(run_path())->load();
} else {
Dotenv::createMutable(run_path())->load();
}
}
$appConfig = require $appConfigFile;
if ($timezone = $appConfig['default_timezone'] ?? '') {
date_default_timezone_set($timezone);
}
if ($errorReporting = $appConfig['error_reporting'] ?? '') {
error_reporting($errorReporting);
}
if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) {
require_once __DIR__ . '/support/bootstrap.php';
} else {
if (class_exists('Support\App')) {
Support\App::loadAllConfig(['route']);
} else {
Config::reload(config_path(), ['route', 'container']);
}
}
$cli = new Command();
$cli->setName('webman cli');
$cli->installInternalCommands();
if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) {
$cli->installCommands($command_path);
}
foreach (config('plugin', []) as $firm => $projects) {
if (isset($projects['app'])) {
foreach (['', '/app'] as $app) {
if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) {
$command_path = base_path() . "/plugin/$firm{$app}/$command_str";
$cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str");
}
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
$project['command'] ??= [];
array_walk($project['command'], [$cli, 'createCommandInstance']);
}
}
$cli->run();
+3
View File
@@ -0,0 +1,3 @@
CHCP 65001
php windows.php
pause
+136
View File
@@ -0,0 +1,136 @@
<?php
/**
* Start file for windows
*/
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
use Dotenv\Dotenv;
use support\App;
use Workerman\Worker;
ini_set('display_errors', 'on');
error_reporting(E_ALL);
if (class_exists('Dotenv\Dotenv') && file_exists(base_path() . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(base_path())->load();
} else {
Dotenv::createMutable(base_path())->load();
}
}
App::loadAllConfig(['route']);
$errorReporting = config('app.error_reporting');
if (isset($errorReporting)) {
error_reporting($errorReporting);
}
$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows';
$paths = [
$runtimeProcessPath,
runtime_path('logs'),
runtime_path('views')
];
foreach ($paths as $path) {
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
}
$processFiles = [];
if (config('server.listen')) {
$processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php';
}
foreach (config('process', []) as $processName => $config) {
$processFiles[] = write_process_file($runtimeProcessPath, $processName, '');
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['process'] ?? [] as $processName => $config) {
$processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name");
}
}
foreach ($projects['process'] ?? [] as $processName => $config) {
$processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm);
}
}
function write_process_file($runtimeProcessPath, $processName, $firm): string
{
$processParam = $firm ? "plugin.$firm.$processName" : $processName;
$configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']";
$fileContent = <<<EOF
<?php
require_once __DIR__ . '/../../vendor/autoload.php';
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Webman\Config;
use support\App;
ini_set('display_errors', 'on');
error_reporting(E_ALL);
if (is_callable('opcache_reset')) {
opcache_reset();
}
if (!\$appConfigFile = config_path('app.php')) {
throw new RuntimeException('Config file not found: app.php');
}
\$appConfig = require \$appConfigFile;
if (\$timezone = \$appConfig['default_timezone'] ?? '') {
date_default_timezone_set(\$timezone);
}
App::loadAllConfig(['route']);
worker_start('$processParam', $configParam);
if (DIRECTORY_SEPARATOR != "/") {
Worker::\$logFile = config('server')['log_file'] ?? Worker::\$logFile;
TcpConnection::\$defaultMaxPackageSize = config('server')['max_package_size'] ?? 10*1024*1024;
}
Worker::runAll();
EOF;
$processFile = $runtimeProcessPath . DIRECTORY_SEPARATOR . "start_$processParam.php";
file_put_contents($processFile, $fileContent);
return $processFile;
}
if ($monitorConfig = config('process.monitor.constructor')) {
$monitorHandler = config('process.monitor.handler');
$monitor = new $monitorHandler(...array_values($monitorConfig));
}
function popen_processes($processFiles)
{
$cmd = '"' . PHP_BINARY . '" ' . implode(' ', $processFiles);
$descriptorspec = [STDIN, STDOUT, STDOUT];
$resource = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
if (!$resource) {
exit("Can not execute $cmd\r\n");
}
return $resource;
}
$resource = popen_processes($processFiles);
echo "\r\n";
while (1) {
sleep(1);
if (!empty($monitor) && $monitor->checkAllFilesChange()) {
$status = proc_get_status($resource);
$pid = $status['pid'];
shell_exec("taskkill /F /T /PID $pid");
proc_close($resource);
$resource = popen_processes($processFiles);
}
}