diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php index e08fa9b..a900b2d 100644 --- a/app/common/controller/UploadController.php +++ b/app/common/controller/UploadController.php @@ -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文件路径 diff --git a/app/common/logic/UploadedFilesLogic.php b/app/common/logic/UploadedFilesLogic.php new file mode 100644 index 0000000..3490cf2 --- /dev/null +++ b/app/common/logic/UploadedFilesLogic.php @@ -0,0 +1,169 @@ + $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; + } +} \ No newline at end of file diff --git a/app/common/model/UploadedFiles.php b/app/common/model/UploadedFiles.php new file mode 100644 index 0000000..ea8136f --- /dev/null +++ b/app/common/model/UploadedFiles.php @@ -0,0 +1,63 @@ + [ + '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]; + } +} \ No newline at end of file diff --git a/app/common/utils/email/Emails.php b/app/common/utils/email/Emails.php index 68aca42..e910504 100644 --- a/app/common/utils/email/Emails.php +++ b/app/common/utils/email/Emails.php @@ -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 = [ diff --git a/app/common/validate/UploadedFilesValidate.php b/app/common/validate/UploadedFilesValidate.php new file mode 100644 index 0000000..339ff2b --- /dev/null +++ b/app/common/validate/UploadedFilesValidate.php @@ -0,0 +1,82 @@ + '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'); + } +} \ No newline at end of file diff --git a/app/process/Emails.php b/app/process/Emails.php index c37d4e2..2679f00 100644 --- a/app/process/Emails.php +++ b/app/process/Emails.php @@ -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(); }); diff --git a/config/route.php b/config/route.php index a46227c..da3d540 100644 --- a/config/route.php +++ b/config/route.php @@ -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 () {