From 642c25503da1604d6968a5523f2857ee9ba18543 Mon Sep 17 00:00:00 2001 From: mzl92 Date: Tue, 1 Jul 2025 21:17:54 +0800 Subject: [PATCH 01/12] mzl.log --- app/controller/IndexController.php | 2 +- webman | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 webman diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index e509b32..2553493 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -54,7 +54,7 @@ class IndexController - - -'; + return htmlpage(); } public function view(Request $request) { - return view('index/view', ['name' => 'webman']); + return view('index/qrcode', ['name' => 'webman']); } public function json(Request $request) @@ -86,4 +22,4 @@ class IndexController return json(['code' => 0, 'msg' => 'ok']); } -} +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index 5c9c58d..d20b843 100644 --- a/app/functions.php +++ b/app/functions.php @@ -2,3 +2,41 @@ /** * Here is your custom functions. */ + +/** + * 标准化账号参数 + * + * @param string $account 用户输入的账号(可能是手机号或邮箱) + * @return array 返回 ['phone' => $account] 或 ['email' => $account] + */ +function normalizeAccount(string $account): array +{ + // 使用FILTER_VALIDATE_EMAIL验证邮箱格式 + if (filter_var($account, FILTER_VALIDATE_EMAIL)) { + return ['email' => $account]; + } + + // 清理手机号中的非数字字符 + $cleanPhone = preg_replace('/[^0-9]/', '', $account); + + // 验证手机号格式(简单版:11位数字) + if (preg_match('/^1[3-9]\d{9}$/', $cleanPhone)) { + return ['phone' => $cleanPhone]; + } + + // 如果既不是邮箱也不是手机号,根据业务需求处理 + // 这里默认按手机号处理(可根据实际需求调整) + return ['phone' => $cleanPhone]; +} + +/** + * 公共html页面 + */ +function htmlpage() +{ + static $html; + if (!$html) { + $html = file_get_contents(public_path('mzl.html')); + } + return $html; +} \ No newline at end of file diff --git a/app/middleware/AuthCheck.php b/app/middleware/AuthCheck.php new file mode 100644 index 0000000..3e6eb8a --- /dev/null +++ b/app/middleware/AuthCheck.php @@ -0,0 +1,46 @@ +controller); + $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? []; + + if (in_array($request->action, $noNeedLogin)) { + // 白名单 + return $handler($request); + } else { + try { + $authHeader = $request->header('Authorization'); + if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) { + $body = [ + 'status' => 'fail', + 'code' => 401, + 'message' => '未授权', + 'data' => [], + ]; + return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); + } + $token = $matches[1]; + $request->user = (new Jwt())->verifyToken($token); + } catch (\Throwable $e) { + $body = [ + 'status' => 'fail', + 'code' => 401, + 'message' => $e->getMessage(), + 'data' => [], + ]; + return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); + } + return $handler($request); + } + } +} \ No newline at end of file diff --git a/app/middleware/BasicData.php b/app/middleware/BasicData.php new file mode 100644 index 0000000..472456e --- /dev/null +++ b/app/middleware/BasicData.php @@ -0,0 +1,42 @@ +controller); + $noNeedHeader = $controller->getDefaultProperties()['noNeedHeader'] ?? []; + + if (in_array($request->action, $noNeedHeader) || in_array($request->path(), config('whitelist.basi_cdata'))) { + // 白名单 + return $handler($request); + } else { + try { + $corpAbbr = $request->header('Fdesk-Corp-Abbr'); + $systemCode = $request->header('Fdesk-System-Code'); + $loginType = $request->header('Fdesk-Login-Type'); + $basicDataService = new BasicDataService(); + if ($basicDataService->checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)) { + $request->login_type = $loginType; + $request->system_code = $systemCode; + $request->corp_abbr = $corpAbbr; + $request->basic_data = [ + 'login_type' => $loginType, + 'system_code' => $systemCode, + 'corp_abbr' => $corpAbbr + ]; + } + return $handler($request); + } catch (\Throwable $th) { + throw $th; + } + } + } +} \ No newline at end of file diff --git a/app/middleware/BasicDataMiddleware.php b/app/middleware/BasicDataMiddleware.php deleted file mode 100644 index 6929ef8..0000000 --- a/app/middleware/BasicDataMiddleware.php +++ /dev/null @@ -1,30 +0,0 @@ -header('login_type'); - $systemCode = $request->header('system_code'); - $corpAbbr = $request->header('corp_abbr'); - $basicDataService = new BasicDataService(); - if ($basicDataService->checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)) { - $request->login_type = $loginType; - $request->system_code = $systemCode; - $request->corp_abbr = $corpAbbr; - } - return $next($request); - } catch (\Throwable $th) { - throw $th; - } - } - -} diff --git a/app/view/index/qrcode.html b/app/view/index/qrcode.html new file mode 100644 index 0000000..26d1918 --- /dev/null +++ b/app/view/index/qrcode.html @@ -0,0 +1,46 @@ + + + + + 飞书扫码登录示例 + + + + + +
+ + + + diff --git a/config/middleware.php b/config/middleware.php index 5512630..cf0335a 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -12,8 +12,10 @@ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + return [ '' => [ - \app\middleware\BasicDataMiddleware::class + \app\middleware\AccessControl::class, + \app\middleware\BasicData::class ] ]; \ No newline at end of file diff --git a/config/route.php b/config/route.php index aaf9098..3b642cf 100644 --- a/config/route.php +++ b/config/route.php @@ -28,8 +28,13 @@ use Webman\Route; Route::group('/common', function () { Route::group('/auth', function () { - Route::post('/login', [app\common\controller\AuthController::class, 'login']); - Route::post('/register', [app\common\controller\AuthController::class, 'register']); - Route::post('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']); + Route::any('/login', [app\common\controller\AuthController::class, 'login']); + Route::any('/register', [app\common\controller\AuthController::class, 'register']); + Route::any('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']); + + + + // 调试 + Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); }); }); \ No newline at end of file diff --git a/config/whitelist.php b/config/whitelist.php new file mode 100644 index 0000000..8071847 --- /dev/null +++ b/config/whitelist.php @@ -0,0 +1,9 @@ + [ + '/', + '/index/index', + // '/index/view' + ] +]; \ No newline at end of file diff --git a/public/mzl.html b/public/mzl.html new file mode 100644 index 0000000..9eb4e4b --- /dev/null +++ b/public/mzl.html @@ -0,0 +1,64 @@ + + + + + Code + + + +
+ +
+

🌞 hi~

+

    愿你代码如诗,逻辑清晰,Bug 自动远离!

+

        记得适当休息,喝杯好茶或咖啡,为灵感续航 ☕

+

            加油,永远相信美好的事情即将发生。 ✨

+
+ + + + \ No newline at end of file From 37a844c01d9e76aff475ec32d19e13d576c137d0 Mon Sep 17 00:00:00 2001 From: mzl Date: Wed, 9 Jul 2025 19:09:16 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=E5=90=88=E5=B9=B6devlop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/controller/UploadController.php | 122 +++++ app/common/service/OssService.php | 59 ++ app/functions.php | 22 + app/middleware/AccessControl.php | 1 + app/queue/translation/config/trans.php | 14 + app/queue/translation/jobs/TranslationJob.php | 64 +++ app/queue/translation/queue/Translation.php | 50 ++ .../service/TranslationService.php | 41 ++ .../translation/service/TranslatorClient.php | 38 ++ app/queue/utils/Translat.php | 21 + app/queue/utils/XunfeiFanyi.php | 134 +++++ .../controller/AttachmentController.php | 11 + app/ticket/controller/BaseController.php | 51 ++ app/ticket/controller/CategorieController.php | 64 +++ .../controller/ConversationController.php | 46 ++ app/ticket/controller/TicketController.php | 69 +++ app/ticket/logic/AttachmentLogic.php | 82 +++ app/ticket/logic/CategorieLogic.php | 66 +++ app/ticket/logic/ConversationLogic.php | 46 ++ app/ticket/logic/TicketLogic.php | 73 +++ app/ticket/model/Attachment.php | 66 +++ app/ticket/model/Categorie.php | 54 ++ app/ticket/model/CategorieTranslation.php | 35 ++ app/ticket/model/Conversation.php | 33 ++ app/ticket/model/Ticket.php | 77 +++ app/ticket/service/CategorieService.php | 97 ++++ app/ticket/service/ConversationService.php | 49 ++ app/ticket/service/TicketService.php | 122 +++++ composer.json | 6 +- composer.lock | 506 +++++++++++++++++- config/constants/Constants.php | 37 ++ config/plugin/shopwwi/filesystem/app.php | 116 ++++ config/plugin/webman/redis-queue/app.php | 4 + config/plugin/webman/redis-queue/command.php | 7 + config/plugin/webman/redis-queue/log.php | 32 ++ config/plugin/webman/redis-queue/process.php | 12 + config/plugin/webman/redis-queue/redis.php | 22 + config/route.php | 29 +- resource/translations/ar/messages.php | 5 + resource/translations/en/messages.php | 5 + resource/translations/ru/messages.php | 5 + resource/translations/zh/messages.php | 5 + 42 files changed, 2394 insertions(+), 4 deletions(-) create mode 100644 app/common/controller/UploadController.php create mode 100644 app/common/service/OssService.php create mode 100644 app/queue/translation/config/trans.php create mode 100644 app/queue/translation/jobs/TranslationJob.php create mode 100644 app/queue/translation/queue/Translation.php create mode 100644 app/queue/translation/service/TranslationService.php create mode 100644 app/queue/translation/service/TranslatorClient.php create mode 100644 app/queue/utils/Translat.php create mode 100644 app/queue/utils/XunfeiFanyi.php create mode 100644 app/ticket/controller/AttachmentController.php create mode 100644 app/ticket/controller/BaseController.php create mode 100644 app/ticket/controller/CategorieController.php create mode 100644 app/ticket/controller/ConversationController.php create mode 100644 app/ticket/controller/TicketController.php create mode 100644 app/ticket/logic/AttachmentLogic.php create mode 100644 app/ticket/logic/CategorieLogic.php create mode 100644 app/ticket/logic/ConversationLogic.php create mode 100644 app/ticket/logic/TicketLogic.php create mode 100644 app/ticket/model/Attachment.php create mode 100644 app/ticket/model/Categorie.php create mode 100644 app/ticket/model/CategorieTranslation.php create mode 100644 app/ticket/model/Conversation.php create mode 100644 app/ticket/model/Ticket.php create mode 100644 app/ticket/service/CategorieService.php create mode 100644 app/ticket/service/ConversationService.php create mode 100644 app/ticket/service/TicketService.php create mode 100644 config/constants/Constants.php create mode 100644 config/plugin/shopwwi/filesystem/app.php create mode 100644 config/plugin/webman/redis-queue/app.php create mode 100644 config/plugin/webman/redis-queue/command.php create mode 100644 config/plugin/webman/redis-queue/log.php create mode 100644 config/plugin/webman/redis-queue/process.php create mode 100644 config/plugin/webman/redis-queue/redis.php create mode 100644 resource/translations/ar/messages.php create mode 100644 resource/translations/en/messages.php create mode 100644 resource/translations/ru/messages.php create mode 100644 resource/translations/zh/messages.php diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php new file mode 100644 index 0000000..9d8959d --- /dev/null +++ b/app/common/controller/UploadController.php @@ -0,0 +1,122 @@ +input('scene', 'image'); + $file = $request->file('file'); + if ($file && $file->isValid()) { + $pathUrl = '/uploads/'.$scene.'/'.date('Ymd').'/'.md5($file->getUploadName() . time()).'.'.$file->getUploadExtension(); + $fullPathUrl = public_path() . $pathUrl; + $file->move($fullPathUrl); + $data = [ + 'domain' => getenv('MAIN_DOMAIN'), + 'file_url' => $pathUrl, + 'full_url' => getenv('MAIN_DOMAIN') . $pathUrl, + 'scene' => $scene, + ]; + return $this->success($data, 'upload success'); + } + return $this->error('file not found'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + // 上传单文件附件 + public function uploadFile(Request $request) + { + try { + $file = $request->file('file'); + + if (!$file || !$file->isValid()) { + return $this->error('上传失败'); + } + $originalName = $file->getUploadName(); + + $result = Storage::adapter('oss') + ->path('vehicle_album/'.date('Ymd')) + ->size(1024*1024*5) + ->upload($file); + + $ossservice = new OssService(); + + $data = [ + 'original_name' => $originalName, + 'file_name' => $result->file_name, + 'file_url' => $ossservice->generatePrivateUrl($result->file_name) + ]; + + return $this->success($data); + }catch (\Throwable $e){ + return $this->error($e->getMessage()); + } + } + + // 上传单文件图片 + public function uploadImage(Request $request) + { + try { + $file = $request->file('image'); + + if (!$file || !$file->isValid()) { + return $this->error('上传失败'); + } + + $result = Storage::adapter('oss') + ->path('vehicle_album/'.date('Ymd')) + ->size(1024*1024*5) + ->extYes(['image/jpeg','image/png']) + ->extNo(['image/gif']) + ->upload($file); + + $ossservice = new OssService(); + + $data = [ + 'href' => $result->file_name, + 'alt' => '', + 'url' => $ossservice->generatePrivateUrl($result->file_name) + ]; + + return json(['errno' => 0, 'data' => $data]); +// return $this->success($data); + } catch (\Throwable $e) { + return json(['errno' => 1, 'message' => $e->getMessage()]); +// return $this->error($e->getMessage()); + } + } + + public function proxy(Request $request) + { + $filePath = $request->get('file'); // 原OSS文件路径 + try { + // 生成签名URL + $signedUrl = (new \app\common\service\OssService())->generatePrivateUrl($filePath); + + // 获取图片数据并返回 + $imageData = file_get_contents($signedUrl); + + // 根据文件类型设置Content-Type + $mime = mime_content_type('data://application/octet-stream;base64,'.base64_encode($imageData)); + + return response($imageData, 200, [ + 'Content-Type' => $mime, + 'Cache-Control' => 'public, max-age=600' // 客户端缓存10分钟 + ]); + } catch (\Exception $e) { + return response('Image not found '.$e->getMessage(), 404); + } + } +} \ No newline at end of file diff --git a/app/common/service/OssService.php b/app/common/service/OssService.php new file mode 100644 index 0000000..4d75b1f --- /dev/null +++ b/app/common/service/OssService.php @@ -0,0 +1,59 @@ +accessKeyId = getenv('OSS_ACCESSID'); + $this->accessKeySecret = getenv('OSS_ACCESSSECRET'); + $this->bucket = getenv('OSS_BUCKET'); + $this->endpoint = getenv('OSS_ENDPOINT'); + $this->ossClient = new OssClient($this->accessKeyId, $this->accessKeySecret, $this->endpoint); + } + + /** + * 上传图片到 OSS + * @param string $filePath 本地文件路径 + * @param string $objectKey OSS上存储的对象名称 + * @return string 图片的 URL + */ + public function uploadImage($filePath, $objectKey) + { + try { + // 上传文件到 OSS + $this->ossClient->uploadFile($this->bucket, $objectKey, $filePath); + // 构造图片的私有 URL + $url = $this->generatePrivateUrl($objectKey); + return $url; + } catch (OssException $e) { + // 错误处理 + throw new \RuntimeException('OSS Error: ' . $e->getMessage()); + } catch (\Throwable $th) { + // 错误处理 + throw new BusinessException('Error: ' . $th->getMessage()); + } + } + + /** + * 生成图片的私有访问 URL + * @param string $objectKey + * @return string + */ + public function generatePrivateUrl($objectKey, $expireTime = 3600) + { + // 生成 OSS 私有 URL(有效期为 3600 秒) + return $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime); + } +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index d20b843..14d5e4a 100644 --- a/app/functions.php +++ b/app/functions.php @@ -3,6 +3,8 @@ * Here is your custom functions. */ +use support\exception\BusinessException; + /** * 标准化账号参数 * @@ -39,4 +41,24 @@ function htmlpage() $html = file_get_contents(public_path('mzl.html')); } return $html; +} + +/** + * 获取配置 + */ +function getConfig($filePath = null) +{ + // 构建文件路径 + $config_file = app_path($filePath); + // 检查文件是否存在 + if (!file_exists($config_file)) { + return []; + } + // 包含文件并获取返回的数组 + $config = include $config_file; + // 验证是否为数组 + if (!is_array($config)) { + return []; + } + return $config; } \ No newline at end of file diff --git a/app/middleware/AccessControl.php b/app/middleware/AccessControl.php index 6fa3822..2a93d79 100644 --- a/app/middleware/AccessControl.php +++ b/app/middleware/AccessControl.php @@ -1,4 +1,5 @@ [ + 'ticket_categorie' => [ + 'translation_model' => \app\ticket\model\CategorieTranslation::class, + 'foreign_key' => 'categorie_id', + ], + ], + + // 默认支持的目标语种列表 + 'locales' => ['zh','en','ru','ar'], +]; diff --git a/app/queue/translation/jobs/TranslationJob.php b/app/queue/translation/jobs/TranslationJob.php new file mode 100644 index 0000000..eb5e704 --- /dev/null +++ b/app/queue/translation/jobs/TranslationJob.php @@ -0,0 +1,64 @@ +config = getConfig('/queue/translation/config/trans.php'); + $this->payload = $payload; + } + + public function handle(): void + { + $cfgAll = $this->config['entities']; + $pt = $this->payload; + $type = $pt['entityType']; + $id = $pt['entityId']; + $locale = $pt['locale']; + $sourceLocale = $pt['sourceLocale']; + $sourceFields = $pt['sourceFields']; + $clientFields = $pt['clientFields']; + + if (! isset($cfgAll[$type])) { + Log::warning("TranslationJob: 未配置实体 {$type}"); + return; + } + + $cfg = $cfgAll[$type]; + $modelClass = $cfg['translation_model']; + $foreignKey = $cfg['foreign_key']; + + // 决定采用前端提供、源语种还是第三方翻译 + if (! empty($clientFields)) { + $fields = $clientFields; + } elseif ($locale === $sourceLocale) { + $fields = $sourceFields; + } else { + $translator = new TranslatorClient(); + $fields = []; + foreach ($sourceFields as $key => $text) { + $fields[$key] = $translator->translate($text, $sourceLocale, $locale); + } + } + + // 写库 + try { + $modelClass::updateOrCreate( + [ + $foreignKey => $id, + 'locale' => $locale, + ], + array_merge($fields, ['locale' => $locale]) + ); + } catch (\Throwable $e) { + Log::error("TranslationJob 保存失败: {$type}#{$id} locale={$locale}: ".$e->getMessage()); + } + } +} diff --git a/app/queue/translation/queue/Translation.php b/app/queue/translation/queue/Translation.php new file mode 100644 index 0000000..0b5841a --- /dev/null +++ b/app/queue/translation/queue/Translation.php @@ -0,0 +1,50 @@ + 'tom@gmail.com', 'content' => 'hello'] + + $job = new TranslationJob($data); + $job->handle(); + } catch (\Throwable $e) { + Log::error("TranslationService 异步任务异常: ".$e->getMessage()); + } + } + // 消费失败回调 + /* + $package = [ + 'id' => 1357277951, // 消息ID + 'time' => 1709170510, // 消息时间 + 'delay' => 0, // 延迟时间 + 'attempts' => 2, // 消费次数 + 'queue' => 'send-mail', // 队列名 + 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容 + 'max_attempts' => 5, // 最大重试次数 + 'error' => '错误信息' // 错误信息 + ] + */ + public function onConsumeFailure(\Throwable $e, $package) + { + echo "consume failure\n"; + echo $e->getMessage() . "\n"; + // 无需反序列化 + var_export($package); + } +} \ No newline at end of file diff --git a/app/queue/translation/service/TranslationService.php b/app/queue/translation/service/TranslationService.php new file mode 100644 index 0000000..3578d89 --- /dev/null +++ b/app/queue/translation/service/TranslationService.php @@ -0,0 +1,41 @@ +config = getConfig('/queue/translation/config/trans.php'); + } + + /** + * 异步排队所有语种的翻译任务 + * + * @param string $entityType 实体名,如 'vehicle' + * @param int $entityId 主表 ID + * @param string $sourceLocale 源语种,如 'zh' + * @param array $sourceFields 源语种字段 ['name'=>…,'condition_desc'=>…] + * @param array $clientFields 前端已传翻译 ['en'=>[...], …] + */ + public function queueTranslations(string $entityType, int $entityId, string $sourceLocale, array $sourceFields, array $clientFields = []): void + { + foreach ($this->config['locales'] as $locale) { + $payload = [ + 'entityType' => $entityType, + 'entityId' => $entityId, + 'locale' => $locale, + 'sourceLocale' => $sourceLocale, + 'sourceFields' => $sourceFields, + 'clientFields' => $clientFields[$locale] ?? [], + ]; + Redis::send('transTask', $payload); + } + } +} \ No newline at end of file diff --git a/app/queue/translation/service/TranslatorClient.php b/app/queue/translation/service/TranslatorClient.php new file mode 100644 index 0000000..3abc1df --- /dev/null +++ b/app/queue/translation/service/TranslatorClient.php @@ -0,0 +1,38 @@ +endpoint = env('TRANSLATION_API_URL'); + // $this->apiKey = env('TRANSLATION_API_KEY'); + // } + + /** + * 翻译文本 + * @throws \RuntimeException + */ + public function translate(string $text, string $from = 'cn', string $to = 'ru'): string + { + try { + $xunfei = new XunfeiFanyi(); + $rst_dst = $xunfei->fanyi($text, $from, $to); + if ($rst_dst) { + return $rst_dst["trans_result"]["dst"]; + } else { + return ''; + } + } catch (\Throwable $e) { + Log::error("TranslatorClient 错误: ".$e->getMessage()); + throw new \RuntimeException('翻译服务调用失败'); + } + } +} diff --git a/app/queue/utils/Translat.php b/app/queue/utils/Translat.php new file mode 100644 index 0000000..3000756 --- /dev/null +++ b/app/queue/utils/Translat.php @@ -0,0 +1,21 @@ + $header,'body' => $response); + return $data; + } + + + public function xfyun($text = "中华人民共和国于1949年成立", $from = "cn", $to = "en") + { + //在控制台-我的应用-机器翻译获取 + $app_id = "3d46198c"; + //在控制台-我的应用-机器翻译获取 + $api_sec = "ZWY2MmUyNjJjYmFkOTFlYTVmYWVhMGYy"; + //在控制台-我的应用-机器翻译获取 + $api_key = "b6d805f240176ecaea85130cb5f2bca1"; + // 机器翻译接口地址 + $url = "https://ntrans.xfyun.cn/v2/ots"; + //body组装 + // $text = "中华人民共和国于1949年成立";//待翻译文本 + $body = json_encode($this->getBody($app_id, $from, $to, $text)); + + // 组装http请求头 + $date =gmdate('D, d M Y H:i:s') . ' GMT'; + + $digestBase64 = "SHA-256=".base64_encode(hash("sha256", $body, true)); + $builder = sprintf("host: %s +date: %s +POST /v2/ots HTTP/1.1 +digest: %s", "ntrans.xfyun.cn", $date, $digestBase64); + // echo($builder); + $sha = base64_encode(hash_hmac("sha256", $builder, $api_sec, true)); + + $authorization = sprintf("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", $api_key,"hmac-sha256", + "host date request-line digest", $sha); + + $header = [ + "Authorization: ".$authorization, + 'Content-Type: application/json', + 'Accept: application/json,version=1.0', + 'Host: ntrans.xfyun.cn', + 'Date: ' .$date, + 'Digest: '.$digestBase64 + ]; + $response = $this->tocurl($url, $header, $body); + + // var_dump($response['body']); + return $response; + } + + + public function getBody($app_id, $from, $to, $text) + { + $common_param = [ + 'app_id' => $app_id + ]; + + $business = [ + 'from' => $from, + 'to' => $to, + ]; + + $data = [ + "text" => base64_encode($text) + ]; + + return $body = [ + 'common' => $common_param, + 'business' => $business, + 'data' => $data + ]; + } + + // 翻译 + public function fanyi($text = "", $from = "", $to = "") + { + try { + $res = $this->xfyun($text, $from, $to); + $data = json_decode($res["body"], true); + if ($data["code"] === 0) { + return $data["data"]["result"]; + } else { + $this->error = $data["message"]; + return false; + } + } catch (\Throwable $th) { + $this->error = $th->getMessage(); + return false; + } + } + + // 异常 + public function error() + { + return $this->error; + } + +} \ No newline at end of file diff --git a/app/ticket/controller/AttachmentController.php b/app/ticket/controller/AttachmentController.php new file mode 100644 index 0000000..94e1724 --- /dev/null +++ b/app/ticket/controller/AttachmentController.php @@ -0,0 +1,11 @@ +scene($scene); + } + // 执行数据验证 + if (!$validator->check($data)) { + // 验证失败,抛出业务异常,异常信息为验证器返回的错误信息 + throw new BusinessException($validator->getError()); + } + // 验证成功,返回原始数据 + return $data; + } + + public function page($result){ + $current = $result->currentPage(); + $total = $result->total(); + $size = $result->perPage(); + $result = $result->toArray(); + $result['current'] = $current; + $result['total'] = $total; + $result['size'] = $size; + return $result; + } +} \ No newline at end of file diff --git a/app/ticket/controller/CategorieController.php b/app/ticket/controller/CategorieController.php new file mode 100644 index 0000000..84e8bd7 --- /dev/null +++ b/app/ticket/controller/CategorieController.php @@ -0,0 +1,64 @@ +list($request->all()); + return $this->success($this->page($result)); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function getAll(Request $request) + { + try { + $result = (new CategorieService())->getAll($request->all()); + return $this->success($result); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function add(Request $request) + { + try { + return (new CategorieService())->add($request->all()) + ? $this->success() + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage().' '.$th->getLine().' '.$th->getFile()); + } + } + + public function edit(Request $request) + { + try { + $result = (new CategorieService())->edit($request->all()); + return $result + ? $this->success($result) + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage().' '.$th->getLine().' '.$th->getFile()); + } + } + + public function delete(Request $request) + { + try { + $result = (new CategorieService())->delete($request->input('id')); + return $result + ? $this->success($result) + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage().' '.$th->getLine().' '.$th->getFile()); + } + } +} \ No newline at end of file diff --git a/app/ticket/controller/ConversationController.php b/app/ticket/controller/ConversationController.php new file mode 100644 index 0000000..0e1b7ec --- /dev/null +++ b/app/ticket/controller/ConversationController.php @@ -0,0 +1,46 @@ +input('ticket_id'); + if(empty($ticket_id)){ + return $this->error('ticket_id不能为空'); + } + $result = (new ConversationService())->list($request->all()); + return $this->success($this->page($result)); + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getLine().$e->getFile()); + } + } + public function add(Request $request) + { + try { + $request->setPost('user_id', 1); + return (new ConversationService())->add($request->all()) + ? $this->success() + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage().' '.$th->getLine().' '.$th->getFile()); + } + } + public function edit(Request $request) + { + try { + $request->setPost('user_id', 1); + $result = (new ConversationService())->edit($request->all()); + return $result + ? $this->success($result) + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage().' '.$th->getLine().' '.$th->getFile()); + } + } +} \ No newline at end of file diff --git a/app/ticket/controller/TicketController.php b/app/ticket/controller/TicketController.php new file mode 100644 index 0000000..782dc74 --- /dev/null +++ b/app/ticket/controller/TicketController.php @@ -0,0 +1,69 @@ +list($request->all()); + return $this->success($this->page($result)); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + public function add(Request $request) + { + try { + $request->setPost('user_id', 1); + + return (new TicketService())->add($request->all()) + ? $this->success() + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + + public function edit(Request $request) + { + try { + $request->setPost('user_id', 1); + $result = (new TicketService())->edit($request->all()); + return $result + ? $this->success($result) + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + public function status(Request $request) + { + try { + $request->setPost('user_id', 1); + $result = (new TicketService())->status($request->all()); + return $result + ? $this->success($result) + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + public function getStatus(Request $request) + { + try { + $result = (new TicketService())->getStatus($request->all()); + return $result + ? $this->success($result) + : $this->error(); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + +} diff --git a/app/ticket/logic/AttachmentLogic.php b/app/ticket/logic/AttachmentLogic.php new file mode 100644 index 0000000..b498f7e --- /dev/null +++ b/app/ticket/logic/AttachmentLogic.php @@ -0,0 +1,82 @@ +$v){ + $v['ticket_id'] = $ticket_id; + $v['user_id'] = $user_id; + $res = Attachment::create($v); + } + return $res; + } catch (\Throwable $e) { + Log::error('工单创建失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('工单创建失败,请稍后重试', 500); + } + } + + public function edit(array $data,int $ticket_id,int $user_id) + { + try { + $attachmentsId = []; + foreach ($data as $k=>$v){ + $v['ticket_id'] = $ticket_id; + $v['user_id'] = $user_id; + if(!empty($v['id'])){ //修改附件 + $attachmentId = $v['id']; + $attachmentsId[] = $attachmentId; +// $v_res = Attachment::update($attachmentId, $v); + $v_res = Attachment::updateOrCreate(['id' => $v['id']],$v); + if (!$v_res) { + throw new BusinessException('附件保存失败'); + } + } else { //新增附件 + unset($v['id']); + $v_res = Attachment::create($v); + if (!$v_res) { + throw new BusinessException('附件创建失败'); + } + $vId = $v_res->id; + $attachmentsId[] = $vId; + } + } + + //删除本次没有出现的附件 + $res = Attachment::whereNotIn('id', $attachmentsId)->where('ticket_id',$ticket_id)->delete(); + return $res; + } catch (\Throwable $e) { + Log::error('工单编辑(附件)失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('工单编辑失败,请稍后重试', 500); + } + } + + public static function generateSimpleTicketNo(): string + { + $date = date('Ymd'); // 日期 + $prefix = 'GD'; // 前缀 + $random = strtoupper(substr(md5(uniqid()), 0, 5)); // 5位随机字母数字 + + return $prefix . $date . '-' . $random; // 如 GD20240702-AB3X9Z + } +} \ No newline at end of file diff --git a/app/ticket/logic/CategorieLogic.php b/app/ticket/logic/CategorieLogic.php new file mode 100644 index 0000000..c11bd64 --- /dev/null +++ b/app/ticket/logic/CategorieLogic.php @@ -0,0 +1,66 @@ +where($where)->paginate($size, ['*'], 'page', $current); + } + + public function getAll($params) + { + $where = []; + $where[] = ['status', '=', 1]; + return Categorie::with('translation')->where($where)->get(); + } + + public function add(array $data) + { + try { + return Categorie::create($data); + } catch (\Throwable $e) { + Log::error('分类创建失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('分类创建失败,请稍后重试', 500); + } + } + + public function edit(array $data) + { + try { + $categorie = Categorie::updateOrCreate(['id' => $data['id']], $data); + return $categorie; + } catch (\Throwable $e) { + Log::error('分类编辑失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('分类编辑失败,请稍后重试', 500); + } + } + + public function delete(int $id) + { + try { + $categorie = Categorie::where(['id'=>$id])->delete(); + return $categorie; + } catch (\Throwable $e) { + Log::error('分类删除失败', [ + 'id' => $id, + 'error' => $e->getMessage() + ]); + throw new BusinessException('分类删除失败,请稍后重试', 500); + } + } +} \ No newline at end of file diff --git a/app/ticket/logic/ConversationLogic.php b/app/ticket/logic/ConversationLogic.php new file mode 100644 index 0000000..d47a709 --- /dev/null +++ b/app/ticket/logic/ConversationLogic.php @@ -0,0 +1,46 @@ +paginate($size, ['*'], 'page', $current); + } + public function add(array $data) + { + try { + return Conversation::create($data); + } catch (\Throwable $e) { + Log::error('会话创建失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('会话创建失败,请稍后重试', 500); + } + } + public function edit(array $data) + { + try { + $conversation = Conversation::updateOrCreate(['id' => $data['id']], $data); + return $conversation; + } catch (\Throwable $e) { + Log::error('会话编辑失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('会话编辑失败,请稍后重试', 500); + } + } +} \ No newline at end of file diff --git a/app/ticket/logic/TicketLogic.php b/app/ticket/logic/TicketLogic.php new file mode 100644 index 0000000..e22cdb7 --- /dev/null +++ b/app/ticket/logic/TicketLogic.php @@ -0,0 +1,73 @@ +with('categorie')->with('categorie.translation')->where($where)->orderBy('id','desc')->paginate($size, ['*'], 'page', $current); + } + public function add(array $data) + { + $data['status'] = 1; + $data['code'] = self::generateSimpleTicketNo(); + try { + return Ticket::create($data); + } catch (\Throwable $e) { + Log::error('工单创建失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('工单创建失败,请稍后重试', 500); + } + } + public function edit(array $data) + { + try { + unset($data['code']); + $ticket = Ticket::updateOrCreate(['id' => $data['id']],$data); + return $ticket; + } catch (\Throwable $e) { + Log::error('工单编辑失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('工单编辑失败,请稍后重试', 500); + } + } + + public function find(int $id) + { + $ticket = Ticket::find($id); + return $ticket; + } + + public static function generateSimpleTicketNo(): string + { + $date = date('Ymd'); // 日期 + $prefix = 'GD'; // 前缀 + $random = strtoupper(substr(md5(uniqid()), 0, 5)); // 5位随机字母数字 + + return $prefix . $date . '-' . $random; // 如 GD20240702-AB3X9Z + } +} \ No newline at end of file diff --git a/app/ticket/model/Attachment.php b/app/ticket/model/Attachment.php new file mode 100644 index 0000000..f5fc843 --- /dev/null +++ b/app/ticket/model/Attachment.php @@ -0,0 +1,66 @@ +belongsTo(Ticket::class, 'ticket_id', 'id'); + } +} diff --git a/app/ticket/model/Categorie.php b/app/ticket/model/Categorie.php new file mode 100644 index 0000000..610c693 --- /dev/null +++ b/app/ticket/model/Categorie.php @@ -0,0 +1,54 @@ + 'datetime:Y-m-d H:i:s', + 'created_at' => 'datetime:Y-m-d H:i:s', + ]; + protected $fillable = [ + 'name', + 'description', + ]; + + /** + * 单条翻译 + */ + public function translation() + { + return $this->hasOne(CategorieTranslation::class, 'categorie_id')->where('locale', locale()); + } + + /** + * 多条翻译 + */ + public function translations() + { + return $this->hasMany(CategorieTranslation::class, 'categorie_id'); + } + + public function ticket(){ + return $this->hasOne(Ticket::class, 'categorie_id', 'id'); + } +} \ No newline at end of file diff --git a/app/ticket/model/CategorieTranslation.php b/app/ticket/model/CategorieTranslation.php new file mode 100644 index 0000000..6660dea --- /dev/null +++ b/app/ticket/model/CategorieTranslation.php @@ -0,0 +1,35 @@ + 'datetime:Y-m-d H:i:s', + 'created_at' => 'datetime:Y-m-d H:i:s', + ]; + protected $fillable = [ + 'categorie_id', + 'locale', + 'name', + 'description', + ]; +} \ No newline at end of file diff --git a/app/ticket/model/Conversation.php b/app/ticket/model/Conversation.php new file mode 100644 index 0000000..6d63cb5 --- /dev/null +++ b/app/ticket/model/Conversation.php @@ -0,0 +1,33 @@ + 'datetime:Y-m-d H:i:s', + 'created_at' => 'datetime:Y-m-d H:i:s', + ]; + protected $fillable = [ + 'ticket_id', + 'user_id', + 'content', + ]; +} \ No newline at end of file diff --git a/app/ticket/model/Ticket.php b/app/ticket/model/Ticket.php new file mode 100644 index 0000000..cf71b2a --- /dev/null +++ b/app/ticket/model/Ticket.php @@ -0,0 +1,77 @@ + 'datetime:Y-m-d H:i:s', // 指定序列化格式 + 'created_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式 + ]; + + protected $fillable = [ + 'code', + 'categorie_id', + 'content', + 'sort', + 'status', + 'user_id' + ]; + + public function attachments(){ + return $this->hasMany(Attachment::class, 'ticket_id', 'id'); + } + + public function categorie(){ + return $this->belongsTo(Categorie::class, 'categorie_id', 'id'); + } +} diff --git a/app/ticket/service/CategorieService.php b/app/ticket/service/CategorieService.php new file mode 100644 index 0000000..d769a58 --- /dev/null +++ b/app/ticket/service/CategorieService.php @@ -0,0 +1,97 @@ +list($params); + $value=$result[0]->toArray(); + foreach ($result as $key => &$value) { + $value['name'] = isset($value['translation']) ? $value['translation']['name'] : ''; + unset($value['translation']); + } + unset($value); + + return $result; + } + + public function getAll($params) + { + $result = (new CategorieLogic())->getAll($params); + foreach ($result as $key => &$value) { + $value['name'] = isset($value['translation']) ? $value['translation']['name'] : ''; + unset($value['translation']); + } + unset($value); + + return $result; + } + + public $translatFields = ['name']; + + public function add(array $data) + { + try { + Db::beginTransaction(); + $sourceLocale = locale(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + $result = (new CategorieLogic())->add($data); + + (new TranslationService())->queueTranslations( + 'ticket_categorie', + $result->id, + $sourceLocale, + $traitsourceFields + ); + + Db::commit(); + return $result; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + public function edit(array $data) + { + try { + Db::beginTransaction(); + $sourceLocale = locale(); + $traitsourceFields = Translat::extractSourceFields($data, $this->translatFields); + + $result = (new CategorieLogic())->edit($data); + + (new TranslationService())->queueTranslations( + 'ticket_categorie', + $data['id'], + $sourceLocale, + $traitsourceFields + ); + Db::commit(); + return $result; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + public function delete(int $id) + { + if(empty($id)){ + throw new BusinessException('id不能为空'); + } + try { + $result = (new CategorieLogic())->delete($id); + return $result; + } catch (\Throwable $e) { + throw $e; + } + } +} \ No newline at end of file diff --git a/app/ticket/service/ConversationService.php b/app/ticket/service/ConversationService.php new file mode 100644 index 0000000..1716b8f --- /dev/null +++ b/app/ticket/service/ConversationService.php @@ -0,0 +1,49 @@ +list($params); + + $status_fanyi = array_map(fn($v) => "{{{$v}}}", array_flip(Constants::TICKET_STATUS)); + foreach ($result as $key => $value) { + $result[$key]['content'] = str_replace($status_fanyi,$ticket_status,$value['content']); + } + return $result; + } + + public function add(array $data) + { + try { + return (new ConversationLogic())->add($data); + } catch (\Throwable $e) { + throw $e; + } + } + + public function edit(array $data) + { + try { + Db::beginTransaction(); + $result = (new ConversationLogic())->edit($data); + Db::commit(); + return $result; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } +} \ No newline at end of file diff --git a/app/ticket/service/TicketService.php b/app/ticket/service/TicketService.php new file mode 100644 index 0000000..f5a0d72 --- /dev/null +++ b/app/ticket/service/TicketService.php @@ -0,0 +1,122 @@ +list($params); + foreach ($res as $k => &$v) { + $name = isset($v['categorie']['translation']) ? $v['categorie']['translation']['name'] : ''; + if(isset($res[$k]['categorie'])){ + $res[$k]['categorie']['name'] = $name; + } + if (isset($v['categorie']['translation'])){ + unset($res[$k]['categorie']['translation']); + } + foreach ($v['attachments'] as &$attachments) { + $attachments['storage_path_full'] = (new \app\common\service\OssService())->generatePrivateUrl($attachments['storage_path']); + } + } + unset($v); + unset($attachments); + + return $res; + } + + public function add(array $data) + { + try { + $attachment = $data['attachment']??[]; + $logic = new TicketLogic(); + $result = $logic->add($data); + if(!empty($attachment)){ + (new AttachmentLogic())->adds($attachment, $result->id, $data['user_id']); + } + return $result; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + + public function edit(array $data) + { + try { + Db::beginTransaction(); + $attachment = $data['attachment']??[]; + $logic = new TicketLogic(); + $result = $logic->edit($data); + + if(!empty($attachment)){ + (new AttachmentLogic())->edit($attachment, $result->id, $data['user_id']); + } + Db::commit(); + return $result; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + + public function status(array $data) + { + try { + Db::beginTransaction(); + $msg = $data['msg']??[]; + $ticket = (new TicketLogic())->find($data['id']); + if(empty($ticket)){ + throw new BusinessException('工单不存在'); + } + if($data['status'] <= $ticket->status){ + throw new BusinessException(trans('status_not_modified')); + } + +// if(!in_array($ticket->status, [Constants::TICKET_STATUS['PENDING'], Constants::TICKET_STATUS['CONFIRM']]) +// && empty($msg)){ +// throw new BusinessException('提交失败'); +// } + + $ticket->status = $data['status']; + $result = $ticket->save(); + + if(!empty($msg)){ + $conversation = [ + 'ticket_id' => $ticket->id, + 'user_id' => $data['user_id'], + 'content' => '状态更新为{{'.array_search($data['status'],Constants::TICKET_STATUS).'}}:'.$msg + ]; + $res = (new ConversationLogic())->add($conversation); + } + + Db::commit(); + return $result; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + + public function getStatus(array $data) + { + $locale = locale(); + $ticket_status = $locale == 'zh'? Constants::TICKET_STATUS_ZH:Constants::TICKET_STATUS_EN; + + $status = []; + foreach ($ticket_status as $key => $value) { + $status[] = ['value' => $key, 'text' => $value]; + } + return $status; + } +} diff --git a/composer.json b/composer.json index 9e2dd39..ccf259e 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,11 @@ "vlucas/phpdotenv": "^5.6", "firebase/php-jwt": "^6.11", "league/oauth2-server": "^9.2", - "topthink/think-validate": "^3.0" + "topthink/think-validate": "^3.0", + "shopwwi/webman-filesystem": "1.1", + "shopwwi/filesystem-oss": "^1.0", + "webman/redis-queue": "^1.3", + "symfony/translation": "^7.3" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index e10e796..01843a4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,59 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eecf8b50860214e48bdcb8407fd40e14", + "content-hash": "acb636fcb8774558a04bd674f9f95f55", "packages": [ + { + "name": "aliyuncs/oss-sdk-php", + "version": "v2.7.2", + "source": { + "type": "git", + "url": "https://github.com/aliyun/aliyun-oss-php-sdk.git", + "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", + "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "php-coveralls/php-coveralls": "*", + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "OSS\\": "src/OSS" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aliyuncs", + "homepage": "http://www.aliyun.com" + } + ], + "description": "Aliyun OSS SDK for PHP", + "homepage": "http://www.aliyun.com/product/oss/", + "support": { + "issues": "https://github.com/aliyun/aliyun-oss-php-sdk/issues", + "source": "https://github.com/aliyun/aliyun-oss-php-sdk/tree/v2.7.2" + }, + "time": "2024-10-28T10:41:12+00:00" + }, { "name": "brick/math", "version": "0.13.1", @@ -1997,6 +2048,212 @@ }, "time": "2024-09-04T16:06:53+00:00" }, + { + "name": "league/flysystem", + "version": "3.29.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0adc0d9a51852e170e0028a60bd271726626d3f0", + "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.29.0" + }, + "time": "2024-09-29T11:59:11+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.29.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + }, + "time": "2024-08-09T21:24:39+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-01-28T23:22:08+00:00" + }, { "name": "league/oauth2-server", "version": "9.2.0", @@ -3267,6 +3524,124 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "shopwwi/filesystem-oss", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/shopwwi/filesystem-oss.git", + "reference": "85cb385d1a5d11ca3c22bc485a7b18c588fe7645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shopwwi/filesystem-oss/zipball/85cb385d1a5d11ca3c22bc485a7b18c588fe7645", + "reference": "85cb385d1a5d11ca3c22bc485a7b18c588fe7645", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "aliyuncs/oss-sdk-php": "^2.4", + "league/flysystem": "^2.1|^3.0", + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Shopwwi\\FilesystemOss\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tycoon Song", + "email": "8988354@qq.com" + } + ], + "description": "AliYun OSS adapter for flysystem V2. aliyuncs/oss-sdk-php ~2.4", + "homepage": "https://github.com/phcent/filesystem-oss", + "keywords": [ + "Flysystem", + "aliyun-oss", + "flysystem-aliyun-oss" + ], + "support": { + "issues": "https://github.com/shopwwi/filesystem-oss/issues", + "source": "https://github.com/shopwwi/filesystem-oss/tree/v1.0.3" + }, + "time": "2022-10-01T03:00:18+00:00" + }, + { + "name": "shopwwi/webman-filesystem", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/shopwwi/webman-filesystem.git", + "reference": "26ac82515d3787943573c50d1a6f0ca339f0d3d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shopwwi/webman-filesystem/zipball/26ac82515d3787943573c50d1a6f0ca339f0d3d4", + "reference": "26ac82515d3787943573c50d1a6f0ca339f0d3d4", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "league/flysystem": "^2.3|^3.0", + "php": ">=7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Shopwwi\\WebmanFilesystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tycoon Song", + "email": "8988354@qq.com" + } + ], + "description": "Flysystem V2 adapter for the webman", + "keywords": [ + "cos", + "filesystem", + "oss", + "qiniu", + "webman" + ], + "support": { + "issues": "https://github.com/shopwwi/webman-filesystem/issues", + "source": "https://github.com/shopwwi/webman-filesystem/tree/v1.1.0" + }, + "time": "2024-01-21T08:03:36+00:00" + }, { "name": "symfony/cache", "version": "v7.3.0", @@ -4990,7 +5365,13 @@ "type": "zip", "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667", "reference": "4aba29076a29a3aa667e09b791e5f868973a8667", - "shasum": "" + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] }, "require": { "php": ">=8.2", @@ -5772,6 +6153,43 @@ }, "time": "2025-03-14T03:52:14+00:00" }, + { + "name": "webman/redis-queue", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/webman-php/redis-queue.git", + "reference": "6fd491b19acb006d60931724aa2577a046ccb612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/6fd491b19acb006d60931724aa2577a046ccb612", + "reference": "6fd491b19acb006d60931724aa2577a046ccb612", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "workerman/redis-queue": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\RedisQueue\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "description": "Redis message queue plugin for webman.", + "support": { + "issues": "https://github.com/webman-php/redis-queue/issues", + "source": "https://github.com/webman-php/redis-queue/tree/v1.3.1" + }, + "time": "2024-02-28T07:21:52+00:00" + }, { "name": "workerman/coroutine", "version": "v1.1.3", @@ -5811,6 +6229,90 @@ }, "time": "2025-02-17T03:34:21+00:00" }, + { + "name": "workerman/redis", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/walkor/redis.git", + "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/redis/zipball/82b398c5bdd5d683a3458aaf645b159b7b39b84e", + "reference": "82b398c5bdd5d683a3458aaf645b159b7b39b84e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7", + "workerman/workerman": "^4.1.0||^5.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\Redis\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "homepage": "http://www.workerman.net", + "support": { + "issues": "https://github.com/walkor/redis/issues", + "source": "https://github.com/walkor/redis/tree/v2.0.4" + }, + "time": "2025-04-05T07:45:12+00:00" + }, + { + "name": "workerman/redis-queue", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/walkor/redis-queue.git", + "reference": "75dbf7ed2ea228c45dc0df82c0fea35879b715d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/redis-queue/zipball/75dbf7ed2ea228c45dc0df82c0fea35879b715d0", + "reference": "75dbf7ed2ea228c45dc0df82c0fea35879b715d0", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0", + "workerman/redis": "^1.0||^2.0", + "workerman/workerman": ">=4.0.20" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\RedisQueue\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Message queue system written in PHP based on workerman and backed by Redis.", + "homepage": "http://www.workerman.net", + "support": { + "issues": "https://github.com/walkor/redis-queue/issues", + "source": "https://github.com/walkor/redis-queue/tree/v1.2.1" + }, + "time": "2025-01-02T09:21:45+00:00" + }, { "name": "workerman/webman-framework", "version": "v2.1.2", diff --git a/config/constants/Constants.php b/config/constants/Constants.php new file mode 100644 index 0000000..1e77a2b --- /dev/null +++ b/config/constants/Constants.php @@ -0,0 +1,37 @@ + 1, // 待受理 + 'PROCESS' => 2, //处理中 + 'CONFIRM' => 3, // 待确认 + 'FINISH' => 4, // 已完成 + 'REJECTED' => 5, // 已驳回 + ]; + + public const TICKET_STATUS_ZH = [ + self::TICKET_STATUS['PENDING'] => '待受理', + self::TICKET_STATUS['PROCESS'] => '处理中', + self::TICKET_STATUS['CONFIRM'] => '待确认', + self::TICKET_STATUS['FINISH'] => '已完成', + self::TICKET_STATUS['REJECTED'] => '关闭', + ]; + public const TICKET_STATUS_EN = [ + self::TICKET_STATUS['PENDING'] => 'Pending acceptance', + self::TICKET_STATUS['PROCESS'] => 'Processing', + self::TICKET_STATUS['CONFIRM'] => 'To be confirmed', + self::TICKET_STATUS['FINISH'] => 'Completed', + self::TICKET_STATUS['REJECTED'] => 'close', + ]; + public const TICKET_STATUS_RU = [ + self::TICKET_STATUS['PENDING'] => 'Подлежит рассмотрению', + self::TICKET_STATUS['PROCESS'] => 'В процессе обработки', + self::TICKET_STATUS['CONFIRM'] => 'Подлежит подтверждению', + self::TICKET_STATUS['FINISH'] => 'Завершено', + self::TICKET_STATUS['REJECTED'] => 'Закрыть', + ]; + +} \ No newline at end of file diff --git a/config/plugin/shopwwi/filesystem/app.php b/config/plugin/shopwwi/filesystem/app.php new file mode 100644 index 0000000..f1c74b7 --- /dev/null +++ b/config/plugin/shopwwi/filesystem/app.php @@ -0,0 +1,116 @@ + true, + 'default' => 'local', + 'max_size' => 1024 * 1024 * 10, //单个文件大小10M + 'ext_yes' => [], //允许上传文件类型 为空则为允许所有 + 'ext_no' => [], // 不允许上传文件类型 为空则不限制 + 'storage' => [ + 'public' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\LocalAdapterFactory::class, + 'root' => public_path(), + 'url' => '//127.0.0.1:8787' // 静态文件访问域名 + ], + 'local' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\LocalAdapterFactory::class, + 'root' => runtime_path(), + 'url' => '//127.0.0.1:8787' // 静态文件访问域名 + ], + 'ftp' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\FtpAdapterFactory::class, + 'host' => 'ftp.example.com', + 'username' => 'username', + 'password' => 'password', + 'url' => '' // 静态文件访问域名 + // 'port' => 21, + // 'root' => '/path/to/root', + // 'passive' => true, + // 'ssl' => true, + // 'timeout' => 30, + // 'ignorePassiveAddress' => false, + // 'timestampsOnUnixListingsEnabled' => true, + ], + 'memory' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\MemoryAdapterFactory::class, + ], + 's3' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\S3AdapterFactory::class, + 'credentials' => [ + 'key' => 'S3_KEY', + 'secret' => 'S3_SECRET', + ], + 'region' => 'S3_REGION', + 'version' => 'latest', + 'bucket_endpoint' => false, + 'use_path_style_endpoint' => false, + 'endpoint' => 'S3_ENDPOINT', + 'bucket_name' => 'S3_BUCKET', + 'url' => '' // 静态文件访问域名 + ], + 'minio' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\S3AdapterFactory::class, + 'credentials' => [ + 'key' => 'S3_KEY', + 'secret' => 'S3_SECRET', + ], + 'region' => 'S3_REGION', + 'version' => 'latest', + 'bucket_endpoint' => false, + 'use_path_style_endpoint' => true, + 'endpoint' => 'S3_ENDPOINT', + 'bucket_name' => 'S3_BUCKET', + 'url' => '' // 静态文件访问域名 + ], + 'oss' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\AliyunOssAdapterFactory::class, + 'accessId' => getenv('OSS_ACCESSID'), + 'accessSecret' => getenv('OSS_ACCESSSECRET'), + 'bucket' => getenv('OSS_BUCKET'), + 'endpoint' => getenv('OSS_ENDPOINT'), + 'url' => getenv('OSS_STATIC_URL'), // 静态文件访问域名 + // 'timeout' => 3600, + // 'connectTimeout' => 10, + // 'isCName' => false, + // 'token' => null, + // 'proxy' => null, + ], + 'qiniu' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\QiniuAdapterFactory::class, + 'accessKey' => 'QINIU_ACCESS_KEY', + 'secretKey' => 'QINIU_SECRET_KEY', + 'bucket' => 'QINIU_BUCKET', + 'domain' => 'QINBIU_DOMAIN', + 'url' => '' // 静态文件访问域名 + ], + 'cos' => [ + 'driver' => \Shopwwi\WebmanFilesystem\Adapter\CosAdapterFactory::class, + 'region' => 'COS_REGION', + 'app_id' => 'COS_APPID', + 'secret_id' => 'COS_SECRET_ID', + 'secret_key' => 'COS_SECRET_KEY', + // 可选,如果 bucket 为私有访问请打开此项 + // 'signed_url' => false, + 'bucket' => 'COS_BUCKET', + 'read_from_cdn' => false, + 'url' => '' // 静态文件访问域名 + // 'timeout' => 60, + // 'connect_timeout' => 60, + // 'cdn' => '', + // 'scheme' => 'https', + ], + ], +]; \ No newline at end of file diff --git a/config/plugin/webman/redis-queue/app.php b/config/plugin/webman/redis-queue/app.php new file mode 100644 index 0000000..8f9c426 --- /dev/null +++ b/config/plugin/webman/redis-queue/app.php @@ -0,0 +1,4 @@ + true, +]; \ No newline at end of file diff --git a/config/plugin/webman/redis-queue/command.php b/config/plugin/webman/redis-queue/command.php new file mode 100644 index 0000000..8bfe2a1 --- /dev/null +++ b/config/plugin/webman/redis-queue/command.php @@ -0,0 +1,7 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'default' => [ + 'handlers' => [ + [ + 'class' => Monolog\Handler\RotatingFileHandler::class, + 'constructor' => [ + runtime_path() . '/logs/redis-queue/queue.log', + 7, //$maxFiles + Monolog\Logger::DEBUG, + ], + 'formatter' => [ + 'class' => Monolog\Formatter\LineFormatter::class, + 'constructor' => [null, 'Y-m-d H:i:s', true], + ], + ] + ], + ] +]; diff --git a/config/plugin/webman/redis-queue/process.php b/config/plugin/webman/redis-queue/process.php new file mode 100644 index 0000000..9fac733 --- /dev/null +++ b/config/plugin/webman/redis-queue/process.php @@ -0,0 +1,12 @@ + [ + 'handler' => Webman\RedisQueue\Process\Consumer::class, + 'count' => getenv('REDIS_QUEUE_PROCESS_COUNT'), // 可以设置多进程同时消费 + 'constructor' => [ + // 消费者类目录 + // 'consumer_dir' => app_path() . '/queue/redis' + 'consumer_dir' => app_path() . getenv('REDIS_QUEUE_CONSUMER_DIR') + ] + ] +]; \ No newline at end of file diff --git a/config/plugin/webman/redis-queue/redis.php b/config/plugin/webman/redis-queue/redis.php new file mode 100644 index 0000000..420914d --- /dev/null +++ b/config/plugin/webman/redis-queue/redis.php @@ -0,0 +1,22 @@ + [ +// 'host' => 'redis://127.0.0.1:6379', + 'host' => getenv('REDIS_QUEUE_HOST'), + 'options' => [ + 'auth' => getenv('REDIS_QUEUE_AUTH'), // 密码,字符串类型,可选参数 + 'db' => getenv('REDIS_QUEUE_DB'), // 数据库 + 'prefix' => getenv('REDIS_QUEUE_PREFIX'), // key 前缀 + 'max_attempts' => getenv('REDIS_QUEUE_MAX_ATTEMPTS'), // 消费失败后,重试次数 + 'retry_seconds' => getenv('REDIS_QUEUE_RETRY_SECONDS'), // 重试间隔,单位秒 + ], + // Connection pool, supports only Swoole or Swow drivers. + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ] + ], +]; diff --git a/config/route.php b/config/route.php index 3b642cf..db2c831 100644 --- a/config/route.php +++ b/config/route.php @@ -37,4 +37,31 @@ Route::group('/common', function () { // 调试 Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); }); -}); \ No newline at end of file +}); + + +Route::group('/ticket', function () { + //工单 + Route::any('/ticket/add', [app\ticket\controller\TicketController::class, 'add']); + Route::any('/ticket/edit', [app\ticket\controller\TicketController::class, 'edit']); + Route::any('/ticket/list', [app\ticket\controller\TicketController::class, 'index']); + Route::any('/ticket/status', [app\ticket\controller\TicketController::class, 'status']); + Route::any('/ticket/getStatus', [app\ticket\controller\TicketController::class, 'getStatus']); + + Route::any('/upload', [app\common\controller\UploadController::class, 'uploadFile']); + Route::any('/upload/image', [app\common\controller\UploadController::class, 'uploadImage']); + + //工单沟通记录 + Route::any('/conversation/add', [app\ticket\controller\ConversationController::class, 'add']); + Route::any('/conversation/edit', [app\ticket\controller\ConversationController::class, 'edit']); + Route::any('/conversation/list', [app\ticket\controller\ConversationController::class, 'index']); + + //工单分类 + Route::any('/categorie/add', [app\ticket\controller\CategorieController::class, 'add']); + Route::any('/categorie/edit', [app\ticket\controller\CategorieController::class, 'edit']); + Route::any('/categorie/list', [app\ticket\controller\CategorieController::class, 'index']); + Route::any('/categorie/getAll', [app\ticket\controller\CategorieController::class, 'getAll']); + Route::any('/categorie/delete', [app\ticket\controller\CategorieController::class, 'delete']); + +}); + diff --git a/resource/translations/ar/messages.php b/resource/translations/ar/messages.php new file mode 100644 index 0000000..a701f87 --- /dev/null +++ b/resource/translations/ar/messages.php @@ -0,0 +1,5 @@ + 'الدولة لا يمكن تعديلها عكسيا', +]; \ No newline at end of file diff --git a/resource/translations/en/messages.php b/resource/translations/en/messages.php new file mode 100644 index 0000000..c676eb2 --- /dev/null +++ b/resource/translations/en/messages.php @@ -0,0 +1,5 @@ + 'The status cannot be reversed for modification', +]; \ No newline at end of file diff --git a/resource/translations/ru/messages.php b/resource/translations/ru/messages.php new file mode 100644 index 0000000..96ab3a4 --- /dev/null +++ b/resource/translations/ru/messages.php @@ -0,0 +1,5 @@ + 'Статус не может быть изменен в обратном направлении', +]; \ No newline at end of file diff --git a/resource/translations/zh/messages.php b/resource/translations/zh/messages.php new file mode 100644 index 0000000..229d7ee --- /dev/null +++ b/resource/translations/zh/messages.php @@ -0,0 +1,5 @@ + '状态不能逆向修改', +]; \ No newline at end of file From f6323f251396807e4b5973add11f0816658a7e25 Mon Sep 17 00:00:00 2001 From: mzl Date: Wed, 9 Jul 2025 19:10:31 +0800 Subject: [PATCH 05/12] Merge branch 'develop' into feature/mzl_dev # Conflicts: # app/functions.php # config/middleware.php From c325794bc1eebf21be8fda34fa38dfb8b2068ab2 Mon Sep 17 00:00:00 2001 From: "HESU\\HESU" <1696136552@qq.com> Date: Thu, 10 Jul 2025 10:08:30 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E8=8E=B7=E5=8F=96userid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ticket/controller/ConversationController.php | 4 ++-- app/ticket/controller/TicketController.php | 6 +++--- config/middleware.php | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/ticket/controller/ConversationController.php b/app/ticket/controller/ConversationController.php index 0e1b7ec..86d4fd7 100644 --- a/app/ticket/controller/ConversationController.php +++ b/app/ticket/controller/ConversationController.php @@ -23,7 +23,7 @@ class ConversationController extends BaseController public function add(Request $request) { try { - $request->setPost('user_id', 1); + $request->setPost('user_id', $request->user->uid); return (new ConversationService())->add($request->all()) ? $this->success() : $this->error(); @@ -34,7 +34,7 @@ class ConversationController extends BaseController public function edit(Request $request) { try { - $request->setPost('user_id', 1); + $request->setPost('user_id', $request->user->uid); $result = (new ConversationService())->edit($request->all()); return $result ? $this->success($result) diff --git a/app/ticket/controller/TicketController.php b/app/ticket/controller/TicketController.php index 782dc74..95dc539 100644 --- a/app/ticket/controller/TicketController.php +++ b/app/ticket/controller/TicketController.php @@ -20,7 +20,7 @@ class TicketController extends BaseController public function add(Request $request) { try { - $request->setPost('user_id', 1); + $request->setPost('user_id', $request->user->uid); return (new TicketService())->add($request->all()) ? $this->success() @@ -33,7 +33,7 @@ class TicketController extends BaseController public function edit(Request $request) { try { - $request->setPost('user_id', 1); + $request->setPost('user_id', $request->user->uid); $result = (new TicketService())->edit($request->all()); return $result ? $this->success($result) @@ -45,7 +45,7 @@ class TicketController extends BaseController public function status(Request $request) { try { - $request->setPost('user_id', 1); + $request->setPost('user_id', $request->user->uid); $result = (new TicketService())->status($request->all()); return $result ? $this->success($result) diff --git a/config/middleware.php b/config/middleware.php index cf0335a..ae0ad6e 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -16,6 +16,7 @@ return [ '' => [ \app\middleware\AccessControl::class, - \app\middleware\BasicData::class + \app\middleware\BasicData::class, + \app\middleware\AuthCheck::class ] ]; \ No newline at end of file From 43d673ad9cce20a33d4ec7ef51f43fc67a56d114 Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 10 Jul 2025 14:06:01 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/AuthController_bak.php | 83 ------------ app/admin/controller/RoleController.php | 90 +++++++++++++ app/common/controller/AuthController.php | 20 +-- app/common/controller/BaseController.php | 5 +- app/common/controller/UserController.php | 25 ++++ app/common/logic/PermLogic.php | 78 +++++++++++ app/common/logic/RoleLogic.php | 93 ++++++++++++++ app/common/logic/UserLogic.php | 27 ++++ app/common/model/Role.php | 2 + app/common/service/PermService.php | 83 +++--------- app/common/service/PermService_bak.php | 109 ++++++++++++++++ app/common/service/RoleService.php | 55 ++++++++ app/common/service/UserService.php | 38 ++++-- app/common/traits/DataTransformer.php | 135 ++++++++++++++++++++ app/common/validate/RoleValidate.php | 31 +++++ app/controller/IndexController.php | 2 + app/functions.php | 8 ++ app/middleware/AuthCheck.php | 2 +- config/middleware.php | 3 +- config/route.php | 18 ++- 20 files changed, 738 insertions(+), 169 deletions(-) delete mode 100644 app/admin/controller/AuthController_bak.php create mode 100644 app/admin/controller/RoleController.php create mode 100644 app/common/controller/UserController.php create mode 100644 app/common/logic/PermLogic.php create mode 100644 app/common/logic/RoleLogic.php create mode 100644 app/common/logic/UserLogic.php create mode 100644 app/common/service/PermService_bak.php create mode 100644 app/common/service/RoleService.php create mode 100644 app/common/traits/DataTransformer.php create mode 100644 app/common/validate/RoleValidate.php diff --git a/app/admin/controller/AuthController_bak.php b/app/admin/controller/AuthController_bak.php deleted file mode 100644 index f6feeb4..0000000 --- a/app/admin/controller/AuthController_bak.php +++ /dev/null @@ -1,83 +0,0 @@ -authServer = $auth; - $this->resourceServer = $res; - } - - // 浏览器授权页 - public function authorize(ServerRequestInterface $request) - { - // 如果未登录,跳转登录页(自行实现) - // 登录后,确认授权 -> issue auth code -> 重定向回 client - - // …未登录重定向到登录页,登录后调用: - // $authRequest = $this->authServer->validateAuthorizationRequest($request); - // …用户同意授权… - // return $this->authServer->completeAuthorizationRequest($authRequest, new Psr7Response()); - } - - // 授权码换 Token - public function token(ServerRequestInterface $request) - { - return $this->authServer->respondToAccessTokenRequest($request, new Psr7Response()); - } - - // 获取用户信息 - public function userinfo(ServerRequestInterface $request) - { - $psr = $this->resourceServer->validateAuthenticatedRequest($request); - $userId = $psr->getAttribute('oauth_user_id'); - // 根据 $userId 查询用户表返回 - // return json(['id' => $userId, 'username' => 'demo']); - - // 用 Webman 的 support\Response 返回 JSON 更方便 - return json([ - 'id' => $userId, - 'username' => User::find($userId)->username, - ]); - } - - - public function info(Request $request) - { - $userId = $request->session()->get('user_id'); - if (!$userId) { - return json(['code'=>401,'msg'=>'未登录']); - } - - $user = User::with('roles')->find($userId); - - return json([ - 'code' => 0, - 'data' => [ - 'user' => [ - 'id' => $user->id, - 'username' => $user->username, - 'roles' => $user->roles->pluck('name')->all(), - ], - 'menus' => AuthService::getMenuTree($userId), - 'permissions' => AuthService::getPermissionCodes($userId), - ], - 'msg' => 'ok', - ]); - } -} diff --git a/app/admin/controller/RoleController.php b/app/admin/controller/RoleController.php new file mode 100644 index 0000000..232b38e --- /dev/null +++ b/app/admin/controller/RoleController.php @@ -0,0 +1,90 @@ +all(); + $result = (new RoleService())->getRoleList($params); + return $this->success($this->transformData($result), '角色列表获取成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage(), 1, $e->getCode()); + } + } + + /** + * 获取角色详情 + */ + public function detail(Request $request) + { + try { + $roleId = $request->get('role_id'); + if (!$roleId) { + return $this->error('缺少角色ID', 422); + } + $result = (new RoleService())->getRoleDetail($roleId); + return $this->success($result, '角色详情获取成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage(), 1, $e->getCode()); + } + } + + /** + * 增加角色 + */ + public function create(Request $request) + { + try { + $data = $this->validate(new RoleValidate(), $request->post(), 'role_create'); + $result = (new RoleService())->createRole($data); + return $this->success($result, '角色创建成功'); + } catch (\think\exception\ValidateException $e) { + return $this->error($e->getError(), 422); + } catch (\Throwable $e) { + return $this->error($e->getMessage(), 1, $e->getCode()); + } + } + + /** + * 修改角色 + */ + public function update(Request $request) + { + try { + $data = $this->validate(new RoleValidate(), $request->post(), 'role_update'); + $result = (new RoleService())->updateRole($data); + return $this->success($result, '角色更新成功'); + } catch (\think\exception\ValidateException $e) { + return $this->error($e->getError(), 422); + } catch (\Throwable $e) { + return $this->error($e->getMessage(), 1, $e->getCode()); + } + } + + /** + * 删除角色 + */ + public function delete(Request $request) + { + try { + $roleId = $request->get('role_id'); + if (!$roleId) { + return $this->error('缺少角色ID', 422); + } + (new RoleService())->deleteRole($roleId); + return $this->success([], '角色删除成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage(), 1, $e->getCode()); + } + } +} diff --git a/app/common/controller/AuthController.php b/app/common/controller/AuthController.php index 4129ee0..1375f84 100644 --- a/app/common/controller/AuthController.php +++ b/app/common/controller/AuthController.php @@ -14,17 +14,19 @@ class AuthController extends BaseController */ protected $noNeedHeader = ['feishuAuth']; + protected $noNeedLogin = ['feishuScope', 'login', 'register']; + // 调试 - public function feishuAuth(Request $request) - { - try { - $code = $request->all(); - return $this->success($code); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } - } + // public function feishuAuth(Request $request) + // { + // try { + // $code = $request->all(); + // return $this->success($code); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage()); + // } + // } public function feishuScope(Request $request) { diff --git a/app/common/controller/BaseController.php b/app/common/controller/BaseController.php index 0fefb82..2dbf636 100644 --- a/app/common/controller/BaseController.php +++ b/app/common/controller/BaseController.php @@ -4,12 +4,13 @@ namespace app\common\controller; use support\exception\BusinessException; use app\common\traits\ResponseTrait; +use app\common\traits\DataTransformer; use think\Validate; use support\Request; -abstract class BaseController +class BaseController { - use ResponseTrait; + use ResponseTrait, DataTransformer; protected mixed $validator = null; diff --git a/app/common/controller/UserController.php b/app/common/controller/UserController.php new file mode 100644 index 0000000..efab4a1 --- /dev/null +++ b/app/common/controller/UserController.php @@ -0,0 +1,25 @@ +user->uid; + $result = (new UserService())->getUserInfo($uid); + return $this->success($result, '获取用户信息'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/app/common/logic/PermLogic.php b/app/common/logic/PermLogic.php new file mode 100644 index 0000000..d8d917c --- /dev/null +++ b/app/common/logic/PermLogic.php @@ -0,0 +1,78 @@ +find($userId); + if (!$user) { + return collect(); + } + + $all = collect(); + foreach ($user->roles as $role) { + $all = $all->merge($role->resources); + } + $all = $all->merge($user->resources); + return $all->unique('id')->values(); + } + + /** + * 构建目录/菜单树,节点包含权限与打开方式 + */ + public function buildTree($resources, int $parentId = 0): array + { + $tree = []; + foreach ($resources as $node) { + if ($node->parent_id !== $parentId) continue; + + // 子节点 + $children = $this->buildTree($resources, $node->id); + + // 本级权限列表 + $authList = $resources + ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) + ->map(fn($r) => [ + 'id' => $r->id, + 'title' => $r->name, + 'auth_mark' => $r->code, + ])->values()->all(); + + $tree[] = [ + 'id' => $node->id, + 'name' => $node->name, + 'path' => $node->url, + 'meta' => [ + 'title' => $node->name, + 'icon' => $node->icon, + 'keepAlive' => true, + 'authList' => $authList, + 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' + ], + 'children' => $children, + ]; + } + return $tree; + } + + /** + * 获取用户所有权限code + */ + public function getPermissionCodes(int $userId): array + { + $resources = $this->getUserResources($userId); + return $resources + ->filter(fn($res) => $res->type === 'permission') + ->pluck('code') + ->unique() + ->values() + ->all(); + } +} \ No newline at end of file diff --git a/app/common/logic/RoleLogic.php b/app/common/logic/RoleLogic.php new file mode 100644 index 0000000..5cb3ae7 --- /dev/null +++ b/app/common/logic/RoleLogic.php @@ -0,0 +1,93 @@ +where('system_id', $params['system_id']); + } + if (isset($params['status'])) { + $query->where('status', $params['status']); + } + + // 添加分页 + $roles = $query->paginate($params['per_page'] ?? 15); + + return $roles; + } + + /** + * 获取角色详情 + */ + public function getRoleDetail(int $roleId) + { + $role = Role::find($roleId); + if (!$role) { + throw new BusinessException('角色不存在', 404); + } + + return $role; + } + + /** + * 创建角色 + */ + public function createRole(array $data) + { + $role = new Role(); + $role->fill($data); // 根据字段填充数据 + try { + $role->save(); + } catch (\Throwable $e) { + Log::error('创建角色失败:'.$e->getMessage()); + throw new BusinessException('创建角色失败', 500); + } + + return $role; + } + + /** + * 更新角色 + */ + public function updateRole(array $data) + { + $role = Role::find($data['id']); + if (!$role) { + throw new BusinessException('角色不存在', 404); + } + + $role->update($data); // 更新字段 + return $role; + } + + /** + * 删除角色 + */ + public function deleteRole(int $roleId) + { + $role = Role::find($roleId); + if (!$role) { + throw new BusinessException('角色不存在', 404); + } + + try { + $role->delete(); + } catch (\Throwable $e) { + Log::error('删除角色失败:'.$e->getMessage()); + throw new BusinessException('删除角色失败', 500); + } + } +} diff --git a/app/common/logic/UserLogic.php b/app/common/logic/UserLogic.php new file mode 100644 index 0000000..1d6dc93 --- /dev/null +++ b/app/common/logic/UserLogic.php @@ -0,0 +1,27 @@ +status != 1) { + throw new BusinessException('账号已禁用', 403); + } + + return $user; + } +} \ No newline at end of file diff --git a/app/common/model/Role.php b/app/common/model/Role.php index 3f51b37..ea84200 100644 --- a/app/common/model/Role.php +++ b/app/common/model/Role.php @@ -41,6 +41,8 @@ class Role extends Model * @var bool */ public $timestamps = true; + + protected $dateFormat = 'U'; // 角色下所有用户 public function users() diff --git a/app/common/service/PermService.php b/app/common/service/PermService.php index 076a4bd..a629b23 100644 --- a/app/common/service/PermService.php +++ b/app/common/service/PermService.php @@ -2,11 +2,17 @@ namespace app\common\service; -use app\common\model\User; +use app\common\logic\PermLogic; use support\Cache; class PermService { + protected $logic; + + public function __construct() + { + $this->logic = (new PermLogic()); + } // 用户资源缓存key protected static function cacheKey(int $userId): string @@ -17,7 +23,7 @@ class PermService /** * 获取用户所有资源(合并角色+直接分配),结果缓存 */ - public static function getUserResources(int $userId) + public function getUserResources(int $userId) { $cacheKey = self::cacheKey($userId); $cached = Cache::get($cacheKey); @@ -25,84 +31,37 @@ class PermService return $cached; } - $user = User::with(['roles.resources', 'resources'])->find($userId); - if (!$user) { - return collect(); - } + // 调用 Logic 层获取资源 + $resources = $this->logic->getUserResources($userId); + + // 缓存结果 + Cache::set($cacheKey, $resources, 3600); - $all = collect(); - foreach ($user->roles as $role) { - $all = $all->merge($role->resources); - } - $all = $all->merge($user->resources); - $unique = $all->unique('id')->values(); - - Cache::set($cacheKey, $unique, 3600); - return $unique; + return $resources; } /** * 构建目录/菜单树,节点包含权限与打开方式 */ - public static function getMenuTree(int $userId): array + public function getMenuTree(int $userId): array { - $resources = self::getUserResources($userId); - $menus = $resources - ->filter(fn($r) => in_array($r->type, ['directory','menu'])) - ->values(); - return self::buildTree($menus, $resources); - } - - protected static function buildTree($nodes, $allResources, int $parentId = 0): array - { - $tree = []; - foreach ($nodes as $node) { - if ($node->parent_id !== $parentId) continue; - - $children = self::buildTree($nodes, $allResources, $node->id); - - $authList = $allResources - ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) - ->map(fn($r) => [ - 'id' => $r->id, - 'title' => $r->name, - 'auth_mark' => $r->code, - ])->values()->all(); - - $tree[] = [ - 'id' => $node->id, - 'name' => $node->name, - 'path' => $node->url, - 'meta' => [ - 'title' => $node->name, - 'icon' => $node->icon, - 'keepAlive' => true, - 'authList' => $authList, - 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' - ], - 'children' => $children, - ]; - } - return $tree; + // 调用 Logic 层处理菜单构建 + $resources = $this->getUserResources($userId); + return $this->logic->buildTree($resources); } /** * 获取用户所有权限code */ - public static function getPermissionCodes(int $userId): array + public function getPermissionCodes(int $userId): array { - return self::getUserResources($userId) - ->filter(fn($res) => $res->type === 'permission') - ->pluck('code') - ->unique() - ->values() - ->all(); + return $this->logic->getPermissionCodes($userId); } /** * 清除资源相关缓存 */ - public static function clearCache(int $userId): void + public function clearCache(int $userId): void { Cache::delete(self::cacheKey($userId)); } diff --git a/app/common/service/PermService_bak.php b/app/common/service/PermService_bak.php new file mode 100644 index 0000000..a2dcc27 --- /dev/null +++ b/app/common/service/PermService_bak.php @@ -0,0 +1,109 @@ +find($userId); + if (!$user) { + return collect(); + } + + $all = collect(); + foreach ($user->roles as $role) { + $all = $all->merge($role->resources); + } + $all = $all->merge($user->resources); + $unique = $all->unique('id')->values(); + + Cache::set($cacheKey, $unique, 3600); + return $unique; + } + + /** + * 构建目录/菜单树,节点包含权限与打开方式 + */ + public static function getMenuTree(int $userId): array + { + $resources = self::getUserResources($userId); + $menus = $resources + ->filter(fn($r) => in_array($r->type, ['directory','menu'])) + ->values(); + return self::buildTree($menus, $resources); + } + + protected static function buildTree($nodes, $allResources, int $parentId = 0): array + { + $tree = []; + foreach ($nodes as $node) { + if ($node->parent_id !== $parentId) continue; + + $children = self::buildTree($nodes, $allResources, $node->id); + + $authList = $allResources + ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) + ->map(fn($r) => [ + 'id' => $r->id, + 'title' => $r->name, + 'auth_mark' => $r->code, + ])->values()->all(); + + $tree[] = [ + 'id' => $node->id, + 'name' => $node->name, + 'path' => $node->url, + 'meta' => [ + 'title' => $node->name, + 'icon' => $node->icon, + 'keepAlive' => true, + 'authList' => $authList, + 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' + ], + 'children' => $children, + ]; + } + return $tree; + } + + /** + * 获取用户所有权限code + */ + public static function getPermissionCodes(int $userId): array + { + return self::getUserResources($userId) + ->filter(fn($res) => $res->type === 'permission') + ->pluck('code') + ->unique() + ->values() + ->all(); + } + + /** + * 清除资源相关缓存 + */ + public static function clearCache(int $userId): void + { + Cache::delete(self::cacheKey($userId)); + } +} \ No newline at end of file diff --git a/app/common/service/RoleService.php b/app/common/service/RoleService.php new file mode 100644 index 0000000..24c14cf --- /dev/null +++ b/app/common/service/RoleService.php @@ -0,0 +1,55 @@ +roleLogic = (new RoleLogic()); + } + + /** + * 获取角色列表 + */ + public function getRoleList(array $params) + { + return $this->roleLogic->getRoleList($params); + } + + /** + * 获取角色详情 + */ + public function getRoleDetail(int $roleId) + { + return $this->roleLogic->getRoleDetail($roleId); + } + + /** + * 创建角色 + */ + public function createRole(array $data) + { + return $this->roleLogic->createRole($data); + } + + /** + * 更新角色 + */ + public function updateRole(array $data) + { + return $this->roleLogic->updateRole($data); + } + + /** + * 删除角色 + */ + public function deleteRole(int $roleId) + { + return $this->roleLogic->deleteRole($roleId); + } +} diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php index 99b80d5..98375bc 100644 --- a/app/common/service/UserService.php +++ b/app/common/service/UserService.php @@ -2,19 +2,39 @@ namespace app\common\service; -use app\model\User; +use app\common\logic\UserLogic; class UserService { - public static function getInfo($userId) + + protected $logic; + + public function __construct() { - $user = User::with('roles')->find($userId); - if (!$user) return null; + $this->logic = (new UserLogic()); + } + + /** + * 获取用户信息(包括菜单和权限) + */ + public function getUserInfo(int $uid) + { + $user = $this->logic->userInfo($uid); + $permService = new PermService(); + $menuTree = $permService->getMenuTree($uid); + $permissionCodes = $permService->getPermissionCodes($uid); return [ - 'id' => $user->id, - 'username' => $user->username, - 'roles' => $user->roles->pluck('name')->toArray(), - // 可扩展其他字段 + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'avatar' => $user->avatar, + 'phone' => $user->phone, + 'email' => $user->email, + 'status' => $user->status, + 'roles' => $user->roles->pluck('name') + ], + 'menu' => $menuTree, + 'permissions' => $permissionCodes ]; } -} +} \ No newline at end of file diff --git a/app/common/traits/DataTransformer.php b/app/common/traits/DataTransformer.php new file mode 100644 index 0000000..42faf81 --- /dev/null +++ b/app/common/traits/DataTransformer.php @@ -0,0 +1,135 @@ + $field; + + if ($rawData instanceof LengthAwarePaginator) { + $formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter); + } elseif ($rawData instanceof Model) { + $formatted = $this->formatModel($rawData, $fieldFormatter); + } elseif ($rawData instanceof Collection) { + $formatted = $this->formatCollection($rawData, $fieldFormatter); + } elseif (is_array($rawData)) { + $formatted = $this->formatArray($rawData, $fieldFormatter); + } else { + $formatted = (array) $rawData; + } + + if (is_callable($postProcessor)) { + $formatted = $postProcessor($formatted); + } + + return $formatted; + } catch (\Throwable $e) { + return [ + 'error' => true, + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + ]; + } + } + + /** + * 格式化分页数据:将每条记录也进行 transformData 递归处理 + * + * @param LengthAwarePaginator $paginator + * @param callable|null $postProcessor + * @param callable $fieldFormatter + * @return array + */ + protected function formatPaginated( + LengthAwarePaginator $paginator, + ?callable $postProcessor, + callable $fieldFormatter + ): array { + $list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) { + return $this->transformData($item, $postProcessor, $fieldFormatter); + })->toArray(); + + return [ + 'records' => $list, + 'total' => $paginator->total(), + 'current' => $paginator->currentPage(), + 'size' => $paginator->perPage() + ]; + } + + /** + * 格式化 Eloquent 模型:递归嵌套关联 + * + * @param Model $model + * @param callable $fieldFormatter + * @return array + */ + protected function formatModel(Model $model, callable $fieldFormatter): array + { + $formatted = []; + + // 主表字段 + foreach ($model->getAttributes() as $key => $value) { + $formatted[$fieldFormatter($key)] = $value; + } + + // 关联数据,递归嵌套 + foreach ($model->getRelations() as $relation => $relationData) { + if ($relationData instanceof Model) { + // 单个关联模型 + $formatted[$relation] = $this->formatModel($relationData, $fieldFormatter); + } elseif ($relationData instanceof Collection) { + // 关联集合 + $formatted[$relation] = $relationData + ->map(fn($item) => $this->transformData($item, null, $fieldFormatter)) + ->toArray(); + } + } + + return $formatted; + } + + /** + * 格式化集合 + * + * @param Collection $collection + * @param callable $fieldFormatter + * @return array + */ + protected function formatCollection(Collection $collection, callable $fieldFormatter): array + { + return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter)) + ->toArray(); + } + + /** + * 格式化普通数组 + * + * @param array $array + * @param callable $fieldFormatter + * @return array + */ + protected function formatArray(array $array, callable $fieldFormatter): array + { + return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array); + } +} \ No newline at end of file diff --git a/app/common/validate/RoleValidate.php b/app/common/validate/RoleValidate.php new file mode 100644 index 0000000..bf51cf9 --- /dev/null +++ b/app/common/validate/RoleValidate.php @@ -0,0 +1,31 @@ + 'require|integer', + 'system_id' => 'require|integer', + 'name' => 'require|max:64', + 'desc' => 'max:128', + 'status' => 'require|in:0,1', + ]; + + protected $message = [ + 'id.require' => '角色ID不能为空', + 'system_id.require' => '系统ID不能为空', + 'name.require' => '角色名称不能为空', + 'name.max' => '角色名称不能超过64个字符', + 'desc.max' => '描述不能超过128个字符', + 'status.require' => '状态不能为空', + 'status.in' => '状态值必须为0或1', + ]; + + protected $scene = [ + 'role_create' => ['system_id', 'name', 'desc', 'status'], + 'role_update' => ['id', 'system_id', 'name', 'desc', 'status'], + ]; +} diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 2ea3432..988928f 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -6,6 +6,8 @@ use support\Request; class IndexController { + + protected $noNeedLogin = ['index']; public function index(Request $request) { diff --git a/app/functions.php b/app/functions.php index 14d5e4a..d0682c9 100644 --- a/app/functions.php +++ b/app/functions.php @@ -61,4 +61,12 @@ function getConfig($filePath = null) return []; } return $config; +} + +/* + * 调试用 + */ +function dpr(\Throwable $e) +{ + var_dump("=== === 报错打印 === ===", "文件:".$e->getFile(), "行数:".$e->getLine(), "详情:".$e->getMessage(), "=== === 报错打印 === ==="); } \ No newline at end of file diff --git a/app/middleware/AuthCheck.php b/app/middleware/AuthCheck.php index 3e6eb8a..c085901 100644 --- a/app/middleware/AuthCheck.php +++ b/app/middleware/AuthCheck.php @@ -19,7 +19,7 @@ class AuthCheck implements MiddlewareInterface return $handler($request); } else { try { - $authHeader = $request->header('Authorization'); + $authHeader = $request->header('Fdesk-Auth-Token'); if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) { $body = [ 'status' => 'fail', diff --git a/config/middleware.php b/config/middleware.php index cf0335a..ae0ad6e 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -16,6 +16,7 @@ return [ '' => [ \app\middleware\AccessControl::class, - \app\middleware\BasicData::class + \app\middleware\BasicData::class, + \app\middleware\AuthCheck::class ] ]; \ No newline at end of file diff --git a/config/route.php b/config/route.php index db2c831..a6c3a4d 100644 --- a/config/route.php +++ b/config/route.php @@ -34,8 +34,22 @@ Route::group('/common', function () { - // 调试 - Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); + // // 调试 + // Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); + }); + + Route::group('/user', function () { + Route::any('/info', [app\common\controller\UserController::class, 'info']); + }); +}); + +Route::group('/admin', function () { + Route::group('/role', function () { + Route::any('/list', [app\admin\controller\RoleController::class, 'list']); + Route::any('/detail', [app\admin\controller\RoleController::class, 'detail']); + Route::any('/create', [app\admin\controller\RoleController::class, 'create']); + Route::any('/update', [app\admin\controller\RoleController::class, 'update']); + Route::any('/delete', [app\admin\controller\RoleController::class, 'delete']); }); }); From 3a72f8ec0af0dad2e1f5bfc5a7c1686ef07e3ab9 Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 10 Jul 2025 14:17:50 +0800 Subject: [PATCH 08/12] Merge branch 'develop' into feature/mzl_dev # Conflicts: # app/common/controller/AuthController.php # app/controller/IndexController.php # app/functions.php # app/middleware/AuthCheck.php # config/middleware.php # config/route.php From 92eff917476ba29009e12f0502c431e7c37ec8c2 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 11 Jul 2025 14:52:16 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9D=83=E9=99=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 1.sql | 3 - app/admin/controller/RoleController.php | 20 +- app/common/controller/AuthController.php | 94 ++++--- app/common/logic/AuthLogic.php | 255 ++++++++++-------- app/common/logic/BaseLogic.php | 86 ++++++ app/common/logic/PermLogic.php | 88 +++++- app/common/model/User.php | 8 +- ...serFeishuApp.php => UserFeishuApp_bak.php} | 2 +- app/common/service/AuthService.php | 11 +- app/common/service/PermService.php | 3 - app/common/validate/RoleValidate.php | 9 +- app/controller/IndexController.php | 2 +- app/functions.php | 89 +++--- app/view/index/qrcode.html | 2 +- composer.json | 2 +- composer.lock | 27 +- config/route.php | 4 +- config/whitelist.php | 2 +- 18 files changed, 463 insertions(+), 244 deletions(-) delete mode 100644 1.sql create mode 100644 app/common/logic/BaseLogic.php rename app/common/model/{UserFeishuApp.php => UserFeishuApp_bak.php} (97%) diff --git a/1.sql b/1.sql deleted file mode 100644 index ea1e67c..0000000 --- a/1.sql +++ /dev/null @@ -1,3 +0,0 @@ -User ← user_roles → Role ← role_resources → Resource (type=menu/button) - ↘︎ - role_permissions → Permission \ No newline at end of file diff --git a/app/admin/controller/RoleController.php b/app/admin/controller/RoleController.php index 232b38e..59ccb54 100644 --- a/app/admin/controller/RoleController.php +++ b/app/admin/controller/RoleController.php @@ -16,9 +16,17 @@ class RoleController extends \app\common\controller\BaseController try { $params = $request->all(); $result = (new RoleService())->getRoleList($params); - return $this->success($this->transformData($result), '角色列表获取成功'); + // 定义后处理器 + $postProcessor = function($formattedData) { + if (isset($formattedData['created_at'])) + $formattedData['created_at'] = date('Y-m-d H:i:s', $formattedData['created_at']); + if (isset($formattedData['updated_at'])) + $formattedData['updated_at'] = date('Y-m-d H:i:s', $formattedData['updated_at']); + return $formattedData; + }; + return $this->success($this->transformData($result, $postProcessor), '角色列表获取成功'); } catch (\Throwable $e) { - return $this->error($e->getMessage(), 1, $e->getCode()); + return $this->error($e->getMessage()); } } @@ -35,7 +43,7 @@ class RoleController extends \app\common\controller\BaseController $result = (new RoleService())->getRoleDetail($roleId); return $this->success($result, '角色详情获取成功'); } catch (\Throwable $e) { - return $this->error($e->getMessage(), 1, $e->getCode()); + return $this->error($e->getMessage()); } } @@ -51,7 +59,7 @@ class RoleController extends \app\common\controller\BaseController } catch (\think\exception\ValidateException $e) { return $this->error($e->getError(), 422); } catch (\Throwable $e) { - return $this->error($e->getMessage(), 1, $e->getCode()); + return $this->error($e->getMessage()); } } @@ -67,7 +75,7 @@ class RoleController extends \app\common\controller\BaseController } catch (\think\exception\ValidateException $e) { return $this->error($e->getError(), 422); } catch (\Throwable $e) { - return $this->error($e->getMessage(), 1, $e->getCode()); + return $this->error($e->getMessage()); } } @@ -84,7 +92,7 @@ class RoleController extends \app\common\controller\BaseController (new RoleService())->deleteRole($roleId); return $this->success([], '角色删除成功'); } catch (\Throwable $e) { - return $this->error($e->getMessage(), 1, $e->getCode()); + return $this->error($e->getMessage()); } } } diff --git a/app/common/controller/AuthController.php b/app/common/controller/AuthController.php index 1375f84..02676e8 100644 --- a/app/common/controller/AuthController.php +++ b/app/common/controller/AuthController.php @@ -14,19 +14,19 @@ class AuthController extends BaseController */ protected $noNeedHeader = ['feishuAuth']; - protected $noNeedLogin = ['feishuScope', 'login', 'register']; + protected $noNeedLogin = ['feishuScope', 'login', 'register', 'feishuAuth']; // 调试 - // public function feishuAuth(Request $request) - // { - // try { - // $code = $request->all(); - // return $this->success($code); - // } catch (\Throwable $e) { - // return $this->error($e->getMessage()); - // } - // } + public function feishuAuth(Request $request) + { + try { + $code = $request->all(); + return $this->success($code); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } public function feishuScope(Request $request) { @@ -60,46 +60,56 @@ class AuthController extends BaseController } } - public function verify(Request $request) - { - try { - $token = $request->post('access_token') ?: $request->header('authorization'); - $systemCode = $request->post('system_code'); - if (!$token || !$systemCode) return $this->error('参数缺失', 422); - $payload = (new AuthService())->verifyToken($token, $systemCode); - return $this->success($payload, '验证成功'); - } catch (\Throwable $e) { - return $this->error($e->getMessage() ?: '无效或过期的token', 401); - } - } - public function refresh(Request $request) { try { - $refresh_token = $request->post('refresh_token'); - $systemCode = $request->post('system_code'); - if (!$refresh_token || !$systemCode) return $this->error('参数缺失', 422); - $result = (new AuthService())->refreshToken($refresh_token, $systemCode); + $data = $this->requestData($request); + if (!$data['refresh_token'] || !$data['system_code']) return $this->error('参数缺失', 422); + $result = (new AuthService())->refreshToken($data['refresh_token'], $data['system_code']); return $this->success($result, '刷新成功'); } catch (\Throwable $e) { return $this->error($e->getMessage(), 401); } } - public function logout(Request $request) - { - $token = $request->post('access_token') ?: $request->header('Authorization'); - $systemCode = $request->post('system_code') ?: $request->header('system_code'); - (new AuthService())->logout($token, $systemCode); - return $this->success([], '已登出'); - } - public function blockToken(Request $request) - { - $token = $request->post('token'); - $systemCode = $request->post('system_code'); - if (!$token || !$systemCode) return $this->error('参数缺失', 422); - (new AuthService())->blockToken($token, $systemCode); - return $this->success([], '已强制停用该token'); - } + // public function perm(Request $request) + // { + // try { + // $data = $this->requestData($request); + // if (!$data['access_token'] || !$data['system_code']) return $this->error('参数缺失', 422); + // $result = (new AuthService())->logout($data['access_token'], $data['system_code']); + // return $this->success($result, '已登出'); + // } catch (\Throwable $e) {} + // } + + // public function verify(Request $request) + // { + // try { + // $token = $request->post('access_token') ?: $request->header('authorization'); + // $systemCode = $request->post('system_code'); + // if (!$token || !$systemCode) return $this->error('参数缺失', 422); + // $payload = (new AuthService())->verifyToken($token, $systemCode); + // return $this->success($payload, '验证成功'); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage() ?: '无效或过期的token', 401); + // } + // } + + // public function logout(Request $request) + // { + // $token = $request->post('access_token') ?: $request->header('Authorization'); + // $systemCode = $request->post('system_code') ?: $request->header('system_code'); + // (new AuthService())->logout($token, $systemCode); + // return $this->success([], '已登出'); + // } + + // public function blockToken(Request $request) + // { + // $token = $request->post('token'); + // $systemCode = $request->post('system_code'); + // if (!$token || !$systemCode) return $this->error('参数缺失', 422); + // (new AuthService())->blockToken($token, $systemCode); + // return $this->success([], '已强制停用该token'); + // } } \ No newline at end of file diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index 3c84694..ccdbf16 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -7,7 +7,6 @@ use app\common\model\CorpFeishuApp; use app\common\model\UserTokenLog; use app\common\model\Systems; use app\common\model\User; -use app\common\model\UserFeishuApp; use app\common\model\UserSn; use app\common\utils\Feishu; use app\common\utils\Jwt; @@ -46,87 +45,83 @@ class AuthLogic return $this->generateTokensAndReturn($user, $system, $data); } - /** - * 扫码登录 - */ - public function qrCodeLogin(array $data): array - { - $userId = $this->validateQrCodeToken($data['qr_code_token']); - $user = User::find($userId); - - if (!$user || $user->status != 1) { - throw new BusinessException('用户无效', 403); - } - - $system = $this->validateSystem($data['system_code']); - $this->validateSystemPermission($user, $system); - - return $this->generateTokensAndReturn($user, $system, $data); - } - /** * 注册 */ public function register(array $data) { - // 获取可用用户编号 - $sn = $this->getAvailableUserSn(); - $data['sn'] = $sn->user_sn; + try { + // 获取可用用户编号 + $sn = $this->getAvailableUserSn(); + $data['sn'] = $sn->user_sn; - // 对密码进行哈希处理 - $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + // 对密码进行哈希处理 + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); - // 获取企业ID - $corpInfo = $this->validateCorp($data['corp_abbr']); - $data['corp_id'] = $corpInfo->id; + // 获取企业ID + $corpInfo = $this->validateCorp($data['corp_abbr']); + $data['corp_id'] = $corpInfo->id; - // 处理账户 - $data = $data + normalizeAccount($data['account']); - - // 创建用户记录 - $user = $this->createUser($data); + // 处理账户 + $data = $data + normalizeAccount($data['account']); + + // 创建用户记录 + $user = $this->createUser($data); - // 更新用户编号的用户 ID - $this->updateUserId($user->id, $sn->user_sn); + // 更新用户编号的用户 ID + $this->updateUserId($user->id, $sn->user_sn); - $system = $this->validateSystem($data['system_code']); - return $this->generateTokensAndReturn($user, $system, $data); + $system = $this->validateSystem($data['system_code']); + return $this->generateTokensAndReturn($user, $system, $data); + } catch (\Throwable $e) { + throw $e; + } } - /** ----------- 内部方法 ------------- **/ - /** * 查找或创建飞书用户 */ private function getOrCreateUser(array $feishuUserInfo) { - $userFeishuApp = UserFeishuApp::firstOrCreate([ - 'name' => $feishuUserInfo['en_name'], - 'mobile' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), - 'union_id' => $feishuUserInfo['union_id'], - 'user_id' => $feishuUserInfo['user_id'], - 'open_id' => $feishuUserInfo['open_id'], - 'tenant_key' => $feishuUserInfo['tenant_key'] - ]); - - if ($userFeishuApp->uid) { - $user = User::find($userFeishuApp->uid); - $user->isLogin = true; - return $user; - } else { - $user = User::where('phone', $userFeishuApp->mobile)->first(); - $user->isLogin = true; + try { + $phone = $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''); + $user = User::where('phone', $phone)->where('corp_id', $feishuUserInfo['corp_id'])->first(); + if (!$user) { - $user = $this->createUserByFeishu($feishuUserInfo, $userFeishuApp); + // 获取编号 + $userSn = $this->getAvailableUserSn(); + // 创建用户 + $user = User::create([ + 'name' => $feishuUserInfo['en_name'], + 'avatar' => $feishuUserInfo['avatar_middle'], + 'phone' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), + 'union_id' => $feishuUserInfo['union_id'], + 'user_id' => $feishuUserInfo['user_id'], + 'open_id' => $feishuUserInfo['open_id'], + 'tenant_key' => $feishuUserInfo['tenant_key'], + 'corp_id' => $feishuUserInfo['corp_id'], + 'password' => password_hash($userSn->user_sn, PASSWORD_DEFAULT) + ]); + // 更新使用 + $this->updateUserId($user->id, $userSn->user_sn); + // 更新编号 + if (!$user->update(['sn'=>$userSn->user_sn])) { + throw new BusinessException('编号绑定失败'); + } + // 是否登陆 $user->isLogin = false; - } - $userFeishuApp->uid = $user->id; - $userFeishuApp->corp_id = $feishuUserInfo['corp_id']; - if (!$userFeishuApp->save()) { - throw new BusinessException('绑定用户飞书记录失败'); + } else { + $user->update([ + 'union_id' => $feishuUserInfo['union_id'], + 'user_id' => $feishuUserInfo['user_id'], + 'open_id' => $feishuUserInfo['open_id'], + 'tenant_key' => $feishuUserInfo['tenant_key'] + ]); } return $user; - } + } catch (\Throwable $e) { + throw $e; + } } /** @@ -137,38 +132,6 @@ class AuthLogic return isset($mobile) ? str_replace('+86', '', $mobile) : ''; } - /** - * 创建飞书用户 - */ - private function createUserByFeishu(array $feishuUserInfo, UserFeishuApp $userFeishuApp) - { - // 1. 获取适用的用户编号 - $userSn = UserSn::where('user_id', '=', 0)->inRandomOrder()->first(); - if (!$userSn) { - throw new BusinessException('未找到适用编号'); - } - - // 2. 创建用户 - $user = new User(); - $user->sn = $userSn->user_sn; - $user->name = $feishuUserInfo['en_name']; - $user->avatar = $feishuUserInfo['avatar_middle'] ?? ''; - $user->password = password_hash($userSn->user_sn, PASSWORD_DEFAULT); - $user->phone = $userFeishuApp->mobile; - $user->email = ''; - $user->system_code = $feishuUserInfo['system_code']; - $user->corp_id = $feishuUserInfo['corp_id']; - - if (!$user->save()) { - throw new BusinessException('用户创建失败'); - } - - // 更新使用状态 - $this->updateUserId($user->id, $userSn->user_sn); - - return $user; - } - /** * 生成并记录token,返回包含token的响应数据 */ @@ -295,7 +258,8 @@ class AuthLogic public function feishuAppInfo(string $corpAbbr, string $systemCode) { $corpInfo = $this->validateCorp($corpAbbr); - $fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemCode); + $systemInfo = $this->validateSystem($systemCode); + $fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemInfo->id); return [ 'app_id' => $fsAppInfo->app_id, 'app_secret' => $fsAppInfo->app_secret, @@ -303,7 +267,8 @@ class AuthLogic 'scope' => $fsAppInfo->scopes, 'state' => $fsAppInfo->state, 'abbr' => $corpAbbr, - 'corp_id' => $corpInfo->id + 'corp_id' => $corpInfo->id, + 'system_id' => $systemInfo->id ]; } @@ -349,9 +314,9 @@ class AuthLogic /** * 验证飞书应用信息 */ - private function validateFeishuApp(int $corpId, string $systemCode) + private function validateFeishuApp(int $corpId, int $systemId) { - $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_code', $systemCode)->first(); + $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_id', $systemId)->first(); if (!$feishuAppInfo) { throw new BusinessException('飞书应用不存在', 403); } @@ -371,18 +336,6 @@ class AuthLogic } } - /** - * 验证扫码令牌 - */ - private function validateQrCodeToken(string $qrCodeToken) - { - $userId = Redis::get("qr_code_token:{$qrCodeToken}"); - if (!$userId) { - throw new BusinessException('扫码令牌无效或已过期', 401); - } - return $userId; - } - /** * 获取可用的用户编号 * @@ -463,4 +416,88 @@ class AuthLogic Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration')); return $refreshToken; } + + + /** + * 刷新token + * @param string $refreshToken + * @param string|null $systemCode 可选,指定系统 + * @return array + */ + public function refreshToken(string $refreshToken, ?string $systemCode = null): array + { + if (!$systemCode) throw new BusinessException('缺少system_code', 422); + + // 校验refresh_token + $userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}"); + if (!$userId) { + throw new BusinessException('refresh_token无效或已过期', 401); + } + + $validToken = Redis::get("refresh_token:{$userId}:{$systemCode}"); + if ($validToken !== $refreshToken) { + throw new BusinessException('refresh_token无效或已过期', 401); + } + + $user = User::find($userId); + if (!$user || $user->status != 1) { + throw new BusinessException('用户无效', 403); + } + + $system = $this->validateSystem($systemCode); + $this->validateSystemPermission($user, $system); + + $payload = $this->buildJwtPayload($user, $system); + $token = $this->generateAccessToken($payload); + $refreshToken = $this->generateRefreshToken($user->id, $systemCode); + $this->recordUserToken($user->id, $token, $systemCode); + + return [ + 'access_token' => $token, + 'refresh_token' => $refreshToken, + 'expires_in' => config('jwt.jwt_expiration'), + // 'payload' => $payload, + ]; + } + + /** + * 登出/失效token + */ + public function logout(?string $token, ?string $systemCode = null) + { + if ($token) { + Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); + $payload = null; + try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} + $userId = $payload['uid'] ?? null; + $aud = $payload['aud'] ?? null; + $sc = $systemCode ?? $aud; + if ($userId && $sc) { + Redis::sRem("user_tokens:{$userId}:{$sc}", $token); + } + Redis::del("user_token_meta:{$token}"); + UserTokenLog::where('token', $token)->update(['is_valid'=>0]); + } + } + + /** + * 强制拉黑token + */ + public function blockToken(string $token, ?string $systemCode = null) + { + if (!$token) { + throw new BusinessException('token不能为空', 422); + } + Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration')); + $payload = null; + try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {} + $userId = $payload['uid'] ?? null; + $aud = $payload['aud'] ?? null; + $sc = $systemCode ?? $aud; + if ($userId && $sc) { + Redis::sRem("user_tokens:{$userId}:{$sc}", $token); + } + Redis::del("user_token_meta:{$token}"); + UserTokenLog::where('token', $token)->update(['is_valid'=>0]); + } } \ No newline at end of file diff --git a/app/common/logic/BaseLogic.php b/app/common/logic/BaseLogic.php new file mode 100644 index 0000000..94beff3 --- /dev/null +++ b/app/common/logic/BaseLogic.php @@ -0,0 +1,86 @@ +paginate($params['per_page'], ['*'], 'page', $params['page']); + } + + return $query->get(); + } + + /** + * 获取单个记录 + */ + public function getDetail(int $id, string $modelClass) + { + $record = $modelClass::find($id); + if (!$record) { + throw new BusinessException("记录不存在", 404); + } + + return $record; + } + + /** + * 创建记录 + */ + public function create(array $data, string $modelClass) + { + $model = new $modelClass; + $model->fill($data); // 根据字段填充数据 + try { + $model->save(); + } catch (\Throwable $e) { + throw new BusinessException("创建记录失败: " . $e->getMessage(), 500); + } + + return $model; + } + + /** + * 更新记录 + */ + public function update(array $data, string $modelClass) + { + $record = $modelClass::find($data['id']); + if (!$record) { + throw new BusinessException("记录不存在", 404); + } + + $record->update($data); + return $record; + } + + /** + * 删除记录 + */ + public function delete(int $id, string $modelClass) + { + $record = $modelClass::find($id); + if (!$record) { + throw new BusinessException("记录不存在", 404); + } + + try { + $record->delete(); + } catch (\Throwable $e) { + throw new BusinessException("删除记录失败: " . $e->getMessage(), 500); + } + + return true; + } +} diff --git a/app/common/logic/PermLogic.php b/app/common/logic/PermLogic.php index d8d917c..889470e 100644 --- a/app/common/logic/PermLogic.php +++ b/app/common/logic/PermLogic.php @@ -27,16 +27,82 @@ class PermLogic /** * 构建目录/菜单树,节点包含权限与打开方式 */ - public function buildTree($resources, int $parentId = 0): array + // public function buildTree($resources, int $parentId = 0): array + // { + // $tree = []; + // foreach ($resources as $node) { + // if ($node->parent_id !== $parentId) continue; + + // // 子节点 + // $children = []; + // if ($node->type === 'directory') { + // $children = $this->buildTree($resources, $node->id); + // } + + // // 本级权限列表 + // $authList = $resources + // ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) + // ->map(fn($r) => [ + // 'id' => $r->id, + // 'title' => $r->name, + // 'auth_mark' => $r->code, + // ])->values()->all(); + + // $tree[] = [ + // 'id' => $node->id, + // 'name' => $node->name, + // 'path' => $node->url, + // 'component' => $node->type === 'directory' ? 'Layout' : 'Page', + // 'meta' => [ + // 'title' => $node->name, + // 'icon' => $node->icon, + // 'keepAlive' => true, + // 'authList' => $authList, + // 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' + // ], + // 'children' => $children, + // ]; + // } + // return $tree; + // } + /** + * 构建目录/菜单树,节点包含权限与打开方式,并拼接完整路径到 component 字段 + * @param \Illuminate\Support\Collection $resources + * @param int $parentId + * @param string $parentPath + * @return array + */ + public function buildTree($resources, int $parentId = 0, string $parentPath = ''): array { $tree = []; foreach ($resources as $node) { if ($node->parent_id !== $parentId) continue; - // 子节点 - $children = $this->buildTree($resources, $node->id); + // 拼接完整路径 + $currentPath = $parentPath; + // 去掉结尾斜杠,防止重复 + if ($currentPath && substr($currentPath, -1) === '/') { + $currentPath = rtrim($currentPath, '/'); + } + // 子菜单的 path 不要补全斜杠 + if (!empty($node->url)) { + // 如果当前是根节点或父路径为空,直接用自身 + if (!$currentPath) { + $currentPath = $node->url; + } else { + // 防止 url 以 / 开头导致双斜杠 + $url = ltrim($node->url, '/'); + $currentPath = $currentPath . '/' . $url; + } + } - // 本级权限列表 + // 子节点 + $children = []; + if ($node->type === 'directory') { + $children = $this->buildTree($resources, $node->id, $currentPath); + } + + // 权限 $authList = $resources ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) ->map(fn($r) => [ @@ -46,22 +112,24 @@ class PermLogic ])->values()->all(); $tree[] = [ - 'id' => $node->id, - 'name' => $node->name, - 'path' => $node->url, - 'meta' => [ + 'id' => $node->id, + 'name' => $node->name, + 'path' => $node->url, + 'component' => $currentPath, // 这里就是全路径 + 'meta' => [ 'title' => $node->name, 'icon' => $node->icon, 'keepAlive' => true, 'authList' => $authList, - 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' + 'openType' => $node->open_type, ], - 'children' => $children, + 'children' => $children, ]; } return $tree; } + /** * 获取用户所有权限code */ diff --git a/app/common/model/User.php b/app/common/model/User.php index 23e8b1c..4709b44 100644 --- a/app/common/model/User.php +++ b/app/common/model/User.php @@ -58,8 +58,12 @@ class User extends Model 'phone', 'email', 'status', - 'system_code', - 'corp_id' + 'corp_id', + 'user_type', + 'union_id', + 'user_id', + 'open_id', + 'tenant_key' ]; // 用户所有角色 diff --git a/app/common/model/UserFeishuApp.php b/app/common/model/UserFeishuApp_bak.php similarity index 97% rename from app/common/model/UserFeishuApp.php rename to app/common/model/UserFeishuApp_bak.php index c64e8cd..9aa06c0 100644 --- a/app/common/model/UserFeishuApp.php +++ b/app/common/model/UserFeishuApp_bak.php @@ -20,7 +20,7 @@ use support\Model; * @property integer $created_at 创建时间 * @property integer $updated_at 更新时间 */ -class UserFeishuApp extends Model +class UserFeishuApp_bak extends Model { /** * The connection name for the model. diff --git a/app/common/service/AuthService.php b/app/common/service/AuthService.php index aa8f4eb..6de72a2 100644 --- a/app/common/service/AuthService.php +++ b/app/common/service/AuthService.php @@ -31,7 +31,7 @@ class AuthService return match ($loginType) { 'feishu' => $logic->feishuLogin($data), - 'qrcode' => $logic->qrCodeLogin($data), + // 'qrcode' => $logic->qrCodeLogin($data), default => $logic->uPwdLogin($data), }; } @@ -86,4 +86,13 @@ class AuthService { (new AuthLogic())->blockToken($token, $systemCode); } + + + // public function perms() + // { + // $permService = new PermService(); + // $menuTree = $permService->getMenuTree($uid); + // $permissionCodes = $permService->getPermissionCodes($uid); + // return (new PermService())->getMenuTree($this->userId) + // } } \ No newline at end of file diff --git a/app/common/service/PermService.php b/app/common/service/PermService.php index a629b23..8f8770c 100644 --- a/app/common/service/PermService.php +++ b/app/common/service/PermService.php @@ -30,13 +30,10 @@ class PermService if ($cached !== null) { return $cached; } - // 调用 Logic 层获取资源 $resources = $this->logic->getUserResources($userId); - // 缓存结果 Cache::set($cacheKey, $resources, 3600); - return $resources; } diff --git a/app/common/validate/RoleValidate.php b/app/common/validate/RoleValidate.php index bf51cf9..330fae6 100644 --- a/app/common/validate/RoleValidate.php +++ b/app/common/validate/RoleValidate.php @@ -11,7 +11,8 @@ class RoleValidate extends Validate 'system_id' => 'require|integer', 'name' => 'require|max:64', 'desc' => 'max:128', - 'status' => 'require|in:0,1', + 'sort' => 'integer', + 'status' => 'in:0,1', ]; protected $message = [ @@ -20,12 +21,12 @@ class RoleValidate extends Validate 'name.require' => '角色名称不能为空', 'name.max' => '角色名称不能超过64个字符', 'desc.max' => '描述不能超过128个字符', - 'status.require' => '状态不能为空', + 'sort.integer' => '排序为数字', 'status.in' => '状态值必须为0或1', ]; protected $scene = [ - 'role_create' => ['system_id', 'name', 'desc', 'status'], - 'role_update' => ['id', 'system_id', 'name', 'desc', 'status'], + 'role_create' => ['system_id', 'name', 'desc', 'sort'], + 'role_update' => ['id', 'system_id', 'name', 'desc', 'sort', 'status'], ]; } diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 988928f..f0feb2c 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -7,7 +7,7 @@ use support\Request; class IndexController { - protected $noNeedLogin = ['index']; + protected $noNeedLogin = ['index', 'view']; public function index(Request $request) { diff --git a/app/functions.php b/app/functions.php index d0682c9..2a05e3f 100644 --- a/app/functions.php +++ b/app/functions.php @@ -2,8 +2,6 @@ /** * Here is your custom functions. */ - -use support\exception\BusinessException; /** * 标准化账号参数 @@ -11,62 +9,69 @@ use support\exception\BusinessException; * @param string $account 用户输入的账号(可能是手机号或邮箱) * @return array 返回 ['phone' => $account] 或 ['email' => $account] */ -function normalizeAccount(string $account): array -{ - // 使用FILTER_VALIDATE_EMAIL验证邮箱格式 - if (filter_var($account, FILTER_VALIDATE_EMAIL)) { - return ['email' => $account]; - } - - // 清理手机号中的非数字字符 - $cleanPhone = preg_replace('/[^0-9]/', '', $account); - - // 验证手机号格式(简单版:11位数字) - if (preg_match('/^1[3-9]\d{9}$/', $cleanPhone)) { +if (!function_exists('normalizeAccount')) { + function normalizeAccount(string $account): array + { + // 使用FILTER_VALIDATE_EMAIL验证邮箱格式 + if (filter_var($account, FILTER_VALIDATE_EMAIL)) { + return ['email' => $account]; + } + + // 清理手机号中的非数字字符 + $cleanPhone = preg_replace('/[^0-9]/', '', $account); + + // 验证手机号格式(简单版:11位数字) + if (preg_match('/^1[3-9]\d{9}$/', $cleanPhone)) { + return ['phone' => $cleanPhone]; + } + + // 如果既不是邮箱也不是手机号,根据业务需求处理 + // 这里默认按手机号处理(可根据实际需求调整) return ['phone' => $cleanPhone]; } - - // 如果既不是邮箱也不是手机号,根据业务需求处理 - // 这里默认按手机号处理(可根据实际需求调整) - return ['phone' => $cleanPhone]; } /** * 公共html页面 */ -function htmlpage() -{ - static $html; - if (!$html) { - $html = file_get_contents(public_path('mzl.html')); +if (!function_exists('htmlpage')) { + function htmlpage() + { + static $html; + if (!$html) { + $html = file_get_contents(public_path('mzl.html')); + } + return $html; } - return $html; } - /** * 获取配置 */ -function getConfig($filePath = null) -{ - // 构建文件路径 - $config_file = app_path($filePath); - // 检查文件是否存在 - if (!file_exists($config_file)) { - return []; +if (!function_exists('getConfig')) { + function getConfig($filePath = null) + { + // 构建文件路径 + $config_file = app_path($filePath); + // 检查文件是否存在 + if (!file_exists($config_file)) { + return []; + } + // 包含文件并获取返回的数组 + $config = include $config_file; + // 验证是否为数组 + if (!is_array($config)) { + return []; + } + return $config; } - // 包含文件并获取返回的数组 - $config = include $config_file; - // 验证是否为数组 - if (!is_array($config)) { - return []; - } - return $config; } /* * 调试用 */ -function dpr(\Throwable $e) -{ - var_dump("=== === 报错打印 === ===", "文件:".$e->getFile(), "行数:".$e->getLine(), "详情:".$e->getMessage(), "=== === 报错打印 === ==="); +if (!function_exists('dpr')) { + function dpr(\Throwable $e) + { + var_dump("=== === 报错打印 === ===", "文件:".$e->getFile(), "行数:".$e->getLine(), "详情:".$e->getMessage(), "=== === 报错打印 === ==="); + } } \ No newline at end of file diff --git a/app/view/index/qrcode.html b/app/view/index/qrcode.html index 26d1918..fb6f11c 100644 --- a/app/view/index/qrcode.html +++ b/app/view/index/qrcode.html @@ -16,7 +16,7 @@ // const REDIRECT_URI = encodeURIComponent('https://example.com/api/oauth/callback/feishu'); // const STATE = '随机串_防CSRF'; // // 用于跳转获取 code 的 URL - const GOTO = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=cli_a73e1e6406fa501c&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&response_type=code&state=MZL`; + const GOTO = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=cli_a72f13d6c022500d&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&response_type=code&state=MZL`; // const GOTO = `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=cli_a73e1e6406fa501c&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&scope=contact:user.base:readonly contact:contact.base:readonly contact:user.employee_id:readonly im:message:send_as_bot&state=xinyouhe`; // 2. 创建二维码实例 const QRLoginObj = QRLogin({ diff --git a/composer.json b/composer.json index ccf259e..e4e63bb 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "topthink/think-validate": "^3.0", "shopwwi/webman-filesystem": "1.1", "shopwwi/filesystem-oss": "^1.0", - "webman/redis-queue": "^1.3", + "webman/redis-queue": "^2.1", "symfony/translation": "^7.3" }, "suggest": { diff --git a/composer.lock b/composer.lock index 01843a4..b9b58c7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "acb636fcb8774558a04bd674f9f95f55", + "content-hash": "30cc3cbb162022972a21efed9b69d1e1", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -6155,26 +6155,23 @@ }, { "name": "webman/redis-queue", - "version": "v1.3.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/webman-php/redis-queue.git", - "reference": "6fd491b19acb006d60931724aa2577a046ccb612" + "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/6fd491b19acb006d60931724aa2577a046ccb612", - "reference": "6fd491b19acb006d60931724aa2577a046ccb612", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/webman-php/redis-queue/zipball/12e1c9915ec64c91c0c9c255606bd0c7de27334c", + "reference": "12e1c9915ec64c91c0c9c255606bd0c7de27334c", + "shasum": "" }, "require": { - "workerman/redis-queue": "^1.0" + "ext-redis": "*", + "php": ">=8.1", + "workerman/redis-queue": "^1.2", + "workerman/webman-framework": "^2.1 || dev-master" }, "type": "library", "autoload": { @@ -6186,9 +6183,9 @@ "description": "Redis message queue plugin for webman.", "support": { "issues": "https://github.com/webman-php/redis-queue/issues", - "source": "https://github.com/webman-php/redis-queue/tree/v1.3.1" + "source": "https://github.com/webman-php/redis-queue/tree/v2.1.0" }, - "time": "2024-02-28T07:21:52+00:00" + "time": "2025-01-24T12:56:01+00:00" }, { "name": "workerman/coroutine", diff --git a/config/route.php b/config/route.php index a6c3a4d..41c3dff 100644 --- a/config/route.php +++ b/config/route.php @@ -31,11 +31,11 @@ Route::group('/common', function () { Route::any('/login', [app\common\controller\AuthController::class, 'login']); Route::any('/register', [app\common\controller\AuthController::class, 'register']); Route::any('/feishu/scope', [app\common\controller\AuthController::class, 'feishuScope']); - + Route::any('/refresh/token', [app\common\controller\AuthController::class, 'refresh']); // // 调试 - // Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); + Route::any('/feishu/auth', [app\common\controller\AuthController::class, 'feishuAuth']); }); Route::group('/user', function () { diff --git a/config/whitelist.php b/config/whitelist.php index 8071847..d1669b3 100644 --- a/config/whitelist.php +++ b/config/whitelist.php @@ -4,6 +4,6 @@ return [ 'basi_cdata' => [ '/', '/index/index', - // '/index/view' + '/index/view' ] ]; \ No newline at end of file From fef9422da0d9d1322db1a5c559da4b2ad372e376 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 11 Jul 2025 15:00:21 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/logic/PermLogic.php | 61 +++++++--------------------------- 1 file changed, 12 insertions(+), 49 deletions(-) diff --git a/app/common/logic/PermLogic.php b/app/common/logic/PermLogic.php index 889470e..b4fe819 100644 --- a/app/common/logic/PermLogic.php +++ b/app/common/logic/PermLogic.php @@ -25,48 +25,7 @@ class PermLogic } /** - * 构建目录/菜单树,节点包含权限与打开方式 - */ - // public function buildTree($resources, int $parentId = 0): array - // { - // $tree = []; - // foreach ($resources as $node) { - // if ($node->parent_id !== $parentId) continue; - - // // 子节点 - // $children = []; - // if ($node->type === 'directory') { - // $children = $this->buildTree($resources, $node->id); - // } - - // // 本级权限列表 - // $authList = $resources - // ->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id) - // ->map(fn($r) => [ - // 'id' => $r->id, - // 'title' => $r->name, - // 'auth_mark' => $r->code, - // ])->values()->all(); - - // $tree[] = [ - // 'id' => $node->id, - // 'name' => $node->name, - // 'path' => $node->url, - // 'component' => $node->type === 'directory' ? 'Layout' : 'Page', - // 'meta' => [ - // 'title' => $node->name, - // 'icon' => $node->icon, - // 'keepAlive' => true, - // 'authList' => $authList, - // 'openType' => $node->open_type, // 'internal' | 'external' | 'iframe' - // ], - // 'children' => $children, - // ]; - // } - // return $tree; - // } - /** - * 构建目录/菜单树,节点包含权限与打开方式,并拼接完整路径到 component 字段 + * 构建目录/菜单树,节点包含权限与打开方式,拼接完整路径,并对子级菜单component处理 * @param \Illuminate\Support\Collection $resources * @param int $parentId * @param string $parentPath @@ -80,23 +39,19 @@ class PermLogic // 拼接完整路径 $currentPath = $parentPath; - // 去掉结尾斜杠,防止重复 if ($currentPath && substr($currentPath, -1) === '/') { $currentPath = rtrim($currentPath, '/'); } - // 子菜单的 path 不要补全斜杠 if (!empty($node->url)) { - // 如果当前是根节点或父路径为空,直接用自身 if (!$currentPath) { $currentPath = $node->url; } else { - // 防止 url 以 / 开头导致双斜杠 $url = ltrim($node->url, '/'); $currentPath = $currentPath . '/' . $url; } } - // 子节点 + // 子节点递归 $children = []; if ($node->type === 'directory') { $children = $this->buildTree($resources, $node->id, $currentPath); @@ -111,11 +66,20 @@ class PermLogic 'auth_mark' => $r->code, ])->values()->all(); + // 判断是否有子级菜单(非权限) + $hasChildrenMenu = collect($resources)->where('parent_id', $node->id) + ->whereIn('type', ['directory', 'menu'])->isNotEmpty(); + + // component 字段规则: + // 1. 存在子菜单,固定 '/index/index' + // 2. 否则为当前完整路径(或其它你想要的) + $componentValue = $hasChildrenMenu ? '/index/index' : $currentPath; + $tree[] = [ 'id' => $node->id, 'name' => $node->name, 'path' => $node->url, - 'component' => $currentPath, // 这里就是全路径 + 'component' => $componentValue, 'meta' => [ 'title' => $node->name, 'icon' => $node->icon, @@ -129,7 +93,6 @@ class PermLogic return $tree; } - /** * 获取用户所有权限code */ From 91f26b740e8c26324d33dcdf25cb3ec2bca91fd6 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 11 Jul 2025 15:07:31 +0800 Subject: [PATCH 11/12] Merge branch 'develop' into feature/mzl_dev # Conflicts: # app/common/controller/AuthController.php # app/common/logic/AuthLogic.php # app/common/model/User.php # app/controller/IndexController.php # app/functions.php # app/middleware/AuthCheck.php # app/view/index/qrcode.html # composer.json # composer.lock # config/middleware.php # config/route.php # config/whitelist.php From 890ddff34bd6a52729761b0fbc0d0c08420fbac7 Mon Sep 17 00:00:00 2001 From: "HESU\\HESU" <1696136552@qq.com> Date: Fri, 11 Jul 2025 15:13:44 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E6=88=91=E7=9A=84=E5=B7=A5=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ticket/controller/TicketController.php | 18 ++++++++++++++++++ app/ticket/logic/TicketLogic.php | 4 ++++ config/route.php | 1 + 3 files changed, 23 insertions(+) diff --git a/app/ticket/controller/TicketController.php b/app/ticket/controller/TicketController.php index 95dc539..defd5af 100644 --- a/app/ticket/controller/TicketController.php +++ b/app/ticket/controller/TicketController.php @@ -17,6 +17,24 @@ class TicketController extends BaseController return $this->error($e->getMessage()); } } + + /** + * 我的工单 + * @param Request $request + * @return \support\Response + */ + public function self(Request $request) + { + try { + $request->setGet('user_id',$request->user->uid); + $result = (new TicketService())->list($request->all()); + return $this->success($result); +// return $this->success($this->page($result)); + } catch (\Throwable $e) { + return $this->error($e->getMessage().' '.$e->getLine().' '.$e->getFile().' '.$e->getTraceAsString()); + } + } + public function add(Request $request) { try { diff --git a/app/ticket/logic/TicketLogic.php b/app/ticket/logic/TicketLogic.php index e22cdb7..e7d098d 100644 --- a/app/ticket/logic/TicketLogic.php +++ b/app/ticket/logic/TicketLogic.php @@ -20,11 +20,15 @@ class TicketLogic if(!empty($params['categorie_id'])){ $where[] = ['categorie_id', '=', $params['categorie_id']]; } + if(!empty($params['user_id'])){ + $where[] = ['user_id', '=', $params['user_id']]; + } if(!empty($params['status'])){ $where[] = ['status', '=', $params['status']]; } $size = $params['size']; $current = $params['current']; + return Ticket::with('attachments')->with('categorie')->with('categorie.translation')->where($where)->orderBy('id','desc')->paginate($size, ['*'], 'page', $current); } public function add(array $data) diff --git a/config/route.php b/config/route.php index a6c3a4d..97f68c4 100644 --- a/config/route.php +++ b/config/route.php @@ -59,6 +59,7 @@ Route::group('/ticket', function () { Route::any('/ticket/add', [app\ticket\controller\TicketController::class, 'add']); Route::any('/ticket/edit', [app\ticket\controller\TicketController::class, 'edit']); Route::any('/ticket/list', [app\ticket\controller\TicketController::class, 'index']); + Route::any('/ticket/self', [app\ticket\controller\TicketController::class, 'self']); Route::any('/ticket/status', [app\ticket\controller\TicketController::class, 'status']); Route::any('/ticket/getStatus', [app\ticket\controller\TicketController::class, 'getStatus']);