Commit 02863d7d authored by Administrator's avatar Administrator

Merge branch 'feature-global_log-zdy' into 'master'

feat: 全局操作日志

See merge request !4
parents 7ab3e0e0 f7058748
.idea
/vendor
composer.lock
{
"name": "meibuyu/common",
"description": "美不语微服务公共库",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Rain",
"email": "823773695@qq.com"
}
],
"require": {
"php": ">=7.2"
},
"autoload": {
"psr-4": {
"Meibuyu\\Common\\": "src/"
}
},
"extra": {
"hyperf": {
"config": "Meibuyu\\Rpc\\ConfigProvider"
}
}
}
<?php
declare(strict_types=1);
namespace Meibuyu\Common\Exceptions;
class HttpResponseException extends \Exception
{
}
\ No newline at end of file
<?php
/**
* 项目操作日志
*
* @author zhangdongying
* @date 2023-02-28
*/
declare(strict_types=1);
namespace Meibuyu\Common\GlobalLog;
use Hyperf\Contract\ConfigInterface;
use Hyperf\DbConnection\Model\Model;
use Hyperf\HttpServer\Contract\RequestInterface;
use Meibuyu\Common\GlobalLog\Service\OperateLogService;
use Meibuyu\Micro\Model\Auth;
use Psr\Container\ContainerInterface;
class AppOperateLogService
{
/**
* 配置
*/
protected $config;
/**
* 队列服务
*/
protected $operateLogService;
/**
* 初始化
*
* @param ContainerInterface $container 容器实例
* @throws \Throwable
*/
public function __construct(ContainerInterface $container)
{
$this->config = $container->get(ConfigInterface::class);
$this->operateLogService = $container->get(OperateLogService::class);
}
/**
* 将数组转换成JSON
*
* @param array $array 数组
* @return string
*/
public static function encodeArrayToJson(array $array): string
{
return json_encode($array, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
/**
* 获取客户端IP
*
* @return string
*/
public function getClientIp(): string
{
$request = make(RequestInterface::class);
$header = $request->getHeaders();
if (isset($header['http_client_ip'][0])) {
return $header['http_client_ip'][0];
} elseif (isset($header['x-real-ip'][0])) {
return $header['x-real-ip'][0];
} elseif (isset($header['x-forwarded-for'][0])) {
return $header['x-forwarded-for'][0];
} elseif (isset($header['http_x_forwarded_for'][0])) {
return $header['http_x_forwarded_for'][0];
} else {
$server = $request->getServerParams();
return $server['remote_addr'] ?? '';
}
}
/**
* 格式化原始数据
*
* @param mixed $data 原始数据
* @return string
*/
public function formatSourceData($data): string
{
if ($data instanceof Model) {
return self::encodeArrayToJson($data->toArray());
} elseif (is_array($data)) {
return self::encodeArrayToJson($data);
} else {
return (string)$data;
}
}
/**
* 添加用户操作日志
*
* @param string $tableName 表名
* @param string|int $recordId 记录ID
* @param string $operateType 操作类型,例如:{主菜单}-{子菜单}-{操作},产品管理-产品列表-产品删除
* @param string|array|Model $param 参数
* @param string|array|Model $before 修改之前数据
* @param string|array|Model $after 修改之后数据
* @param string $remark 备注
* @return bool
* @throws \Exception
*/
public function addUserOperateLog(
string $tableName,
$recordId,
string $operateType,
$param,
$before,
$after,
string $remark = ''
): bool
{
return $this->addOperateLog(
make(RequestInterface::class)->header('hwq-request-id', ''),
Auth::id() ?? 0,
Auth::user()['name'] ?? '',
$this->getClientIp(),
make(RequestInterface::class)->url(),
$tableName,
$recordId,
$operateType,
$param,
$before,
$after,
$remark
);
}
/**
* 添加系统操作日志
*
* @param string $action 方法全路径
* @param string $tableName 表名
* @param string|int $recordId 记录ID
* @param string $operateType 操作类型,例如:物流运单推送物流服务商
* @param string|array|Model $param 参数
* @param string|array|Model $before 修改之前数据
* @param string|array|Model $after 修改之后数据
* @param string $remark 备注
* @return bool
* @throws \Exception
*/
public function addSystemOperateLog(
string $action,
string $tableName,
$recordId,
string $operateType,
$param,
$before,
$after,
string $remark = ''
): bool
{
return $this->addOperateLog(
'',
0,
'system',
'',
$action,
$tableName,
$recordId,
$operateType,
$param,
$before,
$after,
$remark
);
}
/**
* 添加系统操作日志
*
* @param string $requestId 请求ID
* @param string|int $operatorId 操作人ID
* @param string $operatorName 操作人名称
* @param string $clientIp 请求IP
* @param string $url 请求URL或者请求方法
* @param string $tableName 表名
* @param string|int $recordId 记录ID
* @param string $operateType 操作类型,例如:物流运单推送物流服务商
* @param string|array|Model $param 参数
* @param string|array|Model $before 修改之前数据
* @param string|array|Model $after 修改之后数据
* @param string $remark 备注
* @return bool
* @throws \Exception
*/
public function addOperateLog(
string $requestId,
$operatorId,
string $operatorName,
string $clientIp,
string $url,
string $tableName,
$recordId,
string $operateType,
$param,
$before,
$after,
string $remark
): bool
{
return $this->operateLogService->addOperateLog(
$requestId,
$this->config->get('app_name'),
(int)$operatorId,
$operatorName,
$clientIp,
$url,
$tableName,
(string)$recordId,
$operateType,
$this->formatSourceData($param),
$this->formatSourceData($before),
$this->formatSourceData($after),
$remark
);
}
}
\ No newline at end of file
<?php
/**
* 操作日志示例
*
* @author zhangdongying
* @date 2023-03-01
*/
declare(strict_types=1);
namespace Meibuyu\Common\GlobalLog\Example;
use Meibuyu\Common\GlobalLog\AppOperateLogService;
use Psr\Container\ContainerInterface;
class AppOperateLogExample
{
/**
* 容器实例
*/
protected $container;
/**
* 初始化
*
* @param ContainerInterface $container 容器实例
* @throws \Throwable
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* addUserOperateLog 调用示例
*
* @return void
* @throws \Throwable
*/
public function addUserOperateLogExample()
{
$this->container->get(AppOperateLogService::class)->addUserOperateLog(
'products',
'1',
'产品管理-产品列表-产品编辑',
['product_id' => 1, 'name' => 'bbb'],
['id' => 1, 'name' => 'aaa'],
['id' => 2, 'name' => 'bbb']
);
}
/**
* addSystemOperateLog 调用示例
*
* @return void
* @throws \Throwable
*/
public function addSystemOperateLogExample()
{
$this->container->get(AppOperateLogService::class)->addSystemOperateLog(
'AppOperateLogExample@addSystemOperateLogExample',
'products',
'1',
'物流运单推送物流服务商',
['product_id' => 1, 'name' => 'bbb'],
['id' => 1, 'name' => 'aaa'],
['id' => 2, 'name' => 'bbb']
);
}
}
\ No newline at end of file
<?php
/**
* 操作日志
*
* @author zhangdongying
* @date 2023-02-28
*/
declare(strict_types=1);
namespace Meibuyu\Common\GlobalLog\Service;
use Psr\Container\ContainerInterface;
class OperateLogService
{
/**
* 队列服务
*/
protected $queueService;
/**
* 操作日志队列名称
*
* @var string
*/
protected $queue = 'APP:QUEUE:GLOBAL_OPERATE_LOG';
/**
* 初始化
*
* @param ContainerInterface $container 容器实例
* @throws \Throwable
*/
public function __construct(ContainerInterface $container)
{
$this->queueService = $container->get(RedisQueueService::class);
}
/**
* 设置队列名称
*
* @param string $name 队列名称
* @return OperateLogService
*/
public function setQueueName(string $name): OperateLogService
{
$this->queue = $name;
return $this;
}
/**
* 生成唯一ID
*
* @return string
*/
public static function generateUniqueId(): string
{
return sha1(uniqid('', true) . mt_rand(10000, 99999));
}
/**
* 添加操作日志
*
* @param string $requestId 请求ID
* @param string $appName 项目名称
* @param int $operatorId 操作人ID
* @param string $operatorName 操作人名称
* @param string $clientIp 客户端IP
* @param string $url 请求链接
* @param string $tableName 表名
* @param string $recordId 记录ID
* @param string $operateType 操作类型
* @param string $param 参数
* @param string $before 修改之前数据
* @param string $after 修改之后数据
* @param string $remark 备注
* @return bool
* @throws \Exception
*/
public function addOperateLog(
string $requestId,
string $appName,
int $operatorId,
string $operatorName,
string $clientIp,
string $url,
string $tableName,
string $recordId,
string $operateType,
string $param,
string $before,
string $after,
string $remark
): bool
{
$data = [
'log_sn' => self::generateUniqueId(),
'request_id' => $requestId,
'app_name' => $appName,
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'client_ip' => $clientIp,
'url' => $url,
'table_name' => $tableName,
'record_id' => $recordId,
'operate_type' => $operateType,
'param' => $param,
'before' => $before,
'after' => $after,
'remark' => $remark,
'created_at' => date('Y-m-d H:i:s'),
];
return (bool)$this->queueService->push($this->queue, json_encode($data));
}
}
\ No newline at end of file
<?php
/**
* REDIS队列服务
*
* @author zhangdongying
* @date 2023-02-28
*/
declare(strict_types=1);
namespace Meibuyu\Common\GlobalLog\Service;
use Hyperf\Redis\Redis;
use Psr\Container\ContainerInterface;
class RedisQueueService
{
/**
* REDIS实例
*/
protected $redis;
/**
* 初始化
*
* @param ContainerInterface $container 容器实例
* @throws \Throwable
*/
public function __construct(ContainerInterface $container)
{
$this->redis = $container->get(Redis::class);
}
/**
* 添加到队列中
*
* @param string $queue 队列名称
* @param string $data 数据
* @return mixed
* @throws \Exception
*/
public function push(string $queue, string $data)
{
return $this->redis->lPush($queue, $data);
}
}
\ No newline at end of file
<?php
namespace Meibuyu\Common\UploadOss\Example\Common\Enum;
/**
* @author chentianyu
*/
class OssEnum
{
const PRODUCT = 'product';
const PRODUCT_CHILD = 'product-child';
}
\ No newline at end of file
<?php
namespace Meibuyu\Common\UploadOss\Example;
use App\Controller\AbstractController;
use Hyperf\HttpMessage\Upload\UploadedFile;
use Meibuyu\Common\UploadOss\UploadOssService;
use Meibuyu\Micro\Exceptions\HttpResponseException;
use Meibuyu\Common\UploadOss\Example\Common\Enum\OssEnum;
use Meibuyu\Micro\Model\Auth;
/**
* @author chentianyu
*/
class UploadExample extends AbstractController
{
/**
* @Inject
* @var UploadOssService
*/
private $service;
/**
* 先创建记录上传信息表 oss_files;
*/
//CREATE TABLE `oss_files` (
//`id` int(11) NOT NULL AUTO_INCREMENT,
//`type` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件类型',
//`module` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模块名',
//`source_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '源文件名',
//`name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件名',
//`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件路径',
//`user_id` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户id',
//`ext` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件后缀',
//`size` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文件大小',
//`created_at` timestamp NULL DEFAULT NULL,
//PRIMARY KEY (`id`)
//) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='oss文件表'
/**
* 上传图片
* @return mixed
* @throws HttpResponseException
* @throws \Meibuyu\Micro\Exceptions\HttpResponseException
* @author chentianyu
*/
public function image()
{
$image = $this->request->file('image');
$userId = Auth::id()??0;
$module = OssEnum::PRODUCT;
if (!$image) {
throw new HttpResponseException('请上传图片');
}
/**
* UploadedFile $image
* string $module 模块名称,常量定义在 namespace App\Common\Enum\OssEnum
* int $userId
* bool $uniqueFileName 是否重新生成唯一文件名(默认否,沿用源文件名,注意文件重复上传会报oss错误。)
*/
$res = $this->service->uploadImage($image, $module, $userId, $uniqueFileName = false);
return success('上传成功', $res);
}
/**
* 上传文件
* @return mixed
* @throws HttpResponseException
* @throws \Meibuyu\Micro\Exceptions\HttpResponseException
* @author chentianyu
*/
public function file()
{
$file = $this->request->file('file');
$userId = Auth::id()??0;
$module = OssEnum::PRODUCT_CHILD;
if (!$file) {
throw new HttpResponseException('请上传文件');
}
/**
* UploadedFile $file
* string $module 模块名称,常量定义在 namespace App\Common\Enum\OssEnum
* int $userId
* bool $uniqueFileName 是否重新生成唯一文件名(默认否,沿用源文件名,注意文件重复上传会报oss错误。)
* array $options 其他选项
*/
$res = $this->service->uploadFile($file, $module, $userId, $uniqueFileName = false, $options = ['maxSize' => 10 * 1024 * 1024]);
return success('上传成功', $res);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Meibuyu\Common\UploadOss;
use Hyperf\DbConnection\Model\Model;
/**
* oss模型类 OssFiles
* @package App\Model
* @property integer $id
* @property string $type 文件类型
* @property string $module 模块名
* @property string $source_name 源文件名
* @property string $name 文件名
* @property string $path 文件路径
* @property string $user_id 用户id
* @property string $ext 文件后缀
* @property string $size 文件大小
* @property string $created_at
*/
class FileModel extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'oss_files';
const UPDATED_AT = null;
protected $fillable = ['type', 'module', 'source_name', 'name', 'path', 'user_id', 'ext', 'size'];
}
\ No newline at end of file
<?php
namespace Meibuyu\Common\UploadOss;
use Hyperf\Contract\ConfigInterface;
use League\Flysystem\Filesystem;
use Hyperf\Filesystem\FilesystemFactory;
use Hyperf\HttpMessage\Upload\UploadedFile;
use League\Flysystem\FileExistsException;
use Meibuyu\Common\Exceptions\HttpResponseException;
use Psr\Container\ContainerInterface;
class UploadOssService
{
/**
* @var FilesystemFactory
*/
private $factory;
/**
* @var ConfigInterface
*/
private $config;
/**
* @var string
*/
private $urlPrefix;
/**
* @var string
*/
private $appDev;
/**
* @var string
*/
private $appName;
/**
* 是否自动保存数据库
* @var bool
*/
private $autoSaveDataBase = true;
public static $options = [
'maxSize' => 10 * 1024 * 1024, // 文件大小,10M
'mime' => ['jpeg', 'png', 'gif', 'jpg', 'svg', 'txt', 'pdf', 'xlsx', 'xls', 'doc', 'docx', 'rar', 'zip', 'csv'], // 允许上传的文件类型
];
public function __construct(ContainerInterface $container)
{
$this->config = $container->get(ConfigInterface::class);
$this->factory = $container->get(FilesystemFactory::class);
$ossConfig = $this->config->get('file.storage.oss');
$this->urlPrefix = 'https://' . $ossConfig['bucket'] . '.' . $ossConfig['endpoint'] . '/';
$this->appDev = $this->config->get('app_env');
$this->appName = $this->config->get('app_name');
}
/**
* 设置是否自动保存记录到数据库
* @param bool $bool
* @author chentianyu
*/
public function setAutoSaveDataBase(bool $bool)
{
$this->autoSaveDataBase = $bool;
}
/**
* 上传图片
* @param \Hyperf\HttpMessage\Upload\UploadedFile $image
* @param string $module
* @return array|string
* @throws \Meibuyu\Common\Exceptions\HttpResponseException
* @author chentianyu
*/
public function uploadImage(UploadedFile $image, string $module, $userId, $uniqueFileName = true)
{
$ext = $image->getExtension();
$clientFilename = $image->getClientFilename();
$fileName = $uniqueFileName ? ($this->genUniqueFileName() . '.' . $ext) : $clientFilename;
$options = ['mime' => ['jpeg', 'png', 'gif', 'jpg', 'svg']];
$filePath = 'oss2/' . $this->appDev . "/" . $this->appName . "/$module/images/" . today() . "/$userId/" . $fileName;
$this->upload($image, $filePath, $options);
if($this->autoSaveDataBase){
$model = new FileModel();
$model = $model->newInstance([
'type' => 'image',
'module' => $module,
'user_id' => $userId,
'source_name' => $clientFilename,
'name' => $fileName,
'path' => $this->urlPrefix . $filePath,
'ext' => $ext,
'size' => num_2_file_size($image->getSize()),
]);
$model->save();
return $model->toArray();
}else{
return $this->urlPrefix . $filePath;
}
}
/**
* 上传文件
* @param \Hyperf\HttpMessage\Upload\UploadedFile $image
* @param string $module
* @return array|string
* @throws \Meibuyu\Common\Exceptions\HttpResponseException
* @author chentianyu
*/
public function uploadFile(UploadedFile $file, string $module, $userId, $uniqueFileName = true, array $options = [])
{
$ext = $file->getExtension();
$clientFilename = $file->getClientFilename();
$fileName = $uniqueFileName ? ($this->genUniqueFileName() . '.' . $ext) : $clientFilename;
$filePath = 'oss2/' . $this->appDev . "/" . $this->appName . "/$module/files/" . today() . "/$userId/" . $fileName;
$this->upload($file, $filePath, $options);
if($this->autoSaveDataBase){
$model = new FileModel();
$model = $model->newInstance([
'type' => 'file',
'module' => $module,
'user_id' => $userId,
'source_name' => $clientFilename,
'name' => $fileName,
'path' => $this->urlPrefix . $filePath,
'ext' => $ext,
'size' => num_2_file_size($file->getSize()),
]);
$model->save();
return $model->toArray();
}else{
return $this->urlPrefix . $filePath;
}
}
/**
* @param \Hyperf\HttpMessage\Upload\UploadedFile $file
* @param $filePath
* @param array $options
* @return bool
* @throws \Meibuyu\Common\Exceptions\HttpResponseException
* @author chentianyu
*/
public function upload(UploadedFile $file, string $filePath, array $options = [])
{
if ($file->isValid()) {
$options = array_merge(self::$options, $options);
$extension = strtolower($file->getExtension());
// 通过扩展名判断类型
if (!in_array($extension, $options['mime'])) {
throw new HttpResponseException('文件类型不支持,目前只支持' . implode(',', $options['mime']));
}
// 判断文件大小
if ($file->getSize() > $options['maxSize']) {
throw new HttpResponseException('文件超出系统规定的大小,最大不能超过' . num_2_file_size($options['maxSize']));
}
try {
$oss = $this->factory->get('oss');
$stream = fopen($file->getRealPath(), 'r+');
$res = $oss->writeStream($filePath, $stream);
is_resource($stream) && fclose($stream);
return $res;
} catch (FileExistsException $e) {
throw new HttpResponseException($e->getMessage());
}
} else {
throw new HttpResponseException('文件无效');
}
}
/**
* 上传本地文件到oss
* @param string $localFilePath
* @param string $ossFilePath 枚举key
* @param bool $fixedDir 使用oss固定目录
* @return mixed
* @throws HttpResponseException
* @author Liu lu
* date 2023-02-09
*/
public function uploadLocalFile(string $localFilePath, string $ossFilePath,$fixedDir=false)
{
$extension = pathinfo(parse_url($localFilePath,PHP_URL_PATH),PATHINFO_EXTENSION);
if($fixedDir){
$ossFilePath = 'oss2/'.env('APP_ENV').'/'.env('APP_NAME') ."/{$ossFilePath}/". md5($localFilePath).'.'.$extension;
}else{
$fileName = $this->genUniqueFileName() . '.' . $extension;
$ossFilePath = 'oss2/'. $this->appDev .'/'. $this->appName ."/{$ossFilePath}/". today() .'/'. $fileName;
}
try {
$oss = $this->factory->get('oss');
$stream = fopen($localFilePath, 'r+');
$oss->writeStream($ossFilePath, $stream);
is_resource($stream) && fclose($stream);
return $this->urlPrefix . $ossFilePath;
} catch (FileExistsException $e) {
throw new HttpResponseException($e->getMessage());
}
}
/**
* 生成唯一文件名
* @return string
* @author chentianyu
*/
public function genUniqueFileName()
{
return date('YmdHis') . uniqid();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment