Merge branch 'feature/mzl_dev' of http://219.157.255.213:25311/xinyouhe/fdesk into feature/mzl_dev

This commit is contained in:
mzl
2025-09-08 20:01:31 +08:00
8 changed files with 686 additions and 14 deletions
+153 -3
View File
@@ -3,8 +3,11 @@
namespace app\common\controller;
use app\common\service\OssService;
use app\common\service\UploadedFilesService;
use app\common\traits\ResponseTrait;
use support\Log;
use support\Request;
use support\exception\BusinessException;
use Shopwwi\WebmanFilesystem\Facade\Storage;
class UploadController
@@ -22,15 +25,41 @@ class UploadController
$scene = $request->input('scene', 'image');
$file = $request->file('file');
if ($file && $file->isValid()) {
$pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.md5($file->getUploadName() . time()).'.'.$file->getUploadExtension();
$fileName = md5($file->getUploadName() . time()).'.'.$file->getUploadExtension();
$pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.$fileName;
$fullPathUrl = public_path() . $pathUrl;
$file->move($fullPathUrl);
$data = [
'domain' => getenv('MAIN_DOMAIN'),
'file_url' => $pathUrl,
'full_url' => getenv('MAIN_DOMAIN') . $pathUrl,
'scene' => $scene,
];
// 记录文件信息到数据库
try {
$uploadedFilesService = new UploadedFilesService();
$partition = $scene;
$category = $request->input('category', 'general');
$fileInfo = [
'file_name' => $fileName,
'file_path' => $pathUrl,
'file_size' => $file->getSize(),
'file_type' => $file->getUploadMimeType(),
'original_name' => $file->getUploadName(),
];
$recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category);
$data['file_id'] = $recordResult['id'];
$data['partition'] = $partition;
$data['category'] = $category;
} catch (\Throwable $e) {
// 记录失败不影响上传结果,只记录日志
Log::error('记录文件信息失败: ' . $e->getMessage());
}
return $this->success($data, 'upload success');
}
return $this->error('file not found');
@@ -43,15 +72,19 @@ class UploadController
public function uploadFile(Request $request)
{
try {
$scene = $request->input('scene');
$file = $request->file('file');
if (!$file || !$file->isValid()) {
return $this->error('上传失败');
}
if (empty($scene)) {
return $this->error('上传场景不能为空');
}
$originalName = $file->getUploadName();
$result = Storage::adapter('oss')
->path('vehicle_album/'.date('Ymd'))
->path('vehicle_album/' . $scene . '/' . date('Ymd'))
->size(1024*1024*5)
->upload($file);
@@ -63,6 +96,11 @@ class UploadController
'file_url' => $ossservice->generatePrivateUrl($result->file_name)
];
// 记录文件信息到数据库
$category = $request->input('category', 'general');
$res = $this->recordFileInfo($result,$request,$file,$scene,$category);
$data = array_merge($data, $res);
return $this->success($data);
}catch (\Throwable $e){
return $this->error($e->getMessage());
@@ -73,14 +111,18 @@ class UploadController
public function uploadImage(Request $request)
{
try {
$scene = $request->input('scene');
$file = $request->file('image');
if (!$file || !$file->isValid()) {
return $this->error('上传失败');
}
if (empty($scene)) {
return $this->error('上传场景不能为空');
}
$result = Storage::adapter('oss')
->path('vehicle_album/'.date('Ymd'))
->path('vehicle_album/' . $scene . '/' . date('Ymd'))
->size(1024*1024*5)
->extYes(['image/jpeg','image/png'])
->extNo(['image/gif'])
@@ -94,6 +136,11 @@ class UploadController
'url' => $ossservice->generatePrivateUrl($result->file_name)
];
// 记录文件信息到数据库
$category = $request->input('category', 'general');
$res = $this->recordFileInfo($result,$request,$file,$scene,$category);
$data = array_merge($data, $res);
return json(['errno' => 0, 'data' => $data]);
// return $this->success($data);
} catch (\Throwable $e) {
@@ -102,6 +149,109 @@ class UploadController
}
}
public function recordFileInfo($result, $request, $file, $scene, $category)
{
try {
$uploadedFilesService = new UploadedFilesService();
$partition = $scene;
$fileInfo = [
'file_name' => $result->file_name,
'file_path' => $result->file_name, // OSS存储路径
'file_size' => $file->getSize(),
'file_type' => $file->getUploadMimeType(),
'original_name' => $file->getUploadName(),
];
$recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category);
$data = [];
$data['file_id'] = $recordResult['id'];
$data['partition'] = $partition;
$data['category'] = $category;
return $data;
} catch (\Throwable $e) {
// 记录失败不影响上传结果,只记录日志
Log::error('记录文件信息失败: ' . $e->getMessage());
return [];
}
}
/**
* 获取文件信息
*
* @param Request $request
* @return \support\Response
*/
public function getFileInfo(Request $request)
{
try {
$fileId = $request->input('id');
if (!$fileId) {
return $this->error('文件ID不能为空');
}
$uploadedFilesService = new UploadedFilesService();
$fileInfo = $uploadedFilesService->getFileInfo($fileId);
if (!$fileInfo) {
return $this->error('文件不存在');
}
return $this->success($fileInfo);
} catch (\Throwable $e) {
return $this->error('获取文件信息失败: ' . $e->getMessage());
}
}
/**
* 删除文件
*
* @param Request $request
* @return \support\Response
*/
public function deleteFile(Request $request)
{
try {
$fileId = $request->input('id');
if (!$fileId) {
return $this->error('文件ID不能为空');
}
$uploadedFilesService = new UploadedFilesService();
$result = $uploadedFilesService->deleteFile($fileId);
if ($result) {
return $this->success([], '文件删除成功');
} else {
return $this->error('文件删除失败');
}
} catch (\Throwable $e) {
return $this->error('文件删除失败: ' . $e->getMessage());
}
}
/**
* 获取分区配置
*
* @param Request $request
* @return \support\Response
*/
public function getPartitionConfig(Request $request)
{
try {
$uploadedFilesService = new UploadedFilesService();
$config = $uploadedFilesService->getPartitionConfig();
return $this->success($config);
} catch (\Throwable $e) {
return $this->error('获取分区配置失败: ' . $e->getMessage());
}
}
public function proxy(Request $request)
{
$filePath = $request->get('file'); // 原OSS文件路径
+169
View File
@@ -0,0 +1,169 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\UploadedFiles;
class UploadedFilesLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UploadedFiles::class;
/**
* 根据分区获取文件列表
*
* @param string $partition
* @param int $limit
* @param int $page
* @return mixed
*/
public function getFilesByPartition(string $partition, int $limit = 15, int $page = 1)
{
$config = [
'model' => $this->modelClass,
'where' => ['partition' => $partition],
'order' => ['created_at' => 'desc', 'id' => 'desc']
];
return $this->logicList($config, $limit, $page);
}
/**
* 根据分类获取文件列表
*
* @param string $category
* @param int $limit
* @param int $page
* @return mixed
*/
public function getFilesByCategory(string $category, int $limit = 15, int $page = 1)
{
$config = [
'model' => $this->modelClass,
'where' => ['category' => $category],
'order' => ['created_at' => 'desc', 'id' => 'desc']
];
return $this->logicList($config, $limit, $page);
}
/**
* 根据文件类型获取文件列表
*
* @param string $fileType
* @param int $limit
* @param int $page
* @return mixed
*/
public function getFilesByType(string $fileType, int $limit = 15, int $page = 1)
{
$config = [
'model' => $this->modelClass,
'where' => ['file_type' => $fileType],
'order' => ['created_at' => 'desc', 'id' => 'desc']
];
return $this->logicList($config, $limit, $page);
}
/**
* 搜索文件
*
* @param string $keyword
* @param int $limit
* @param int $page
* @return mixed
*/
public function searchFiles(string $keyword, int $limit = 15, int $page = 1)
{
$config = [
'model' => $this->modelClass,
'where' => [
'or' => [
['file_name', 'like', '%' . $keyword . '%'],
['original_name', 'like', '%' . $keyword . '%'],
['category', 'like', '%' . $keyword . '%']
]
],
'order' => ['created_at' => 'desc', 'id' => 'desc']
];
return $this->logicList($config, $limit, $page);
}
/**
* 获取文件统计信息
*
* @return array
*/
public function getFileStatistics(): array
{
$model = new $this->modelClass();
$totalFiles = $model->count();
$totalSize = $model->sum('file_size');
$partitionStats = $model->selectRaw('partition, COUNT(*) as count, SUM(file_size) as size')
->groupBy('partition')
->get()
->toArray();
$categoryStats = $model->selectRaw('category, COUNT(*) as count, SUM(file_size) as size')
->groupBy('category')
->get()
->toArray();
$typeStats = $model->selectRaw('file_type, COUNT(*) as count, SUM(file_size) as size')
->groupBy('file_type')
->get()
->toArray();
return [
'total_files' => $totalFiles,
'total_size' => $totalSize,
'partition_stats' => $partitionStats,
'category_stats' => $categoryStats,
'type_stats' => $typeStats
];
}
/**
* 清理过期文件
*
* @param int $days 保留天数
* @return int 清理的文件数量
*/
public function cleanExpiredFiles(int $days = 30): int
{
$expiredTime = time() - ($days * 24 * 60 * 60);
$config = [
'model' => $this->modelClass,
'where' => [
['created_at', '<', $expiredTime]
]
];
$files = $this->logicListAll($config);
$count = 0;
foreach ($files as $file) {
// 删除物理文件
$fullPath = public_path() . $file->file_path;
if (file_exists($fullPath)) {
unlink($fullPath);
}
// 删除数据库记录
$this->logicDeleteBy(['where' => ['id' => $file->id]]);
$count++;
}
return $count;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace app\common\model;
use support\Model;
class UploadedFiles extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'uploaded_files';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = [
'id',
'file_name',
'file_path',
'file_size',
'file_type',
'category',
'original_name',
'extension',
'partition',
'upload_ip',
'user_agent',
'created_at',
'updated_at'
];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
}
+200
View File
@@ -0,0 +1,200 @@
<?php
namespace app\common\service;
use app\common\validate\UploadedFilesValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\UploadedFiles;
use support\Request;
use support\exception\BusinessException;
class UploadedFilesService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = UploadedFiles::class;
/**
* 文件上传分区配置
*/
protected array $uploadPartitions = [
'image' => [
'path' => 'images',
'max_size' => 5 * 1024 * 1024, // 5MB
'allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp']
],
'document' => [
'path' => 'documents',
'max_size' => 10 * 1024 * 1024, // 10MB
'allowed_types' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'extensions' => ['pdf', 'doc', 'docx']
],
'audio' => [
'path' => 'audios',
'max_size' => 20 * 1024 * 1024, // 20MB
'allowed_types' => ['audio/mp3', 'audio/wav', 'audio/ogg'],
'extensions' => ['mp3', 'wav', 'ogg']
],
'other' => [
'path' => 'others',
'max_size' => 5 * 1024 * 1024, // 5MB
'allowed_types' => [],
'extensions' => []
]
];
/**
* 记录文件信息到数据库
*
* @param array $fileInfo 文件信息
* @param Request $request 请求对象
* @param string $partition 分区名称
* @param string $category 文件分类
* @return array
* @throws BusinessException
*/
public function recordFileInfo(array $fileInfo, Request $request, string $partition = 'other', string $category = 'general'): array
{
// 验证必需的文件信息
$requiredFields = ['file_name', 'file_path', 'file_size', 'file_type'];
foreach ($requiredFields as $field) {
if (!isset($fileInfo[$field])) {
throw new BusinessException("缺少必需的文件信息字段: {$field}");
}
}
// 获取文件扩展名
$fileExtension = pathinfo($fileInfo['file_name'], PATHINFO_EXTENSION);
// 记录到数据库
$fileData = [
'file_name' => $fileInfo['file_name'],
'file_path' => $fileInfo['file_path'],
'file_size' => $fileInfo['file_size'],
'file_type' => $fileInfo['file_type'],
'category' => $category,
'original_name' => $fileInfo['original_name'] ?? '',
'extension' => $fileExtension,
'partition' => $partition,
'upload_ip' => $request->getRealIp(),
'user_agent' => $request->header('User-Agent', ''),
];
$uploadedFile = $this->logicCreate($fileData);
return [
'id' => $uploadedFile->id,
'file_name' => $fileInfo['file_name'],
'file_path' => $fileInfo['file_path'],
'file_url' => getenv('MAIN_DOMAIN') . $fileInfo['file_path'],
'file_size' => $fileInfo['file_size'],
'file_type' => $fileInfo['file_type'],
'category' => $category,
'partition' => $partition,
'original_name' => $fileInfo['original_name'] ?? '',
'extension' => $fileExtension,
];
}
/**
* 获取文件信息
*
* @param int $fileId
* @return array|null
*/
public function getFileInfo(int $fileId): ?array
{
$file = $this->logicShow(['where' => ['id' => $fileId]]);
if (!$file) {
return null;
}
return [
'id' => $file->id,
'file_name' => $file->file_name,
'file_path' => $file->file_path,
'file_url' => getenv('MAIN_DOMAIN') . $file->file_path,
'file_size' => $file->file_size,
'file_type' => $file->file_type,
'category' => $file->category,
'partition' => $file->partition ?? 'other',
'original_name' => $file->original_name ?? '',
'extension' => $file->extension ?? '',
'created_at' => $file->created_at,
];
}
/**
* 删除文件
*
* @param int $fileId
* @return bool
*/
public function deleteFile(int $fileId): bool
{
$file = $this->logicShow(['where' => ['id' => $fileId]]);
if (!$file) {
return false;
}
// 删除物理文件
$fullPath = public_path() . $file->file_path;
if (file_exists($fullPath)) {
unlink($fullPath);
}
// 删除数据库记录
return $this->logicDeleteBy(['where' => ['id' => $fileId]]);
}
/**
* 获取分区配置
*
* @return array
*/
public function getPartitionConfig(): array
{
return $this->uploadPartitions;
}
/**
* 生成文件名
*
* @param $file
* @return string
*/
protected function generateFileName($file): string
{
$extension = $file->getUploadExtension();
$timestamp = time();
$random = substr(md5(uniqid()), 0, 8);
return $timestamp . '_' . $random . '.' . $extension;
}
/**
* 格式化文件大小
*
* @param int $bytes
* @return string
*/
protected function formatFileSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
}
+9 -3
View File
@@ -431,12 +431,12 @@ class Emails
$enterUser = User::where('id', $enterUserId)->first();
// 固定渠道
$channel = 8;
$channel = 12;
$channelInfo = InquiryChannel::where('id', $channel)->first();
// 初始化 UID 轮询器
$uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804');
$uidRotator->setUids([64, 63, 83, 93, 108, 118, 132, 145, 150, 143, 156, 157, 158], false);
$uidRotator->setUids([64, 63, 83, 93, 108, 132, 145, 150, 143, 156, 157, 158], false);
EmailParsedResult::where('status', 0)
->chunkById($batchSize, function ($records) use ($delaySeconds, $throttleAfter, $throttleSeconds, $enterUser, $channelInfo, $uidRotator) {
@@ -452,7 +452,13 @@ class Emails
$assignedUid = $uidRotator->next();
// 获取分配的用户信息
$recipientInfo = User::where('id', $assignedUid)->first();
$recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first();
if (!$recipientInfo) {
flg("用户ID=" . $assignedUid . " 不存在或已离职跳过分配,需要手动移除 UID");
$assignedUid = $uidRotator->next();
$recipientInfo = User::where('id', $assignedUid)->where('status', 1)->first();
}
// 生成录单
$log = [
@@ -0,0 +1,82 @@
<?php
namespace app\common\validate;
use think\Validate;
class UploadedFilesValidate extends Validate
{
/**
* 验证规则
*/
protected $rule = [
'file_name' => 'require|max:255',
'file_path' => 'require|max:500',
'file_size' => 'require|integer|gt:0',
'file_type' => 'require|max:100',
'category' => 'require|max:50',
'original_name' => 'max:255',
'extension' => 'max:20',
'partition' => 'max:50',
'upload_ip' => 'ip',
'user_agent' => 'max:500',
];
/**
* 验证消息
*/
protected $message = [
'file_name.require' => '文件名不能为空',
'file_name.max' => '文件名长度不能超过255个字符',
'file_path.require' => '文件路径不能为空',
'file_path.max' => '文件路径长度不能超过500个字符',
'file_size.require' => '文件大小不能为空',
'file_size.integer' => '文件大小必须是整数',
'file_size.gt' => '文件大小必须大于0',
'file_type.require' => '文件类型不能为空',
'file_type.max' => '文件类型长度不能超过100个字符',
'category.require' => '文件分类不能为空',
'category.max' => '文件分类长度不能超过50个字符',
'original_name.max' => '原始文件名长度不能超过255个字符',
'extension.max' => '文件扩展名长度不能超过20个字符',
'partition.max' => '分区名称长度不能超过50个字符',
'upload_ip.ip' => '上传IP格式不正确',
'user_agent.max' => '用户代理长度不能超过500个字符',
];
/**
* 验证场景
*/
protected $scene = [
'create' => ['file_name', 'file_path', 'file_size', 'file_type', 'category'],
'update' => ['file_name', 'file_path', 'file_size', 'file_type', 'category', 'original_name', 'extension', 'partition'],
'search' => ['category', 'partition', 'file_type'],
];
/**
* 验证文件上传
*/
public function sceneUpload()
{
return $this->append('file_name', 'require|max:255')
->append('file_path', 'require|max:500')
->append('file_size', 'require|integer|gt:0')
->append('file_type', 'require|max:100')
->append('category', 'require|max:50')
->append('original_name', 'max:255')
->append('extension', 'max:20')
->append('partition', 'max:50')
->append('upload_ip', 'ip')
->append('user_agent', 'max:500');
}
/**
* 验证文件信息更新
*/
public function sceneUpdateInfo()
{
return $this->append('file_name', 'require|max:255')
->append('category', 'require|max:50')
->append('original_name', 'max:255');
}
}
+8 -8
View File
@@ -29,26 +29,26 @@ class Emails
// });
if (getenv('ENVIRONMENT') === 'master') {
// 拉取新邮件 - 每3小时的第0分钟0秒执行
new Crontab('0 0 */3 * * *', function(){
// 拉取新邮件 - 每小时的第20分钟0秒执行
new Crontab('0 */20 * * * *', function(){
$email = new EmailsUtils();
$email->uids();
});
// 邮件详情 - 每3小时的第10分钟0秒执行
new Crontab('0 10 */3 * * *', function(){
// 邮件详情 - 每小时的第25分钟0秒执行
new Crontab('0 */25 * * * *', function(){
$email = new EmailsUtils();
$email->details();
});
// 邮件处理 - 每3小时的第20分钟0秒执行
new Crontab('0 20 */3 * * *', function(){
// 邮件处理 - 每小时的第30分钟0秒执行
new Crontab('0 */30 * * * *', function(){
$email = new EmailsUtils();
$email->process();
});
// 邮件分派 - 每3小时的第30分钟0秒执行
new Crontab('0 30 */3 * * *', function(){
// 邮件分派 - 每小时的第35分钟0秒执行
new Crontab('0 */35 * * * *', function(){
$email = new EmailsUtils();
$email->fenpai();
});
+2
View File
@@ -35,6 +35,8 @@ Route::any('/feishu/event', [app\common\controller\NotifyController::class, 'fei
Route::any('/feishu/callback', [app\common\controller\NotifyController::class, 'feishuCallback']);
// 代理oss文件地址
Route::any('/proxy/ossurl', [app\common\controller\UploadController::class, 'proxy']);
Route::any('/upload', [app\common\controller\UploadController::class, 'uploadFile']);
Route::any('/upload/image', [app\common\controller\UploadController::class, 'uploadImage']);
// 公共
Route::group('/common', function () {