协程版仓库后端项目
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,34 @@
<?php
declare(strict_types=1);
namespace App\Amqp\Consumer;
use App\Service\AliLogsSignService;
use Hyperf\Amqp\Result;
use Hyperf\Amqp\Annotation\Consumer;
use Hyperf\Di\Annotation\Inject;
/**
* 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.
*/
#[Consumer(exchange: 'wh_ali_sls', routingKey: 'wh_ali_sls_key', queue: 'wh_ali_sls_queue', name: "AliSlsConsumer", nums: 5)]
class AliSlsConsumer extends BaseConsumer
{
#[Inject]
protected AliLogsSignService $aliLogsSignService;
public function handle($data): Result
{
$this->aliLogsSignService->putWebTracking($data);
return Result::ACK;
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6: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.
*/
declare(strict_types=1);
namespace App\Amqp\Consumer;
use Hyperf\Amqp\Message\ConsumerMessage;
use App\Context\QueueContext;
use Hyperf\Amqp\Result;
use Hyperf\DbConnection\Db;
use PhpAmqpLib\Exception\AMQPChannelClosedException;
use PhpAmqpLib\Message\AMQPMessage;
use App\Log\Log;
use Exception;
use PhpAmqpLib\Exception\AMQPConnectionClosedException;
/**
* 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.
*/
abstract class BaseConsumer extends ConsumerMessage
{
public function consumeMessage($data, AMQPMessage $message): Result
{
$consumerClass = get_class($this);
// 设置用户信息上下文信息
if (!empty($data['user'])) {
QueueContext::setUser($data['user']);
}
if (!empty($company = $data['company'])) {
QueueContext::setCompanyInfo($company);
}
Db::beginTransaction();
try {
$handle = $this->handle($data);
Db::commit();
return $handle;
} catch (AMQPChannelClosedException|AMQPConnectionClosedException $e) {
Log::get('queue', 'queue')->error("AMQP通道关闭异常 ($consumerClass): " . $e->getMessage(), [
'data' => $data,
'exception' => $e,
]);
Db::rollBack();
// 可选:重连逻辑 or 丢弃
return Result::ACK; // 或 NACK
} catch (Exception $e) {
Log::get('queue', 'queue')->error("AMQP消费者异常 ($consumerClass): " . $e->getMessage(), [
'data' => $data,
'exception' => $e,
]);
Db::rollBack();
return Result::ACK;
}
}
/**
* 子类实现的核心处理逻辑
* @param mixed $data
* @return Result
*/
abstract protected function handle(mixed $data): Result;
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Amqp\Consumer;
use Hyperf\Amqp\Result;
use Hyperf\Amqp\Annotation\Consumer;
use Hyperf\Amqp\Message\ConsumerMessage;
use PhpAmqpLib\Message\AMQPMessage;
#[Consumer(exchange: 'wh_user_import', routingKey: 'wh_user_import_key', queue: 'wh_user_import_queue', name: "UserImportConsumer", nums: 5)]
class UserImportConsumer extends BaseConsumer
{
public function handle($data): Result
{
return Result::ACK;
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Amqp\Producer;
use Hyperf\Amqp\Annotation\Producer;
#[Producer(exchange: 'wh_ali_sls', routingKey: 'wh_ali_sls_key')]
class AliSlsProducer extends BaseProducer
{
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/6
* Time: 上午9: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\Amqp\Producer;
use App\Context\UserContext;
use Hyperf\Amqp\Message\ProducerMessage;
/**
* Author: ykxiao
* Date: 2025/6/6
* Time: 上午9:57
* Description: amqp生产者抽象基类.
*
* (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.
*/
abstract class BaseProducer extends ProducerMessage
{
public function __construct(array $data)
{
// 设置用户信息上下文信息
if (UserContext::hasCurrentUser()) {
$data['user'] = UserContext::getCurrentUser();
}
$this->payload = $data;
$this->properties['delivery_mode'] = 2; // 消息持久化
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Amqp\Producer;
use Hyperf\Amqp\Annotation\Producer;
use Hyperf\Amqp\Message\ProducerMessage;
#[Producer(exchange: 'wh_user_import', routingKey: 'wh_user_import_key')]
class UserImportProducer extends BaseProducer
{
}

View File

@ -0,0 +1,56 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午11:50
* 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\Aspect;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Throwable;
#[Aspect]
class TransactionalAspect extends AbstractAspect
{
public array $classes = [
'App\Controller\*',
];
public array $annotations = [
];
/**
* 处理方法执行过程通过AOP的方式对方法执行进行事务控制。
*
* @param ProceedingJoinPoint $proceedingJoinPoint AOP中的连接点对象代表正在执行的方法
* @return mixed 返回执行方法的结果
* @throws Throwable 如果执行过程中发生异常,则抛出
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint): mixed
{
Db::beginTransaction(); // 开始事务
try {
$result = $proceedingJoinPoint->process(); // 执行目标方法
Db::commit(); // 方法执行成功,提交事务
return $result;
} catch (Throwable $e) {
Db::rollback(); // 发生异常,回滚事务
throw $e; // 重新抛出捕获的异常
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Author: ykxiao
* Date: 2024/3/6
* Time: 11:08
* Description:
*/
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\ConstantsCollector;
class AbsConst extends AbstractConstants
{
/**
* 获取所有常量的键值对列表.
*/
public static function getConstantsList(): array
{
$class = static::class;
$constants = ConstantsCollector::list();
$list = [];
if (isset($constants[$class])) {
foreach ($constants[$class] as $name => $value) {
// 确保获取的是当前常量对应的消息
$message = $value['message'] ?? '';
$list[] = [
'label' => $message,
'value' => $name,
];
}
}
return $list;
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Author: ykxiao
* Date: 2024/3/15
* Time: 11:13
* Description:
*/
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class ActiveStatusConst extends AbsConst
{
/**
* @Message("禁用")
*/
public const int ACTIVE_DISABLE = 0;
/**
* @Message("启用")
*/
public const int ACTIVE_ENABLE = 1;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class CompanyTypeConst extends AbsConst
{
/**
* @Message("开证公司")
*/
public const int ISSUING_COMPANY = 1;
/**
* @Message("收货公司")
*/
public const int RECEIVING_COMPANY = 2;
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class CustomerTypeConst extends AbstractConstants
{
/**
* @Message("代理")
*/
public const int AGENT = 1;
/**
* @Message("非代理")
*/
public const int NON_AGENT = 2;
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class RoleTypeConst extends AbsConst
{
/**
* @Message("普通用户")
*/
const int NORMAL_USER = 0;
/**
* @Message("管理员")
*/
const int ADMIN = 1;
/**
* @Message("超级管理员")
*/
const int SUPER_ADMIN = 2;
}

21
app/Constants/SourceConst.php Executable file
View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class SourceConst extends AbsConst
{
/**
* @Message("管理后台")
*/
public const int SOURCE_PC = 1;
/**
* @Message("手机端")
*/
public const int SOURCE_MOBILE = 2;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Constants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class UserTypeConst extends AbsConst
{
/**
* @Message("系统用户!")
*/
public const int SYSTEM_USER = 1;
/**
* @Message("注册用户!")
*/
public const int REGISTER_USER = 2;
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6:40
* 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\Context;
use Hyperf\Context\Context;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午7: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 QueueContext
{
private const string USER_KEY = 'queue.user';
private const string COMPANY_KEY = 'company';
public static function setUser(array $user): void
{
Context::set(self::USER_KEY, $user);
}
public static function getUser(): ?array
{
return Context::get(self::USER_KEY);
}
/**
* 设置当前公司信息
* @param array $companyInfo
* @return void
*/
public static function setCompanyInfo(array $companyInfo): void
{
Context::set(self::COMPANY_KEY, $companyInfo);
}
/**
* 获取当前公司信息
* @return array|null
*/
public static function getCompanyInfo(): ?array
{
return Context::get(self::COMPANY_KEY);
}
public static function clear(): void
{
Context::destroy(self::USER_KEY);
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午10:38
* 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\Context;
use App\Repository\Company\FirstCompanyRepository;
use Exception;
use Hyperf\Context\Context;
use function Hyperf\Support\make;
class UserContext
{
private const string USER_KEY = 'user';
private const string TOKEN_KEY = 'token';
/**
* 设置当前用户信息到 Context
*
* @param array|string|int $user 用户信息数组
* @throws Exception
*/
public static function setCurrentUser(array|string|int $user): void
{
$companyRepository = make(FirstCompanyRepository::class);
if (!empty($companyInfo = $companyRepository->getCompanyByFullName($user['user']['full_name']))) {
$user['company'] = $companyInfo;
}
Context::set(self::USER_KEY, $user);
}
/**
* 从 Context 获取当前用户信息
*
* @return array|null 返回用户信息数组或 null 如果未找到
*/
public static function getCurrentUser(): ?array
{
return Context::get(self::USER_KEY);
}
public static function hasCurrentUser(): bool
{
return Context::has(self::USER_KEY);
}
/**
* 设置当前用户令牌
* @param string $token
* @return void
*/
public static function setCurrentToken(string $token): void
{
Context::set(self::TOKEN_KEY, $token);
}
/**
* 从 Context 获取当前用户令牌
* @return string|null
*/
public static function getCurrentToken(): ?string
{
return Context::get(self::TOKEN_KEY);
}
/**
* 清除当前用户信息
*/
public static function clearCurrentUser(): void
{
Context::set(self::USER_KEY, null);
}
}

View File

@ -0,0 +1,86 @@
<?php
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\Controller;
use App\Context\UserContext;
use App\Service\OpLogsService;
use App\Utils\ApiResponse;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Container\ContainerInterface;
abstract class AbstractController
{
#[Inject]
protected ContainerInterface $container;
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
#[Inject]
protected ApiResponse $apiResponse;
#[Inject]
protected OpLogsService $opLogsService;
/**
* 获取默认分页参数.
* @return array
*/
protected function getPage(): array
{
$params = $this->request->all();
return [$params['page'] ?? 1, $params['pageSize'] ?? 100];
}
/**
* 获取当前用户令牌
* @return string|null
*/
public function token(): ?string
{
return UserContext::getCurrentToken();
}
/**
* 获取当前用户信息
* @return array
*/
public function user(): array
{
return UserContext::getCurrentUser() ?? [];
}
/**
* 获取当前公司信息
* @return array
*/
public function company(): array
{
return $this->user()['company'] ?? [];
}
/**
* 操作日志.
* @param string $log
* @param int $source
*/
protected function opLogs(string $log, int $source = 0): void
{
$this->opLogsService->operatorLogs($log, $source);
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午6:04
* 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\Controller;
use App\Repository\Company\CompanyRepository;
use App\Request\CompanyRequest;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpMessage\Server\Response;
use Hyperf\Validation\Annotation\Scene;
class CompanyController extends AbstractController
{
#[Inject]
protected CompanyRepository $companyRepository;
/**
* 添加公司.
* @param CompanyRequest $request
* @return Response
*/
#[Scene(scene: 'addCompany', argument: 'request')]
public function addCompany(CompanyRequest $request): Response
{
$data = $request->all();
$this->companyRepository->add($data);
return $this->apiResponse->success();
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午9:29
* 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\Controller;
use App\Repository\Company\FirstCompanyRepository;
use App\Request\FirstCompanyRequest;
use Exception;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpMessage\Server\Response;
use Hyperf\Validation\Annotation\Scene;
class FirstCompanyController extends AbstractController
{
#[Inject]
protected FirstCompanyRepository $firstCompanyRepository;
/**
* 添加公司.
* @param FirstCompanyRequest $request
* @return Response
* @throws Exception
*/
#[Scene(scene: 'addFirstCompany', argument: 'request')]
public function addFirstCompany(FirstCompanyRequest $request): Response
{
$data = $request->all();
$this->firstCompanyRepository->addCompany($data);
return $this->apiResponse->success();
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:35
* 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\Controller;
use App\Repository\Purchase\PurchaseRepository;
use App\Request\PurchaseRequest;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpMessage\Server\Response;
use Hyperf\Validation\Annotation\Scene;
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:42
* 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 PurchaseController extends AbstractController
{
#[Inject]
protected PurchaseRepository $purchaseRepository;
/**
* 新增采购单入库单
* @param PurchaseRequest $request
* @return Response
*/
#[Scene(scene: 'addPurchase', argument: '')]
public function addPurchase(PurchaseRequest $request): Response
{
$this->purchaseRepository->addPurchase($request->all());
return $this->apiResponse->success();
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 下午2:52
* 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\Controller;
use App\JsonRpc\UserAuthServiceInterface;
use App\Request\RoleRequest;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpMessage\Server\Response;
use Hyperf\Validation\Annotation\Scene;
class RoleController extends AbstractController
{
#[Inject]
protected UserAuthServiceInterface $userAuthServiceInterface;
/**
* 添加角色.
* @param RoleRequest $request
* @return Response
*/
#[Scene(scene: 'addRole', argument: 'request')]
public function addRole(RoleRequest $request): Response
{
$params = $request->all();
$data = [
'companyInfo' => $this->company(),
'id' => $params['id'] ?? null,
'role_name' => $params['role_name'],
'active_status' => $params['active_status'],
'sort' => $params['sort'],
];
// 添加角色.
$this->userAuthServiceInterface->addRole($data);
return $this->apiResponse->success();
}
}

View File

@ -0,0 +1,110 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:20
* 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\Controller;
use App\Amqp\Producer\UserImportProducer;
use App\Constants\UserTypeConst;
use App\Context\UserContext;
use App\JsonRpc\UserAuthServiceInterface;
use App\Request\UserRequest;
use Exception;
use Hyperf\Amqp\Producer;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpMessage\Server\Response;
use Hyperf\Validation\Annotation\Scene;
class UserController extends AbstractController
{
#[Inject]
protected UserAuthServiceInterface $userAuthService;
#[Inject]
protected Producer $producer;
/**
* 用户登录.
* @param UserRequest $request
* @return Response
* @throws Exception
*/
#[Scene(scene: 'userLogin', argument: 'request')]
public function userLogin(UserRequest $request): Response
{
$rpcUser = $this->userAuthService->userLogin([
'login_name' => $request->input('login_name'),
'password' => $request->input('password'),
]);
$user = $rpcUser['result'] ?? [];
if (empty($user)) {
return $this->apiResponse->error('用户名不存在');
}
// 设置用户信息上下文
UserContext::setCurrentUser($user['user']);
$this->opLogs('[用户登录]登录名 ' . $request->input('login_name'));
return $this->apiResponse->success($user);
}
/**
* 添加用户.
* @param UserRequest $request
* @return Response
*/
#[Scene(scene: 'addUser', argument: 'request')]
public function addUser(UserRequest $request): Response
{
$data = $request->all();
$data['token'] = $this->token();
$data['user_type'] = UserTypeConst::SYSTEM_USER;
$data['companyInfo'] = $this->company();
$data['role_ids'] = $request->input('role_ids', []); // 角色ID列表
$this->userAuthService->addUser($data);
return $this->apiResponse->success();
}
/**
* 获取用户列表.
* @param UserRequest $request
* @return Response
*/
#[Scene(scene: 'getUserList', argument: 'request')]
public function getUserList(UserRequest $request): Response
{
$data = [
'companyInfo' => $this->company(),
'userInfo' => UserContext::getCurrentUser(),
'getPage' => $this->getPage(),
'params' => $request->all()
];
$rpcResult = $this->userAuthService->userList($data);
return $this->apiResponse->success($rpcResult['result']);
}
public function importUser(): Response
{
$data = [];
$this->producer->produce(new UserImportProducer($data));
return $this->apiResponse->success();
}
}

432
app/Dao/AbstractDao.php Normal file
View File

@ -0,0 +1,432 @@
<?php
/**
* Author: ykxiao
* Date: 2025/1/6
* Time: 下午4:07
* 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\Dao;
use App\Model\Model;
use App\Service\Trait\ColumnConfigTrait;
use Exception;
use Hyperf\Collection\Collection;
use Hyperf\Database\Model\Builder;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
/**
* Author: ykxiao
* Date: 2025/2/28
* Time: 上午8:32
* Description: AbstractDao.
*
* (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.
*/
abstract class AbstractDao
{
use ColumnConfigTrait;
// 支持的表达式 - 白名单操作符
protected static array $OPERATORS_THAT_SUPPORT_VALUES = [
// 比较运算符
'=', '<', '<=', '>', '>=', '<>', '!=',
// 逻辑运算符
'between', 'not between', 'in', 'not in', 'like', 'is null', 'is not null',
];
#[Inject]
protected RequestInterface $request;
private int|string $keyId;
private array $queryResult;
/**
* @return Model
*/
abstract protected function getModel(): string;
/**
* 数据操作.
*/
public function builder(): Builder
{
return $this->getModel()::query();
}
/**
* 实例化.
*/
public function make(): AbstractDao
{
return new $this();
}
/**
* 记录查询.
* @throws Exception
*/
public function qInfo(array $params = []): Builder
{
$query = $this->builder();
$params = $params ?: $this->request->all();
if (!empty($params['ids']) && is_array($params['ids'])) {
$query->whereIn('id', $params['ids']);
} else {
$query->where('id', intval($params['id']));
}
if (!$query->exists()) {
throw new Exception('记录不存在');
}
return $query;
}
/**
* 通用创建&更新.
* @param array $data 需要新增更新的数据
* @param array $updateCondition 更新条件
* @return $this
* @throws Exception
*/
public function commonCreate(array $data = [], array $updateCondition = []): AbstractDao
{
$ignoreUpdateFields = ['id', 'company_id', 'creator_id', 'creator_name']; // 不允许更新的字段
// 1根据条件更新或创建
if (!empty($updateCondition)) {
$updateData = array_diff_key($data, array_flip($ignoreUpdateFields));
$this->builder()->updateOrCreate($updateCondition, $updateData);
// 为保证查询条件一致,重新构造查询
$query = $this->builder()->where($updateCondition);
$records = $query->get();
$this->queryResult = $records->count() > 1
? $records->toArray()
: $records->first()?->toArray();
return $this;
}
// 2根据主键 ID 更新
if (!empty($data['id'])) {
$this->keyId = $data['id'];
$query = $this->qInfo($data);
$query->lockForUpdate()->first(); // 获取锁
$updateData = array_diff_key($data, array_flip($ignoreUpdateFields));
$res = $query->update($updateData);
if (!$res) {
throw new Exception('数据更新失败');
}
// 更新后重新查询,避免条件变化导致查询不到
$this->queryResult = $this->builder()->where('id', $this->keyId)->first()?->toArray();
return $this;
}
// 3创建新记录
$res = $this->builder()->create($data);
if (!$res) {
throw new Exception('数据新增失败');
}
$this->keyId = $res->getKey();
$this->queryResult = $res->toArray();
return $this;
}
/**
* 返回更新&新增主键.
*/
public function getKey(): int
{
return $this->keyId;
}
/**
* 返回数据库操作结果.
*/
public function result(): array
{
return $this->queryResult;
}
/**
* 数据验证
*/
public function verifyData(Builder $builder, array $params = []): Builder
{
$map = [];
$condition = [];
if (!empty($params['id']) && is_numeric($params['id'])) {
$condition = ['id' => $params['id']];
}
foreach ($condition as $k => $v) {
$map[] = [$k, '!=', $v];
}
return clone $builder->where($map);
}
/**
* 更新状态
* @throws Exception
*/
public function updateStatus(): null|object
{
$params = $this->request->all();
$query = $this->qInfo()->lockForUpdate();
$info = $query->first();
if ($info['active_status'] == $params['status']) {
throw new Exception('状态不对,请确认');
}
$query->update(['active_status' => $params['status']]);
return $info;
}
/***************************************************************************************
* 构建查询数据条件参数.
* $params = [
* 'mail_sync_id' => 123,
* 'sn' => ['operator' => '!=', 'value' => 'ABC123'],
* 'aliid' => ['operator' => 'like', 'value' => '%example%'],
* 'serial_number' => ['operator' => '>', 'value' => 100],
* 'ali_model' => 'ModelX',
* 'model' => 'XYZ',
* 'date' => ['operator' => 'between', 'value' => ['2023-01-01', '2023-01-31']],
* 'id' => [
* ['operator' => '>=', 'value' => 10],
* ['operator' => '<=', 'value' => 100]
* ],
* 'status' => ['operator' => 'in', 'value' => ['active', 'pending']],
* ];
*************************************************************************************
* @throws Exception
*/
public static function daoBuildWhere(Builder $builder, array $params, array $fields): Builder
{
if (empty($params)) return $builder; // 如果没有参数,直接返回
// 用于存储所有 like 条件
$likeConditions = [];
foreach ($fields as $field) {
// 跳过空参数
if (!isset($params[$field]) || $params[$field] === '') {
continue;
}
$param = $params[$field];
// 如果参数是数组
if (is_array($param)) {
// 处理同一个字段的多个条件
if (isset($param[0]) && is_array($param[0])) {
self::addMultipleConditions($builder, $field, $param);
} else {
// 处理单个条件
self::handleSingleCondition($builder, $likeConditions, $field, $param);
}
} else {
// 处理字符串条件,字符串条件,强制使用绑定参数
$builder->where($field, '=', $param);
}
}
// 应用所有 like 条件
self::applyLikeConditions($builder, $likeConditions);
// 添加排序功能
if (!empty($params['sort_orders'])) {
self::applySortOrders($builder, $params['sort_orders'], $fields);
}
return $builder;
}
private static function addMultipleConditions(Builder $builder, string $field, array $conditions): void
{
// 处理同一个字段的多个条件
$builder->where(/**
* @throws Exception
*/ function ($query) use ($field, $conditions) {
foreach ($conditions as $condition) {
// 应用每个条件
self::applyCondition($query, $field, $condition);
}
});
}
private static function handleSingleCondition(Builder $builder, array &$likeConditions, string $field, array $param): void
{
// 收集 like 条件或处理其他单个条件
if (isset($param['operator']) && strtolower($param['operator']) === 'like') {
// 如果是 like 条件,将其添加到 likeConditions 数组中
$likeConditions[] = ['field' => $field, 'condition' => $param];
} else {
// 否则,直接应用条件
$builder->where(
/**
* @throws Exception
*/ function ($query) use ($field, $param) {
self::applyCondition($query, $field, $param);
});
}
}
private static function applyLikeConditions(Builder $builder, array $likeConditions): void
{
// 应用所有 like 条件
if (!empty($likeConditions)) {
$builder->where(function ($query) use ($likeConditions) {
foreach ($likeConditions as $likeCondition) {
if (empty($likeCondition['condition']['value'])) continue;
// 使用 orWhere 添加每个 like 条件
$query->orWhere($likeCondition['field'], 'like', $likeCondition['condition']['value']);
}
});
}
}
/**
* 应用排序功能.
* @param Builder $builder
* @param array $sortOrders
* @param array $fields
* @return void
* @throws Exception
*/
private static function applySortOrders(Builder $builder, array $sortOrders, array $fields): void
{
foreach ($sortOrders as $field => $sortOrder) {
if (!in_array($sortOrder, ['asc', 'desc'])) {
throw new Exception('排序方式不正确');
}
if (!in_array($field, $fields)) {
throw new Exception('排序字段不正确');
}
$builder->orderBy($field, $sortOrder);
}
}
/**
* 应用单个字段的查询条件.
* 调用方式
* builder,'name','='|['operate'=>'bett'].
* @throws Exception
*/
private static function applyCondition(Builder &$builder, string $field, mixed $condition): void
{
if (is_array($condition) && isset($condition['operator']) && isset($condition['value'])) {
if (empty($condition['value'])) {
return;
}
$operator = strtolower($condition['operator']);
if (!in_array($operator, self::$OPERATORS_THAT_SUPPORT_VALUES)) {
throw new Exception('不支持的查询表达式: ' . $condition['operator']);
}
$builder = match ($operator) {
'between' => $builder->whereBetween($field, $condition['value']),
'not between' => $builder->whereNotBetween($field, $condition['value']),
'in' => $builder->whereIn($field, $condition['value']),
'not in' => $builder->whereNotIn($field, $condition['value']),
'is null' => $builder->whereNull($field),
'is not null' => $builder->whereNotNull($field),
'like' => $builder->orWhere($field, 'like', $condition['value']),
default => $builder->where($field, $operator, $condition['value']),
};
} else {
$builder->where($field, '=', $condition);
}
}
/**
* 获取字段列表(公共).
* @param Builder $builder
* @param array $params
* @param array $fields
* @param string|array $addSelect
* @return array
* @throws Exception
*/
public function selectFields(Builder $builder, array $params, array $fields = [], string|array $addSelect = ''): array
{
self::daoBuildWhere($builder, $params, $fields);
$field = $params['field'] ?? '';
if (!in_array($field, $fields)) {
throw new Exception('字段参数错误');
}
$data = $builder->select(['id', $field]);
if (!empty($addSelect)) {
$data = $data->addSelect($addSelect);
}
return $data->get()->filter(function ($item) use ($field) {
// 过滤空值但不过滤布尔值
return !empty($item[$field]) || is_bool($item[$field]);
})->unique($field)->values()->toArray();
}
/**
* 分页处理函数.
*
* 本函数用于对给定的Builder或Collection对象进行分页处理并返回分页后的数据及其它相关信息。
*
* @param Builder|Collection $builder 可以是Eloquent Builder对象或Collection对象
* @param array $params 包含分页参数的数组,比如页码和每页数量
* @return array 返回一个包含分页数据、字段配置、过滤条件、表格设置等信息的数组
* @throws Exception
*/
public function paginate(Builder|Collection $builder, array $params = []): array
{
try {
// 从参数中获取页码和每页数量,未指定则默认值
$page = empty($params['page']) ? 1 : (int)$params['page'];
$page_size = empty($params['pageSize']) ? 100 : (int)$params['pageSize'];
// 计算跳过的记录数
$skip = ($page - 1) * $page_size;
// 获取总记录数
$total = $builder->count();
// 计算总页数若记录数为0则页数为0
$pageSize = $total == 0 ? 0 : (int)ceil($total / $page_size);
// 根据Builder类型获取分页后的数据
if ($builder instanceof Collection) {
// 对Collection对象进行分页
$rows = $builder->values()->slice($skip, $page_size)->values();
} else {
// 对Builder对象进行分页查询
$rows = $builder->skip($skip)->take($page_size)->orderBy('id', 'desc')->get();
}
// 获取列配置信息
$columnConfig = $this->getColumnConfig();
$tableConfig = $this->getTableConfig();
return compact(
'rows',
'page',
'total',
'pageSize',
'columnConfig',
'tableConfig'
);
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:59
* 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\Dao\Company;
use App\Dao\AbstractDao;
use App\Model\Company;
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午6:00
* Description: CompanyDao类用于提供公司相关的数据访问和操作。它继承自AbstractDao类并实现了CompanyDao接口。
*
* (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 CompanyDao extends AbstractDao
{
public function getModel(): string
{
return Company::class;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午9:24
* 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\Dao\Company;
use App\Dao\AbstractDao;
use App\Model\FirstCompany;
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午9:25
* 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 FirstCompanyDao extends AbstractDao
{
public function getModel(): string
{
return FirstCompany::class;
}
public function getFields(): array
{
return [
'id',
'domain',
'name',
'full_name',
'company_type',
'address',
'logo',
'owner', // 公司负责人
'id_card', // 法人身份证
'mobile',
'org_code',
'remark',
'active_status',
'activation_date',
'created_at',
'updated_at',
];
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:26
* 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\Dao\Purchase;
use App\Dao\AbstractDao;
use App\Model\Purchase;
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:27
* 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 PurchaseDao extends AbstractDao
{
public function getModel(): string
{
return Purchase::class;
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6:44
* 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\Exception;
use Hyperf\Server\Exception\ServerException;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午7: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 ApiException extends ServerException
{
}

View File

@ -0,0 +1,29 @@
<?php
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\Exception;
use App\Constants\ErrorCode;
use Hyperf\Server\Exception\ServerException;
use Throwable;
class BusinessException extends ServerException
{
public function __construct(int $code = 0, string $message = null, Throwable $previous = null)
{
if (is_null($message)) {
$message = ErrorCode::getMessage($code);
}
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,94 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8: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\Exception\Handler;
use App\Exception\ApiException;
use App\Log\Log;
use Hyperf\Context\ApplicationContext;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Validation\ValidationException;
use Psr\Http\Message\ResponseInterface;
use Throwable;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:54
* 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 ApiExceptionHandler extends ExceptionHandler
{
public function __construct(protected ApplicationContext $context)
{
}
/**
* 处理异常
* @param Throwable $throwable
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
$errorData = $this->getErrorData($throwable);
// 记录日志
Log::get('default', 'default')->error($errorData['logMessage']);
// 阻止异常冒泡
$this->stopPropagation();
return $response->withStatus(200)
->withHeader('Content-Type', 'application/json')
->withBody(new SwooleStream(json_encode($errorData['responseData'], JSON_UNESCAPED_UNICODE)));
}
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof ApiException || $throwable instanceof ValidationException;
}
/**
* 获取错误.
*/
protected function getErrorData(Throwable $throwable): array
{
$code = $throwable->getCode();
$errorFile = $throwable->getFile();
$errorLine = $throwable->getLine();
$errorMessage = $throwable->getMessage();
if ($throwable instanceof ValidationException) {
$errors = $throwable->validator->errors()->toArray();
$errorMessage = reset($errors)[0];
}
$responseData = [
'code' => $code,
'message' => $errorMessage,
];
$logMessage = sprintf('%s %s %s %s', $code, $errorFile, $errorLine, $errorMessage);
return ['responseData' => $responseData, 'logMessage' => $logMessage];
}
}

View File

@ -0,0 +1,97 @@
<?php
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\Exception\Handler;
use App\Exception\ApiException;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\RateLimit\Exception\RateLimitException;
use Hyperf\Validation\ValidationException;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Throwable;
use function Hyperf\Support\env;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8: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.
*/
class AppExceptionHandler extends ExceptionHandler
{
public function __construct(protected StdoutLoggerInterface $logger)
{
}
public function handle(Throwable $throwable, ResponseInterface $response): MessageInterface|ResponseInterface
{
$this->logException($throwable);
if ($throwable instanceof RateLimitException) {
return $this->handleRateLimitException($response);
}
return $this->handleOtherExceptions($throwable, $response);
}
public function isValid(Throwable $throwable): bool
{
return ! $throwable instanceof ApiException;
}
protected function createResponseBody($message, $code): StreamInterface
{
$data = [
'code' => $code,
'message' => $message,
];
return new SwooleStream(json_encode($data, JSON_UNESCAPED_UNICODE));
}
protected function logException(Throwable $throwable): void
{
$this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
$this->logger->error($throwable->getTraceAsString());
}
protected function handleRateLimitException(ResponseInterface $response): ResponseInterface
{
$message = '触发请求频率限流规则';
return $response->withStatus(429)
->withHeader('Content-Type', 'application/json')
->withBody($this->createResponseBody($message, 429));
}
protected function handleOtherExceptions(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
$message = $throwable->getMessage();
if (env('APP_ENV') === 'production' && ! $throwable instanceof ValidationException) {
return $response->withStatus(200)
->withHeader('Content-Type', 'application/json')
->withBody($this->createResponseBody($message, 0));
}
return $response->withHeader('Server', 'mes-auto')
->withStatus(200)
->withBody($this->createResponseBody($message, 0));
}
}

83
app/Job/BaseJob.php Normal file
View File

@ -0,0 +1,83 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午10:55
* 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\Job;
use App\Context\QueueContext;
use App\Log\Log;
use Exception;
use Hyperf\AsyncQueue\Job;
use Hyperf\DbConnection\Db;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午10:57
* Description: Job基础任务类.
*
* (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.
*/
abstract class BaseJob extends Job
{
public function __construct(public array $data)
{
}
public function handle(): void
{
// 设置用户信息上下文信息
if (!empty($user = $this->data['user'])) {
QueueContext::setUser($user);
}
if (!empty($company = $this->data['company'])) {
QueueContext::setCompanyInfo($company);
}
// 运行业务逻辑, 总是在事务中执行,保证事务的原子性
Db::beginTransaction();
try {
$this->process();
Db::commit();
} catch (Exception $e) {
Db::rollBack();
$this->logError($e);
}
}
/**
* 子类必须实现的业务逻辑处理方法
*/
abstract protected function process(): void;
/**
* 日志记录,可按需扩展日志通道
*/
protected function logError(Exception $e): void
{
Log::get('queue', 'queue')->error(sprintf(
"[%s] %s in %s:%d\n%s",
static::class,
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
}
}

141
app/Job/ColumnConfigJob.php Executable file
View File

@ -0,0 +1,141 @@
<?php
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\Job;
use App\Model\ColumnConfig;
use App\Scope\CompanyScope;
use Hyperf\Coroutine\Parallel;
use function Hyperf\Collection\collect;
use function Hyperf\Config\config;
class ColumnConfigJob extends BaseJob
{
protected function process(): void
{
['user' => $user, 'method' => $method, 'params' => $params] = $this->data;
// 提交了保存列配置
if (!isset($params['save_column'])) return;
if ($params['save_column'] === false) {
$this->deleteColumnConfig($user, $method);
return;
}
// 获取默认配置, 没有配置则不处理
$configList = config('column_config.' . $method) ?? [];
if (empty($configList)) return;
$submitConfig = $params['column_config'];
$submittedMap = collect($submitConfig)->keyBy('prop')->toArray();
$parallel = new Parallel();
foreach ($configList as $key => $value) {
$parallel->add(function () use ($key, $value, $submittedMap, $user) {
$value['sort'] = $key + 1;
// 存在前端提交的列
if (isset($submittedMap[$value['prop']])) {
$merged = array_merge([
'condition' => 'like',
'search_type' => 'text',
'is_search' => 1,
'sortable' => 0,
], $value, $submittedMap[$value['prop']]);
return $this->prepareConfig($merged, $user);
}
// 否则删除该列配置
ColumnConfig::query()->withoutGlobalScope(CompanyScope::class)
->where([
'prop' => $value['prop'],
'method' => $this->data['method'],
'creator_id' => $user['id'],
])
->forceDelete();
return null;
});
}
// 执行并获取所有结果
$results = $parallel->wait();
// 过滤非 null 的更新数据
$updates = array_filter($results);
if (!empty($updates)) {
$this->updateOrCreateColumnConfigs($updates, $method, $user);
}
}
/**
* 更新或创建列配置.
*/
private function updateOrCreateColumnConfigs(array $configs, string $method, array $creator): void
{
// 默认配置key值
$defaultKeys = (new ColumnConfig())->getFillable();
// 删除$configs里不存在$defaultKeys里的字段防止前端传递的额外字段导致更新失败
$configs = array_map(function ($config) use ($defaultKeys) {
$newConfig = array_intersect_key($config, array_flip($defaultKeys));
$newConfig['sort'] = $config['sort'] ?? 0;
return $newConfig;
}, $configs);
// 添加公共字段
foreach ($configs as &$config) {
$config['company_id'] = $creator['company_id'] ?? 0;
$config['method'] = $method;
$config['creator_id'] = $creator['id'];
$config['creator_name'] = $creator['name'];
$config['created_at'] = $config['updated_at'] = time();
}
unset($config);
$updateKeys = array_keys($configs[0]);
// 删除不需要更新的字段
unset(
$updateKeys[array_search('creator_name', $updateKeys, true)],
$updateKeys[array_search('prop', $updateKeys, true)],
$updateKeys[array_search('method', $updateKeys, true)],
$updateKeys[array_search('creator_id', $updateKeys, true)],
);
// 批量插入或更新
ColumnConfig::query()->withoutGlobalScope(CompanyScope::class)
->upsert($configs, ['prop', 'method', 'creator_id'], array_values($updateKeys));
}
/**
* 删除列配置.
*/
private function deleteColumnConfig(array $user, string $method): void
{
ColumnConfig::query()->withoutGlobalScope(CompanyScope::class)
->where(['method' => $method, 'creator_id' => $user['id']])
->forceDelete();
}
private function prepareConfig(array $config, array $user): array
{
return array_merge($config, [
'company_id' => $user['company_id'] ?? 0,
'creator_id' => $user['id'],
'creator_name' => $user['name'],
]);
}
}

87
app/Job/OpLogsJob.php Executable file
View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Job;
use App\Constants\SourceConst;
use App\JsonRpc\EasyAppServiceInterface;
use App\Model\OperatorLogs;
use Exception;
use Hyperf\Collection\Collection;
use function Hyperf\Collection\collect;
use function Hyperf\Config\config;
use function Hyperf\Support\make;
/**
* Author: ykxiao
* Date: 2024/12/25
* Time: 上午9:36
* 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 OpLogsJob extends BaseJob
{
/**
* 处理日志记录逻辑。
* 该方法首先尝试获取日志操作信息,如果信息不为空,则根据这些信息填充操作日志数组,
* 包括日志类型、标题、计时器以及来源信息。随后,它将尝试获取当前用户信息,并最后创建操作日志记录。
*
* @return void 该方法没有返回值。
* @throws Exception
*/
protected function process(): void
{
// 尝试获取日志操作信息
$logAction = $this->getLogAction();
// 如果日志操作信息为空,则直接返回,不进行后续操作
if ($logAction->isEmpty()) {
return;
}
$rpcResult = make(EasyAppServiceInterface::class)->getClientIPInfo(['ip' => $this->data['ip']]);
// 构建请求日志消息
$clientIPInfo = $rpcResult['result']['ip_info'] ?? '';
// 从日志操作信息中提取日志类型,并保存到操作日志数组中
$type = $logAction->first()['id'] ?? 0;
$this->data['type'] = $type;
// 同样从日志操作信息中提取日志标题,并保存
$this->data['log_title'] = $logAction->first()['name'] ?? '';
// 初始化计时器
$this->data['timer'] = 0;
// 设置日志来源,优先使用已存在的来源信息,如果不存在,则尝试从路由前缀获取来源信息
$this->data['source'] = $this->data['source'] ?: $this->getRoutePrefix();
// 查询IP属地并保存到操作日志数组中
$this->data['location'] = $clientIPInfo;
// 创建操作日志记录
OperatorLogs::query()->create($this->data);
}
private function getLogAction(): Collection
{
$defineLogs = config('op_logs');
return collect($defineLogs)->where('action', '=', $this->data['action']);
}
/**
* 获取路由前缀.
*/
private function getRoutePrefix(): int
{
$arr = explode('/', $this->data['route']);
$res = $arr[0] ?? '';
return match ($res) {
'api' => SourceConst::SOURCE_PC,
'mobile' => SourceConst::SOURCE_MOBILE,
default => 0,
};
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Job;
use App\JsonRpc\EasyAppServiceInterface;
use App\Log\Log;
use function Hyperf\Support\make;
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 上午10:23
* 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 RequestWriteLogsJob extends BaseJob
{
protected function process(): void
{
$params = $this->data['params']; // 提取请求参数
// 从参数中删除敏感信息
foreach ($params as $key => $value) {
if (in_array($key, ['password', 'pwd', 'pwd_conf', 'original_pwd'])) {
unset($params[$key]);
}
}
$rpcResult = make(EasyAppServiceInterface::class)->getClientIPInfo([
'ip' => $this->data['client_ip']
]);
$clientIPInfo = $rpcResult['result']['ip_info'] ?? '';
$logMessage = sprintf('%s %s %s',
$this->data['client_ip'] . ' ' . $clientIPInfo,
$this->data['method'],
$this->data['uri']
);
if (!empty($params)) {
$logMessage .= ' params: ' . json_encode($params, JSON_UNESCAPED_UNICODE);
}
// 记录日志
$log = Log::get('request', 'request');
$log->info($logMessage);
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 上午10:11
* 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\JsonRpc;
use Hyperf\RpcClient\AbstractServiceClient;
class EasyAppServiceConsumer extends AbstractServiceClient implements EasyAppServiceInterface
{
// 定义对应服务提供者的服务名
protected string $serviceName = 'EasyAppService';
// 定义对应服务提供者的服务协议
protected string $protocol = 'jsonrpc-http';
/**
* 获取客户端IP信息
* @param array $params
* @return array
*/
public function getClientIpInfo(array $params): array
{
return $this->__request(__FUNCTION__, $params);
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 上午10:10
* 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\JsonRpc;
interface EasyAppServiceInterface
{
/**
* 获取客户端IP信息
* @param array $params
* @return array
*/
public function getClientIpInfo(array $params): array;
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午1:58
* 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\JsonRpc;
use Hyperf\RpcClient\AbstractServiceClient;
class InventoryServiceConsumer extends AbstractServiceClient implements InventoryServiceInterface
{
protected string $serviceName = 'InventoryService';
protected string $protocol = 'jsonrpc-http';
/**
* 新增库存-手动单条.
*/
public function addInventory(array $data): void
{
$this->__request(__FUNCTION__, $data);
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午1:58
* 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\JsonRpc;
interface InventoryServiceInterface
{
/**
* 新增库存-手动单条.
* @param array $data
* @return void
*/
public function addInventory(array $data): void;
}

View File

@ -0,0 +1,86 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:36
* 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\JsonRpc;
use Hyperf\RpcClient\AbstractServiceClient;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8: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 UserAuthServiceConsumer extends AbstractServiceClient implements UserAuthServiceInterface
{
protected string $serviceName = 'UserAuthService';
protected string $protocol = 'jsonrpc-http';
/**
* 用户登录
* @param array $data
* @return array
*/
public function userLogin(array $data): array
{
return $this->__request(__FUNCTION__, $data);
}
/**
* 添加用户.
* @param array $data
* @return void
*/
public function addUser(array $data): void
{
$this->__request(__FUNCTION__, $data);
}
/**
* 获取用户信息
* @param array $data
* @return array
*/
public function userInfoByToken(array $data): array
{
return $this->__request(__FUNCTION__, $data);
}
/**
* 添加角色.
* @param array $data
* @return void
*/
public function addRole(array $data): void
{
$this->__request(__FUNCTION__, $data);
}
/**
* 获取用户列表.
* @param array $data
* @return array
*/
public function userList(array $data): array
{
return $this->__request(__FUNCTION__, $data);
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:35
* 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\JsonRpc;
interface UserAuthServiceInterface
{
/**
* 用户登录
* @param array $data
* @return array
*/
public function userLogin(array $data): array;
/**
* 新增用户.
*/
public function addUser(array $data): void;
/**
* 获取用户信息
* @param array $data
* @return array
*/
public function userInfoByToken(array $data): array;
/**
* 添加角色.
* @param array $data
* @return void
*/
public function addRole(array $data): void;
/**
* 获取用户列表.
* @param array $data
* @return array
*/
public function userList(array $data): array;
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Listener;
use App\Context\QueueContext;
use App\Context\UserContext;
use Hyperf\Database\Model\Events\Creating;
use Hyperf\Database\Model\Events\Saving;
use Hyperf\Event\Annotation\Listener;
use Psr\Container\ContainerInterface;
use Hyperf\Event\Contract\ListenerInterface;
/**
* Author: ykxiao
* Date: 2025/05/24
* Time: 下午4:27
* 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.
*/
#[Listener]
class AutoMainFieldsListener implements ListenerInterface
{
public function __construct(protected ContainerInterface $container)
{
}
public function listen(): array
{
return [
Creating::class,
Saving::class,
];
}
public function process(object $event): void
{
/**
* 在模型创建或保存事件中自动设置模型的company_id和creator_id等相关字段。
* 这个逻辑只在用户上下文存在且事件为Creating或Saving时触发。
*
* @param object $event 事件对象预期为Creating或Saving事件之一。
* @return void
*/
$user = QueueContext::getUser() ?? UserContext::getCurrentUser();
$company = $user['company'] ?? [];
$model = $event->getModel();
// 判断事件类型是否符合条件,以及用户上下文是否存在
if (!($event instanceof Creating || $event instanceof Saving) || !$user) {
return;
}
$schema = $model->getConnection()->getSchemaBuilder();
// 检查模型表是否有company_id字段若有则设置company_id
if ($schema->hasColumn($model->getTable(), 'company_id')) {
$model->company_id = $model->company_id ?? $company['id'] ?? 0;
}
// 检查模型表是否有creator_id和creator_name字段若有则设置对应的值
if ($schema->hasColumn($model->getTable(), 'creator_id')) {
$model->creator_id ??= $user['id'];
$model->creator_name ??= $user['name'] ?? '';
}
}
}

View File

@ -0,0 +1,80 @@
<?php
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\Listener;
use Hyperf\Collection\Arr;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
#[Listener]
class DbQueryExecutedListener implements ListenerInterface
{
/**
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('sql', 'sql');
}
public function listen(): array
{
return [
QueryExecuted::class,
];
}
/**
* @param object $event
*/
public function process(object $event): void
{
/**
* 处理QueryExecuted事件记录查询的SQL和执行时间。
*
* @param QueryExecuted $event 该事件对象包含执行的SQL语句、绑定参数和执行时间
*/
if ($event instanceof QueryExecuted) {
$sql = $event->sql; // 获取执行的SQL语句
// 检查绑定参数是否为关联数组,如果不是,将 "?" 替换为实际的参数值
if (!Arr::isAssoc($event->bindings)) {
$position = 0; // 初始化绑定参数的搜索位置
foreach ($event->bindings as $value) { // 遍历绑定参数
$position = strpos($sql, '?', $position); // 查找下一个 "?" 位置
if ($position === false) { // 如果找不到 "?",则退出循环
break;
}
$value = "'$value'"; // 将参数值包裹在单引号中
$sql = substr_replace($sql, $value, $position, 1); // 替换 "?" 为实际参数值
$position += strlen($value); // 更新搜索位置
}
}
// 使用logger记录SQL语句和执行时间
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}
}

View File

@ -0,0 +1,74 @@
<?php
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\Listener;
use Hyperf\AsyncQueue\AnnotationJob;
use Hyperf\AsyncQueue\Event\AfterHandle;
use Hyperf\AsyncQueue\Event\BeforeHandle;
use Hyperf\AsyncQueue\Event\Event;
use Hyperf\AsyncQueue\Event\FailedHandle;
use Hyperf\AsyncQueue\Event\RetryHandle;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
#[Listener]
class QueueHandleListener implements ListenerInterface
{
protected LoggerInterface $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('queue');
}
public function listen(): array
{
return [
AfterHandle::class,
BeforeHandle::class,
FailedHandle::class,
RetryHandle::class,
];
}
public function process(object $event): void
{
if ($event instanceof Event && $event->getMessage()->job()) {
$job = $event->getMessage()->job();
$jobClass = get_class($job);
if ($job instanceof AnnotationJob) {
$jobClass = sprintf('Job[%s@%s]', $job->class, $job->method);
}
$date = date('Y-m-d H:i:s');
switch (true) {
case $event instanceof BeforeHandle:
$this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass));
break;
case $event instanceof AfterHandle:
$this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass));
break;
case $event instanceof FailedHandle:
$this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass));
$this->logger->error((string) $event->getThrowable());
break;
case $event instanceof RetryHandle:
$this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass));
break;
}
}
}
}

View File

@ -0,0 +1,35 @@
<?php
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\Listener;
use Hyperf\Command\Event\AfterExecute;
use Hyperf\Coordinator\Constants;
use Hyperf\Coordinator\CoordinatorManager;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
#[Listener]
class ResumeExitCoordinatorListener implements ListenerInterface
{
public function listen(): array
{
return [
AfterExecute::class,
];
}
public function process(object $event): void
{
CoordinatorManager::until(Constants::WORKER_EXIT)->resume();
}
}

46
app/Log/AliSlsHandler.php Normal file
View File

@ -0,0 +1,46 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6:33
* 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\Log;
use App\Amqp\Producer\AliSlsProducer;
use Hyperf\Amqp\Producer;
use Hyperf\Di\Annotation\Inject;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\LogRecord;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午7: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 AliSlsHandler extends AbstractProcessingHandler
{
#[Inject]
protected Producer $producer;
protected function write(LogRecord $record): void
{
$logs = ['channel' => $record['channel'] ?? '', 'formatted' => $record['formatted'] ?? ''];
$message = new AliSlsProducer($logs);
$this->producer->produce($message, true);
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6:33
* 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\Log;
use Hyperf\Context\Context;
use Hyperf\Coroutine\Coroutine;
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午7:19
* Description: 添加请求ID和协程ID
*
* (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 AppendRequestIdProcessor implements ProcessorInterface
{
public const string REQUEST_ID = 'log.request.id';
public function __invoke(LogRecord $record): array|LogRecord
{
$record['extra']['request_id'] = Context::getOrSet(self::REQUEST_ID, uniqid('xw_cloud'));
$record['extra']['coroutine_id'] = Coroutine::id();
return $record;
}
}

62
app/Log/Log.php Normal file
View File

@ -0,0 +1,62 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6:42
* 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\Log;
use App\Exception\ApiException;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use function Hyperf\Support\make;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午7: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 Log
{
#[Inject]
protected LoggerInterface $logger;
/**
* 根据提供的名称和分组获取日志记录器实例。
*
* @param string $name 日志记录器的名称,默认为'app'。
* @param string $group 日志记录器的分组,默认为'job'。
* @return LoggerInterface 返回一个日志记录器实例。
*/
public static function get(string $name = 'app', string $group = 'job'): LoggerInterface
{
try {
// 尝试从应用上下文容器中获取LoggerFactory实例并进一步获取指定名称和分组的日志记录器
return ApplicationContext::getContainer()->get(LoggerFactory::class)->get($name, $group);
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
// 如果在获取过程中发生异常使用默认的日志记录器记录错误信息并抛出ApiException
$logs = make(LoggerFactory::class)->get('default');
$logs->error($e->getMessage());
throw new ApiException($e->getMessage());
}
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6:03
* 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\Log;
use Hyperf\Context\ApplicationContext;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午6:04
* 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 StdoutLoggerFactory
{
/**
* @param ContainerInterface $container
* @return LoggerInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container): LoggerInterface
{
return ApplicationContext::getContainer()->get(LoggerFactory::class)->get();
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 下午2:40
* 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\Middleware;
use App\Context\UserContext;
use App\Exception\ApiException;
use App\JsonRpc\UserAuthServiceInterface;
use App\Repository\Company\FirstCompanyRepository;
use Exception;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 下午2:43
* Description: 验证Token中间件.
*
* (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 CheckTokenMiddleware implements MiddlewareInterface
{
#[Inject]
protected UserAuthServiceInterface $userAuthServiceInterface;
public function __construct(protected ContainerInterface $container)
{
}
/**
* @throws Exception
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$token = $this->extractToken($request);
$rpcResult = $this->userAuthServiceInterface->userInfoByToken(['token' => $token]);
$userInfo = $rpcResult['result']['user'] ?? [];
if (!$userInfo) {
throw new ApiException('用户信息不存在', 401);
}
UserContext::setCurrentUser($userInfo);
return $handler->handle($request);
}
protected function extractToken(ServerRequestInterface $request): string
{
$authorizationHeader = $request->getHeaderLine('Authorization') ?? '';
if (!$authorizationHeader) {
throw new ApiException('授权标头无效', 401);
}
return str_starts_with($authorizationHeader, 'Bearer ') ? substr($authorizationHeader, 7) : '';
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午10:43
* 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\Middleware;
use App\Service\QueueService;
use App\Service\SysService;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午10:44
* 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 RequestLogsMiddleware implements MiddlewareInterface
{
#[Inject]
protected QueueService $queueService;
#[Inject]
protected SysService $sysService;
public function __construct(protected ContainerInterface $container)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$method = $request->getMethod(); // 请求方法
$uri = $request->getUri()->getPath(); // 请求的URI路径
$params = $request->getMethod() === 'POST' ? $request->getParsedBody() : $request->getQueryParams();
$clientIp = $this->sysService->getClientIpInfo($request);
$data = [
'params' => $params,
'method' => $method,
'uri' => $uri,
'client_ip' => $clientIp,
];
// 添加请求日志,存储到队列中
$this->queueService->make($data)->writeRequestLogs();
return $handler->handle($request);
}
}

49
app/Model/ColumnConfig.php Executable file
View File

@ -0,0 +1,49 @@
<?php
/**
* Author: ykxiao
* Date: 2024/12/24
* Time: 下午3:26
* 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\Model;
class ColumnConfig extends Model
{
protected ?string $table = 'column_config';
protected array $fillable = [
'company_id',
'method',
'prop',
'label',
'sortable',
'sort',
'width',
'hide',
'fix',
'filter',
'is_search',
'search_type',
'condition',
'creator_id',
'creator_name',
];
protected array $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'sortable' => 'boolean',
'hide' => 'boolean',
'fix' => 'boolean',
'filter' => 'boolean',
'is_search' => 'boolean',
];
}

27
app/Model/Company.php Normal file
View File

@ -0,0 +1,27 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:58
* 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\Model;
class Company extends Model
{
protected ?string $table = 'company';
protected array $fillable = [
'company_type',
'name',
'status',
];
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午9: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.
*/
declare(strict_types=1);
namespace App\Model;
class FirstCompany extends Model
{
protected ?string $table = 'first_company';
protected array $fillable = [
'domain',
'name',
'full_name',
'company_type',
'address',
'logo',
'owner', // 公司负责人
'id_card', // 法人身份证
'mobile',
'org_code',
'remark',
'active_status',
'activation_date',
];
}

27
app/Model/Model.php Normal file
View File

@ -0,0 +1,27 @@
<?php
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\Model;
use Hyperf\Database\Model\SoftDeletes;
use Hyperf\DbConnection\Model\Model as BaseModel;
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
abstract class Model extends BaseModel implements CacheableInterface
{
use Cacheable, SoftDeletes;
protected ?string $dateFormat = 'U';
protected array $hidden = ['deleted_at', 'password'];
}

View File

@ -0,0 +1,63 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午7:33
* 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\Model;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\SoftDeletingScope;
class OperatorLogs extends Model
{
protected ?string $table = 'operator_logs';
protected array $fillable = [
'company_id',
'location',
'type',
'log_title',
'route',
'params',
'ip',
'source',
'timer',
'agent',
'remark',
'creator_id',
'creator_name',
];
protected array $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'params' => 'json',
];
public function setIpAttribute(mixed $value): void
{
$this->attributes['ip'] = sprintf('%u', ip2long($value));
}
public function getIpAttribute(mixed $value): string
{
return long2ip($value);
}
// 忽略软删除
public function newQuery(bool $cache = false): Builder
{
$builder = parent::newQuery();
return $builder->withoutGlobalScope(SoftDeletingScope::class);
}
}

43
app/Model/Purchase.php Normal file
View File

@ -0,0 +1,43 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午4:42
* 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\Model;
class Purchase extends Model
{
protected ?string $table = 'purchase';
protected array $fillable = [
'purchase_sn',
'warehouse_ids',
'contract_sn',
'purchase_time',
'customer_type',
'container_type',
'container_count',
'original_count',
'original_cube',
'receiving_container',
'receiving_count',
'receiving_cube',
'kz_company_id',
'kz_company_name',
'sh_company_id',
'sh_company_name',
'remark',
'is_from_erp',
'erp_return_id',
];
}

34
app/Model/TableConfig.php Executable file
View File

@ -0,0 +1,34 @@
<?php
/**
* Author: ykxiao
* Date: 2024/12/24
* Time: 下午3:34
* 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\Model;
class TableConfig extends Model
{
protected ?string $table = 'table_config';
protected array $fillable = [
'company_id',
'method',
'size',
'config',
];
protected array $casts = [
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
'config' => 'json',
];
}

View File

@ -0,0 +1,21 @@
<?php
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\Process;
use Hyperf\AsyncQueue\Process\ConsumerProcess;
use Hyperf\Process\Annotation\Process;
#[Process]
class AsyncQueueConsumer extends ConsumerProcess
{
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Author: ykxiao
* Date: 2024/12/24
* Time: 下午3:04
* 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\Repository;
use App\Service\Trait\ColumnConfigTrait;
/**
* Author: ykxiao
* Date: 2024/12/24
* Time: 下午3:06
* Description: BaseRepository类用于提供一个基本的仓库模式实现。它包含一个DAO数据访问对象属性并允许通过魔术方法动态调用DAO上的方法。
*
* (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 AbstractRepository
{
use ColumnConfigTrait;
/**
* 存储数据访问对象的属性。
* @var string
*/
protected mixed $dao;
/**
* 当尝试调用不存在的实例方法时,自动调用此方法。
* 允许通过对象的动态方法调用DAO上的相应方法。
* @param string $name 被调用的方法名
* @param array $arguments 被调用方法的参数数组
* @return mixed 返回DAO方法的执行结果
*/
public function __call(string $name, array $arguments)
{
// 动态调用DAO上的方法。
return call_user_func_array([$this->dao, $name], $arguments);
}
/**
* 设置DAO对象。
* 用于设置存储数据访问对象的属性。
* @param string $dao 数据访问对象的实例
*/
public function setDao(string $dao): void
{
$this->dao = $dao;
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午6:00
* 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\Repository\Company;
use App\Dao\Company\CompanyDao;
use App\Repository\AbstractRepository;
use Exception;
use function Hyperf\Collection\collect;
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午6:01
* Description: CompanyRepository类用于提供公司相关的数据访问和操作。它继承自AbstractRepository类并实现了CompanyDao接口。
*
* (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 CompanyRepository extends AbstractRepository
{
public function __construct(CompanyDao $dao)
{
$this->dao = $dao;
}
/**
* 添加公司。
* @param array $data
* @return void
* @throws Exception
*/
public function add(array $data): void
{
$count = $this->dao->builder();
if (!empty($data['id'])) {
$count->where('id', '!=', $data['id']);
}
// 重复验证邮箱和手机号码
$make = $this->dao->make();
$verify = $make->verifyData(clone $count);
$collect = clone collect($verify->get());
$errors = [];
if ($collect->where('name', '=', $data['name'])->isNotEmpty()) {
$errors[] = '公司名称已存在';
}
if (!empty($errors)) {
throw new Exception(implode(',', $errors));
}
$this->dao->commonCreate($this->paramsData($data));
}
/**
* 处理参数数据。
* @param array $params
* @return array
*/
private function paramsData(array $params): array
{
if (empty($params['id'])) {
return $params;
}
return [
'id' => $params['id'],
'company_type' => $params['company_type'], // 公司类别1开证 2收货
'name' => $params['name'], // 公司名称
'status' => $params['status'] // 状态0禁用 1启用
];
}
}

View File

@ -0,0 +1,111 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午9:25
* 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\Repository\Company;
use App\Dao\Company\FirstCompanyDao;
use App\Repository\AbstractRepository;
use Carbon\Carbon;
use Exception;
use function Hyperf\Collection\collect;
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午9:26
* 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 FirstCompanyRepository extends AbstractRepository
{
public function __construct(FirstCompanyDao $dao)
{
$this->dao = $dao;
}
/**
* 新增公司.
* @throws Exception
*/
public function addCompany(array $data): void
{
$count = $this->dao->builder();
// 重复字段验证
$make = $this->dao->make();
$verify = $make->verifyData(clone $count, $data);
$collect = clone collect($verify->get());
$errors = [];
if ($collect->where('name', '=', $data['name'])->isNotEmpty()) {
$errors[] = '公司简称已存在';
}
if ($collect->where('full_name', '=', $data['full_name'])->isNotEmpty()) {
$errors[] = '公司全称已存在';
}
if ($collect->where('mobile', '=', $data['mobile'])->isNotEmpty()) {
$errors[] = '负责人手机已存在';
}
if (!empty($errors)) {
throw new Exception(implode(',', $errors));
}
$this->dao->commonCreate($this->paramsData($data));
}
/**
* 参数处理.
*/
private function paramsData(array $params): array
{
if (empty($params['id'])) {
return $params;
}
return [
'id' => $params['id'],
'domain' => $params['domain'],
'name' => $params['name'],
'full_name' => $params['full_name'],
'company_type' => $params['company_type'],
'address' => $params['address'] ?? '',
'logo' => $params['logo'] ?? '',
'owner' => $params['owner'] ?? '', // 公司负责人
'id_card' => $params['id_card'] ?? '', // 法人身份证
'mobile' => $params['mobile'],
'org_code' => $params['org_code'] ?? '',
'remark' => $params['remark'] ?? '',
'active_status' => $params['active_status'],
'activation_date' => Carbon::parse($params['activation_date'])->timestamp,
];
}
/**
* 根据公司全称获取公司信息.
* @param string $fullName
* @return array|null
*/
public function getCompanyByFullName(string $fullName): ?array
{
return $this->dao->builder()
->select($this->dao->getFields())
->where('full_name', '=', $fullName)
->first()?->toArray();
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:31
* 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\Repository\Purchase;
use App\Dao\Purchase\PurchaseDao;
use App\Repository\AbstractRepository;
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午5:32
* Description: PurchaseRepository类用于提供采购相关的数据访问和操作。它继承自AbstractRepository类并实现了PurchaseDao接口。
*
* (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 PurchaseRepository extends AbstractRepository
{
public function __construct(PurchaseDao $dao)
{
$this->dao = $dao;
}
public function addPurchase(array $data): void
{
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:22
* 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\Request;
use Hyperf\Validation\Request\FormRequest;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:24
* 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.
*/
abstract class AbstractRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 下午2:54
* 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\Request;
use App\Constants\ActiveStatusConst;
use App\Constants\CompanyTypeConst;
class CompanyRequest extends AbstractRequest
{
public array $scenes = [
'addCompany' => [
'id',
'company_type',
'name',
'status',
],
];
public function rules(): array
{
return [
'id' => 'integer',
'company_type' => 'required|in:' . implode(',', array_column(CompanyTypeConst::getConstantsList(), 'value')),
'name' => 'required|string|max:128',
'status' => 'integer|in:0,1',
];
}
public function attributes(): array
{
return [
'id' => '公司ID',
'company_type' => '公司类型',
'name' => '公司名称',
'status' => '状态',
];
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/5
* Time: 下午9:28
* 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\Request;
class FirstCompanyRequest extends AbstractRequest
{
public array $scenes = [
'addFirstCompany' => [
'domain',
'name',
'full_name',
'company_type',
'address',
'logo',
'owner', // 公司负责人
'id_card', // 法人身份证
'mobile',
'org_code',
'remark',
'active_status',
'activation_date',
],
];
public function rules(): array
{
return [
'id' => 'integer',
'name' => 'required|string|max:60',
// 域名只能是英文、数字、下划线、短横线
'domain' => 'required|string|max:100|regex:/^[a-zA-Z0-9_-]+$/',
'full_name' => 'required|string|max:255',
'company_type' => 'required|in:1,2',
'address' => 'required|string|max:255',
'logo' => 'string|max:255',
'owner' => 'required|string|max:45',
'id_card' => 'string|max:18',
// 验证手机号,加正则验证
'mobile' => 'required|string|max:11|regex:/^1[3-9]\d{9}$/',
'org_code' => 'string|max:64',
'remark' => 'string|max:255',
'active_status' => 'required|integer|in:0,1',
// 激活日期:不能小于当前时间
'activation_date' => 'date|date_format:Y-m-d|after_or_equal:today',
];
}
public function attributes(): array
{
return [
'id' => '公司ID',
'name' => '公司名称',
'domain' => '公司域名',
'full_name' => '公司全称',
'company_type' => '公司类型',
'address' => '公司地址',
'logo' => '公司logo',
'owner' => '公司负责人',
'id_card' => '法人身份证',
'mobile' => '手机号码',
'org_code' => '组织机构代码',
'remark' => '备注',
'active_status' => '激活状态',
'activation_date' => '激活日期',
];
}
public function messages(): array
{
return [
'domain.regex' => '只能是英文、数字、下划线、短横线',
'mobile.regex' => '手机号码格式不正确',
'activation_date.after_or_equal' => '激活日期不能小于当前时间',
];
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:25
* 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\Request;
class PurchaseRequest extends AbstractRequest
{
public array $scenes = [
'userLogin' => ['login_name', 'password'],
'addUser' => [
'login_name', // 登录用户名
'password' => 'string|between:6,15', // 密码
'dept_id', // 部门ID
'department', // 部门名称
'emp_id', // 员工工号
'name', // 姓名
'mobile', // 手机号
'email', // 邮箱地址
'avatar', // 头像
'active_status', // 状态 0禁用 1启用
'remark', // 备注
'role_ids', // 角色ID列表
'role_ids.*' // 角色ID
],
'getUserList' => [
'page', // 页码
'pageSize', // 页大小
]
];
public function rules(): array
{
return [
'login_name' => 'required|string|regex:/^[a-zA-Z0-9_]+$/|between:3,20',
'password' => 'required|string|between:6,15',
'dept_id' => 'integer|min:1',
'department' => 'string|between:2,20',
'emp_id' => 'integer|min:1',
'name' => 'string|between:2,20',
'mobile' => 'string|between:11,11',
'email' => 'string|between:6,30',
'avatar' => 'string|between:1,255',
'active_status' => 'in:0,1',
'remark' => 'string|between:1,255',
'role_ids' => 'array',
'role_ids.*' => 'integer|min:1|distinct',
'page' => 'integer|min:1',
'pageSize' => 'integer|min:1',
];
}
public function attributes(): array
{
return [
'login_name' => '登录账号',
'password' => '登录密码',
'dept_id' => '部门ID',
'department' => '部门名称',
'emp_id' => '员工工号',
'name' => '姓名',
'mobile' => '手机号',
'email' => '邮箱地址',
'avatar' => '头像',
'active_status' => '状态',
'remark' => '备注',
'role_ids' => '角色ID列表',
'role_ids.*' => '角色ID',
'page' => '页码',
'pageSize' => '页大小',
];
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/4
* Time: 下午2:54
* 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\Request;
class RoleRequest extends AbstractRequest
{
public array $scenes = [
'addRole' => [
'id',
'role_name',
'active_status',
'sort',
],
];
public function rules(): array
{
return [
'id' => 'integer',
'role_name' => 'required|string',
'active_status' => 'required|integer',
'sort' => 'required|integer',
];
}
public function attributes(): array
{
return [
'id' => '角色ID',
'role_name' => '角色名称',
'active_status' => '角色状态',
'sort' => '排序',
];
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:25
* 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\Request;
class UserRequest extends AbstractRequest
{
public array $scenes = [
'userLogin' => ['login_name', 'password'],
'addUser' => [
'login_name', // 登录用户名
'password' => 'string|between:6,15', // 密码
'dept_id', // 部门ID
'department', // 部门名称
'emp_id', // 员工工号
'name', // 姓名
'mobile', // 手机号
'email', // 邮箱地址
'avatar', // 头像
'active_status', // 状态 0禁用 1启用
'remark', // 备注
'role_ids', // 角色ID列表
'role_ids.*' // 角色ID
],
'getUserList' => [
'page', // 页码
'pageSize', // 页大小
]
];
public function rules(): array
{
return [
'login_name' => 'required|string|regex:/^[a-zA-Z0-9_]+$/|between:3,20',
'password' => 'required|string|between:6,15',
'dept_id' => 'integer|min:1',
'department' => 'string|between:2,20',
'emp_id' => 'integer|min:1',
'name' => 'string|between:2,20',
'mobile' => 'string|between:11,11',
'email' => 'string|between:6,30',
'avatar' => 'string|between:1,255',
'active_status' => 'in:0,1',
'remark' => 'string|between:1,255',
'role_ids' => 'array',
'role_ids.*' => 'integer|min:1|distinct',
'page' => 'integer|min:1',
'pageSize' => 'integer|min:1',
];
}
public function attributes(): array
{
return [
'login_name' => '登录账号',
'password' => '登录密码',
'dept_id' => '部门ID',
'department' => '部门名称',
'emp_id' => '员工工号',
'name' => '姓名',
'mobile' => '手机号',
'email' => '邮箱地址',
'avatar' => '头像',
'active_status' => '状态',
'remark' => '备注',
'role_ids' => '角色ID列表',
'role_ids.*' => '角色ID',
'page' => '页码',
'pageSize' => '页大小',
];
}
}

73
app/Scope/CompanyScope.php Executable file
View File

@ -0,0 +1,73 @@
<?php
/**
* Author: ykxiao
* Date: 2024/12/24
* Time: 下午3: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.
*/
declare(strict_types=1);
namespace App\Scope;
use App\Context\QueueContext;
use App\Context\UserContext;
use App\Utils\TableUtils;
use Exception;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Model;
use Hyperf\Database\Model\Scope;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
/**
* Author: ykxiao
* Date: 2025/5/7
* Time: 下午4:38
* 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 CompanyScope implements Scope
{
use TableUtils;
#[Inject]
protected Redis $redis;
/**
* 将当前用户所属公司的条件应用于查询构建器。
*
* @param Builder $builder 查询构建器实例。
* @param Model $model 模型实例,通常是想要应用作用域的模型。
* @return Builder 应用了作用域条件的查询构建器实例。
* @throws Exception
*/
public function apply(Builder $builder, Model $model): Builder
{
// 尝试从上下文中获取当前用户信息
$user = QueueContext::getUser() ?? UserContext::getCurrentUser();
$company = $user['company'];
// 如果没有找到用户信息,则不应用任何条件,直接返回查询构建器
if (!$user || !$company) {
return $builder;
}
// 如果存在 company_id应用企业作用域
if (!empty($companyId = $company['id'] ?? null)) {
return self::hasColumn($model, 'company_id')
? $builder->where($model->getTable() . '.company_id', $companyId)
: $builder;
}
return $builder;
}
}

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'] ?? [],
],
);
}
}

119
app/Utils/ApiResponse.php Normal file
View File

@ -0,0 +1,119 @@
<?php
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:30
* 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\Utils;
use Hyperf\HttpMessage\Server\Response;
use Hyperf\HttpMessage\Stream\SwooleStream;
/**
* Author: ykxiao
* Date: 2025/6/3
* Time: 下午8:31
* Description: A utility class for creating API responses.
*
* (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 ApiResponse
{
protected string $token = ''; // Stores the authorization token
protected Response $response; // Holds the response object
/**
* Constructor to initialize ApiResponse object with a response object.
*
* @param Response $response the response object to be used for sending API responses
*/
public function __construct(Response $response)
{
$this->response = $response;
}
/**
* Creates a successful API response with optional data, message, and status code.
*
* @param array $data the data to be returned in the response
* @param string $message a success message to be returned
* @param int $code the HTTP status code for the response
* @return Response the constructed response with success data
*/
public function success(array $data = [], string $message = 'Success', int $code = 200): Response
{
$result = [
'code' => $code,
'message' => $message,
'data' => $data,
];
return self::jsonResponse($result);
}
/**
* Creates an error API response with an error message, optional data, and status code.
*
* @param string $message the error message to be returned
* @param int $code the HTTP status code for the error response
* @param null $data optional additional data to be returned with the error
* @return Response the constructed response with error data
*/
public function error(string $message, int $code = 400, $data = null): Response
{
$result = [
'code' => $code,
'message' => $message,
'data' => $data,
];
return self::jsonResponse($result);
}
/**
* Sets an authorization token to be included in the response headers.
*
* @param string $token the authorization token
* @return static the current instance of ApiResponse with the token set
*/
public function setToken(string $token): static
{
$this->token = $token;
return $this;
}
/**
* Converts data into a JSON response with an optional status code.
* If a token has been set, it includes the token in the authorization header.
*
* @param array $data the data to encode into JSON and send in the response
* @param int $statusCode the HTTP status code for the response
* @return Response the constructed JSON response
*/
public function jsonResponse(array $data, int $statusCode = 200): Response
{
$response = $this->response;
if ($this->token) {
// Adds authorization header to the response if a token is set
$response->withHeader('Authorization', 'Bearer ' . $this->token);
}
// Constructs and returns the response with the specified data and headers
return $response
->withStatus($statusCode)
->withHeader('Content-Type', 'application/json')
->withBody(new SwooleStream(json_encode($data)));
}
}

81
app/Utils/TableUtils.php Normal file
View File

@ -0,0 +1,81 @@
<?php
/**
* Author: ykxiao
* Date: 2025/5/6
* Time: 下午10:42
* 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\Utils;
use App\Exception\ApiException;
use App\Model\Model;
use Hyperf\Context\ApplicationContext;
use Hyperf\DbConnection\Db;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
/**
* Author: ykxiao
* Date: 2025/5/6
* Time: 下午10:44
* 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.
*/
trait TableUtils
{
/**
* 检查数据表是否包含指定列。
*
* @param Model|string $modelOrTable
* @param string $column 列名称。
* @return bool 如果数据表包含指定列,则返回 true否则返回 false。
*/
public static function hasColumn(Model|string $modelOrTable, string $column): bool
{
try {
if ($modelOrTable instanceof Model) {
$connection = $modelOrTable->getConnection();
$tableName = $modelOrTable->getTable();
} else {
// 如果是字符串表名,默认使用全局连接
$connection = Db::connection();
$tableName = $modelOrTable;
}
// 获取前缀(手动拼接)
$prefix = $connection->getConfig('prefix') ?? '';
$fullTable = $prefix . $tableName;
// 使用缓存避免频繁查询
$cacheKey = "table_column_exists:$fullTable:$column";
/** @var CacheInterface $cache */
$cache = ApplicationContext::getContainer()->get(CacheInterface::class);
if (($exists = $cache->get($cacheKey)) !== null) {
return $exists;
}
// 执行 SHOW COLUMNS
$result = $connection->select("SHOW COLUMNS FROM `$fullTable` LIKE '" . addslashes($column) . "'");
$exists = !empty($result);
$cache->set($cacheKey, $exists, 3600);
return $exists;
} catch (ContainerExceptionInterface|NotFoundExceptionInterface|InvalidArgumentException $e) {
throw new ApiException($e->getMessage());
}
}
}