协程版仓库后端项目
Some checks failed
Build Docker / build (push) Has been cancelled

This commit is contained in:
2025-07-08 14:59:47 +08:00
commit 0b2299c427
134 changed files with 19277 additions and 0 deletions

View File

@ -0,0 +1,158 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午7:13
* Description:
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);
namespace App\Service;
use Hyperf\Config\Annotation\Value;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Guzzle\ClientFactory;
use function Hyperf\Coroutine\co;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午7:18
* Description: 阿里云日志服务.
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
class AliLogsSignService
{
protected const string LOG_SIGNATURE_METHOD = 'hmac-sha1';
protected const string LOG_API_VERSION = '0.6.0';
#[Value('alibaba.accessKeyId')]
protected string $accessKeyId;
#[Value('alibaba.accessKeySecret')]
protected string $accessKeySecret;
#[Value('alibaba.logs')]
protected array $logs;
#[Inject]
protected ClientFactory $clientFactory;
/**
* 签名及请求头拼接.
* @param mixed $method
* @param mixed $uri
* @param mixed $params
* @param mixed $body
* @param mixed $logProject
* @param mixed $logEndpoint
*/
public function buildHeaders(
string $method,
string $uri,
array $params,
string $body,
string|object $logProject,
string $logEndpoint
): array
{
$headers = [
'x-log-signaturemethod' => self::LOG_SIGNATURE_METHOD,
'x-log-apiversion' => self::LOG_API_VERSION,
'Host' => sprintf('%s.%s', $logProject, $logEndpoint),
'Content-Type' => 'application/json',
];
$contentLength = 0;
$contentMd5 = '';
if (!empty($body) && strlen($body) > 0) {
$contentLength = strlen($body);
$contentMd5 = strtoupper(md5($body));
$headers['Content-MD5'] = $contentMd5;
}
// date
setlocale(LC_TIME, 'en_US');
$date = gmdate('D, d M Y H:i:s \G\M\T', time());
$headers['Date'] = $date;
$headers['Content-Length'] = (string)$contentLength;
$contentType = $headers['Content-Type'];
$message = $method . "\n" . $contentMd5 . "\n" . $contentType . "\n" . $date . "\n";
// header
$filterHeaders = [];
foreach ($headers as $key => $val) {
if (str_starts_with($key, 'x-log-') || str_starts_with($key, 'x-acs-')) {
$filterHeaders[$key] = $val;
}
}
ksort($filterHeaders);
foreach ($filterHeaders as $key => $val) {
$message .= $key . ':' . $val . "\n";
}
// uri and params
$message .= $uri;
if (sizeof($params) > 0) {
$message .= '?';
}
ksort($params);
$sep = '';
foreach ($params as $key => $val) {
$message .= $sep . $key . '=' . $val;
$sep = '&';
}
// signature & authorization
$signature = $this->generateSignature($message);
$auth = 'LOG ' . $this->accessKeyId . ':' . $signature;
$headers['Authorization'] = $auth;
return $headers;
}
/**
* 实现调用PutWebTracking接口将多条日志合并进行采集.
*/
public function putWebTracking(array $record): void
{
$logEndpoint = $this->logs['log_endpoint'];
$logProject = $this->logs['log_project'];
$logStores = $this->logs['log_store'];
$params = [];
$body = [
'__topic__' => 'mes api logs',
'__source__' => 'admin api',
'__logs__' => [
['Logs' => $record['formatted']],
],
'__tags__' => [
'mes' => $record['channel'],
],
];
$body = json_encode($body);
$sign_url = sprintf('/logstores/%s/track', $logStores);
$headers = $this->buildHeaders('POST', $sign_url, $params, $body, $logProject, $logEndpoint);
$options = [
'headers' => $headers,
'body' => $body,
'query' => $params,
];
$url = sprintf('https://%s.%s/logstores/%s/track', $logProject, $logEndpoint, $logStores);
$client = $this->clientFactory->create();
co(function () use ($client, $url, $options) {
$client->request('POST', $url, $options);
});
}
protected function generateSignature(string $message): string
{
return base64_encode(hash_hmac('sha1', $message, $this->accessKeySecret, true));
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 下午1:57
* Description:
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);
namespace App\Service;
use Hyperf\Database\Schema\Blueprint;
class MigrateService
{
/***
* 在创建的时候自动添加如下字段
* @param Blueprint $blueprint
* @return Blueprint
*/
public static function migrateCreateInfo(Blueprint $blueprint): Blueprint
{
$blueprint->integer('creator_id')->default(0)->unsigned()->comment('创建人ID');
$blueprint->string('creator_name', 45)->default('')->comment('创建人姓名');
$blueprint->integer('created_at')->default(0)->unsigned()->comment('创建时间');
$blueprint->integer('updated_at')->default(0)->unsigned()->comment('更新时间');
$blueprint->integer('deleted_at')->nullable()->unsigned()->comment('软删除时间');
return $blueprint;
}
public static function migrateTime(Blueprint $blueprint): Blueprint
{
$blueprint->integer('created_at')->default(0)->unsigned()->comment('创建时间');
$blueprint->integer('updated_at')->default(0)->unsigned()->comment('更新时间');
$blueprint->integer('deleted_at')->nullable()->unsigned()->comment('软删除时间');
return $blueprint;
}
}

121
app/Service/OpLogsService.php Executable file
View File

@ -0,0 +1,121 @@
<?php
/**
* Author: ykxiao
* Date: 2024/12/25
* Time: 上午9:15
* Description:
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Service;
use App\Context\QueueContext;
use App\Context\UserContext;
use Hyperf\Collection\Arr;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Router\Dispatched;
use function Hyperf\Support\make;
/**
* Author: ykxiao
* Date: 2024/12/25
* Time: 上午9:37
* Description: 操作日志服务类。
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
class OpLogsService
{
/**
* 记录操作日志。
*
* 该方法用于记录与请求相关联的操作日志包括操作动作、路由、参数、IP地址、用户代理等信息。
* 允许通过$logStream参数传递额外的备注信息并可选择指定日志的来源。
*
* @param string $logStream 日志流或额外的备注信息,默认为空字符串
* @param null|int $source 日志来源的标识默认为0
*/
public function operatorLogs(string $logStream = '', ?int $source = 0): void
{
// 创建请求对象
$request = make(RequestInterface::class);
// 尝试从请求中获取调度信息
$dispatched = $request->getAttribute(Dispatched::class);
// 如果不存在调度信息,则不记录日志
if (! $dispatched instanceof Dispatched) {
return;
}
// 解析路由处理器信息
$handler = $dispatched->handler->callback ?? null;
[$routeControllerName, $routeActionName] = $handler;
// 提取控制器名称
$controllerName = Arr::last(explode('\\', $routeControllerName));
// 组装当前执行的动作字符串
$currAction = $controllerName . '@' . $routeActionName;
// 过滤敏感参数后收集请求信息
$params = $this->filterSensitiveParams($request->all());
$clientIP = $this->getClientIP($request);
$agent = $request->getHeaderLine('User-Agent');
// 准备日志数据
$data = [
'action' => $currAction,
'route' => $request->path(),
'params' => $params,
'ip' => $clientIP,
'agent' => $agent,
'remark' => $logStream,
'source' => $source,
];
// 将日志数据加入队列,以便后续处理
make(QueueService::class)->make($data)->opLogs();
}
/**
* 过滤敏感参数.
*/
protected function filterSensitiveParams(array $params): array
{
$sensitiveKeys = ['password', 'pwd', 'pwd_conf', 'original_pwd', 'old_pwd', 'new_pwd', 'conf_pwd'];
foreach ($sensitiveKeys as $key) {
Arr::forget($params, $key);
}
return $params;
}
/**
* 获取客户端 IP.
*/
protected function getClientIP(RequestInterface $request): string
{
$realIP = $request->getHeaderLine('x-real-ip');
$forwardedFor = $request->getHeaderLine('x-forwarded-for');
$clientIP = $realIP ?: $forwardedFor;
return $clientIP ?: '127.0.0.1';
}
}

View File

@ -0,0 +1,92 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午10:53
* Description:
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);
namespace App\Service;
use App\Context\UserContext;
use App\Job\ColumnConfigJob;
use App\Job\OpLogsJob;
use App\Job\RequestWriteLogsJob;
use Hyperf\Amqp\Producer;
use Hyperf\AsyncQueue\Driver\DriverFactory;
use Hyperf\AsyncQueue\Driver\DriverInterface;
use Hyperf\Di\Annotation\Inject;
class QueueService
{
// 存储配置参数
protected array $params = [];
// 当前操作的函数名
protected string $function = '';
// 队列驱动实例
protected DriverInterface $driver;
// 生产者实例,用于发送消息
#[Inject]
protected Producer $producer;
/**
* 构造函数,初始化队列服务。
*
* @param DriverFactory $driverFactory 驱动工厂,用于获取具体的队列驱动实例
*/
public function __construct(DriverFactory $driverFactory)
{
$this->driver = $driverFactory->get('whf');
}
/**
* 创建队列任务并执行。
*
* @param array $params
* @return QueueService
*/
public function make(array $params): static
{
// 将当前用户信息添加到参数中
$params['user'] = UserContext::getCurrentUser();
$this->params = $params;
return $this;
}
/**
* 将请求日志写入文件。
*
*/
public function writeRequestLogs(): void
{
$this->driver->push(new RequestWriteLogsJob($this->params));
}
/**
* 列配置保存.
*/
public function saveColumnConfig(): void
{
$this->driver->push(new ColumnConfigJob($this->params));
}
/**
* 将操作日志推送到队列中.
*/
public function opLogs(): void
{
// 将操作日志封装为OpLogsJob任务并推送到驱动器对应的队列中
$this->driver->push(new OpLogsJob($this->params));
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 上午9:51
* Description:
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);
namespace App\Service;
use Psr\Http\Message\ServerRequestInterface;
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 上午10:19
* Description: 系统服务
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
class SysService
{
/**
* 获取客户端IP信息
* @param ServerRequestInterface $request
* @return string
*/
public function getClientIpInfo(ServerRequestInterface $request): string
{
// 尝试从请求头获取客户端真实IP或转发的IP
$realIP = $request->getHeaderLine('x-real-ip');
$forwardedFor = $request->getHeaderLine('x-forwarded-for');
// 确定客户端IP优先使用 x-real-ip其次是 x-forwarded-for最后是默认IP
$clientIP = $realIP ?: $forwardedFor;
return $clientIP ?: '127.0.0.1';
}
}

View File

@ -0,0 +1,147 @@
<?php
/**
* Author: ykxiao
* Date: 2024/12/24
* Time: 下午3:05
* Description:
*
* (c) ykxiao <yk_9001@hotmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Service\Trait;
use App\Context\UserContext;
use App\Model\ColumnConfig;
use App\Model\TableConfig;
use App\Service\QueueService;
use Exception;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Router\Dispatched;
use Hyperf\Stringable\Str;
use Psr\Http\Message\ServerRequestInterface;
use function Hyperf\Collection\collect;
use function Hyperf\Config\config;
trait ColumnConfigTrait
{
#[Inject]
protected QueueService $queueService;
#[Inject]
protected ServerRequestInterface $clientRequest;
/**
* 获取列配置.
* @throws Exception
*/
public function getColumnConfig(): array
{
$user = UserContext::getCurrentUser();
if (! $user) {
return [];
}
$method = Str::snake($this->getCurrentAction());
$this->saveColumnConfig($user, $method, $this->clientRequest->getParsedBody());
$this->saveTableConfig($user);
// 从数据库中查询对应的列配置详情
$fields = ColumnConfig::query()
->where(['creator_id' => $user['id'], 'method' => $method])
->select(['prop', 'label', 'sortable', 'sort', 'width', 'hide', 'fix', 'filter', 'is_search', 'search_type', 'condition'])
->orderBy('sort')
->get();
return $fields->isNotEmpty() ? $fields->toArray() : $this->getDefaultConfig($method);
}
/**
* 获取表格配置.
* @throws Exception
*/
public function getTableConfig(): array
{
$user = UserContext::getCurrentUser();
if (! $user) {
return [];
}
$config = TableConfig::query()
->where(['creator_id' => $user['id'], 'method' => Str::snake($this->getCurrentAction())])
->select(['size', 'config'])
->first();
return $config ? $config->toArray() : [];
}
/**
* 获取默认配置.
*/
private function getDefaultConfig(string $method): array
{
$configList = config('column_config.' . $method);
if (empty($configList)) {
return [];
}
return collect($configList)->map(function ($item, $key) {
$item['sort'] = $key + 1;
return $item;
})->toArray();
}
/**
* 获取当前请求所匹配的方法.
* @throws Exception
*/
private function getCurrentAction(): string
{
$dispatched = $this->clientRequest->getAttribute(Dispatched::class);
return $dispatched->handler->callback[1] ?? '';
}
/**
* 保存列配置.
* @throws Exception
*/
private function saveColumnConfig(array $user, string $method, array $params): void
{
$this->queueService->make(compact('user', 'method', 'params'))->saveColumnConfig();
}
/**
* @throws Exception
*/
private function saveTableConfig(array $user): void
{
$body = $this->clientRequest->getParsedBody();
if (empty($body['table_config'])) {
return;
}
$query = TableConfig::query();
$params = $body['table_config'];
$method = Str::snake($this->getCurrentAction());
$query->updateOrCreate(
['creator_id' => $user['id'], 'method' => $method],
[
'method' => $method,
'size' => $params['size'] ?? 'medium',
'config' => $params['config'] ?? [],
],
);
}
}