commit d19bb9046b3895005b8b4a46fa3a3eb54121df8a Author: WIN-C27C5MSVTPO\16961 <1696136552@qq.com> Date: Mon Dec 8 17:37:34 2025 +0800 样机小程序 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..516299c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/runtime +/.idea +/.vscode +/vendor +*.log +.env +/tests/tmp +/tests/.phpunit.result.cache diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2c66292 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 walkor and contributors (see https://github.com/walkor/webman/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/admin/controller/AgreementController.php b/app/admin/controller/AgreementController.php new file mode 100644 index 0000000..b007349 --- /dev/null +++ b/app/admin/controller/AgreementController.php @@ -0,0 +1,33 @@ +service = new AgreementService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/BannerController.php b/app/admin/controller/BannerController.php new file mode 100644 index 0000000..818b3db --- /dev/null +++ b/app/admin/controller/BannerController.php @@ -0,0 +1,56 @@ +service = new BannerService(); + } + + /** + * 轮播图模块 + * @return \support\Response + */ + public function getBannerModule() + { + try { + $raw = $this->service->getBannerModule(); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['image_url']) && !empty($data['image_url'])){ + $ossservice = new OssService(); + $data['image_url_pull'] = $ossservice->generatePrivateUrl($data['image_url']); + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/MessageNoticeController.php b/app/admin/controller/MessageNoticeController.php new file mode 100644 index 0000000..c230675 --- /dev/null +++ b/app/admin/controller/MessageNoticeController.php @@ -0,0 +1,33 @@ +service = new MessageNoticeService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/PostCategoryController.php b/app/admin/controller/PostCategoryController.php new file mode 100644 index 0000000..23ef772 --- /dev/null +++ b/app/admin/controller/PostCategoryController.php @@ -0,0 +1,62 @@ +service = new PostCategoryService(); + } + + + /** + * 分类树 + * @param Request $request + * @return \support\Response + */ + public function tree(Request $request) + { + try { + $params = $request->all(); + $result = $this->service->getList($params); + return $this->success($result, '获取分类列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + $ossservice = new OssService(); + if (!empty($data['icon_active'])){ + $data['icon_active_pull'] = $ossservice->generatePrivateUrl($data['icon_active']); + } + if (!empty($data['icon_inactive'])){ + $data['icon_inactive_pull'] = $ossservice->generatePrivateUrl($data['icon_inactive']); + } + if (!empty($data['icon_preset'])){ + $data['icon_preset_pull'] = $ossservice->generatePrivateUrl($data['icon_preset']); + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/PostCommentController.php b/app/admin/controller/PostCommentController.php new file mode 100644 index 0000000..67cde1b --- /dev/null +++ b/app/admin/controller/PostCommentController.php @@ -0,0 +1,62 @@ +service = new PostCommentService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['id'])){ + $data['child'] = []; + $child = PostComment::where(['parent_id' => $data['id']])->first(); + if(!empty($child)){ + $child = $child->toArray(); + } + $data['count'] = PostComment::where(['parent_id' => $data['id']])->count(); + if (isset($child['user_id'])){ + $ossservice = new OssService(); + $child['user'] = User::where('id', $child['user_id'])->select(['nickname', 'avatar', 'openid'])->first(); + if(!str_contains($child['user']['avatar'], 'http') && !empty($child['user']['avatar'])){ + $child['user']['avatar_pull'] = $ossservice->generatePrivateUrl($child['user']['avatar']); + } + $child['created_at'] = date('Y-m-d H:i:s', strtotime($child['created_at'])); + $child['updated_at'] = date('Y-m-d H:i:s', strtotime($child['updated_at'])); + $data['child'][] = $child; + } + } + if (isset($data['user_id'])){ + $ossservice = new OssService(); + $data['user'] = User::where('id', $data['user_id'])->select(['nickname', 'avatar', 'openid'])->first(); + + if(!str_contains($data['user']['avatar'], 'http') && !empty($data['user']['avatar'])){ + $data['user']['avatar_pull'] = $ossservice->generatePrivateUrl($data['user']['avatar']); + } + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/PostController.php b/app/admin/controller/PostController.php new file mode 100644 index 0000000..9c0cd46 --- /dev/null +++ b/app/admin/controller/PostController.php @@ -0,0 +1,92 @@ +service = new PostService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['id'])) + $data['media'] = PostMedia::where('post_id', $data['id'])->get(); + if (isset($data['category_id'])) + $data['category_name'] = PostCategory::where('id', $data['category_id'])->value('name')??''; + + $ossservice = new OssService(); + if (isset($data['id'])){ + $data['media'] = PostMedia::where('post_id', $data['id'])->get()->toArray(); + foreach ($data['media'] as &$media) { + $media['url_pull'] = $ossservice->generatePrivateUrl($media['url']); + } + } + if (isset($data['user_id'])){ + $data['user'] = User::where('id', $data['user_id'])->first(); + unset($data['user']['password']); + $data['user']['avatar_pull'] = $data['user']['avatar']; + if(!str_contains($data['user']['avatar'], 'http') && !empty($data['user']['avatar'])){ + $data['user']['avatar_pull'] = $ossservice->generatePrivateUrl($data['user']['avatar']); + } + } + return $data; + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['id'])) + $data['media'] = PostMedia::where('post_id', $data['id'])->get(); + if (isset($data['category_id'])) + $data['category_name'] = PostCategory::where('id', $data['category_id'])->value('name')??''; + + $ossservice = new OssService(); + if (isset($data['id'])){ + $data['media'] = PostMedia::where('post_id', $data['id'])->get()->toArray(); + foreach ($data['media'] as &$media) { + $media['url_pull'] = $ossservice->generatePrivateUrl($media['url']); + } + } + if (isset($data['user_id'])){ + $data['user'] = User::where('id', $data['user_id'])->first(); + unset($data['user']['password']); + $data['user']['avatar_pull'] = $data['user']['avatar']; + if(!str_contains($data['user']['avatar'], 'http') && !empty($data['user']['avatar'])){ + $data['user']['avatar_pull'] = $ossservice->generatePrivateUrl($data['user']['avatar']); + } + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/PostFavoriteController.php b/app/admin/controller/PostFavoriteController.php new file mode 100644 index 0000000..6ee2d24 --- /dev/null +++ b/app/admin/controller/PostFavoriteController.php @@ -0,0 +1,33 @@ +service = new PostFavoriteService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/PostLikeController.php b/app/admin/controller/PostLikeController.php new file mode 100644 index 0000000..40bff67 --- /dev/null +++ b/app/admin/controller/PostLikeController.php @@ -0,0 +1,33 @@ +service = new PostLikeService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/PostMediaController.php b/app/admin/controller/PostMediaController.php new file mode 100644 index 0000000..dcade9f --- /dev/null +++ b/app/admin/controller/PostMediaController.php @@ -0,0 +1,33 @@ +service = new PostMediaService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/RoleController.php b/app/admin/controller/RoleController.php new file mode 100644 index 0000000..a08ff7e --- /dev/null +++ b/app/admin/controller/RoleController.php @@ -0,0 +1,43 @@ +service = new RoleService(); + } + + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + + public function menuResource(Request $request) + { + try { + // $data = $this->validate($this->service->getValidator(), $request->post(), 'allocation'); + $data = $request->post(); + $res = $this->service->menuResource($request, $data); + return $this->success($res); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/admin/controller/SkuAttributeController.php b/app/admin/controller/SkuAttributeController.php new file mode 100644 index 0000000..0b46eb9 --- /dev/null +++ b/app/admin/controller/SkuAttributeController.php @@ -0,0 +1,33 @@ +service = new SkuAttributeService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/SkuAttributeValueController.php b/app/admin/controller/SkuAttributeValueController.php new file mode 100644 index 0000000..0370606 --- /dev/null +++ b/app/admin/controller/SkuAttributeValueController.php @@ -0,0 +1,33 @@ +service = new SkuAttributeValueService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/SkuController.php b/app/admin/controller/SkuController.php new file mode 100644 index 0000000..73e3f09 --- /dev/null +++ b/app/admin/controller/SkuController.php @@ -0,0 +1,33 @@ +service = new SkuService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/admin/controller/SystemsController.php b/app/admin/controller/SystemsController.php new file mode 100644 index 0000000..3c062c5 --- /dev/null +++ b/app/admin/controller/SystemsController.php @@ -0,0 +1,33 @@ +service = new SystemsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/UserController.php b/app/admin/controller/UserController.php new file mode 100644 index 0000000..b975c0c --- /dev/null +++ b/app/admin/controller/UserController.php @@ -0,0 +1,196 @@ +service = new UserService(); + } + + /** + * 添加 + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function store(Request $request) + { + try { + $data = $request->post(); + if(mb_strlen($data['mobile']) < 11){ + return $this->error('手机号错误'); + } + $res = $this->service->serviceCreateUser($request, $data); + return $this->success([], '添加成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 更新 + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function update(Request $request) + { + try { + $data = $request->post(); + $res = $this->service->serviceUpdateUser($request, $data); + return $this->success([], '更新成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 修改密码 + * @param Request $request + * @return \support\Response + * @throws \Random\RandomException + */ + public function savePassword(Request $request) + { + try { + $data = $request->all(); + if(empty($data['password'])){ + return $this->error('密码不能为空'); + } + $result = $this->service->savePassword($request,$data);; + return $this->success($result, '编辑成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 更新 + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function edit(Request $request) + { + try { + $data = $request->post(); + $data['id'] = $data['id']??$request->user->uid; + $res = $this->service->serviceUpdate($request, $data); + return $this->success([], '更新成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if(isset($data['records'])){ + return $data; + } + $data['sex_txt'] = ''; + if (isset($data['sex']) && $data['sex'] == 1){ + $data['sex_txt'] = '男'; + }elseif(isset($data['sex']) && $data['sex'] == 2){ + $data['sex_txt'] = '女'; + } + + $ossservice = new OssService(); + if(isset($data['avatar']) && !empty($data['avatar']) && strpos($data['avatar'], 'http') === false){ + //$data['avatar_pull'] = $ossservice->generatePrivateUrl($data['avatar']); + $data['avatar_pull'] = '/proxy/ossurl?file='.$data['avatar']; + }else{ + $data['avatar_pull'] = $data['avatar']; + } + + switch ($data['status']){ + case 1: + $data['status_txt'] = '正常'; + break; + case 2: + $data['status_txt'] = '离职'; + break; + default: + $data['status_txt'] = ''; + break; + } + + $data['role_ids'] = []; + $data['system_ids'] = []; + if (isset($data['roles']) && count($data['roles']) > 0) { + foreach ($data['roles'] as $role) { + $data['role_ids'][] = $role['id']; + } + } + if (isset($data['systems']) && count($data['systems']) > 0) { + foreach ($data['systems'] as $system) { + $data['system_ids'][] = $system['id']; + } + } + return $data; + } + + /** + * 授权 + * + * @param Request $request + * @return mixed + */ + public function authorize(Request $request) + { + try { + $data = $request->post(); + $res = $this->service->authorize($request, $data); + return $this->success([], '分配成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 直接授权 + * + * @param Request $request + * @return mixed + */ + public function updateResources(Request $request) + { + try { + $data = $request->post(); + $res = $this->service->updateResources($request, $data); + return $this->success([], '分配成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + public function updateSign(Request $request) + { + try { + $data = $request->post(); + $this->service->serviceUpdateSign($request, $data); + return $this->success([], '签名成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/admin/logic/UserLogic.php b/app/admin/logic/UserLogic.php new file mode 100644 index 0000000..fa750fb --- /dev/null +++ b/app/admin/logic/UserLogic.php @@ -0,0 +1,94 @@ +where('user_id', $userId)->delete(); + if (count($roleIds) > 0) { + $insertDataRole = []; + foreach ($roleIds as $roleId) { + $insertDataRole[] = [ + 'role_id' => $roleId, + 'user_id' => $userId + ]; + } + $res = Db::table('user_roles')->insert($insertDataRole); + var_dump($res); + } + + Db::table('user_systems')->where('user_id', $userId)->delete(); + if (count($systemIds) > 0) { + $insertDataSystem = []; + foreach ($systemIds as $systemId) { + $insertDataSystem[] = [ + 'system_id' => $systemId, + 'user_id' => $userId + ]; + } + Db::table('user_systems')->insert($insertDataSystem); + } + + Db::commit(); + return true; + } catch (\Throwable $exception) { + Db::rollBack(); + return false; + } + } + + + public function updateUserResources(array $data): bool + { + $userId = $data['user_id'] ?? 0; + $resources = $data['resources'] ?? []; + + if (empty($userId)) { + return false; + } + + Db::beginTransaction(); + try { + // 删除原有资源关联 + Db::table('user_resources')->where('user_id', $userId)->delete(); + + // 批量插入新角色资源关联 + if (!empty($resources)) { + $insertData = []; + foreach ($resources as $resourceId) { + $insertData[] = [ + 'user_id' => $userId, + 'resource_id' => $resourceId + ]; + } + Db::table('user_resources')->insert($insertData); + } + + Db::commit(); + return true; + } catch (\Throwable $exception) { + Db::rollBack(); + return false; + } + } +} \ No newline at end of file diff --git a/app/admin/service/RoleService.php b/app/admin/service/RoleService.php new file mode 100644 index 0000000..f2ee190 --- /dev/null +++ b/app/admin/service/RoleService.php @@ -0,0 +1,50 @@ +validator; + } + + /** + * 获取角色资源 + */ + public function menuResource($request, $data) + { + return (new RoleLogic)->updateRoleResources($data); + } + + + public function getList() + { + $config['where']['status'] = 1; + $config['order']['sort'] = 'asc'; + $config['order']['id'] = 'asc'; + return $this->logicListAll($config); + } +} \ No newline at end of file diff --git a/app/admin/service/SystemsService.php b/app/admin/service/SystemsService.php new file mode 100644 index 0000000..8086245 --- /dev/null +++ b/app/admin/service/SystemsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/admin/service/UserService.php b/app/admin/service/UserService.php new file mode 100644 index 0000000..a90ff10 --- /dev/null +++ b/app/admin/service/UserService.php @@ -0,0 +1,180 @@ +validator; + } + + /** 更新 */ + public function serviceCreateUser(Request $request, array $data = []) + { + $config = $this->buildQueryConfig($request, 'create'); + if(isset($data['avatar']) && empty($data['avatar'])){ + $data['avatar'] = 'bsh_datax/user/20251021/a4ee815405a1506abeec1af0f738f70f_68f6e76514b66.jpg'; + } + if(empty($data['password'])){ + $data['password'] = password_hash('12345678', PASSWORD_DEFAULT); + }else{ + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + $created = $this->logicCreate($data); + return $this->postProcess('Create', $data); + } + + /** 更新 */ + public function serviceUpdateUser(Request $request, array $data = []) + { + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'update'); + $updated = $this->logicUpdateBy($config, $data); + return $this->postProcess('Update', $data); + } + + /** 更新 */ + public function serviceUpdate(Request $request, array $data = []) + { + unset($data['roles']); + unset($data['avatar_pull']); + $data = $this->preProcess('Update', $data, $request); + $config = $this->buildQueryConfig($request, 'edit'); + $updated = $this->logicUpdateBy($config, $data); + return $this->postProcess('Update', $data); + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + break; + case 'index': +// $config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + $config['where']['name'] = ['like', $k]; + } + if (isset($p['status']) && $p['status']) { + $config['where']['status'] = $p['status']; + } + if (isset($p['user_id']) && $p['user_id']) { + $config['where']['user_id'] = $p['user_id']; + } + if (isset($p['id']) && $p['id']) { + $config['where']['id'] = $p['id']; + } + if (isset($p['corp_id']) && $p['corp_id']) { + $config['where']['corp_id'] = $p['corp_id']; + } + + $config['fields'] = ['id','sn', 'name', 'email', 'mobile', 'avatar', 'nickname', 'is_release', 'status', 'created_at']; + + $config['with']['roles'][] = 'id'; + $config['with']['roles'][] = 'name'; + $config['with']['systems'][] = 'id'; + $config['with']['systems'][] = 'name'; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'savePassword': + $config['where']['id'] = $p['id']; + break; + case 'edit': + case 'updateSign': + $config['where']['id'] = $request->user->uid; + break; + } + + return $config; + } + + /** + * 授权 + * + * @param Request $request + * @param array $data + * @return bool + */ + public function authorize(Request $request, array $data = []) + { + return (new UserLogic())->updateRoleAndSystem($data); + } + + /** + * 更新用户资源权限 + * + * @param Request $request + * @param array $data + * @return bool + */ + public function updateResources(Request $request, array $data = []) + { + return (new UserLogic())->updateUserResources($data); + } + + /** + * 修改用户密码 + */ + public function savePassword(Request $request, array $data) + { + $config = $this->buildQueryConfig($request, 'savePassword'); + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + $user = $this->logicUpdateBy($config,$data); + return $user; + } + + + + public function serviceUpdateSign(Request $request, array $data) + { + $config = $this->buildQueryConfig($request, 'updateSign'); + $user = $this->logicUpdateBy($config,$data); + return $user; + } +} \ No newline at end of file diff --git a/app/common/controller/AgreementController.php b/app/common/controller/AgreementController.php new file mode 100644 index 0000000..48620d2 --- /dev/null +++ b/app/common/controller/AgreementController.php @@ -0,0 +1,33 @@ +service = new AgreementService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/AuthController.php b/app/common/controller/AuthController.php new file mode 100644 index 0000000..7d93f14 --- /dev/null +++ b/app/common/controller/AuthController.php @@ -0,0 +1,116 @@ +all(); + return $this->success($code); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function feishuScope(Request $request) + { + try { + $result = (new AuthService())->feishuAuthorize($request->corp_abbr, $request->system_code); + return $this->success($result); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function login(Request $request) + { + try { + $data = $this->validate(new UserValidate(), $this->requestData($request), $request->login_type.'_login'); + $result = (new AuthService())->login($data); + return $this->success($result, '登录成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function register(Request $request) + { + try { + $data = $this->validate(new UserValidate(), $this->requestData($request), $request->login_type.'_register'); + $result = (new AuthService())->register($data); + return $this->success($result, '注册成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function refresh(Request $request) + { + try { + $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 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/controller/BannerController.php b/app/common/controller/BannerController.php new file mode 100644 index 0000000..36dd676 --- /dev/null +++ b/app/common/controller/BannerController.php @@ -0,0 +1,65 @@ +service = new BannerService(); + } + + /** + * 轮播 + * @param Request $request + * @return \support\Response + */ + public function getAll(Request $request) + { + try { + $params = $request->all(); + $result = $this->service->getBanner($params); + foreach ($result as $key => $value) { + if (isset($value['image_url']) && !empty($value['image_url'])){ + $ossservice = new OssService(); + $result[$key]['image_url_pull'] = $ossservice->generatePrivateUrl($value['image_url']); + } + } + return $this->success($result, '获取轮播列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['image_url']) && !empty($data['image_url'])){ + $ossservice = new OssService(); + $data['image_url_pull'] = $ossservice->generatePrivateUrl($data['image_url']); + } + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/BaseController.php b/app/common/controller/BaseController.php new file mode 100644 index 0000000..f617239 --- /dev/null +++ b/app/common/controller/BaseController.php @@ -0,0 +1,92 @@ +scene($scene); + } + // 执行数据验证 + if (!$validator->check($data)) { + // 验证失败,抛出业务异常,异常信息为验证器返回的错误信息 + throw new BusinessException($validator->getError()); + } + // 验证成功,返回原始数据 + return $data; + } + + /** + * 参数处理 + */ + public function requestData(Request $request) + { + return $request->all() + $request->basic_data; + } +} \ No newline at end of file diff --git a/app/common/controller/MessageNoticeController.php b/app/common/controller/MessageNoticeController.php new file mode 100644 index 0000000..a0a79db --- /dev/null +++ b/app/common/controller/MessageNoticeController.php @@ -0,0 +1,73 @@ +service = new MessageNoticeService(); + } + + /** + * 获取列表 + * + * @param Request $request 请求对象 + * @return mixed 返回分页数据及总数 + */ + public function index(Request $request) + { + try { + $raw = $this->service->serviceSelfList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['post_id'])){ + $data['post'] = Post::where('id', $data['post_id'])->first(); + $data['post']['media'] = PostMedia::where('post_id', $data['post_id'])->get(); + } + if (isset($data['sender_user_id'])){ + $ossservice = new OssService(); + $data['sender_user'] = User::where('id', $data['sender_user_id'])->select(['nickname', 'avatar', 'openid'])->first(); + + if(!str_contains($data['sender_user']['avatar'], 'http') && !empty($data['sender_user']['avatar'])){ + $data['sender_user']['avatar_pull'] = $ossservice->generatePrivateUrl($data['sender_user']['avatar']); + } + } + if (!empty($data['comment_id'])){ + $data['comment'] = PostComment::where('id', $data['comment_id'])->first(); + } + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/PostCategoryController.php b/app/common/controller/PostCategoryController.php new file mode 100644 index 0000000..c1e4755 --- /dev/null +++ b/app/common/controller/PostCategoryController.php @@ -0,0 +1,38 @@ +service = new PostCategoryService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (!empty($data['icon'])){ + $ossservice = new OssService(); + $data['icon_pull'] = $ossservice->generatePrivateUrl($data['icon']); + } + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/PostCommentController.php b/app/common/controller/PostCommentController.php new file mode 100644 index 0000000..801e6c7 --- /dev/null +++ b/app/common/controller/PostCommentController.php @@ -0,0 +1,79 @@ +service = new PostCommentService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['id'])){ + $data['child'] = []; + $child = PostComment::where(['parent_id' => $data['id']])->first(); + if(!empty($child)){ + $child = $child->toArray(); + } + $data['count'] = PostComment::where(['parent_id' => $data['id']])->count(); + if (isset($child['user_id'])){ + $ossservice = new OssService(); + $child['user'] = User::where('id', $child['user_id'])->select(['nickname', 'avatar', 'openid'])->first(); + if(!str_contains($child['user']['avatar'], 'http') && !empty($child['user']['avatar'])){ + $child['user']['avatar_pull'] = $ossservice->generatePrivateUrl($child['user']['avatar']); + } + $child['created_at'] = date('Y-m-d H:i:s', strtotime($child['created_at'])); + $child['updated_at'] = date('Y-m-d H:i:s', strtotime($child['updated_at'])); + $data['child'][] = $child; + } + } + if (isset($data['user_id'])){ + $ossservice = new OssService(); + $data['user'] = User::where('id', $data['user_id'])->select(['nickname', 'avatar', 'openid'])->first(); + + if(!str_contains($data['user']['avatar'], 'http') && !empty($data['user']['avatar'])){ + $data['user']['avatar_pull'] = $ossservice->generatePrivateUrl($data['user']['avatar']); + } + } + return $data; + } + + /** + * 删除 + * + * @param Request $request 请求对象 + * @return mixed 返回删除操作结果 + */ + public function delete(Request $request) + { + try { + $count = $this->service->serviceDelete($request); + return $this->success([], "删除评论成功"); + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getLine().$e->getFile()); + } + } +} \ No newline at end of file diff --git a/app/common/controller/PostController.php b/app/common/controller/PostController.php new file mode 100644 index 0000000..0300fa3 --- /dev/null +++ b/app/common/controller/PostController.php @@ -0,0 +1,199 @@ +service = new PostService(); + } + /** + * 获取列表 + * + * @param Request $request 请求对象 + * @return mixed 返回分页数据及总数 + */ + public function index(Request $request) + { + try { + $raw = $this->service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + $records = $data['records']; + foreach ($records as &$record) { + if (!isset($record['id'])) + break; + if(!empty($request->user) && !empty($request->user->uid)){ + $where = [ + 'post_id' => $record['id'], + 'user_id' => $request->user->uid + ]; + $record['is_like'] = PostLike::where($where)->count(); + $record['is_favorite'] = PostFavorite::where($where)->count(); + $record['is_comment'] = PostComment::where($where)->count(); + }else{ + $record['is_like'] = 0; + $record['is_favorite'] = 0; + $record['is_comment'] = 0; + } + $created_at = strtotime($record['created_at']); + $record['year'] = date('Y', $created_at); + $record['month'] = date('m', $created_at); + $record['day'] = date('d', $created_at); + } + $data['records'] = $records; + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getFile().$e->getLine()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + $ossservice = new OssService(); + if (isset($data['id'])){ + $data['media'] = PostMedia::where('post_id', $data['id'])->get()->toArray(); + foreach ($data['media'] as &$media) { + $media['url_pull'] = $ossservice->generatePrivateUrl($media['url']); + } + } + if (isset($data['user_id'])){ + $data['user'] = User::where('id', $data['user_id'])->first(); + unset($data['user']['password']); + $data['user']['avatar_pull'] = $data['user']['avatar']; + if(!str_contains($data['user']['avatar'], 'http') && !empty($data['user']['avatar'])){ + $data['user']['avatar_pull'] = $ossservice->generatePrivateUrl($data['user']['avatar']); + } + } + return $data; + } + + /** + * 获取评论,点赞,收藏数量 + * + * @param Request $request 请求对象 + * @param int|string $id 资源 ID + * @return mixed 返回资源详情数据 + */ + public function interaction(Request $request) + { + try { + $param = $request->all(); + $data = Post::where('id', $param['id'])->select(['like','favorite','comment'])->first(); + + if(!empty($request->user) && !empty($request->user->uid)){ + $where = [ + 'post_id' => $data['id'], + 'user_id' => $request->user->uid + ]; + $data['is_like'] = PostLike::where($where)->count(); + $data['is_favorite'] = PostFavorite::where($where)->count(); + $data['is_comment'] = PostComment::where($where)->count(); + }else{ + $data['is_like'] = 0; + $data['is_favorite'] = 0; + $data['is_comment'] = 0; + } + + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getFile().$e->getLine()); + } + } + + /** + * 获取详情 + * + * @param Request $request 请求对象 + * @param int|string $id 资源 ID + * @return mixed 返回资源详情数据 + */ + public function show(Request $request) + { + try { + $raw = $this->service->serviceShow($request); + $data = $this->transformData($raw, method_exists($this, 'showFormat') + ? [$this, 'showFormat'] + : null); + + if(!empty($request->user) && !empty($request->user->uid)){ + $where = [ + 'post_id' => $data['id'], + 'user_id' => $request->user->uid + ]; + $data['is_like'] = PostLike::where($where)->count(); + $data['is_favorite'] = PostFavorite::where($where)->count(); + $data['is_comment'] = PostComment::where($where)->count(); + }else{ + $data['is_like'] = 0; + $data['is_favorite'] = 0; + $data['is_comment'] = 0; + } + + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getFile().$e->getLine()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function showFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + $ossservice = new OssService(); + if (isset($data['id'])){ + $data['media'] = PostMedia::where('post_id', $data['id'])->get()->toArray(); + foreach ($data['media'] as &$media) { + $media['url_pull'] = $ossservice->generatePrivateUrl($media['url']); + } + } + if (isset($data['user_id'])){ + $data['user'] = User::where('id', $data['user_id'])->first(); + unset($data['user']['password']); + $data['user']['avatar_pull'] = $data['user']['avatar']; + if(!str_contains($data['user']['avatar'], 'http') && !empty($data['user']['avatar'])){ + $data['user']['avatar_pull'] = $ossservice->generatePrivateUrl($data['user']['avatar']); + } + } + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/PostFavoriteController.php b/app/common/controller/PostFavoriteController.php new file mode 100644 index 0000000..223d2ea --- /dev/null +++ b/app/common/controller/PostFavoriteController.php @@ -0,0 +1,100 @@ +service = new PostFavoriteService(); + } + + /** + * 获取列表 + * + * @param Request $request 请求对象 + * @return mixed 返回分页数据及总数 + */ + public function self(Request $request) + { + try { + $raw = $this->service->serviceSelfList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['post_id'])){ + $ossservice = new OssService(); + $post = Post::where('id', $data['post_id'])->first()->toArray(); + + if (isset( $post['user_id'])){ + $user = User::where('id', $post['user_id'])->first(); + $postUser = []; + $postUser['name'] = $user['name']; + $postUser['nickname'] = $user['nickname']; + $postUser['avatar'] = $user['avatar']; + $postUser['avatar_pull'] = $user['avatar']; + if(!str_contains($postUser['avatar'], 'http') && !empty($postUser['avatar'])){ + $postUser['avatar_pull'] = $ossservice->generatePrivateUrl($postUser['avatar']); + } + $post['user'] = $postUser; + } + + $ossservice = new OssService(); + if (isset($post['id'])){ + $post['media'] = PostMedia::where('post_id', $post['id'])->get()->toArray(); + foreach ($post['media'] as &$media) { + $media['url_pull'] = $ossservice->generatePrivateUrl($media['url']); + } + } + + $data['post'] = $post; + } + return $data; + } + + /** + * 删除 + * + * @param Request $request 请求对象 + * @return mixed 返回删除操作结果 + */ + public function delete(Request $request) + { + try { + $count = $this->service->serviceDelete($request); + return $this->success([], "取消收藏成功"); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/controller/PostLikeController.php b/app/common/controller/PostLikeController.php new file mode 100644 index 0000000..279aa77 --- /dev/null +++ b/app/common/controller/PostLikeController.php @@ -0,0 +1,69 @@ +service = new PostLikeService(); + } + + /** + * 获取列表 + * + * @param Request $request 请求对象 + * @return mixed 返回分页数据及总数 + */ + public function index(Request $request) + { + try { + $raw = $this->service->serviceSelfList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 删除 + * + * @param Request $request 请求对象 + * @return mixed 返回删除操作结果 + */ + public function delete(Request $request) + { + try { + $count = $this->service->serviceDelete($request); + return $this->success([], "取消点赞成功"); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } +} \ No newline at end of file diff --git a/app/common/controller/PostMediaController.php b/app/common/controller/PostMediaController.php new file mode 100644 index 0000000..00ee673 --- /dev/null +++ b/app/common/controller/PostMediaController.php @@ -0,0 +1,33 @@ +service = new PostMediaService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/PresetController.php b/app/common/controller/PresetController.php new file mode 100644 index 0000000..955e855 --- /dev/null +++ b/app/common/controller/PresetController.php @@ -0,0 +1,90 @@ +getList(); + return $this->success($result, '获取系统列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + public function users() + { + try { + $result = (new UserService())->getList(); + return $this->success($result, '获取用户列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function roles() + { + try { + $result = (new RoleService)->getList(); + return $this->success($result, '获取角色列表'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function cacheClear() + { + try { + Cache::clear(); + return $this->success([], '操作成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + public function menus(Request $request) + { + try { + $uid = $request->user->uid; + $systemCode = $request->system_code; + $permService = new PermService(); + $menuTree = $permService->getMenuTree($uid, $systemCode); + $permissionCodes = $permService->getPermissionCodes($uid, $systemCode); + $result = [ + 'menu' => $menuTree, + 'permissions' => $permissionCodes + ]; + return $this->success($result, '获取菜单权限'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/controller/ResourceController.php b/app/common/controller/ResourceController.php new file mode 100644 index 0000000..aba3192 --- /dev/null +++ b/app/common/controller/ResourceController.php @@ -0,0 +1,82 @@ +service = new ResourceService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function allFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 资源树形结构 + */ + public function listTree(Request $request) + { + try { + $raw = $this->service->serviceListAll($request); + $data = $this->transformData($raw, method_exists($this, 'allFormat') + ? [$this, 'allFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 菜单 + */ + public function menuList(Request $request) + { + try { + $raw = $this->service->serviceMenuList($request); + $data = $this->transformData($raw, method_exists($this, 'allFormat') + ? [$this, 'allFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 菜单 + */ + public function menuTreeList(Request $request) + { + try { + $raw = $this->service->serviceMenuTreeList($request); + $data = $this->transformData($raw, method_exists($this, 'allFormat') + ? [$this, 'allFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getFile().$e->getLine()); + } + } +} \ No newline at end of file diff --git a/app/common/controller/SkuAttributeController.php b/app/common/controller/SkuAttributeController.php new file mode 100644 index 0000000..86e184e --- /dev/null +++ b/app/common/controller/SkuAttributeController.php @@ -0,0 +1,33 @@ +service = new SkuAttributeService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/SkuAttributeValueController.php b/app/common/controller/SkuAttributeValueController.php new file mode 100644 index 0000000..e88b2c1 --- /dev/null +++ b/app/common/controller/SkuAttributeValueController.php @@ -0,0 +1,33 @@ +service = new SkuAttributeValueService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/SkuController.php b/app/common/controller/SkuController.php new file mode 100644 index 0000000..f44dc52 --- /dev/null +++ b/app/common/controller/SkuController.php @@ -0,0 +1,33 @@ +service = new SkuService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php new file mode 100644 index 0000000..9728596 --- /dev/null +++ b/app/common/controller/UploadController.php @@ -0,0 +1,299 @@ +input('scene', 'image'); + $file = $request->file('file'); + if ($file && $file->isValid()) { + $fileName = md5($file->getUploadName() . time()).'.'.$file->getUploadExtension(); + $pathUrl = '/bsh_sample/'.$scene.'/'.date('Ymd').'/'.$fileName; + $fullPathUrl = public_path() . $pathUrl; + $file->move($fullPathUrl); + + $data = [ + 'domain' => getenv('MAIN_DOMAIN'), + 'file_url' => $pathUrl, + 'full_url' => getenv('MAIN_DOMAIN') . $pathUrl, + 'scene' => $scene, + ]; + + // 记录文件信息到数据库 + try { + $uploadedFilesService = new UploadedFilesService(); + $partition = $scene; + $category = $request->input('category', 'general'); + + $fileInfo = [ + 'file_name' => basename($fileName), + 'file_path' => $pathUrl, + 'file_size' => $file->getSize(), + 'file_type' => $file->getUploadMimeType(), + 'original_name' => $file->getUploadName(), + ]; + + $recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category); + $data['file_id'] = $recordResult['id']; + $data['partition'] = $partition; + $data['category'] = $category; + } catch (\Throwable $e) { + // 记录失败不影响上传结果,只记录日志 + Log::error('记录文件信息失败: ' . $e->getMessage()); + } + + return $this->success($data, 'upload success'); + } + return $this->error('file not found'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + // 上传单文件附件 + public function uploadFile(Request $request) + { + try { + $scene = $request->input('scene'); + $file = $request->file('file'); + + if (!$file || !$file->isValid()) { + return $this->error('上传失败'); + } + if (empty($scene)) { +// return $this->error('上传场景不能为空'); + $sceneUrl = ''; + }else{ + $sceneUrl = $scene . '/'; + } + $originalName = $file->getUploadName(); + + $result = Storage::adapter('oss') + ->path('bsh_sample/' . $sceneUrl . date('Ymd')) + ->size(1024*1024*100) + ->upload($file); + + $ossservice = new OssService(); + + $data = [ + 'original_name' => $originalName, + 'file_name' => $result->file_name, + 'file_url' => $ossservice->generatePrivateUrl($result->file_name) + ]; + + // 记录文件信息到数据库 + $category = $request->input('category', 'general'); + $res = $this->recordFileInfo($result,$request,$file,$scene,$category); + $data = array_merge($data, $res); + + return $this->success($data); + }catch (\Throwable $e){ + return $this->error($e->getMessage()); + } + } + + // 上传单文件图片 + public function uploadImage(Request $request) + { + try { + $scene = $request->input('scene'); + $file = $request->file('image'); + + if (!$file || !$file->isValid()) { + return $this->error('上传失败'); + } + if (empty($scene)) { +// return $this->error('上传场景不能为空'); + $sceneUrl = ''; + }else{ + $sceneUrl = $scene . '/'; + } + + $result = Storage::adapter('oss') + ->path('bsh_sample/' . $sceneUrl . date('Ymd')) + ->size(1024*1024*100) + ->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) + ]; + + // 记录文件信息到数据库 + $category = $request->input('category', 'general'); + $res = $this->recordFileInfo($result,$request,$file,$scene,$category); + $data = array_merge($data, $res); + + return json(['errno' => 0, 'data' => $data]); +// return $this->success($data); + } catch (\Throwable $e) { + return json(['errno' => 1, 'message' => $e->getMessage()]); +// return $this->error($e->getMessage()); + } + } + + public function recordFileInfo($result, $request, $file, $scene, $category) + { + try { + $uploadedFilesService = new UploadedFilesService(); + $partition = $scene; + + $fileInfo = [ + 'file_name' => basename($result->file_name), + 'file_path' => $result->file_name, // OSS存储路径 + 'file_size' => $file->getSize(), + 'file_type' => $file->getUploadMimeType(), + 'original_name' => $file->getUploadName(), + ]; + + $partition = !empty($partition)?$partition:'other'; + $category = !empty($category)?$category:'other'; + $recordResult = $uploadedFilesService->recordFileInfo($fileInfo, $request, $partition, $category); + $data = []; + $data['file_id'] = $recordResult['id']; + $data['partition'] = $partition; + $data['category'] = $category; + return $data; + } catch (\Throwable $e) { + // 记录失败不影响上传结果,只记录日志 + Log::error('记录文件信息失败: ' . $e->getMessage()); + return []; + } + } + + /** + * 获取文件信息 + * + * @param Request $request + * @return \support\Response + */ + public function getFileInfo(Request $request) + { + try { + $fileId = $request->input('id'); + + if (!$fileId) { + return $this->error('文件ID不能为空'); + } + + $uploadedFilesService = new UploadedFilesService(); + $fileInfo = $uploadedFilesService->getFileInfo($fileId); + + if (!$fileInfo) { + return $this->error('文件不存在'); + } + + return $this->success($fileInfo); + } catch (\Throwable $e) { + return $this->error('获取文件信息失败: ' . $e->getMessage()); + } + } + + /** + * 删除文件 + * + * @param Request $request + * @return \support\Response + */ + public function deleteFile(Request $request) + { + try { + $fileId = $request->input('id'); + $path = $request->input('path'); + + $uploadedFilesService = new UploadedFilesService(); + if(!empty($path) && empty($fileId)){ + $where = ['file_path' => $path]; + $file = $uploadedFilesService->getFile($where); + $fileId = $file->id; + }elseif(empty($path) && !empty($fileId)){ + $where = ['id' => $fileId]; + $file = $uploadedFilesService->getFile($where); + $path = $file->file_path; + } + if (!$fileId) { + return $this->error('文件ID与路径不能为空'); + } + + // 删除 OSS 上的文件 + $ossService = new OssService(); + $ossService->deleteFile($path); + + $result = $uploadedFilesService->deleteFile($fileId); + + if ($result) { + return $this->success([], '文件删除成功'); + } else { + return $this->error('文件删除失败'); + } + } catch (\Throwable $e) { + return $this->error('文件删除失败: ' . $e->getMessage()); + } + } + + /** + * 获取分区配置 + * + * @param Request $request + * @return \support\Response + */ + public function getPartitionConfig(Request $request) + { + try { + $uploadedFilesService = new UploadedFilesService(); + $config = $uploadedFilesService->getPartitionConfig(); + + return $this->success($config); + } catch (\Throwable $e) { + return $this->error('获取分区配置失败: ' . $e->getMessage()); + } + } + + + + public function proxy(Request $request) + { + $filePath = $request->get('file'); // 原OSS文件路径 + 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/controller/UserController.php b/app/common/controller/UserController.php new file mode 100644 index 0000000..335f816 --- /dev/null +++ b/app/common/controller/UserController.php @@ -0,0 +1,191 @@ +service = new UserService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } + + + /** + * 获取微信二维码 + */ + public function getQrUrl() + { + try { + $result = $this->service->makeQrUrl(); + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + + /** + * 获取用户信息 + */ + public function getUserInfo(Request $request) + { + try { + $params = $request->all(); + $result = $this->service->userInfo($params['code'], $params['state']); + unset($result['password']); + + if (!isset($result['avatar'])){ + return $this->success($result); + } + + $ossservice = new OssService(); + $result['avatar_pull'] = $result['avatar']; + if(strpos($result['avatar'], 'http') === false && !empty($result['avatar'])){ + $result['avatar_pull'] = $ossservice->generatePrivateUrl($result['avatar']); + } + + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + + + /** + * 用户信息(登录后调用) + */ + public function info(Request $request) + { + try { + $uid = $request->user->uid; + $result = (new UserService())->getUserInfo($uid); + return $this->success($result, '获取用户信息'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 绑定用户 + */ + public function bindUser(Request $request) + { + try { + $params = $request->all(); + $result = $this->service->bindUser($params); + unset($result['password']); + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + + /** + * 修改密码 + * @param Request $request + * @return \support\Response + * @throws \Random\RandomException + */ + public function savePassword(Request $request) + { + try { + $uid = $request->user->uid; + $data = $request->all(); + $result = (new UserService())->savePassword($uid,$data);; + return $this->success($result, '编辑成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 修改名字与头像 + * @param Request $request + * @return \support\Response + * @throws \Random\RandomException + */ + public function save(Request $request) + { + try { + $uid = $request->user->uid; + $data = $request->all(); + $result = (new UserService())->save($uid,$data);; + return $this->success($result, '编辑成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + + + public function wechatLogin($request) + { + try { + $params = $request->all(); + $result = $this->service->wechatLogin($params['login_code'], $params['phone_code']); + + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + + + + /** + * 小程序登录 + */ + public function miniappLogin(Request $request): Response + { + try { + $result = $this->service->miniappLogin($request->all()); + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + + + /** + * 获取手机号 + */ + public function miniappPhoneNumber(Request $request): Response + { + try { + $result = $this->service->miniappPhoneNumber($request->all(),$request); + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/logic/AgreementLogic.php b/app/common/logic/AgreementLogic.php new file mode 100644 index 0000000..99f62a6 --- /dev/null +++ b/app/common/logic/AgreementLogic.php @@ -0,0 +1,17 @@ +validateUser($data['account'], $data['password']); + $system = $this->validateSystem($data['system_code']); + $this->validateSystemPermission($user, $system); + + return $this->generateTokensAndReturn($user, $system, $data); + } + + /** + * 飞书登录 + * 如果用户不存在则新增用户信息 + */ + public function feishuLogin(array $data): array + { + $feishuUserInfo = $this->getFeishuUserInfo($data); + $user = $this->getOrCreateUser($feishuUserInfo); + + $system = $this->validateSystem($data['system_code']); + if ($user->isLogin) { + $this->validateSystemPermission($user, $system); + } + + return $this->generateTokensAndReturn($user, $system, $data); + } + + /** + * 注册 + */ + public function register(array $data) + { + try { + // 获取可用用户编号 + $sn = $this->getAvailableUserSn(); + $data['sn'] = $sn->user_sn; + + // 对密码进行哈希处理 + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + + // 处理账户 + $data = $data + normalizeAccount($data['account']); + + // 创建用户记录 + $user = $this->createUser($data); + + // 更新用户编号的用户 ID + $this->updateUserId($user->id, $sn->user_sn); + + $system = $this->validateSystem($data['system_code']); + return $this->generateTokensAndReturn($user, $system, $data); + } catch (\Throwable $e) { + throw $e; + } + } + + /** + * 查找或创建飞书用户 + */ + private function getOrCreateUser(array $feishuUserInfo) + { + try { + $phone = $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''); + $email = $feishuUserInfo['enterprise_email']; + // $user = User::where('corp_id', $feishuUserInfo['corp_id']) + // ->where(function($query) use ($phone, $email) { + // $query->where('phone', $phone) + // ->orWhere('email', $email); + // }) + // ->first(); + $userByPhone = User::where('corp_id', $feishuUserInfo['corp_id']) + ->where('phone', $phone) + ->where('phone', '!=', '') + ->first(); + $userByEmail = User::where('corp_id', $feishuUserInfo['corp_id']) + ->where('email', $email) + ->where('email', '!=', '') + ->first(); + $user = $userByPhone ?: $userByEmail; + if (!$user) { + // 获取编号 + $userSn = $this->getAvailableUserSn(); + // 创建用户 + $user = User::create([ + 'name' => $feishuUserInfo['name'], + 'avatar' => $feishuUserInfo['avatar_middle'], + 'phone' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''), + 'email' => $feishuUserInfo['enterprise_email'], + '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; + + $options = [ + 'corp_id' => $user->corp_id, + 'user_id' => $user->id + ]; + \Webman\RedisQueue\Redis::send('perm-agt', $options); + } else { + $updated = [ + 'union_id' => $feishuUserInfo['union_id'], + 'user_id' => $feishuUserInfo['user_id'], + 'open_id' => $feishuUserInfo['open_id'], + 'tenant_key' => $feishuUserInfo['tenant_key'] + ]; + if ($phone && $phone == $user->phone) { + $updated['phone'] = $phone; + } + if ($phone && $phone == $user->phone) { + $updated['email'] = $email; + } + // 更新飞书信息 + $user->update($updated); + // 是否登陆 + $user->isLogin = true; + } + return $user; + } catch (\Throwable $e) { + throw $e; + } + } + + /** + * 过滤和格式化手机号 + */ + private function sanitizePhone($mobile) + { + return isset($mobile) ? str_replace('+86', '', $mobile) : ''; + } + + /** + * 生成并记录token,返回包含token的响应数据 + */ + private function generateTokensAndReturn(User $user, Systems $system, array $data): array + { + $payload = $this->buildJwtPayload($user, $system); + $token = $this->generateAccessToken($payload); + $refreshToken = $this->generateRefreshToken($user->id, $data['system_code']); + $homeUrl = $this->getHome($system->id); + + // 记录 token 到 Redis 和 MySQL + $this->recordUserToken($user->id, $token, $data['system_code']); + + return [ + 'access_token' => $token, + 'refresh_token' => $refreshToken, + 'expires_in' => config('jwt.jwt_expiration'), + 'system_code' => $data['system_code'], + 'login_type' => $data['login_type'], + 'home_url' => $homeUrl, + // 'payload' => $payload, + ]; + } + + /** + * 获取飞书用户信息 + */ + private function getFeishuUserInfo(array $data) + { + $certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']); + $feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); + $userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']); + $userInfo = $feishu->userInfo($userAccessToken['access_token']); + + if ($userInfo['code'] != 0) { + throw new BusinessException('授权失败,请重试'); + } + + return $userInfo['data'] + $data + $certificate; + } + + /** + * 更新用户编号 + */ + private function updateUserId($userId, $userSn) + { + $updated = UserSn::where('user_sn', $userSn) + ->update(['user_id' => $userId]); + + if ($updated === 0) { + throw new BusinessException('用户编号不存在或更新失败'); + } + + return $updated; + } + + /** + * 记录用户token到 Redis 和 MySQL + */ + protected function recordUserToken(int $userId, string $token, string $systemCode) + { + $request = request(); + $ip = $request ? $request->getRealIp() : ''; + $ua = $request ? $request->header('user-agent', '') : ''; + $now = time(); + $expireAt = $now + config('jwt.jwt_expiration'); + + // Redis Set 用户token集合 + Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token); + Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration')); + + // Redis Hash token元信息 + $meta = [ + 'system_code' => $systemCode, + 'login_ip' => $ip, + 'user_agent' => $ua, + 'login_time' => $now, + 'expired_at' => $expireAt, + ]; + Redis::hMSet("user_token_meta:{$token}", $meta); + Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration')); + + // 可选:MySQL 持久化 + try { + UserTokenLog::create([ + 'user_id' => $userId, + 'token' => $token, + 'system_code'=> $systemCode, + 'login_ip' => $ip, + 'user_agent' => $ua, + 'login_time' => $now, + 'expired_at' => $expireAt, + 'is_valid' => 1, + ]); + } catch (\Throwable $e) { + Log::error('Token log write failed: '.$e->getMessage()); + } + } + + /** + * 校验token有效性 + */ + public function verifyToken(string $token, string $systemCode): array + { + if (!$systemCode) throw new BusinessException('缺少system_code', 422); + + $payload = (new Jwt())->verifyToken($token); + + // 校验黑名单 + if (Redis::has("token_blacklist:{$token}")) { + throw new BusinessException('token已失效', 403); + } + + // 强制校验aud(系统隔离) + if ($systemCode && $payload['aud'] != $systemCode && $payload['aud'] != ($payload['system_code'] ?? '')) { + throw new BusinessException('token不属于当前系统', 403); + } + + return (array)$payload; + } + + /** + * 获取飞书应用信息 + */ + public function feishuAppInfo(string $corpAbbr, string $systemCode) + { + $corpInfo = $this->validateCorp($corpAbbr); + $systemInfo = $this->validateSystem($systemCode); + $fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemInfo->id); + return [ + 'app_id' => $fsAppInfo->app_id, + 'app_secret' => $fsAppInfo->app_secret, + 'r_url' => $fsAppInfo->r_url, + 'scope' => $fsAppInfo->scopes, + 'state' => $fsAppInfo->state, + 'abbr' => $corpAbbr, + 'corp_id' => $corpInfo->id, + 'system_id' => $systemInfo->id + ]; + } + + /** + * 验证用户信息 + */ + private function validateUser(string $account, string $password) + { +// $user = User::byAccount($account)->first(); + $user = User::byAccount($account)->first(); + if (!$user || !password_verify($password, $user->password)) { + throw new BusinessException('账号或密码错误', 401); + } + if ($user->status != 1) { + throw new BusinessException('账号已禁用', 403); + } + return $user; + } + + /** + * 验证系统信息 + */ + private function validateSystem(string $systemCode) + { + $system = Systems::where('code', $systemCode)->first(); + if (!$system) { + throw new BusinessException('系统不存在', 403); + } + return $system; + } + + /** + * 验证公司信息 + */ + private function validateCorp(string $corpAbbr) + { + $corp = Corp::where('abbr', $corpAbbr)->first(); + if (!$corp) { + throw new BusinessException('公司不存在', 403); + } + return $corp; + } + + /** + * 验证飞书应用信息 + */ + private function validateFeishuApp(int $corpId, int $systemId) + { + $feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_id', $systemId)->first(); + if (!$feishuAppInfo) { + throw new BusinessException('飞书应用不存在', 403); + } + return $feishuAppInfo; + } + + /** + * 验证用户系统权限 + */ + private function validateSystemPermission(User $user, Systems $system) + { + if ($system->is_role) { + $systemIds = $user->systems->pluck('id')->toArray(); + if (!in_array($system->id, $systemIds)) { + throw new BusinessException('无该系统权限', 403); + } + } + } + + /** + * 获取可用的用户编号 + * + * @return \app\common\model\UserSn 用户编号模型实例 + * @throws BusinessException 当未找到适用编号时抛出 + */ + private function getAvailableUserSn(): \app\common\model\UserSn + { + $userSnModel = new UserSn(); + $sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first(); + if (!$sn) { + Log::error('用户注册时未找到适用编号'); + throw new BusinessException('未找到适用编号'); + } + return $sn; + } + + /** + * 创建用户记录 + * + * @param array $data 包含用户信息的数组 + * @return \app\common\model\User 用户模型实例 + */ + private function createUser(array $data): \app\common\model\User + { + try { + return User::create($data); + } catch (\Throwable $e) { + Log::error('用户注册时创建用户记录失败', [ + 'data' => $data, + 'error' => $e->getMessage() + ]); + throw new BusinessException('用户注册失败,请稍后重试', 500); + } + } + + /** + * JWT payload 构建 + * 可扩展 payload 字段 + */ + private function buildJwtPayload(User $user, ?Systems $system = null): array + { + $now = time(); + $exp = $now + config('jwt.jwt_expiration'); + $system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : []; + $role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : []; + + return [ + 'sub' => 'user_' . $user->id, + 'aud' => $system->code, + 'iat' => $now, + 'nbf' => $now, + 'exp' => $exp, + 'typ' => 'access', + 'uid' => $user->id, + 'uname' => $user->name, + 'role_ids' => $role_ids, + 'system_ids' => $system_ids + ]; + } + + /** + * JWT签发 + */ + private function generateAccessToken(array $payload) + { + return (new Jwt())->generateToken($payload); + } + + /** + * 刷新令牌生成 + */ + public function generateRefreshToken(int $userId, string $systemCode) + { + $refreshToken = bin2hex(random_bytes(32)); + // user_id+system_code只允许一个refresh_token生效 + Redis::set("refresh_token:{$userId}:{$systemCode}", $refreshToken, config('jwt.jwt_refresh_token_expiration')); + // 建立refresh_token到user_id+system_code映射,便于校验 + 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]); + } + + + public function getHome($systemId) + { + $info = Resource::where('system_id', $systemId) + ->where('is_home', 1) + ->first(); + + if (!$info) { + return '/dashboard/console'; + } + + // 递归拼接 URL + $url = $this->buildParentUrl($info); + + return $url ?: '/dashboard/console'; + } + + /** + * 递归构建父级 URL + * @param Resource $resource + * @return string + */ + protected function buildParentUrl(Resource $resource) + { + $url = $resource->url ?? ''; + + // 如果存在父级,则递归拼接 + if ($resource->parent_id != 0) { + $parent = Resource::find($resource->parent_id); + if ($parent) { + $parentUrl = $this->buildParentUrl($parent); + $url = $parentUrl . ($url ? '/' . ltrim($url, '/') : ''); + } + } + + return $url; + } +} \ No newline at end of file diff --git a/app/common/logic/BannerLogic.php b/app/common/logic/BannerLogic.php new file mode 100644 index 0000000..dc45d47 --- /dev/null +++ b/app/common/logic/BannerLogic.php @@ -0,0 +1,17 @@ +first(); + + // 获取用户及其关联角色资源和直接分配的资源 + $user = User::with(['roles.resources', 'resources'])->find($userId); + + if (!$user) { + return collect(); + } + + $all = collect(); + // 遍历用户的所有角色,合并每个角色所拥有的资源 + foreach ($user->roles as $role) { + $all = $all->merge( + // 过滤当前角色的资源列表 + $role->resources->filter(function ($resource) use($system) { + // 资源过滤条件: + // 1. !$resource->no_auth:资源需要权限认证(排除公共资源) + // 2. 系统匹配条件: + // - $system->id == $resource->system_id:资源属于当前系统 + // - $resource->system_id == 0:资源为全局公共资源(所有系统可用) + return !$resource->no_auth && ($system->id == $resource->system_id || $resource->system_id == 0); + }) + ); + } + + // 合并用户直接分配的资源,排除不需要权限认证的资源 + $all = $all->merge( + $user->resources->filter(function ($resource) { + // 只保留需要权限认证的资源(或公共资源) + return !$resource->no_auth; + }) + ); + + // 加入公共菜单,公共菜单不管是否授权,所有人都能访问 + $publicResources = Resource::where('no_auth', 1) + ->orderBy('sort', 'asc') // 按 sort 字段升序排序 + ->get(); + + // 合并所有公共资源 + $all = $all->merge($publicResources); + + // 返回唯一的资源列表,并按 id 排序 + // 使用 sortBy('sort') 来确保最终结果根据 'sort' 字段排序 + return $all->unique('id')->values()->sortBy('sort'); + } + + /** + * 构建目录/菜单树 + * + * @param \Illuminate\Support\Collection $resources 全部资源集合 + * @param int $parentId 当前父级资源 ID + * @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; + + // 拼接完整路径 + $currentPath = $parentPath; + if ($currentPath && substr($currentPath, -1) === '/') { + $currentPath = rtrim($currentPath, '/'); + } + if (!empty($node->url)) { + if (!$currentPath) { + $currentPath = $node->url; + } else { + $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) => [ + 'id' => $r->id, + 'title' => $r->name, + 'auth_mark' => $r->code, + ])->values()->all(); + + // 判断是否有子级菜单(非权限) + $hasChildrenMenu = collect($resources)->where('parent_id', $node->id) + ->whereIn('type', ['directory', 'menu'])->isNotEmpty(); + + $componentValue = $hasChildrenMenu ? '/index/index' : $currentPath; + + $tree[] = [ + 'id' => $node->id, + 'name' => $node->name, + 'path' => $node->url, + 'component' => $componentValue, + 'meta' => [ + 'title' => $node->name, + 'icon' => $node->icon, + 'keepAlive' => $node->keep_alive == 1 ? true : false, + 'authList' => $authList, + 'openType' => $node->open_type, + 'isHide' => $node->is_hide == 0 ? false : true, + 'isHideTab' => $node->is_hide_tab == 0 ? false : true + ], + 'children' => $children, + ]; + } + return $tree; + } + + /** + * 获取用户所有权限code + */ + public function getPermissionCodes(int $userId, string $systemCode): array + { + $resources = $this->getUserResources($userId, $systemCode); + 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/PostCategoryLogic.php b/app/common/logic/PostCategoryLogic.php new file mode 100644 index 0000000..4086391 --- /dev/null +++ b/app/common/logic/PostCategoryLogic.php @@ -0,0 +1,21 @@ +where($where)->orderBy('sort','desc')->get(); + } +} \ No newline at end of file diff --git a/app/common/logic/PostCommentLogic.php b/app/common/logic/PostCommentLogic.php new file mode 100644 index 0000000..4ab03e7 --- /dev/null +++ b/app/common/logic/PostCommentLogic.php @@ -0,0 +1,17 @@ +logicListAll($config); + + if ($request->get('role_id')) { + $selected = (new RoleLogic())->getRoleResources($request->get('role_id')); + } else if ($request->get('user_id')) { + $selected = (new RoleLogic())->getUserResources($request->get('user_id')); + } else { + $selected = []; + } + + foreach ($data as &$item) { + $item['selected'] = in_array($item['id'], $selected) ? true : false; + } + + return $data; + } +} \ 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..9ec9c59 --- /dev/null +++ b/app/common/logic/RoleLogic.php @@ -0,0 +1,85 @@ +where('role_id', $roleId)->delete(); + + // 批量插入新角色资源关联 + if (!empty($resources)) { + $insertData = []; + foreach ($resources as $resourceId) { + $insertData[] = [ + 'role_id' => $roleId, + 'resource_id' => $resourceId + ]; + } + Db::table('role_resources')->insert($insertData); + } + + Db::commit(); + return true; + } catch (\Throwable $exception) { + Db::rollBack(); + return false; + } + } + + /** + * 获取角色资源 + * + * @param int $roleId 角色ID + * @return array 资源ID列表 + */ + public function getRoleResources(int $roleId): array + { + if (empty($roleId)) { + return []; + } + + return Db::table('role_resources') + ->where('role_id', $roleId) + ->pluck('resource_id') + ->toArray(); + } + + /** + * 获取用户资源 + * + * @param int $userId 用户ID + * @return array 资源ID列表 + */ + public function getUserResources(int $userId): array + { + if (empty($userId)) { + return []; + } + + return Db::table('user_resources') + ->where('user_id', $userId) + ->pluck('resource_id') + ->toArray(); + } +} diff --git a/app/common/logic/SkuAttributeLogic.php b/app/common/logic/SkuAttributeLogic.php new file mode 100644 index 0000000..393ef64 --- /dev/null +++ b/app/common/logic/SkuAttributeLogic.php @@ -0,0 +1,17 @@ +orderBy('sort', 'asc')->get(['id', 'name', 'code', 'type', 'type']); + } +} \ No newline at end of file diff --git a/app/common/logic/UploadedFilesLogic.php b/app/common/logic/UploadedFilesLogic.php new file mode 100644 index 0000000..3490cf2 --- /dev/null +++ b/app/common/logic/UploadedFilesLogic.php @@ -0,0 +1,169 @@ + $this->modelClass, + 'where' => ['partition' => $partition], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 根据分类获取文件列表 + * + * @param string $category + * @param int $limit + * @param int $page + * @return mixed + */ + public function getFilesByCategory(string $category, int $limit = 15, int $page = 1) + { + $config = [ + 'model' => $this->modelClass, + 'where' => ['category' => $category], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 根据文件类型获取文件列表 + * + * @param string $fileType + * @param int $limit + * @param int $page + * @return mixed + */ + public function getFilesByType(string $fileType, int $limit = 15, int $page = 1) + { + $config = [ + 'model' => $this->modelClass, + 'where' => ['file_type' => $fileType], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 搜索文件 + * + * @param string $keyword + * @param int $limit + * @param int $page + * @return mixed + */ + public function searchFiles(string $keyword, int $limit = 15, int $page = 1) + { + $config = [ + 'model' => $this->modelClass, + 'where' => [ + 'or' => [ + ['file_name', 'like', '%' . $keyword . '%'], + ['original_name', 'like', '%' . $keyword . '%'], + ['category', 'like', '%' . $keyword . '%'] + ] + ], + 'order' => ['created_at' => 'desc', 'id' => 'desc'] + ]; + + return $this->logicList($config, $limit, $page); + } + + /** + * 获取文件统计信息 + * + * @return array + */ + public function getFileStatistics(): array + { + $model = new $this->modelClass(); + + $totalFiles = $model->count(); + $totalSize = $model->sum('file_size'); + + $partitionStats = $model->selectRaw('partition, COUNT(*) as count, SUM(file_size) as size') + ->groupBy('partition') + ->get() + ->toArray(); + + $categoryStats = $model->selectRaw('category, COUNT(*) as count, SUM(file_size) as size') + ->groupBy('category') + ->get() + ->toArray(); + + $typeStats = $model->selectRaw('file_type, COUNT(*) as count, SUM(file_size) as size') + ->groupBy('file_type') + ->get() + ->toArray(); + + return [ + 'total_files' => $totalFiles, + 'total_size' => $totalSize, + 'partition_stats' => $partitionStats, + 'category_stats' => $categoryStats, + 'type_stats' => $typeStats + ]; + } + + /** + * 清理过期文件 + * + * @param int $days 保留天数 + * @return int 清理的文件数量 + */ + public function cleanExpiredFiles(int $days = 30): int + { + $expiredTime = time() - ($days * 24 * 60 * 60); + + $config = [ + 'model' => $this->modelClass, + 'where' => [ + ['created_at', '<', $expiredTime] + ] + ]; + + $files = $this->logicListAll($config); + $count = 0; + + foreach ($files as $file) { + // 删除物理文件 + $fullPath = public_path() . $file->file_path; + if (file_exists($fullPath)) { + unlink($fullPath); + } + + // 删除数据库记录 + $this->logicDeleteBy(['where' => ['id' => $file->id]]); + $count++; + } + + return $count; + } +} \ No newline at end of file diff --git a/app/common/logic/UserLogic.php b/app/common/logic/UserLogic.php new file mode 100644 index 0000000..1d2b489 --- /dev/null +++ b/app/common/logic/UserLogic.php @@ -0,0 +1,207 @@ + 'user_' . $user->id, + 'aud' => 'bsh.yw.hnzhwlkj.cn', + 'iat' => $now, + 'nbf' => $now, + 'exp' => $now + config('jwt.jwt_expiration'), + 'typ' => 'access', + 'user_id' => $user->id, + 'name' => $user->name, + 'meta' => $user->toArray(), + 'uid' => $user->id, + ]; + } + + /** + * 用户信息 + */ + public function userInfo(int $uid) + { + $user = User::find($uid); + + if (!$user) { + throw new BusinessException('账户不存在', 404); + } + + if ($user->status != 1) { + throw new BusinessException('账号已禁用', 403); + } + + return $user; + } + + /** + * 检测用户是否存在 + * + * @param string $openid + * @param string $unionid + * @return User|bool + */ + public function checkUser(string $openid, string $unionid): User|bool + { + $uinfo = User::where('status', 1) + ->where('openid', $openid) + ->where('unionid', $unionid) + ->first(); + return $uinfo === null ? false : $uinfo; + } + + /** + * 绑定用户 + * + * @param array $data + * @return User|bool + */ + public function bindUser(array $data): User|bool + { + $uinfo = User::where('status', 1) + ->where('name', $data['name']) + ->where('mobile', $data['mobile']) + ->where('sn', $data['sn']) + ->first(); + + if ($uinfo !== null) { + $uinfo->update([ + 'openid' => $data['openid'], + 'unionid' => $data['unionid'], + 'nickname' => $data['nickname'], + 'avatar' => $data['avatar'], + 'password' => password_hash($data['password'], PASSWORD_DEFAULT) + ]); + return $uinfo; + } + return false; + } + + /** + * 创建用户 + * + * @param array $data + * @return User + */ + public function createUser(array $data): User + { + return User::create($data); + } + + /** + * 获取微信access_token + * + * @param string $code + * @param string $state + * @return array + */ + public function getAccessToken(string $code, string $state): array + { + $wechat = new WechatService(); + return $wechat->wechatWebAccessToken($code, $state); + } + + /** + * 获取微信用户信息 + * + * @param array $token + * @return array + */ + public function getWechatUserInfo(array $token) + { + $wechat = new WechatService(); + return $wechat->wechatWebUserInfo($token); + } + + /** + * 获取用户列表 + */ + public function getList() + { + $list = User::where('status', 1)->orderBy('id', 'desc')->get(); + $arr = []; + foreach ($list->toArray() as $item) { + $arr[] = [ + 'name' => $item['name'], + 'id' => $item['id'], + ]; + } + return $arr; + } + + /** + * 修改密码 + */ + public function savePassword(int $uid, array $data) + { + $password = password_hash($data['password_new'], PASSWORD_DEFAULT); + $res = User::where('id',$uid)->update(['password' => $password]); + + return $res; + } + + //---------------------------------------------小程序------------------------------------------------------------ + /** + * 获取微信小程序access_token + * + * @param string $code + * @param string $state + * @return array + */ + public function getMiniAccessToken(): array + { + $wechat = new WechatService(); + return $wechat->getMiniAccessToken(); + } + + /** + * 获取 openid / session_key + * + * @param string $code + * @param string $state + * @return array + */ + public function miniCode2Session($loginCode): array + { + $wechat = new WechatService(); + return $wechat->miniCode2Session($loginCode); + } + + /** + * 获取 openid / session_key + * + * @param string $code + * @param string $state + * @return array + */ + public function getMiniPhoneNumber($access_token, $phone_code): array + { + $wechat = new WechatService(); + return $wechat->getMiniPhoneNumber($access_token, $phone_code); + } + +} \ No newline at end of file diff --git a/app/common/model/Agreement.php b/app/common/model/Agreement.php new file mode 100644 index 0000000..1b67451 --- /dev/null +++ b/app/common/model/Agreement.php @@ -0,0 +1,49 @@ +belongsToMany(User::class, 'user_roles', 'role_id', 'user_id'); + } + + // 角色拥有的资源(权限点、菜单) + public function resources() + { + return $this->belongsToMany(Resource::class, 'role_resources', 'role_id', 'resource_id'); + } +} diff --git a/app/common/model/Sku.php b/app/common/model/Sku.php new file mode 100644 index 0000000..c631408 --- /dev/null +++ b/app/common/model/Sku.php @@ -0,0 +1,49 @@ +belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id'); + } + + // 用户直接拥有的资源(用于特殊补权) + public function resources() + { + return $this->belongsToMany(Resource::class, 'user_resources', 'user_id', 'resource_id')->orderBy('sort', 'asc'); + } + + // 用户可访问的系统 + public function systems() + { + return $this->belongsToMany(Systems::class, 'user_systems', 'user_id', 'system_id'); + } + + // 账号查询的作用域 + public function scopeByAccount($query, $account) + { + return $query->where('sn', $account) + ->orWhere('mobile', $account) + ->orWhere('email', $account); + } +} \ No newline at end of file diff --git a/app/common/service/AgreementService.php b/app/common/service/AgreementService.php new file mode 100644 index 0000000..0aa5c72 --- /dev/null +++ b/app/common/service/AgreementService.php @@ -0,0 +1,71 @@ +validator; + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + if (isset($p['type']) && !empty($p['type'])) { + $config['where']['type'] = $p['type']; + } + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/common/service/AuthService.php b/app/common/service/AuthService.php new file mode 100644 index 0000000..4060869 --- /dev/null +++ b/app/common/service/AuthService.php @@ -0,0 +1,87 @@ +feishuAppInfo($corpAbbr, $systemCcode); + $feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']); + return ['auth_url' => $feishu->authorize($certificate)]; + } + + /** + * 登录,支持多类型 + */ + public function login(array $data): array + { + $loginType = $data['login_type'] ?? 'upwd'; + $logic = new AuthLogic(); + + return match ($loginType) { + default => $logic->uPwdLogin($data), + }; + } + + /** + * 用户注册 + */ + public function register(array $data) + { + $logic = new AuthLogic(); + try { + Db::beginTransaction(); + $result = $logic->register($data); + Db::commit(); + return $result; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + + /** + * 校验token + */ + public function verifyToken(string $token, string $systemCode): array + { + return (new AuthLogic())->verifyToken($token, $systemCode); + } + + /** + * 刷新token + * @param string $refreshToken + * @param string|null $systemCode + */ + public function refreshToken(string $refreshToken, string $systemCode): array + { + return (new AuthLogic())->refreshToken($refreshToken, $systemCode); + } + + /** + * 登出(失效token) + */ + public function logout(?string $token, string $systemCode): void + { + (new AuthLogic())->logout($token, $systemCode); + } + + /** + * 强制拉黑token + */ + public function blockToken(string $token, string $systemCode): void + { + (new AuthLogic())->blockToken($token, $systemCode); + } +} \ No newline at end of file diff --git a/app/common/service/BannerService.php b/app/common/service/BannerService.php new file mode 100644 index 0000000..e8e819e --- /dev/null +++ b/app/common/service/BannerService.php @@ -0,0 +1,44 @@ +validator; + } + + public function getBanner($params){ + $where = []; + if(isset($params['module'])){ + $where['module'] = $params['module']; + } + return Banner::where('status','1')->where($where)->get(); + } + + public function getBannerModule(){ + return BannerModule::where('status','1')->get(); + } +} \ No newline at end of file diff --git a/app/common/service/BasicDataService.php b/app/common/service/BasicDataService.php new file mode 100644 index 0000000..44975f0 --- /dev/null +++ b/app/common/service/BasicDataService.php @@ -0,0 +1,119 @@ +checkParamInCache(self::LOGIN_TYPE_KEY, $loginType)) { + $this->cacheLoginTypes(); + if (!$this->checkParamInCache(self::LOGIN_TYPE_KEY, $loginType)) { + throw new BusinessException('无效的登录方式'); + } + } + + // 检查并缓存系统码 + if (!$this->checkParamInCache(self::SYSTEM_CODE_KEY, $systemCode)) { + $this->cacheSystemCodes(); + if (!$this->checkParamInCache(self::SYSTEM_CODE_KEY, $systemCode)) { + throw new BusinessException('无效的系统码'); + } + } + + return true; + } + + /** + * 检查参数是否在Redis集合中 + */ + private function checkParamInCache($cacheKey, $param) + { + return Redis::sIsMember($cacheKey, $param); + } + + /** + * 缓存所有登录类型到集合 + */ + private function cacheLoginTypes() + { + $cacheKey = self::LOGIN_TYPE_KEY; + + // 如果集合已存在,直接返回 +// if (Redis::exists($cacheKey)) { +// return; +// } + + $loginTypes = $this->getAllLoginTypes(); + if (empty($loginTypes)) { + throw new BusinessException('未获取到登录类型数据'); + } + + // 批量添加值到集合 + Redis::sAdd($cacheKey, ...$loginTypes); + Redis::expire($cacheKey, 3600); // 1小时有效期 + } + + /** + * 缓存所有系统码到集合 + */ + private function cacheSystemCodes() + { + $cacheKey = self::SYSTEM_CODE_KEY; + + // 如果集合已存在,直接返回 +// if (Redis::exists($cacheKey)) { +// return; +// } + + $systemCodes = $this->getAllSystemCodes(); + if (empty($systemCodes)) { + throw new BusinessException('未获取到系统码数据'); + } + + // 批量添加值到集合 + Redis::sAdd($cacheKey, ...$systemCodes); + Redis::expire($cacheKey, 3600); // 1小时有效期 + } + + /** + * 获取所有有效的登录类型 + */ + private function getAllLoginTypes() + { + return ['upwd', 'feishu', 'wechat', 'qrcode']; // 示例数据 + } + + /** + * 获取所有有效的系统码 + */ + private function getAllSystemCodes() + { + return Systems::pluck('code')->toArray(); + } + + /** + * 获取所有有效的公司标识 + */ + private function getAllCorpAbbrs() + { + return Corp::pluck('abbr')->toArray(); + } +} \ No newline at end of file diff --git a/app/common/service/MessageNoticeService.php b/app/common/service/MessageNoticeService.php new file mode 100644 index 0000000..eaa6a95 --- /dev/null +++ b/app/common/service/MessageNoticeService.php @@ -0,0 +1,92 @@ +validator; + } + + /** 分页列表 */ + public function serviceSelfList(Request $request, string $scene = 'self') + { + $result = $this->cqyProcess(ucfirst($scene), $request); + if ($result === false) { + $config = $this->buildQueryConfig($request, $scene); + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $result = $this->logicList($config, $limit, $page); + } + return $this->postProcess(ucfirst($scene), $result); + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['order']['id'] = 'desc'; + break; + case 'self': + $config['where']['receiver_user_id'] = $request->user->uid; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + /** + * 添加后置处理 + */ + public function beforeServiceSelf($data, Request $request) + { + MessageNotice::where('receiver_user_id', $request->user->uid)->update(['is_read' => 1]); + return $data; + } +} \ 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..ba4c9e1 --- /dev/null +++ b/app/common/service/OssService.php @@ -0,0 +1,89 @@ +accessKeyId = getenv('OSS_ACCESSID'); + $this->accessKeySecret = getenv('OSS_ACCESSSECRET'); + $this->bucket = getenv('OSS_BUCKET'); + $this->endpoint = getenv('OSS_ENDPOINT'); + $this->domain = getenv('OSS_DOMAIN'); + $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 秒) + $originalUrl = $this->ossClient->signUrl($this->bucket, $objectKey, $expireTime); + +// // 使用正则表达式替换 + $originalUrl = preg_replace( + '#^https?://[^/]+#', + 'https://static.hnzhwlkj.cn', + $originalUrl + ); + return $originalUrl; + } + + /** + * 删除 OSS 上的文件 + * @param string $objectKey OSS上存储的对象名称 + * @return bool 删除是否成功 + */ + public function deleteFile($objectKey) + { + try { + // 删除 OSS 上的文件 + $this->ossClient->deleteObject($this->bucket, $objectKey); + return true; + } catch (OssException $e) { + // 错误处理 + throw new \RuntimeException('OSS Delete Error: ' . $e->getMessage()); + } catch (\Throwable $th) { + // 错误处理 + throw new BusinessException('Delete Error: ' . $th->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/service/PermService.php b/app/common/service/PermService.php new file mode 100644 index 0000000..16030b1 --- /dev/null +++ b/app/common/service/PermService.php @@ -0,0 +1,65 @@ +logic = (new PermLogic()); + } + + // 用户资源缓存key + protected static function cacheKey(int $userId, string $systemCode): string + { + return "user_resources_{$userId}_{$systemCode}"; + } + + /** + * 获取用户所有资源(合并角色+直接分配),结果缓存 + */ + public function getUserResources(int $userId, string $systemCode) + { + $cacheKey = self::cacheKey($userId, $systemCode); + $cached = Cache::get($cacheKey); + if ($cached !== null) { + return $cached; + } + // 调用 Logic 层获取资源 + $resources = $this->logic->getUserResources($userId, $systemCode); + // 缓存结果 + Cache::set($cacheKey, $resources, 3600); + return $resources; + } + + /** + * 构建目录/菜单树,节点包含权限与打开方式 + */ + public function getMenuTree(int $userId, string $systemCode): array + { + // 调用 Logic 层处理菜单构建 + $resources = $this->getUserResources($userId, $systemCode); + return $this->logic->buildTree($resources); + } + + /** + * 获取用户所有权限code + */ + public function getPermissionCodes(int $userId, string $systemCode): array + { + return $this->logic->getPermissionCodes($userId, $systemCode); + } + + /** + * 清除资源相关缓存 + */ + public function clearCache(int $userId, string $systemCode): void + { + Cache::delete(self::cacheKey($userId, $systemCode)); + } +} \ No newline at end of file diff --git a/app/common/service/PostCategoryService.php b/app/common/service/PostCategoryService.php new file mode 100644 index 0000000..d98cf8a --- /dev/null +++ b/app/common/service/PostCategoryService.php @@ -0,0 +1,89 @@ +logic = (new PostCategoryLogic()); + } + + /** + * 获取验证器 + */ + public function getValidator() + { + return new $this->validator; + } + + /** + * 获取系统list + */ + public function getList($params) + { + $where = []; + if(isset($params['parent_id']) && $params['parent_id']) { + $where['parent_id'] = $params['parent_id']; + }else{ + $where['parent_id'] = 0; + } + $result = $this->logic->getList($where)->toArray(); + foreach ($result as $key => $value) { + $where = []; + $where['parent_id'] = $value['id']; + $result[$key]['children'] = $this->logic->getList($where)->toArray(); + foreach ( $result[$key]['children'] as $k => $v) { + $where = []; + $where['parent_id'] = $v['id']; + $result[$key]['children'][$k]['children'] = $this->logic->getList($where)->toArray(); + } + } + return $result; + } + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $brand_id = $data['parent_id'] ?? 0; + if($brand_id == 0) { + $data['level'] = 1; + $data['path'] = ''; + return $data; + } + $parent = PostCategory::where('id', $data['parent_id'])->first(); + $parent['path'] = !empty($parent['path'])?:'/'; + $data['level'] = $parent['level']+1; + $data['path'] = $parent['path'].$parent['id'].'/'; + + if($data['level'] > 3) { + throw new \Exception("分类层级不能超过三层"); + } + + return $data; + } +} \ No newline at end of file diff --git a/app/common/service/PostCommentService.php b/app/common/service/PostCommentService.php new file mode 100644 index 0000000..57bf76b --- /dev/null +++ b/app/common/service/PostCommentService.php @@ -0,0 +1,157 @@ +validator; + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + + // 帖子 + if (isset($p['post_id']) && !empty($p['post_id'])) { + $config['where']['post_id'] = $p['post_id']; + } + // 用户 + if (isset($p['user_id']) && !empty($p['user_id'])) { + $config['where']['user_id'] = $p['user_id']; + } + // 只查询根评论 + if (isset($p['parent_id']) && $p['parent_id'] == 0) { + $config['where']['parent_id'] = 0; + } + // 根评论ID + if (isset($p['parent_id']) && !empty($p['parent_id'])) { + $config['where']['root_id'] = $p['parent_id']; + } + // 根评论ID + if (isset($p['received_id']) && !empty($p['received_id'])) { + $config['where']['id'] = ['not in', $p['received_id']]; + } + // 根评论ID + if (isset($p['status']) && !empty($p['status'])) { + $config['where']['status'] = $p['status']; + } + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $data['user_id'] = $request->user->uid;; + $data['root_id'] = 0; + if(!empty($data['parent_id'])){ + $data['root_id'] = PostComment::where('id', $data['parent_id'])->value('root_id'); + if($data['root_id'] == 0){ + $data['root_id'] = $data['parent_id']; + } + } + + return $data; + } + + /** + * 添加后置处理 + */ + public function afterServiceStore($res) + { + $data = []; + $data['post_id'] = $res['post_id']; + $data['receiver_user_id'] = Post::where(['id' => $data['post_id']])->value('user_id'); + $data['sender_user_id'] = $res['user_id']; + $data['comment_id'] = $res['id']; + $data['type'] = 3; + MessageNotice::create($data); + $post = Post::where(['id' => $data['post_id']])->first(); + $post['comment'] = $post['comment']+1; + $post->save(); + } + + + + /** + * 添加后置处理 + */ + public function serviceDelete(Request $request) + { + $param = $request->all(); + + if (empty($param['id'])) { + throw new \Exception("参数错误"); + } + + $where = []; + $where['id'] = $param['id']; + $comment = PostComment::where($where)->first(); + + $post = Post::where(['id' => $comment['post_id']])->first(); + $post['comment'] = $post['comment']-1; + $post->save(); + + $count = $comment->delete(); + + return $count; + } +} \ No newline at end of file diff --git a/app/common/service/PostFavoriteService.php b/app/common/service/PostFavoriteService.php new file mode 100644 index 0000000..c9da19a --- /dev/null +++ b/app/common/service/PostFavoriteService.php @@ -0,0 +1,138 @@ +validator; + } + + /** 分页列表 */ + public function serviceSelfList(Request $request, string $scene = 'self') + { + $result = $this->cqyProcess(ucfirst($scene), $request); + if ($result === false) { + $config = $this->buildQueryConfig($request, $scene); + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $result = $this->logicList($config, $limit, $page); + } + return $this->postProcess(ucfirst($scene), $result); + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['order']['id'] = 'desc'; + break; + case 'self': + $config['where']['user_id'] = $request->user->uid; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $data['user_id'] = $request->user->uid;; + return $data; + } + + /** + * 添加后置处理 + */ + public function afterServiceStore($res) + { + $data = []; + $data['post_id'] = $res['post_id']; + $data['receiver_user_id'] = Post::where(['id' => $data['post_id']])->value('user_id'); + $data['sender_user_id'] = $res['user_id']; + $data['type'] = 2; + MessageNotice::create($data); + $post = Post::where(['id' => $data['post_id']])->first(); + $post['favorite'] = $post['favorite']+1; + $post->save(); + } + + + /** + * 添加后置处理 + */ + public function serviceDelete(Request $request) + { + $param = $request->all(); + + if (empty($param['post_id']) && empty($param['user_id'])) { + throw new \Exception("参数错误"); + } + + $where = []; + $where['post_id'] = $param['post_id']; + $where['user_id'] = $request->user->uid; + $count = PostFavorite::where($where)->delete(); + + if ($count) { + $post = Post::where(['id' => $param['post_id']])->first(); + $post['favorite'] = $post['favorite']-1; + $post['favorite'] = $post['favorite']<0?0:$post['favorite']; + $post->save(); + } + + return $count; + } +} \ No newline at end of file diff --git a/app/common/service/PostLikeService.php b/app/common/service/PostLikeService.php new file mode 100644 index 0000000..0c56839 --- /dev/null +++ b/app/common/service/PostLikeService.php @@ -0,0 +1,138 @@ +validator; + } + + /** 分页列表 */ + public function serviceSelfList(Request $request, string $scene = 'self') + { + $result = $this->cqyProcess(ucfirst($scene), $request); + if ($result === false) { + $config = $this->buildQueryConfig($request, $scene); + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $result = $this->logicList($config, $limit, $page); + } + return $this->postProcess(ucfirst($scene), $result); + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['order']['id'] = 'desc'; + break; + case 'self': + $config['where']['user_id'] = $request->user->uid; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $data['user_id'] = $request->user->uid;; + return $data; + } + + /** + * 添加后置处理 + */ + public function afterServiceStore($res) + { + $data = []; + $data['post_id'] = $res['post_id']; + $data['receiver_user_id'] = Post::where(['id' => $data['post_id']])->value('user_id'); + $data['sender_user_id'] = $res['user_id']; + $data['type'] = 1; + MessageNotice::create($data); + $post = Post::where(['id' => $data['post_id']])->first(); + $post['like'] = $post['like']+1; + $post->save(); + } + + /** + * 添加后置处理 + */ + public function serviceDelete(Request $request) + { + $param = $request->all(); + + if (empty($param['post_id']) && empty($param['user_id'])) { + throw new \Exception("参数错误"); + } + + $where = []; + $where['post_id'] = $param['post_id']; + $where['user_id'] = $request->user->uid; + $count = PostLike::where($where)->delete(); + + if ($count) { + $post = Post::where(['id' => $param['post_id']])->first(); + $post['like'] = $post['like']-1; + $post['like'] = $post['like']<0?0:$post['like']; + $post->save(); + } + + return $count; + } +} \ No newline at end of file diff --git a/app/common/service/PostMediaService.php b/app/common/service/PostMediaService.php new file mode 100644 index 0000000..cc1c6a3 --- /dev/null +++ b/app/common/service/PostMediaService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/PostService.php b/app/common/service/PostService.php new file mode 100644 index 0000000..92e7b3e --- /dev/null +++ b/app/common/service/PostService.php @@ -0,0 +1,200 @@ +validator; + } + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'index': + if (isset($p['category_id']) && $p['category_id']) { + $config['where']['category_id'] = $p['category_id']; + } + if (isset($p['scene']) && $p['scene']=='self') { + if(!empty($request->user) && !empty($request->user->uid)){ + $config['where']['user_id'] = $request->user->uid; + }else{ + $config['where']['user_id'] = -1; + } + } + if(isset($p['year']) && $p['year']){ + $from = strtotime($p['year'].'-01-01'); + $to = strtotime($p['year'].'-12-31 23:59:59'); + $config['where']['created_at'] = [ + 'between', [$from, $to] + ]; + } + if (isset($p['keyword']) && $p['keyword']) { + $config['where']['content'] = ['like', '%'.$p['keyword'].'%']; + } + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + + if (isset($p['favorite']) && $p['favorite']) { + $config['order']['favorite'] = 'desc'; + } elseif (isset($p['browse']) && $p['browse']) { + $config['order']['browse'] = 'desc'; + } else { + $config['order']['id'] = 'desc'; + } + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + /** + * 添加前置处理 + */ + public function beforeServiceStore($data, Request $request) + { + $data['user_id'] = $request->user->uid;; +// $user = User::where('id', $data['user_id'])->first(); +// if($user->is_release == 0){ +// throw new BusinessException('无发布权限'); +// } + $media = serialize($data['media']); + redis::set('PostMedia_'.$data['user_id'],$media,30); + unset($data['media']); + + return $data; + } + + /** + * 添加后置处理 + */ + public function afterServiceStore($res) + { + $media = redis::get('PostMedia_'.$res['user_id']); + $media = unserialize($media); + if(!is_array($media)){ + $media = explode(',',$media); + } + foreach ($media as $v) { + PostMedia::create([ + 'post_id' => $res->id, + 'media_type' => $this->getMediaTypeByUrl($v), + 'url' => $v, + ]); + } + } + + /** + * 添加前置处理 + */ + public function beforeServiceUpdate($data, Request $request) + { + $data['user_id'] = $request->user->uid;; + $media = serialize($data['media']); + redis::set('PostMedia_'.$data['user_id'],$media,30); + unset($data['media']); + + return $data; + } + + /** + * 添加后置处理 + */ + public function afterServiceUpdate($res) + { + PostMedia::where('post_id', $res->id)->delete(); + $media = redis::get('PostMedia_'.$res['user_id']); + $media = unserialize($media); + foreach ($media as $v) { + PostMedia::create([ + 'post_id' => $res->id, + 'media_type' => $this->getMediaTypeByUrl($v['url']), + 'url' => $v['url'], + ]); + } + } + + /** + * 添加前置处理 + */ + public function beforeServiceShow($data, Request $request) + { + $post = Post::find($data['id']); + if(!$post){ + $post->browse = $post->browse+1; + $post->save(); + } + + return $data; + } + + function getMediaTypeByUrl(string $url): int + { + // 提取后缀 + $ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION)); + + // 图片类型 + $imageExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']; + // 视频类型 + $videoExt = ['mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv']; + + if (in_array($ext, $imageExt)) { + return 1; // 图片 + } + if (in_array($ext, $videoExt)) { + return 2; // 视频 + } + + return 0; // 未知类型 + } +} \ No newline at end of file diff --git a/app/common/service/ResourceService.php b/app/common/service/ResourceService.php new file mode 100644 index 0000000..2d093f8 --- /dev/null +++ b/app/common/service/ResourceService.php @@ -0,0 +1,130 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['system_id'] = ['in', [0, $p['system_id']]]; + $config['where']['status'] = 1; + $config['order']['sort'] = 'asc'; + $config['order']['id'] = 'asc'; + break; + case 'menuList': + $config['where']['status'] = 1; + $config['where']['type'] = ['!=', 'permission']; + $config['order']['sort'] = 'asc'; + $config['order']['id'] = 'asc'; + break; + case 'menuTreeList': + $config['where']['system_id'] = ['in', [0, $p['system_id']]]; + $config['where']['status'] = 1; + $config['order']['sort'] = 'asc'; + $config['order']['id'] = 'asc'; + break; + case 'index': + $config['where']['status'] = 1; + $config['order']['sort'] = 'asc'; + $config['order']['id'] = 'asc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + public function serviceMenuList(Request $request) + { + $config = $this->buildQueryConfig($request, 'menuList'); + $result = $this->logicListAll($config); + return $this->postProcess('MenuList', $result); + } + + public function serviceMenuTreeList(Request $request) + { + $config = $this->buildQueryConfig($request, 'menuTreeList'); + $result = (new ResourceLogic())->logicListAllResources($request, $config); + return $this->postProcess('MenuTreeList', $result); + } + + + protected function afterServiceDefault($data) + { + $data = Tree::buildMenuTreeWithReference($data->toArray()); + return $data; + } + + protected function afterServiceMenuList($data) + { + $data = Tree::buildMenuTree($data->toArray()); + return $data; + } + + protected function afterServiceMenuTreeList($data) + { + $data = Tree::buildMenuTree($data->toArray()); + return $data; + } + + + public function beforeServiceStore($data, Request $request) + { + $data['parent_id'] = $data['parent_id'] != '' ? $data['parent_id'] : 0; + $data['system_id'] = $data['system_id'] != '' ? $data['system_id'] : 0; + return $data; + } + + public function beforeServiceUpdate($data, Request $request) + { + $data['parent_id'] = $data['parent_id'] != '' ? $data['parent_id'] : 0; + $data['system_id'] = $data['system_id'] != '' ? $data['system_id'] : 0; + return $data; + } +} \ 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..fc9b3e8 --- /dev/null +++ b/app/common/service/RoleService.php @@ -0,0 +1,50 @@ +validator; + } + + /** + * 获取角色资源 + */ + public function menuResource($request, $data) + { + return (new RoleLogic)->updateRoleResources($data); + } + + + public function getList() + { + $config['where']['status'] = 1; + $config['order']['sort'] = 'asc'; + $config['order']['id'] = 'asc'; + return $this->logicListAll($config); + } +} \ No newline at end of file diff --git a/app/common/service/SkuAttributeService.php b/app/common/service/SkuAttributeService.php new file mode 100644 index 0000000..4b1a5bf --- /dev/null +++ b/app/common/service/SkuAttributeService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/SkuAttributeValueService.php b/app/common/service/SkuAttributeValueService.php new file mode 100644 index 0000000..654370c --- /dev/null +++ b/app/common/service/SkuAttributeValueService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/SkuService.php b/app/common/service/SkuService.php new file mode 100644 index 0000000..9941dbc --- /dev/null +++ b/app/common/service/SkuService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/service/SystemsService.php b/app/common/service/SystemsService.php new file mode 100644 index 0000000..10b80a3 --- /dev/null +++ b/app/common/service/SystemsService.php @@ -0,0 +1,24 @@ +logic = (new SystemsLogic()); + } + + /** + * 获取系统list + */ + public function getList() + { + return $this->logic->getList(); + } +} \ No newline at end of file diff --git a/app/common/service/UploadService.php b/app/common/service/UploadService.php new file mode 100644 index 0000000..14f3ca3 --- /dev/null +++ b/app/common/service/UploadService.php @@ -0,0 +1,177 @@ +findFile(['file_path' => $candidatePath]); + if (!$existing) { + break; + } + // 若存在同路径同名,且类型也相同,则追加序号 + if (isset($existing->file_type) && strtolower($existing->file_type) === strtolower($mime)) { + $suffix = ' (' . $counter . ')'; + $candidateName = $nameBody . $suffix . ($nameExt !== '' ? ('.' . $nameExt) : ''); + $counter++; + continue; + } + // 类型不同则允许同名,结束循环 + break; + } + + $objectKey = $dir . $candidateName; + + $ossService = new OssService(); + $ossService->uploadImage($filePath, $objectKey); + $privateUrl = $ossService->generatePrivateUrl($objectKey); + + // 记录文件信息到数据库 + $uploadedFilesService = new UploadedFilesService(); + $fileInfo = [ + 'file_name' => basename($objectKey), + 'file_path' => $objectKey, + 'file_size' => $size, + 'file_type' => $mime, + 'original_name' => $originalName, + ]; + $recordResult = $uploadedFilesService->recordFileInfo($fileInfo, null, $scene, $category); + + $data = [ + 'original_name' => $originalName, + 'file_name' => $objectKey, + 'file_url' => $privateUrl, + 'file_id' => $recordResult['id'] ?? null, + 'partition' => $scene, + 'category' => $category, + ]; + + return $data; + } catch (\Throwable $e) { + Log::error($e->getMessage(),$param); + throw new BusinessException($e->getMessage()); + } + } + + /** + * 删除 OSS 上的文件 + * + * @param array $param + * - object_key: OSS 对象键名-oss路径(必需) + * - file_id: 数据库文件记录ID(可选,用于删除数据库记录) + * @return bool 删除是否成功 + * + * 示例 + * $uploadService = new UploadService(); + * + * 只删除 OSS 文件 + * $uploadService->deleteOssFile([ + * 'object_key' => 'vehicle_album/documents/20241201/abc123.pdf' + * ]); + * + * 同时删除 OSS 文件和数据库记录 + * $uploadService->deleteOssFile([ + * 'object_key' => 'vehicle_album/images/20241201/abc123.jpg', + * 'file_id' => 123 + * ]); + */ + public function deleteOssFile($objectKey,$fileId = null) + { + try { + + if (empty($objectKey)) { + Log::error('OSS对象键名不能为空', $objectKey); + throw new BusinessException('OSS对象键名不能为空'); + } + + $uploadedFilesService = new UploadedFilesService(); + // 如果提供了文件ID,同时删除数据库记录 + if ($fileId) { + $uploadedFilesService->deleteFile($fileId); + }else{ + $uploadedFilesService = new UploadedFilesService(); + if (empty($fileId)) { + $where = ['file_path' => $objectKey]; + $file = $uploadedFilesService->getFile($where); + $fileId = $file->id; + } + if(!empty($fileId)){ + $uploadedFilesService->deleteFile($fileId); + } + } + + // 删除 OSS 上的文件 + $ossService = new OssService(); + $ossService->deleteFile($objectKey); + + Log::info('OSS文件删除成功', ['object_key' => $objectKey, 'file_id' => $fileId]); + return true; + } catch (\Throwable $e) { + Log::error('删除OSS文件失败: ' . $e->getMessage()); + throw new BusinessException('删除OSS文件失败: ' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/common/service/UploadedFilesService.php b/app/common/service/UploadedFilesService.php new file mode 100644 index 0000000..ed01db2 --- /dev/null +++ b/app/common/service/UploadedFilesService.php @@ -0,0 +1,238 @@ + [ + 'path' => 'images', + 'max_size' => 5 * 1024 * 1024, // 5MB + 'allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], + 'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'] + ], + 'document' => [ + 'path' => 'documents', + 'max_size' => 10 * 1024 * 1024, // 10MB + 'allowed_types' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + 'extensions' => ['pdf', 'doc', 'docx'] + ], + 'audio' => [ + 'path' => 'audios', + 'max_size' => 20 * 1024 * 1024, // 20MB + 'allowed_types' => ['audio/mp3', 'audio/wav', 'audio/ogg'], + 'extensions' => ['mp3', 'wav', 'ogg'] + ], + 'other' => [ + 'path' => 'others', + 'max_size' => 5 * 1024 * 1024, // 5MB + 'allowed_types' => [], + 'extensions' => [] + ] + ]; + + /** + * 记录文件信息到数据库 + * + * @param array $fileInfo 文件信息 + * @param Request|null $request 请求对象(可选) + * @param string $partition 分区名称 + * @param string $category 文件分类 + * @return array + * @throws BusinessException + */ + public function recordFileInfo(array $fileInfo, ?Request $request = null, string $partition = 'other', string $category = 'general'): array + { + + // 验证必需的文件信息 + $requiredFields = ['file_name', 'file_path', 'file_size', 'file_type']; + foreach ($requiredFields as $field) { + if (!isset($fileInfo[$field])) { + throw new BusinessException("缺少必需的文件信息字段: {$field}"); + } + } + + // 获取文件扩展名 + $fileExtension = pathinfo($fileInfo['file_name'], PATHINFO_EXTENSION); + + // 记录到数据库 + $uploadIp = $request ? $request->getRealIp() : '0.0.0.0'; + $userAgent = $request ? $request->header('User-Agent', '') : ''; + + $fileData = [ + 'file_name' => $fileInfo['file_name'], + 'file_path' => $fileInfo['file_path'], + 'file_size' => $fileInfo['file_size'], + 'file_type' => $fileInfo['file_type'], + 'category' => $category, + 'original_name' => $fileInfo['original_name'] ?? '', + 'extension' => $fileExtension, + 'partition' => $partition, + 'upload_ip' => $uploadIp, + 'user_agent' => $userAgent, + ]; + + $uploadedFile = $this->logicCreate($fileData); + + return [ + 'id' => $uploadedFile->id, + 'file_name' => $fileInfo['file_name'], + 'file_path' => $fileInfo['file_path'], + 'file_url' => getenv('MAIN_DOMAIN') . $fileInfo['file_path'], + 'file_size' => $fileInfo['file_size'], + 'file_type' => $fileInfo['file_type'], + 'category' => $category, + 'partition' => $partition, + 'original_name' => $fileInfo['original_name'] ?? '', + 'extension' => $fileExtension, + ]; + } + + /** + * 获取文件信息 + * + * @param int $fileId + * @return array|null + */ + public function getFileInfo(int $fileId): ?array + { + $file = $this->logicShow(['where' => ['id' => $fileId]]); + + if (!$file) { + return null; + } + + return [ + 'id' => $file->id, + 'file_name' => $file->file_name, + 'file_path' => $file->file_path, + 'file_url' => getenv('MAIN_DOMAIN') . $file->file_path, + 'file_size' => $file->file_size, + 'file_type' => $file->file_type, + 'category' => $file->category, + 'partition' => $file->partition ?? 'other', + 'original_name' => $file->original_name ?? '', + 'extension' => $file->extension ?? '', + 'created_at' => $file->created_at, + ]; + } + + /** + * 获取文件 + * + * @param string $path + * @return bool|\Illuminate\Database\Eloquent\Model + */ + public function getFile(array $where) + { + $config['model'] = $this->modelClass; + $config['where'] = $where; + $file = $this->logicShow($config); + + if (!$file) { + return false; + } + + return $file; + } + /** + * 获取文件 + * + * @param string $path + * @return bool|\Illuminate\Database\Eloquent\Model + */ + public function findFile(array $where) + { + $file = UploadedFiles::where(['file_path' => $where['file_path']]); + + if (!$file) { + return false; + } + + return $file; + } + + /** + * 删除文件 + * + * @param int $fileId + * @return bool + */ + public function deleteFile(int $fileId): bool + { + $config['model'] = $this->modelClass; + $config['where'] = ['id' => $fileId]; + $file = $this->logicShow($config); + + if (!$file) { + return false; + } + + // 删除数据库记录 + $config =[ + 'model' => $this->modelClass, + 'where' => ['id' => $fileId], + ]; + return $this->logicDeleteBy($config); + } + + /** + * 获取分区配置 + * + * @return array + */ + public function getPartitionConfig(): array + { + return $this->uploadPartitions; + } + + /** + * 生成文件名 + * + * @param $file + * @return string + */ + protected function generateFileName($file): string + { + $extension = $file->getUploadExtension(); + $timestamp = time(); + $random = substr(md5(uniqid()), 0, 8); + + return $timestamp . '_' . $random . '.' . $extension; + } + + /** + * 格式化文件大小 + * + * @param int $bytes + * @return string + */ + protected function formatFileSize(int $bytes): string + { + $units = ['B', 'KB', 'MB', 'GB']; + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + $bytes /= pow(1024, $pow); + + return round($bytes, 2) . ' ' . $units[$pow]; + } +} \ No newline at end of file diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php new file mode 100644 index 0000000..7c66bb5 --- /dev/null +++ b/app/common/service/UserService.php @@ -0,0 +1,315 @@ +logic = (new UserLogic()); + } + + /** + * 验证器 + */ + protected $validator = UserValidate::class; + + /** + * 获取验证器 + */ + public function getValidator() + { + return new $this->validator; + } + + /** + * 获取微信二维码 + */ + public function makeQrUrl() + { + $wechat = new WechatService(); + return $wechat->makeQrUrl(); + } + + /** + * 获取用户信息 + */ + public function userInfo(string $code, string $state) + { + $logic = new UserLogic(); + $accessToken = $logic->getAccessToken($code, $state); +// $accessToken['openid'] = 'op9DT6xBzVF05dypmQ2JsM5DP6yw'; +// $accessToken['unionid'] = 'o04nT6-St_2MZR6VRduYEwiicmiA'; + $userInfo = $logic->checkUser($accessToken['openid'], $accessToken['unionid']); + + if ($userInfo === false) { + return [ + 'is_bind' => false, + 'access_token' => $accessToken['access_token'], + 'openid' => $accessToken['openid'], + 'unionid' => $accessToken['unionid'], + ]; + } + + $jwt = new Jwt(); + $token = $jwt->generateToken($logic->buildJwtPayload($userInfo)); + $refreshToken = (new AuthLogic())->generateRefreshToken($userInfo->id, 'board'); + return array_merge($userInfo->toArray(), ['access_token' => $token, 'is_bind' => true, 'refresh_token'=> $refreshToken, 'expires_in' => config('jwt.jwt_expiration')]); + } + + + /** + * 获取用户信息(包括菜单和权限) + */ + public function getUserInfo(int $uid) + { + $user = $this->logic->userInfo($uid); + $ossservice = new OssService(); + $user['avatar_pull'] = $user->avatar; + if(strpos($user->avatar, 'http') === false && !empty($user->avatar)){ + $user['avatar_pull'] = $ossservice->generatePrivateUrl($user->avatar); +// $user['avatar_pull'] = '/proxy/ossurl?file='.$user->avatar; + } + return [ + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'avatar' => $user->avatar, + 'avatar_pull' => $user['avatar_pull'], + 'mobile' => $user->mobile, + 'email' => $user->email, + 'nickname' => $user->nickname, + 'status' => $user->status, + 'openid' => $user->openid, + 'unionid' => $user->unionid, + ], + ]; + } + + /** + * 绑定用户 + * + * @param array $data + * @return array + */ + public function bindUser(array $data) + { + $logic = new UserLogic(); + $wechat = [ + 'access_token' => $data['access_token'], + 'openid' => $data['openid'], + ]; + $wechatUserInfo = $logic->getWechatUserInfo($wechat); + $uData = [ + 'nickname' => $wechatUserInfo['nickname'], + 'avatar' => $wechatUserInfo['headimgurl'], + 'unionid' => $wechatUserInfo['unionid'], + 'openid' => $wechatUserInfo['openid'], + 'name' => $data['name'], + 'mobile' => $data['mobile'], + 'sn' => $data['sn'], + 'password' => $data['password'], + ]; + $userinfo = $logic->bindUser($uData); + if ($userinfo === false) { + throw new BusinessException('未查到用户信息'); + } + + $jwt = new Jwt(); + $refreshToken = (new AuthLogic())->generateRefreshToken($userinfo->id, 'board'); + return array_merge($userinfo->toArray(), [ + 'access_token' => $jwt->generateToken($logic->buildJwtPayload($userinfo)), + 'is_bind' => true, + 'refresh_token'=> $refreshToken, + 'expires_in' => config('jwt.jwt_expiration') + ]); + } + + + /** + * 获取用户列表 + */ + public function getList() + { + return $this->logic->getList(); + } + + /** + * 修改用户密码 + */ + public function savePassword(int $uid, array $data) + { + // 对密码进行哈希处理 + $password = $data['password']; + $user = User::find($uid); + if (!password_verify($password, $user->password)) { + throw new BusinessException('密码错误'); + } + + $user = $this->logic->savePassword($uid,$data); + return $user; + } + + /** + * 修改用户密码 + */ + public function save(int $uid, array $data) + { + // 对密码进行哈希处理 + $user = User::find($uid); + $keysToKeep = ['name', 'nickname', 'avatar']; + + $data = array_intersect_key($data, array_flip($keysToKeep)); + + $user = $user->update($data); + return $user; + } + + /** + * 获取微信小程序用户信息(登录及获取手机号) + */ + public function wechatLogin(string $loginCode, string $phoneCode) + { + $logic = new UserLogic(); + //$accessToken = $logic->getAccessToken($code, $state); + + $session = $logic->miniCode2Session($loginCode); + if (!isset($session['openid'])) { + throw new BusinessException('登录失败'); +// return json(['code'=>0, 'msg'=>'登录失败', 'data'=>$session]); + } + + $openid = $session['openid']; + + $tokenData = $logic->getMiniAccessToken(); + $access_token = $tokenData['access_token']; + + // 换手机号 + $phoneData = $logic->getMiniPhoneNumber($access_token, $phoneCode); + + if (!isset($phoneData['phone_info']['phoneNumber'])) { + throw new BusinessException('获取手机号失败'); +// return json(['code'=>0, 'msg'=>'获取手机号失败', 'data'=>$phoneData]); + } + + $phone = $phoneData['phone_info']['phoneNumber']; + + $user = User::firstOrCreate(['phone' => $phone], [ + 'openid' => $openid + ]); + +// $userInfo = $logic->checkUser($accessToken['openid'], $accessToken['unionid']); +// +// if ($userInfo === false) { +// return [ +// 'is_bind' => false, +// 'access_token' => $accessToken['access_token'], +// 'openid' => $accessToken['openid'], +// 'unionid' => $accessToken['unionid'], +// ]; +// } + + $jwt = new Jwt(); + $JwtPayload = $logic->buildJwtPayload($user); + $JwtPayload['exp'] = $JwtPayload['exp'] + (86400 * 30); + $token = $jwt->generateToken($JwtPayload); + $refreshToken = (new AuthLogic())->generateRefreshToken($user->id, 'info'); + return array_merge($user->toArray(), ['access_token' => $token, 'is_bind' => true, 'refresh_token'=> $refreshToken, 'expires_in' => config('jwt.jwt_expiration')]); + } + + + /** + * 小程序登录 + */ + public function miniappLogin(array $data) + { + $logic = new UserLogic(); + $wechatService = new WechatService(); + $wxinfo = $wechatService->xcxOpenid($data['code']); + + $name = '微信用户_' . self::generate(8); + // 用户信息 + $user = User::firstOrCreate( + ['openid' => $wxinfo['openid']], + [ + 'nickname' => $name, + 'unionid' => $wxinfo['unionid'], + 'name' => $name, + 'avatar' => 'bsh_datax/user/20251021/a4ee815405a1506abeec1af0f738f70f_68f6e76514b66.jpg' + ] + ); + + + $jwt = new Jwt(); + $JwtPayload = $logic->buildJwtPayload($user); + $JwtPayload['exp'] = $JwtPayload['exp'] + (86400 * 30); + $token = $jwt->generateToken($JwtPayload); + $refreshToken = (new AuthLogic())->generateRefreshToken($user->id, 'info'); + + return array_merge($user->toArray(), ['token' => $token, 'is_bind' => true, 'refresh_token'=> $refreshToken, 'expires_in' => config('jwt.jwt_expiration')]); + } + + /** + * 获取手机号 + */ + public function miniappPhoneNumber(array $data,Request $request) + { + // 手机号 + $wechatService = new WechatService(); + $wxinfo = $wechatService->xcxPhoneNumber($data['phone_code']); + + $uinfo = User::where('id', $request->user->uid)->first(); + $uinfo->mobile = $wxinfo['phone_info']['phoneNumber']; + $uinfo->save(); + + return $wxinfo; + } + + /** + * 生成随机字符串编码 + * + * @param int $length 字符串长度 + * @param bool $uppercaseOnly 是否只使用大写字母 + * @return string + */ + public static function generate(int $length = 8, bool $uppercaseOnly = false): string + { + $characters = self::$characters; + + if ($uppercaseOnly) { + $characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + } + + $charactersLength = strlen($characters); + $randomString = ''; + + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[random_int(0, $charactersLength - 1)]; + } + + return $randomString; + } +} \ No newline at end of file diff --git a/app/common/service/WechatService.php b/app/common/service/WechatService.php new file mode 100644 index 0000000..f9517f8 --- /dev/null +++ b/app/common/service/WechatService.php @@ -0,0 +1,236 @@ +web = $cfg['web']; + $this->mini = $cfg['mini']; + $this->login = $cfg['login']; + } + + /** + * 获取微信二维码 + */ + public function makeQrUrl() + { + try { + $state = bin2hex(random_bytes(16)); + $ttl = (int)($this->login['state_ttl'] ?? 300); + + // 记录 state + Redis::setEx("wx:login:state:$state", $ttl, json_encode([ + 'redirect' => $this->web['redirect'], + 'ts' => time(), + ], JSON_UNESCAPED_UNICODE)); + + // 组装跳转地址 + $qrUrl = sprintf( + 'https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=%s&scope=%s&state=%s#wechat_redirect', + $this->web['app_id'], + urlencode($this->web['redirect']), + 'code', + 'snsapi_login', + $state, + ); + + $data = [ + 'state' => $state, + 'qr_url' => $qrUrl + ]; + return $data; + } catch (\Throwable $th) { + // var_dump($th->getFile().'|'.$th->getLine().'|'.$th->getMessage()); + throw new BusinessException('获取微信二维码失败'); + } + } + + /** + * 获取微信access_token + */ + public function wechatWebAccessToken(string $code, string $state): array + { + try { + // 校验 state + $stateJson = Redis::get("wx:login:state:$state"); + if (!$stateJson) { + throw new \RuntimeException('state 已过期或无效'); + } + + // 拼接地址 + $tokenUrl = sprintf( + 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code', + $this->web['app_id'], + $this->web['app_secret'], + $code + ); + + // code 换 token + $token = $this->httpGetJson($tokenUrl); + if (!isset($token['access_token'], $token['openid'])) { + throw new \RuntimeException('换取 access_token 失败:'. json_encode($token, JSON_UNESCAPED_UNICODE)); + } + return $token; + } catch (\Throwable $th) { + throw new BusinessException('获取 access_token 失败'); + } + } + + /** + * 获取微信用户信息 + * + * @param array $token + * @return array + */ + public function wechatWebUserInfo(array $accessToken): array + { + try { + // 拉取用户信息 + return $this->httpGetJson(sprintf( + 'https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s', + $accessToken['access_token'], + $accessToken['openid'] + )); + } catch (\Throwable $th) { + // var_dump($th->getFile().'|'.$th->getLine().'|'.$th->getMessage()); + throw new BusinessException('获取用户信息失败'); + } + } + + /** + * tools - httpGetJson + */ + protected function httpGetJson(string $url): array + { + $client = new Client([ + 'verify' => false, // 禁用 SSL 验证 + 'timeout' => 10, + ]); + $json = $client->get($url)->getBody()->getContents(); + return json_decode($json, true) ?: []; + } + + + //----------------------------小程序登录---------------------------------------- + + /** + * 获取 openid / session_key + * @param $code + * @return mixed + */ + public function miniCode2Session($code) + { + $appid = $this->mini['app_id']; + $secret = $this->mini['app_secret']; + $url = "https://api.weixin.qq.com/sns/jscode2session?appid=$appid&secret=$secret&js_code=$code&grant_type=authorization_code"; + return json_decode(file_get_contents($url), true); + } + + /** + * 获取 access_token + * @return mixed + */ + public function getMiniAccessToken() + { + $appid = $this->mini['app_id']; + $secret = $this->mini['app_secret']; + $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appid&secret=$secret"; + return json_decode(file_get_contents($url), true); + } + + /** + * 调用微信接口取手机号 + * @param $access_token + * @param $phone_code + * @return mixed + */ + public function getMiniPhoneNumber($access_token, $phone_code) + { + $url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={$access_token}"; + + $data = json_encode([ + 'code' => $phone_code + ]); + + $options = [ + 'http' => [ + 'header' => "Content-type: application/json", + 'method' => 'POST', + 'content' => $data, + ], + ]; + + return json_decode(file_get_contents($url, false, stream_context_create($options)), true); + } + + /** + * 小程序获取openid + * @param string $code + * @return array + * @throws BusinessException + */ + public function xcxOpenid(string $code): array + { + + $config = [ + 'app_id' => getenv('WECHAT_MINI_APPID'), + 'secret' => getenv('WECHAT_MINI_SECRET'), + 'response_type' => 'array', + 'log' => [ + 'level' => 'debug', + 'file' => runtime_path().'/mzl/wechat.log', + ] + ]; + // 创建微信小程序对象 + $wechat = new xcxApplication($config); + // 获取 session_key 和 openid + return $wechat->getUtils()->codeToSession($code); + } + + /** + * 小程序获取手机号 + * @param string $phone_code + * @return mixed + * @throws BusinessException + */ + public function xcxPhoneNumber(string $phone_code) + { + $config = [ + 'app_id' => getenv('WECHAT_MINI_APPID'), + 'secret' => getenv('WECHAT_MINI_SECRET'), + 'response_type' => 'array', + 'log' => [ + 'level' => 'debug', + 'file' => runtime_path().'/mzl/wechat.log', + ] + ]; + // 创建微信小程序对象 + $wechat = new xcxApplication($config); + // getPhoneNumber + $data = ['code' => $phone_code]; + $result = $wechat->getClient()->postJson('wxa/business/getuserphonenumber', $data); + if (isset($result['phone_info'])) { + // 业务处理 $result['phone_info']['phoneNumber'] + return $result; + } else { + throw new BusinessException($result['errmsg']); + } + } +} \ 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..adde8e6 --- /dev/null +++ b/app/common/traits/DataTransformer.php @@ -0,0 +1,152 @@ + callable 字段名格式化回调,如 fn($f)=>snake_case($f) + * - 'postFormatter' => callable 结果级加工回调,接收整个数组并返回 + * + * @return array 转换并加工后的纯 PHP 数组 + */ + protected function transformData( + mixed $rawData, + ?callable $postFormatter = null, + ?callable $fieldFormatter = null + ): array { + try { + $fieldFormatter = $fieldFormatter ?: fn($field) => $field; + if ($rawData instanceof LengthAwarePaginator) { + $arr = $this->formatPaginated($rawData, $postFormatter, $fieldFormatter); + } elseif ($rawData instanceof Model) { + $arr = $this->formatModel($rawData, $fieldFormatter); + } elseif ($rawData instanceof Collection) { + $arr = $this->formatCollection($rawData, $fieldFormatter); + } elseif (is_array($rawData)) { + $arr = $this->formatArray($rawData, $fieldFormatter); + } else { + $arr = (array)$rawData; + } + + if (is_callable($postFormatter)) { + $arr = $postFormatter($arr); + } + + return $arr; + } catch (\Throwable $e) { + return [ + 'error' => true, + 'message' => $e->getMessage(), + // 'message' => $e->getMessage()."\n".$e->getLine()."\n".$e->getFile(), + 'code' => $e->getCode(), + ]; + } + } + + /** + * 格式化分页数据:转换每条记录 + * + * @param LengthAwarePaginator $p + * @param callable $fmt + * @return array{records: array, total: int, current: int, size: int} + */ + protected function formatPaginated( + LengthAwarePaginator $p, + ?callable $postFormatter, + callable $fmt + ): array { + $records = $p->getCollection() + ->map(fn($item) => $this->transformData($item, $postFormatter, $fmt)) + ->toArray(); + + return [ + 'records' => $records, + 'total' => $p->total(), + 'current' => $p->currentPage(), + 'size' => $p->perPage(), + ]; + } + + /** + * 格式化 Eloquent 模型:主表属性 + 关联递归 + * + * @param Model $model + * @param callable $fmt + * @return array + */ + protected function formatModel( + Model $model, + callable $fmt + ): array { + $out = []; + // 1) 主表字段 + foreach ($model->getAttributes() as $key => $value) { + $out[$fmt($key)] = $value; + } + // 2) 关联递归 + foreach ($model->getRelations() as $rel => $relData) { + if ($relData instanceof Model) { + $out[$rel] = $this->formatModel($relData, $fmt); + } elseif ($relData instanceof Collection) { + $out[$rel] = $relData + ->map(fn($item) => $this->transformData($item, null, $fmt)) + ->toArray(); + } else { + $out[$rel] = $relData; + } + } + return $out; + } + + /** + * 格式化集合数据:对每个元素递归调用 transformData + * + * @param Collection $c + * @param callable $fmt + * @return array + */ + protected function formatCollection( + Collection $c, + callable $fmt + ): array { + return $c + ->map(fn($item) => $this->transformData($item, null, $fmt)) + ->toArray(); + } + + /** + * 格式化普通数组:递归处理可能出现的模型或集合 + * + * @param array $arr + * @param callable $fmt + * @return array + */ + protected function formatArray( + array $arr, + callable $fmt + ): array { + return array_map(function($item) use ($fmt) { + if (is_array($item) + || $item instanceof Model + || $item instanceof Collection + || $item instanceof LengthAwarePaginator + ) { + return $this->transformData($item, null, $fmt); + } + return $item; + }, $arr); + } +} \ No newline at end of file diff --git a/app/common/traits/ResponseTrait.php b/app/common/traits/ResponseTrait.php new file mode 100644 index 0000000..cc20773 --- /dev/null +++ b/app/common/traits/ResponseTrait.php @@ -0,0 +1,77 @@ + 'application/json'] + ): Response { + return $this->result($status, $code, $httpStatus, $message, $data, $header); + } + + /** + * 返回错误响应 + * + * @param string $message 响应错误消息,默认“请求失败” + * @param int $code 内部状态码,默认 1 表示错误 + * @param int $httpStatus HTTP 状态码,默认 400 + * @param mixed $data 可选的错误数据 + * @param array $header 响应头,默认 JSON 格式 + * @return Response + */ + public function error( + string $message = '请求失败', + int $code = 400, + int $httpStatus = 400, + $data = [], + array $header = ['Content-Type' => 'application/json'] + ): Response { + return $this->result('fail', $code, $httpStatus, $message, $data, $header); + } + + /** + * 统一响应格式方法 + * + * @param string $status 响应状态,例如 "success" 或 "fail" + * @param int $code 内部状态码 + * @param int $httpStatus HTTP 状态码 + * @param string $message 响应消息 + * @param mixed $data 响应数据 + * @param array $header 响应头 + * @return Response + */ + protected function result( + string $status, + int $code, + int $httpStatus, + string $message, + $data, + array $header + ): Response { + $body = [ + 'status' => $status, + 'code' => $code, + 'message' => $message, + 'data' => $data, + ]; + return response(json_encode($body, JSON_UNESCAPED_UNICODE), $httpStatus, $header); + } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudControllerTrait.php b/app/common/traits/crud/CrudControllerTrait.php new file mode 100644 index 0000000..ca30ba3 --- /dev/null +++ b/app/common/traits/crud/CrudControllerTrait.php @@ -0,0 +1,148 @@ +service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 获取详情 + * + * @param Request $request 请求对象 + * @param int|string $id 资源 ID + * @return mixed 返回资源详情数据 + */ + public function show(Request $request) + { + try { + $raw = $this->service->serviceShow($request); + $data = $this->transformData($raw, method_exists($this, 'showFormat') + ? [$this, 'showFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 创建 + * + * @param Request $request 请求对象 + * @return mixed 返回创建成功的数据或信息 + */ + public function store(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'store'); + $res = $this->service->serviceStore($request, $data); + return $this->success($res, '创建成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 更新 + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function update(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'update'); + $res = $this->service->serviceUpdate($request, $data); + return $this->success([], '更新成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 删除 + * + * @param Request $request 请求对象 + * @return mixed 返回删除操作结果 + */ + public function destroy(Request $request) + { + try { + $count = $this->service->serviceDestroy($request); + return $this->success([], "成功删除了 {$count} 条"); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + // ---------------------------------------------------------------- + // 以下三个钩子方法,**在使用此 Trait 的 Controller** 中实现 + // ---------------------------------------------------------------- + + /** + * 字段名格式化:例如全部下划线 + * + * @param string $field + * @return string + */ + // protected function fieldFormatter(string $field): string + // { + // return snake_case($field); + // } + + /** + * 字段级后置加工规则 + * + * @return array + * 键为 “点号嵌套”或 “通配符” 字段路径, + * 值为 fn($value, $row)=>mixed + */ + // protected function getFieldTransforms(): array + // { + // return [ + // 'name' => fn($v,$row)=>strtoupper($v), + // 'orders.*.total' => fn($v,$row)=>number_format($v,2), + // ]; + // } + + /** + * 结果级后置加工:接收整个数组,可任意增删改 + * + * @param array $data + * @return array + */ + // protected function postFormat(array $data): array + // { + // // 例如重命名 records 为 items + // if (isset($data['records'])) { + // $data['items'] = $data['records']; + // unset($data['records']); + // } + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudLogicTrait.php b/app/common/traits/crud/CrudLogicTrait.php new file mode 100644 index 0000000..98c133d --- /dev/null +++ b/app/common/traits/crud/CrudLogicTrait.php @@ -0,0 +1,80 @@ +modelClass; + return CrudUtil::paginate(CrudUtil::buildQuery($config), $limit, $page); + } + + /** + * 全量列表 + */ + public function logicListAll(array $config) + { + $config['model'] = $this->modelClass; + return CrudUtil::getAll(CrudUtil::buildQuery($config)); + } + + /** + * 查询单条 + */ + public function logicShow(array $config) + { + return CrudUtil::findOneBy(CrudUtil::buildQuery($config)); + } + + /** + * 创建(可带 dataLimit) + */ + public function logicCreate(array $data, array $dataLimit = []) + { + return CrudUtil::create($this->modelClass, $data, $dataLimit); + } + + /** + * 任意条件更新(可带 dataLimit) + */ + public function logicUpdateBy(array $config, array $data) + { + return CrudUtil::updateBy(CrudUtil::buildQuery($config), $data); + } + + /** + * 任意条件删除(可带 dataLimit) + */ + public function logicDeleteBy(array $config) + { + return CrudUtil::deleteBy(CrudUtil::buildQuery($config)); + } + + /** + * 复杂查询回调 + */ + public function logicComplex(callable $callback) + { + return CrudUtil::executeComplex($this->modelClass, $callback); + } + + /** + * 根据主键查询 + */ + public function findByPk(int $id, array $with = []) + { + return CrudUtil::findByPk($this->modelClass, $id, $with); + } +} diff --git a/app/common/traits/crud/CrudServiceTrait.php b/app/common/traits/crud/CrudServiceTrait.php new file mode 100644 index 0000000..d956cad --- /dev/null +++ b/app/common/traits/crud/CrudServiceTrait.php @@ -0,0 +1,202 @@ +validator; + } + + /** 分页列表 */ + public function serviceList(Request $request, string $scene = 'index') + { + $result = $this->cqyProcess(ucfirst($scene), $request); + if ($result === false) { + $config = $this->buildQueryConfig($request, $scene); + $limit = (int)$request->get('size', 15); + $page = (int)$request->get('current', 1); + $result = $this->logicList($config, $limit, $page); + } + return $this->postProcess(ucfirst($scene), $result); + } + + /** 全量列表 */ + public function serviceListAll(Request $request, string $scene = 'default') + { + $result = $this->cqyProcess(ucfirst($scene), $request); + if ($result === false) { + $config = $this->buildQueryConfig($request, $scene); + $result = $this->logicListAll($config); + } + return $this->postProcess(ucfirst($scene), $result); + } + + /** 查看单条 */ + public function serviceShow(Request $request, string $scene = 'show') + { + $config = $this->buildQueryConfig($request, $scene); + $item = $this->logicShow($config); + return $this->postProcess(ucfirst($scene), $item); + } + + /** 创建 */ + public function serviceStore(Request $request, array $data = [], string $scene = 'store') + { + $data = $this->preProcess(ucfirst($scene), $data, $request); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); + return $this->postProcess(ucfirst($scene), $created); + } + + /** 更新 */ + public function serviceUpdate(Request $request, array $data = [], string $scene = 'update') + { + $data = $this->preProcess(ucfirst($scene), $data, $request); + $config = $this->buildQueryConfig($request, $scene); + $updated = $this->logicUpdateBy($config, $data); + return $this->postProcess(ucfirst($scene), $data); + } + + /** 删除 */ + public function serviceDestroy(Request $request, string $scene = 'destroy') + { + $config = $this->buildQueryConfig($request, $scene); + $deleted = $this->logicDeleteBy($config); + return $this->postProcess(ucfirst($scene), $deleted); + } + + /** ----------------- 公共辅助方法 ----------------- */ + + /** + * 构造 CrudUtil 查询配置 + */ + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'default': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'index': + $config['where']['status'] = 1; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; + break; + case 'show': + $config['where']['id'] = $p['id']; + $config['where']['status'] = 1; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } + + /** 解析 withFields 字符串 "rel:field,field;..." */ + protected function parseWithFields(string $str): array + { + if (!$str) { + return []; + } + $out = []; + foreach (explode(';', $str) as $pair) { + [$rel, $flds] = explode(':', $pair); + $out[$rel] = explode(',', $flds); + } + return $out; + } + + /** 从请求读取 dataLimitType, dataLimitField */ + protected function getDataLimitFromRequest(Request $req): array + { + $type = $req->get('dataLimitType'); + if (!$type) { + return []; + } + return [ + 'type' => $type, + 'field' => $req->get('dataLimitField', 'admin_id'), + ]; + } + + /** + * $data 前置处理 + * + * @param array $data + * @param Request $request + * @return array + */ + protected function preProcess(string $op, mixed $data, Request $request): mixed + { + $hook = "beforeService{$op}"; + return method_exists($this, $hook) + ? $this->{$hook}($data, $request) + : $data; + } + + /** + * 根据操作类型,调用对应的后置钩子 + * + * @param string $op 操作:List, Show, Store, Update, Destroy + * @param mixed $data 原始返回值 + * @return mixed 处理后的返回值 + */ + protected function postProcess(string $op, mixed $data): mixed + { + $hook = "afterService{$op}"; + if (method_exists($this, $hook)) { + $args = func_get_args(); + array_shift($args); + array_shift($args); + $params = array_merge([$data], $args); + return call_user_func_array([$this, $hook], $params); + } + return $data; + } + + /** + * 复杂/自定义查询 + * + * @param string $op 场景 + * @param mixed $data 原始返回值 + * @return mixed 处理后的返回值 + */ + protected function cqyProcess(string $op, Request $request): mixed + { + $hook = "complexQuery{$op}"; + return method_exists($this, $hook) + ? $this->{$hook}($request) + : false; + } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudUtil.php b/app/common/traits/crud/CrudUtil.php new file mode 100644 index 0000000..ade8b3f --- /dev/null +++ b/app/common/traits/crud/CrudUtil.php @@ -0,0 +1,808 @@ + User::class, + * 'where' => [ + * 'status' => 1, + * 'name' => ['like','%张%'], + * 'roles.name' => '管理员', // 关联表字段过滤 + * 'posts.created_at'=> ['>','2025-01-01'], + * ], + * 'order' => ['id'=>'desc'], + * 'fields' => ['id','name','email'], + * 'with' => ['roles','posts'], + * 'withFields' => [ + * 'roles' => ['id','name'], // 关联表字段选择 + * 'posts' => ['id','title','user_id'], + * ], + * 'dataLimit' => [ + * 'type' => 'personal|same_role|role_tree|relation', // personal:个人数据,same_role:同角色,role_tree:角色树,relation:从属关联数据 + * 'userId' => 1, + * 'roleId' => 2, + * 'userField' => 'user_id', + * 'roleField' => 'role_id' + * ] + * ]; + */ + public static function buildQuery(array $config): Builder + { + /** @var Model $model */ + $model = new $config['model']; + $q = $model->newQuery(); + + // eager-load:支持 ['rel','rel2'] 和 ['rel'=>fields[], ...] + if (! empty($config['with'])) { + foreach ($config['with'] as $rel => $fields) { + if (is_int($rel)) { + // 普通关联 + $q->with($fields); + } else { + // 带字段筛选的关联 + $q->with([$rel => function ($qr) use ($fields) { + $key = $qr->getModel()->getKeyName(); + if (! in_array($key, $fields, true)) { + $fields[] = $key; + } + $qr->select($fields); + }]); + } + } + } + + // 表字段 + if (!empty($config['fields'])) { + $q->select($config['fields']); + } + + // where + if (!empty($config['where'])) { + foreach ($config['where'] as $col => $val) { + if (strpos($col, '.') !== false) { + // 多层支持 + $segments = explode('.', $col); + $leaf = array_pop($segments); + self::applyNestedWhere($q, $segments, $leaf, $val); + } else { + self::applyWhere($q, $col, $val); + } + } + } + + // or where + if (!empty($config['orWhere'])) { + $q->where(function($q) use($config) { + foreach ($config['orWhere'] as $cond) { + [$col,$op,$val] = $cond; + $q->orWhere($col,$op,$val); + } + }); + } + + // 排序 + if (!empty($config['order'])) { + foreach ($config['order'] as $col => $dir) { + $q->orderBy($col, strtolower($dir) === 'asc' ? 'asc' : 'desc'); + } + } + + // 数据权限过滤 + if (!empty($config['dataLimit'])) { + self::applyDataLimit($q, $config['dataLimit']); + } + + return $q; + } + + /** + * 分页查询 + * + * @param Builder $query + * @param int $perPage + * @param int $page + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public static function paginate(Builder $q, int $perPage, int $page) + { + return $q->paginate($perPage, ['*'], 'page', $page); + } + + /** + * 非分页查询 + * + * @param Builder $query + * @return \Illuminate\Database\Eloquent\Collection + */ + public static function getAll(Builder $q) + { + return $q->get(); + } + + /** + * 执行复杂查询——仅在显式调用时生效 + * + * @param string $modelClass 模型类 + * @param callable $callback function(Builder $q) { ... } + * @return mixed 由 callback 返回的结果 + */ + public static function executeComplex(string $modelClass, callable $cb) + { + /** @var Model $model */ + $model = new $modelClass; + return $cb($model->newQuery()); + } + + /** + * 主键单条查询 + * + * @param string $modelClass + * @param mixed $id + * @param array $with + * @return Model + * @throws BusinessException + */ + public static function findByPk(string $modelClass, $id, array $with = []): Model + { + $m = $modelClass::with($with)->find($id); + if (!$m) { + throw new BusinessException("记录 ID={$id} 不存在", 404); + } + return $m; + } + + /** + * 任意条件单条查询,支持 dataLimit + * + * @param Builder $query + * @return Model + * @throws BusinessException + */ + public static function findOneBy(Builder $q): Model + { + $model = $q->first(); + if (!$model) { + throw new BusinessException("记录不存在", 404); + } + return $model; + } + + /** + * 创建,支持 dataLimit + * + * @param string $modelClass + * @param array $data + * @param array $dataLimit + * @return Model + * @throws BusinessException + */ + public static function create( + string $modelClass, + array $data, + array $dataLimit = [] + ): Model { + // dataLimit 检查/注入 + if (!empty($dataLimit)) { + self::applyDataLimitOnMutate($data, $dataLimit); + } + return $modelClass::create($data); + } + + /** + * 主键更新,支持 dataLimit + * + * @param string $modelClass + * @param mixed $id + * @param array $data + * @param array $dataLimit + * @return Model + * @throws BusinessException + */ + public static function updateByPk( + string $modelClass, + $id, + array $data, + array $dataLimit = [] + ): Model { + /** @var Model $m */ + $m = $modelClass::find($id); + if (!$m) { + throw new BusinessException("记录 ID={$id} 不存在", 404); + } + // dataLimit 校验 + if (!empty($dataLimit)) { + self::applyDataLimitOnMutate($m, $dataLimit, $data); + } + $m->fill($data)->save(); + return $m; + } + + /** + * 非主键条件更新(批量),支持 dataLimit + * + * @param Builder $query + * @param array $data + * @return int 受影响行数 + */ + public static function updateBy(Builder $q, array $data): int + { + return $q->update($data); + } + + /** + * 主键删除(支持批量) + * + * @param string $modelClass + * @param array $ids + * @return int 删除条数 + */ + public static function deleteByPk(string $modelClass, array $ids): int + { + return $modelClass::destroy($ids); + } + + /** + * 非主键条件删除,支持 dataLimit + * + * @param Builder $query + * @return int 删除条数 + */ + public static function deleteBy(Builder $q): int + { + return $q->delete(); + } + + + //=================== Internal Methods ===================// + + + /** + * 支持多层的 whereHas:$relations=['roles','permissions'],$leaf='name' + */ + protected static function applyNestedWhere(Builder $q, array $relations, string $leaf, $val): void + { + $rel = array_shift($relations); + $q->whereHas($rel, function (Builder $qr) use ($relations, $leaf, $val) { + if ($relations) { + // 还有更深层,继续递归 + self::applyNestedWhere($qr, $relations, $leaf, $val); + } else { + // 最后一层,使用普通 applyWhere + self::applyWhere($qr, $leaf, $val); + } + }); + } + + /** + * 单字段 where 支持: + * - 多条件数组 [ [op1,val1], [op2,val2], … ] + * - 单条件 ['op', val] + * - 直接值 + * + * @param Builder $q + * @param string $col + * @param mixed $val + * @return void + */ + protected static function applyWhere(Builder $q, string $col, $val): void + { + // 多条件数组 + if (is_array($val) && isset($val[0]) && is_array($val[0])) { + foreach ($val as $pair) { + if (is_array($pair) && count($pair) === 2) { + self::applySingleWhere($q, $col, $pair[0], $pair[1]); + } + } + } + // 单条件 + elseif (is_array($val) && count($val) === 2) { + self::applySingleWhere($q, $col, $val[0], $val[1]); + } + // 直接等值 + else { + $q->where($col, $val); + } + } + + /** + * 执行条件 where + */ + protected static function applySingleWhere(Builder $q, string $col, string $op, $v): void + { + switch (strtolower($op)) { + case 'like': + $q->where($col, 'like', $v); + break; + case 'in': + $q->whereIn($col, (array)$v); + break; + case 'not in': + $q->whereNotIn($col, (array)$v); + break; + case 'null': + $q->whereNull($col); + break; + case 'not null': + $q->whereNotNull($col); + break; + case 'between': + $q->whereBetween($col, (array)$v); + break; + default: + $q->where($col, $op, $v); + } + } + + + // ================== Data Limit ================== + + /** + * 应用数据权限过滤到查询(buildQuery 场景) + */ + protected static function applyDataLimit(Builder $q, array $dataLimit): void + { + $type = $dataLimit['type'] ?? 'personal'; + $userId = $dataLimit['userId'] ?? 0; + $roleId = $dataLimit['roleId'] ?? 0; + $userField = $dataLimit['userField'] ?? 'user_id'; + $roleField = $dataLimit['roleField'] ?? 'role_id'; + + self::applyQueryPermission($q, $type, $userId, $roleId, $userField, $roleField); + } + + /** + * 应用数据权限到"创建/更新"操作 + * + * - $target 可能是 array|Model 对象 + * - $newData 仅在 updateByPk 场景时传入,用以校验变更归属 + */ + protected static function applyDataLimitOnMutate(&$target, array $dataLimit, array $newData = []): void + { + $type = $dataLimit['type'] ?? 'personal'; + $userId = $dataLimit['userId'] ?? 0; + $roleId = $dataLimit['roleId'] ?? 0; + $userField = $dataLimit['userField'] ?? 'user_id'; + $roleField = $dataLimit['roleField'] ?? 'role_id'; + + self::applyMutationPermission($target, $type, $userId, $roleId, $newData, $userField, $roleField); + } + + /** + * 应用数据权限过滤到查询 + * + * @param Builder $query 查询构造器 + * @param string $type 权限类型:superadmin, personal, same_role, role_tree, relation + * @param int $currentUserId 当前用户ID + * @param int $currentRoleId 当前角色ID + * @param string $userField 用户ID字段名 + * @param string $roleField 角色ID字段名 + */ + public static function applyQueryPermission( + Builder $query, + string $type, + int $currentUserId = 0, + int $currentRoleId = 0, + string $userField = 'user_id', + string $roleField = 'role_id' + ): void { + // 超管不需要过滤 + if (self::isSuperAdmin($currentUserId)) { + return; + } + + switch ($type) { + case 'personal': + // 个人权限:只能访问自己的数据 + $query->where($userField, $currentUserId); + break; + + case 'same_role': + // 同角色权限:访问同角色的数据 + $userIds = self::getSameRoleUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'role_tree': + // 角色树权限:访问自己角色及子角色的数据 + $userIds = self::getRoleTreeUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'relation': + // 从属关系权限:访问自己及下属的数据 + $userIds = self::getRelationUserIds($currentUserId); + $query->whereIn($userField, $userIds); + break; + + default: + // 默认个人权限 + $query->where($userField, $currentUserId); + } + } + + /** + * 应用数据权限到创建/更新操作 + * + * @param mixed $target 创建时为数组,更新时为Model对象 + * @param string $type 权限类型 + * @param int $currentUserId 当前用户ID + * @param int $currentRoleId 当前角色ID + * @param array $newData 更新时的数据 + * @param string $userField 用户ID字段名 + * @param string $roleField 角色ID字段名 + */ + public static function applyMutationPermission( + &$target, + string $type, + int $currentUserId, + int $currentRoleId, + array $newData = [], + string $userField = 'user_id', + string $roleField = 'role_id' + ): void { + // 超管不需要检查 + if (self::isSuperAdmin($currentUserId)) { + return; + } + + switch ($type) { + case 'personal': + self::handlePersonalPermission($target, $newData, $userField, $currentUserId); + break; + + case 'same_role': + self::handleSameRolePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'role_tree': + self::handleRoleTreePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'relation': + self::handleRelationPermission($target, $newData, $userField, $currentUserId); + break; + + default: + throw new BusinessException("未定义的数据权限类型: $type", 403); + } + } + + /** + * 获取同角色的用户ID列表 + * + * @param int $roleId 角色ID + * @return array 用户ID列表 + */ + public static function getSameRoleUserIds(int $roleId): array + { + return Db::table('user_roles') + ->where('role_id', $roleId) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取角色树下的所有用户ID列表 + * + * @param int $roleId 当前角色ID + * @return array 用户ID列表 + */ + public static function getRoleTreeUserIds(int $roleId): array + { + // 获取角色树中的所有角色ID + $roleIds = self::getRoleTreeIds($roleId); + + // 获取这些角色下的所有用户ID + return Db::table('user_roles') + ->whereIn('role_id', $roleIds) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取从属关系下的用户ID列表(包括自己和下属) + * + * @param int $userId 当前用户ID + * @return array 用户ID列表 + */ + public static function getRelationUserIds(int $userId): array + { + // 获取所有下属用户ID(递归) + $subordinateIds = self::getSubordinateIds($userId); + + // 返回自己和所有下属的ID + return array_merge([$userId], $subordinateIds); + } + + /** + * 获取直接下属的用户ID列表 + * + * @param int $userId 用户ID + * @return array 直接下属用户ID列表 + */ + public static function getDirectSubordinateIds(int $userId): array + { + return Db::table('user') + ->where('report_to', $userId) + ->pluck('id') + ->toArray(); + } + + /** + * 递归获取所有下属用户ID列表 + * + * @param int $userId 用户ID + * @return array 所有下属用户ID列表 + */ + private static function getSubordinateIds(int $userId): array + { + $subordinates = self::getDirectSubordinateIds($userId); + $allSubordinates = $subordinates; + + foreach ($subordinates as $subordinateId) { + $allSubordinates = array_merge($allSubordinates, self::getSubordinateIds($subordinateId)); + } + + return $allSubordinates; + } + + /** + * 处理个人权限 + */ + private static function handlePersonalPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + if (is_array($target)) { + // 创建时强制设置用户ID + $target[$userField] = $currentUserId; + } else { + // 更新时校验原始记录 + if ($target->{$userField} !== $currentUserId) { + throw new BusinessException("无权操作该记录", 403); + } + + // 禁止变更归属字段 + if (isset($newData[$userField]) && $newData[$userField] !== $currentUserId) { + throw new BusinessException("无权变更归属字段", 403); + } + } + } + + /** + * 处理同角色权限 + */ + private static function handleSameRolePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取同角色用户ID列表 + $sameRoleUserIds = self::getSameRoleUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时设置归属用户并校验 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录 + if (!in_array($target->{$userField}, $sameRoleUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 禁止变更归属字段到无权限的用户 + if (isset($newData[$userField]) && !in_array($newData[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权变更归属字段", 403); + } + } + } + + /** + * 处理角色树权限 + */ + private static function handleRoleTreePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取角色树用户ID列表 + $roleTreeUserIds = self::getRoleTreeUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $roleTreeUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 处理从属关系权限 + */ + private static function handleRelationPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + // 获取从属关系用户ID列表 + $relationUserIds = self::getRelationUserIds($currentUserId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $relationUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentUserId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $relationUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $relationUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 获取角色树ID(当前角色及其所有子角色) + * 从数据库中动态获取角色树关系 + */ + private static function getRoleTreeIds(int $roleId): array + { + static $roleTreeCache = []; + + if (isset($roleTreeCache[$roleId])) { + return $roleTreeCache[$roleId]; + } + + // 从数据库中递归获取角色树 + $ids = [$roleId]; + $queue = [$roleId]; + + while (!empty($queue)) { + $current = array_shift($queue); + // 查找当前角色的所有子角色 + $childRoles = Db::table('roles') + ->where('parent_id', $current) + ->where('status', 1) // 只获取启用的角色 + ->pluck('id') + ->toArray(); + + foreach ($childRoles as $childId) { + if (!in_array($childId, $ids)) { + $ids[] = $childId; + $queue[] = $childId; + } + } + } + + $roleTreeCache[$roleId] = $ids; + return $ids; + } + + /** + * 检查是否为超管 + * + * @param int $userId 用户ID + * @return bool 是否为超管 + */ + public static function isSuperAdmin(int $userId): bool + { + // 检查用户是否关联了超级管理员角色 + // 设定角色ID为1的是超级管理员角色 + $superAdminRoleId = 1; + + return Db::table('user_roles') + ->where('user_id', $userId) + ->where('role_id', $superAdminRoleId) + ->exists(); + } + + /** + * 获取用户的角色ID + * + * @param int $userId 用户ID + * @return int 角色ID + */ + public static function getUserIdRoleId(int $userId): int + { + // 从 user_roles 表中获取当前用户的角色ID + $roleId = Db::table('user_roles') + ->where('user_id', $userId) + ->value('role_id'); + + return $roleId ?: 0; + } + + /** + * 获取用户的所有角色ID(包括继承的角色) + * + * @param int $userId 用户ID + * @return array 角色ID列表 + */ + public static function getUserAllRoleIds(int $userId): array + { + // 获取用户直接关联的角色 + $directRoleIds = Db::table('user_roles') + ->where('user_id', $userId) + ->pluck('role_id') + ->toArray(); + + // 获取所有继承的角色 + $allRoleIds = []; + foreach ($directRoleIds as $roleId) { + $allRoleIds = array_merge($allRoleIds, self::getRoleTreeIds($roleId)); + } + + // 去重并返回 + return array_unique($allRoleIds); + } +} \ No newline at end of file diff --git a/app/common/utils/Jwt.php b/app/common/utils/Jwt.php new file mode 100644 index 0000000..c270e0d --- /dev/null +++ b/app/common/utils/Jwt.php @@ -0,0 +1,108 @@ +secretKey = config('jwt.jwt_secret_key'); + $this->algorithm = config('jwt.jwt_algorithm'); + $this->issuer = config('jwt.jwt_issuer'); + $this->expiration = config('jwt.jwt_expiration'); + } + + /** + * 生成 JWT 令牌 + * + * @param array $payload + * @return string + * @throws BusinessException + */ + public function generateToken(array $payload): string + { + $defaultPayload = [ + // // —— 注册 Claim —— + 'iss' => $this->issuer, // 签发者 + // 'sub' => 'user_1001', // 面向的用户 + // 'aud' => 'api.gateway.example.com', // 接收方 + // 'iat' => time(), // 签发时间 + // 'nbf' => time(), // 生效时间 + // 'exp' => time() + $this->expiration, // 1 小时后过期 + 'jti' => bin2hex(random_bytes(16)), // 唯一 ID + // // —— 自定义 Claim —— + // 'uid' => 1001, // 业务用户 ID + // 'roles' => ['admin', 'editor'], // 用户角色 + // 'meta' => ['region' => 'CN', 'tier' => 5], // 任意自定义数据 + ]; + + $payload = array_merge($defaultPayload, $payload); + + try { + return FirebaseJWT::encode($payload, $this->secretKey, $this->algorithm); + } catch (\Exception $e) { + throw new BusinessException("令牌生成失败:" . $e->getMessage(), 500); + } + } + + /** + * 验证 JWT 令牌 + * + * @param string $token + * @return object + * @throws BusinessException + */ + public function verifyToken(string $token) + { + try { + return FirebaseJWT::decode($token, new Key($this->secretKey, $this->algorithm)); + } catch (\Firebase\JWT\ExpiredException $e) { + throw new BusinessException('令牌已过期', 401); + } catch (\Firebase\JWT\SignatureInvalidException $e) { + throw new BusinessException('无效的令牌签名', 401); + } catch (\Firebase\JWT\BeforeValidException $e) { + throw new BusinessException('令牌尚未生效', 401); + } catch (\Exception $e) { + throw new BusinessException('令牌验证失败:' . $e->getMessage(), 400); + } + } + + + public function describe() + { + /* payload + 1. 注册 Claim 详解 + + Claim 名称 含义 类型 建议取值 + iss Issuer,发行者,标识该 JWT 的签发主体 string or URI 你的服务域名或项目标识,比如 https://api.example.com + sub Subject,主题,标识该 JWT 所面向的用户 string or int 用户在你系统里的唯一 ID + aud Audience,受众,标识该 JWT 的接收方 string or array 接收该令牌的服务标识,比如 ["service-a","service-b"] + exp Expiration Time,过期时间,Unix 时间戳,到达此时间后不可用 int time() + 3600(1 小时后过期) + nbf Not Before,生效时间,Unix 时间戳,表示在此时间之前不可用 int time() 或者 time() + 60(1 分钟后生效) + iat Issued At,签发时间,Unix 时间戳 int time() + jti JWT ID,JWT 的唯一标识符,用于去重或一次性令牌 string uniqid('', true) 或者 UUID + + 注意:exp、nbf、iat 必须是整数(秒级时间戳),否则库会抛类型异常或验证失败。 + + 2. 自定义 Claim + + 除了上面这些“注册”字段,你可以任意添加,比如: + + php + $payload['uid'] = 123; // 业务用户 ID + $payload['roles'] = ['admin','user']; // 权限数组 + $payload['feature'] = 'beta'; // 标记某个功能开关 + + 这些字段在你的应用程序里可以自由取用。 + */ + } +} \ No newline at end of file diff --git a/app/common/utils/TableInspector.php b/app/common/utils/TableInspector.php new file mode 100644 index 0000000..6b00995 --- /dev/null +++ b/app/common/utils/TableInspector.php @@ -0,0 +1,57 @@ +getDatabaseName(); + + $columns = Db::select(" + SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, + COLUMN_DEFAULT, COLUMN_COMMENT, EXTRA + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION + ", [$database, $table]); + + return array_map(function ($col) { + return [ + 'name' => $col->COLUMN_NAME, + 'type' => $col->DATA_TYPE, + 'column_type' => $col->COLUMN_TYPE, + 'nullable' => $col->IS_NULLABLE === 'YES', + 'default' => $col->COLUMN_DEFAULT, + 'comment' => $col->COLUMN_COMMENT, + 'extra' => $col->EXTRA, + ]; + }, $columns); + } + + /** + * 获取字段名列表 + */ + public static function getColumnNames(string $table, ?string $database = null): array + { + return array_column(self::getColumns($table, $database), 'name'); + } + + /** + * 获取字段注释 map:字段名 => 注释 + */ + public static function getColumnComments(string $table, ?string $database = null): array + { + $columns = self::getColumns($table, $database); + $map = []; + foreach ($columns as $col) { + $map[$col['name']] = $col['comment']; + } + return $map; + } +} \ No newline at end of file diff --git a/app/common/utils/Tree.php b/app/common/utils/Tree.php new file mode 100644 index 0000000..b831185 --- /dev/null +++ b/app/common/utils/Tree.php @@ -0,0 +1,505 @@ + $item) { + // if ($item[$pidField] == 0) { + // $tree[] = &$items[$id]; + // } else { + // if (isset($items[$item[$pidField]])) { + // $items[$id]['level'] = $items[$item[$pidField]]['level'] + 1; + + // if (!isset($items[$item[$pidField]]['children'])) { + // $items[$item[$pidField]]['children'] = []; + // } + + // $items[$item[$pidField]]['children'][] = &$items[$id]; + // } + // } + // } + + // return $tree; + // } + + // /** + // * 将树形结构扁平化为列表(带层级前缀) + // * + // * @param array $tree 树形结构数据 + // * @param string $prefix 前缀字符 + // * @param string $field 显示字段名 + // * @param int $level 层级 + // * @return array + // */ + // public static function flattenWithPrefix(array $tree, string $prefix = '--', string $field = 'name', int $level = 0): array + // { + // $result = []; + + // foreach ($tree as $item) { + // $item[$field . '_with_prefix'] = str_repeat($prefix, $item['level']) . $item[$field]; + // $result[] = $item; + + // if (isset($item['children']) && !empty($item['children'])) { + // $children = static::flattenWithPrefix($item['children'], $prefix, $field, $item['level'] + 1); + // $result = array_merge($result, $children); + // } + // } + + // return $result; + // } + + // /** + // * 根据父ID查找所有子节点(包括子孙节点) + // * + // * @param array $data 数据列表 + // * @param int $parentId 父级ID + // * @param string $pidField 父级字段名 + // * @param string $idField ID字段名 + // * @return array + // */ + // public static function findChildren(array $data, int $parentId, string $pidField = 'parent_id', string $idField = 'id'): array + // { + // $children = []; + + // foreach ($data as $item) { + // if ($item[$pidField] == $parentId) { + // $children[] = $item; + // $grandChildren = static::findChildren($data, $item[$idField], $pidField, $idField); + // $children = array_merge($children, $grandChildren); + // } + // } + + // return $children; + // } + + // /** + // * 获取指定节点的所有父级节点 + // * + // * @param array $data 数据列表 + // * @param int $id 当前节点ID + // * @param string $pidField 父级字段名 + // * @param string $idField ID字段名 + // * @return array + // */ + // public static function getParents(array $data, int $id, string $pidField = 'parent_id', string $idField = 'id'): array + // { + // $parents = []; + + // foreach ($data as $item) { + // if ($item[$idField] == $id) { + // if ($item[$pidField] != 0) { + // $parents[] = $item; + // $parentParents = static::getParents($data, $item[$pidField], $pidField, $idField); + // $parents = array_merge($parents, $parentParents); + // } + // break; + // } + // } + + // return $parents; + // } + + // /** + // * 获取指定节点的子孙节点ID列表 + // * + // * @param array $data 数据列表 + // * @param int $parentId 父级ID + // * @param string $pidField 父级字段名 + // * @param string $idField ID字段名 + // * @return array + // */ + // public static function getChildrenIds(array $data, int $parentId, string $pidField = 'parent_id', string $idField = 'id'): array + // { + // $ids = []; + // $children = static::findChildren($data, $parentId, $pidField, $idField); + + // foreach ($children as $child) { + // $ids[] = $child[$idField]; + // } + + // return $ids; + // } + + // /** + // * 检查是否有子节点 + // * + // * @param array $data 数据列表 + // * @param int $parentId 父级ID + // * @param string $pidField 父级字段名 + // * @return bool + // */ + // public static function hasChildren(array $data, int $parentId, string $pidField = 'parent_id'): bool + // { + // foreach ($data as $item) { + // if ($item[$pidField] == $parentId) { + // return true; + // } + // } + + // return false; + // } + + + // /** + // * 构建带有权限列表的资源树(权限不显示在树结构中) + // * + // * @param array $data 资源数据列表 + // * @param int $parentId 父级ID + // * @param string $pidField 父级字段名 + // * @param string $idField ID字段名 + // * @param string $typeField 类型字段名 + // * @param string $permissionType 权限类型标识 + // * @param int $level 当前层级 + // * @return array + // */ + // public static function buildWithAuthList( + // array $data, + // int $parentId = 0, + // string $pidField = 'parent_id', + // string $idField = 'id', + // string $typeField = 'type', + // string $permissionType = 'permission', + // int $level = 0 + // ): array { + // $tree = []; + + // foreach ($data as $item) { + // // 只处理指定父级的非权限节点 + // if ($item[$pidField] == $parentId && $item[$typeField] != $permissionType) { + // $item['level'] = $level; + + // // 递归处理子节点(非权限类型) + // $children = static::buildWithAuthList($data, $item[$idField], $pidField, $idField, $typeField, $permissionType, $level + 1); + + // if (!empty($children)) { + // $item['children'] = $children; + // } + + // // 收集当前节点下的所有权限 + // $item['auth_list'] = static::collectDirectPermissions($data, $item[$idField], $pidField, $typeField, $permissionType); + + // $tree[] = $item; + // } + // } + + // return $tree; + // } + + // /** + // * 收集指定节点下的直接权限项(只收集直接子节点中的权限) + // * + // * @param array $data 数据列表 + // * @param int $parentId 父级ID + // * @param string $pidField 父级字段名 + // * @param string $typeField 类型字段名 + // * @param string $permissionType 权限类型标识 + // * @return array + // */ + // public static function collectDirectPermissions1( + // array $data, + // int $parentId, + // string $pidField = 'parent_id', + // string $typeField = 'type', + // string $permissionType = 'permission' + // ): array { + // $permissions = []; + + // foreach ($data as $item) { + // // 收集直接子节点中的权限类型 + // if ($item[$pidField] == $parentId && $item[$typeField] == $permissionType) { + // $permissions[] = $item; + // } + // } + + // return $permissions; + // } + + // /** + // * 使用引用方式构建带有权限列表的资源树(权限不显示在树结构中) + // * + // * @param array $data 资源数据列表 + // * @param string $pidField 父级字段名 + // * @param string $idField ID字段名 + // * @param string $typeField 类型字段名 + // * @param string $permissionType 权限类型标识 + // * @return array + // */ + // public static function buildWithAuthListAndReference( + // array $data, + // string $pidField = 'parent_id', + // string $idField = 'id', + // string $typeField = 'type', + // string $permissionType = 'permission' + // ): array { + // $items = []; + // $tree = []; + + // // 建立以id为索引的数组(只包含非权限节点) + // foreach ($data as $item) { + // if ($item[$typeField] != $permissionType) { + // $item['level'] = 0; + // $items[$item[$idField]] = $item; + // } + // } + + // // 构建树形结构 + // foreach ($items as $id => $item) { + // if ($item[$pidField] == 0) { + // $tree[] = &$items[$id]; + // } else { + // if (isset($items[$item[$pidField]])) { + // $items[$id]['level'] = $items[$item[$pidField]]['level'] + 1; + + // if (!isset($items[$item[$pidField]]['children'])) { + // $items[$item[$pidField]]['children'] = []; + // } + + // $items[$item[$pidField]]['children'][] = &$items[$id]; + // } + // } + // } + + // // 为每个节点添加权限列表 + // foreach ($items as &$item) { + // $item['auth_list'] = static::collectDirectPermissions($data, $item[$idField], $pidField, $typeField, $permissionType); + // } + + // return $tree; + // } + + + + // 8.13 add + /** + * 构建前端需要的菜单树结构 + * + * @param array $data 服务端菜单数据 + * @param int $parentId 父级ID + * @return array + */ + public static function buildMenuTree(array $data, int $parentId = 0): array + { + $tree = []; + + foreach ($data as $item) { + // 只处理非权限类型的节点 + if ($item['parent_id'] == $parentId && $item['type'] != 'permission') { + // 转换数据结构以匹配前端需要的格式 + $menuItem = [ + 'id' => $item['id'], + 'path' => $item['url'] ?? '', + 'name' => $item['name'], + 'component' => $item['url'] ?? '', + 'meta' => [ + 'title' => $item['name'], + 'icon' => $item['icon'] ?? '', + 'isHide' => !empty($item['is_hide']), + 'isHideTab' => !empty($item['is_hide_tab']), + 'keepAlive' => !empty($item['keep_alive']), + ] + ]; + + // 收集当前节点下的所有权限 + $permissions = static::collectDirectPermissions($data, $item['id']); + if (!empty($permissions)) { + $authList = []; + foreach ($permissions as $permission) { + $authList[] = [ + 'id' => $permission['id'], + 'title' => $permission['name'] ?? '', + 'authMark' => $permission['code'] ?? '', + 'selected' => isset($permission['selected']) && $permission['selected'] ? true : false + ]; + } + $menuItem['meta']['authList'] = $authList; + } + + // 将服务端额外字段放入meta中 + $extraFields = static::getExtraFields($item); + if (!empty($extraFields)) { + $menuItem['meta'] = array_merge($menuItem['meta'], $extraFields); + } + + // 递归处理子菜单 + $children = static::buildMenuTree($data, $item['id']); + if (!empty($children)) { + $menuItem['children'] = $children; + } + + $tree[] = $menuItem; + } + } + + return $tree; + } + + /** + * 使用引用方式构建前端需要的菜单树结构(性能更好) + * + * @param array $data 服务端菜单数据 + * @return array + */ + public static function buildMenuTreeWithReference(array $data): array + { + $items = []; + $tree = []; + + // 过滤出菜单类型的节点并转换数据结构 + $menuData = array_filter($data, function($item) { + return ($item['type'] ?? '') != 'permission'; + }); + + // 建立以id为索引的数组并转换数据结构 + foreach ($menuData as $item) { + $items[$item['id']] = [ + 'id' => $item['id'], + 'path' => $item['url'] ?? '', + 'name' => $item['name'], + 'component' => $item['url'] ?? '', + 'meta' => [ + 'title' => $item['name'], + 'icon' => $item['icon'] ?? '', + 'isHide' => !empty($item['is_hide']), + 'isHideTab' => !empty($item['is_hide_tab']), + 'keepAlive' => !empty($item['keep_alive']), + ] + ]; + + // 将服务端额外字段放入meta中 + $extraFields = static::getExtraFields($item); + if (!empty($extraFields)) { + $items[$item['id']]['meta'] = array_merge($items[$item['id']]['meta'], $extraFields); + } + } + + // 构建树形结构 + foreach ($menuData as $item) { + if ($item['parent_id'] == 0) { + $tree[] = &$items[$item['id']]; + } else { + if (isset($items[$item['parent_id']])) { + if (!isset($items[$item['parent_id']]['children'])) { + $items[$item['parent_id']]['children'] = []; + } + $items[$item['parent_id']]['children'][] = &$items[$item['id']]; + } + } + } + + // 为每个节点添加权限列表 + foreach ($items as &$item) { + $parentId = $item['id']; + $permissions = static::collectDirectPermissions($data, $parentId); + if (!empty($permissions)) { + $authList = []; + foreach ($permissions as $permission) { + $authList[] = [ + 'id' => $permission['id'], + 'title' => $permission['name'] ?? '', + 'authMark' => $permission['code'] ?? '' + ]; + } + $item['meta']['authList'] = $authList; + } + } + + return $tree; + } + + /** + * 收集指定节点下的直接权限项(只收集直接子节点中的权限) + * + * @param array $data 数据列表 + * @param int $parentId 父级ID + * @return array + */ + public static function collectDirectPermissions(array $data, int $parentId): array + { + $permissions = []; + + foreach ($data as $item) { + // 收集直接子节点中的权限类型 + if ($item['parent_id'] == $parentId && ($item['type'] ?? '') == 'permission') { + $permissions[] = $item; + } + } + + return $permissions; + } + + /** + * 获取需要放入meta的额外字段 + * + * @param array $item 菜单项 + * @return array + */ + public static function getExtraFields(array $item): array + { + $extraFields = []; + $standardFields = [ + 'id', 'name', 'url', 'icon', 'is_hide', 'is_hide_tab', 'keep_alive', 'level' + ]; + + foreach ($item as $key => $value) { + // 如果不是标准字段且不是auth_list,则放入extraFields + if (!in_array($key, $standardFields) && $key !== 'auth_list') { + // 转换下划线命名到驼峰命名 + $camelKey = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))); + $extraFields[$camelKey] = $value; + } + } + + return $extraFields; + } +} \ No newline at end of file diff --git a/app/common/utils/scaffold/Generate.php b/app/common/utils/scaffold/Generate.php new file mode 100644 index 0000000..2ed10a0 --- /dev/null +++ b/app/common/utils/scaffold/Generate.php @@ -0,0 +1,194 @@ +options = $options; + } + + public function doGenerate() + { + $fs = new Filesystem(); + + $templateDir = __DIR__ . '/templates/'; + + $resultStr = ''; + + if (!empty($this->options['controller'])) { + $controllerPath = __DIR__ . "/../../../{$this->options['controller']['module']}/controller/".$this->rName($this->options['controller']['name'])."Controller.php"; + $fs->mkdir(__DIR__ . "/../../../{$this->options['controller']['module']}/controller"); + $this->deleteFileIfExists($controllerPath); + $ControllerStub = $this->generateFile($templateDir, 'ControllerStub.stub', $controllerPath, 'controller'); + $resultStr .= $ControllerStub; + } + + if (!empty($this->options['service'])) { + $servicePath = __DIR__ . "/../../../{$this->options['service']['module']}/service/".$this->rName($this->options['service']['name'])."Service.php"; + $fs->mkdir(__DIR__ . "/../../../{$this->options['service']['module']}/service"); + $this->deleteFileIfExists($servicePath); + $ServiceStub = $this->generateFile($templateDir, 'ServiceStub.stub', $servicePath, 'service'); + $resultStr .= $ServiceStub; + } + + if (!empty($this->options['logic'])) { + $logicPath = __DIR__ . "/../../../{$this->options['logic']['module']}/logic/".$this->rName($this->options['logic']['name'])."Logic.php"; + $fs->mkdir(__DIR__ . "/../../../{$this->options['logic']['module']}/logic"); + $this->deleteFileIfExists($logicPath); + $LogicStub = $this->generateFile($templateDir, 'LogicStub.stub', $logicPath, 'logic'); + $resultStr .= $LogicStub; + } + + if (!empty($this->options['model'])) { + $modelPath = __DIR__ . "/../../../{$this->options['model']['module']}/model/".$this->rName($this->options['model']['name']).".php"; + $fs->mkdir(__DIR__ . "/../../../{$this->options['model']['module']}/model"); + $this->deleteFileIfExists($modelPath); + $ModelStub = $this->generateFile($templateDir, 'ModelStub.stub', $modelPath, 'model'); + $resultStr .= $ModelStub; + } + + if (!empty($this->options['validate'])) { + $validatePath = __DIR__ . "/../../../{$this->options['validate']['module']}/validate/".$this->rName($this->options['validate']['name'])."Validate.php"; + $fs->mkdir(__DIR__ . "/../../../{$this->options['validate']['module']}/validate"); + $this->deleteFileIfExists($validatePath); + $ValidatorStub = $this->generateFile($templateDir, 'ValidateStub.stub', $validatePath, 'validate'); + $resultStr .= $ValidatorStub; + } + + return $resultStr ? $resultStr . "文件生成成功!
" : '未生成任何文件'; + } + + /** + * 根据模板生成文件 + * + * @param string $templateDir 模板目录 + * @param string $templateFile 模板文件名 + * @param string $outputPath 输出文件路径 + */ + protected function generateFile($templateDir, $templateFile, $outputPath, $fileType) + { + $templatePath = $templateDir . $templateFile; + + if (!file_exists($templatePath)) { + echo "模板文件 {$templateFile} 不存在! \n"; + return; + } + + $content = file_get_contents($templatePath); + + switch ($fileType) { + case 'controller': + $content = str_replace('{{controllerModule}}', $this->options['controller']['module'], $content); + $content = str_replace('{{serviceModule}}', $this->options['service']['module'], $content); + $content = str_replace('{{serviceName}}', $this->rName($this->options['service']['name']), $content); + $content = str_replace('{{controllerName}}', $this->rName($this->options['controller']['name']), $content); + break; + case 'service': + $content = str_replace('{{serviceModule}}', $this->options['service']['module'], $content); + $content = str_replace('{{validateModule}}', $this->options['validate']['module'], $content); + $content = str_replace('{{validateName}}', $this->rName($this->options['validate']['name']), $content); + $content = str_replace('{{modelModule}}', $this->options['model']['module'], $content); + $content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content); + $content = str_replace('{{serviceName}}', $this->rName($this->options['service']['name']), $content); + break; + case 'logic': + $content = str_replace('{{logicModule}}', $this->options['logic']['module'], $content); + $content = str_replace('{{modelModule}}', $this->options['model']['module'], $content); + $content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content); + $content = str_replace('{{logicName}}', $this->rName($this->options['logic']['name']), $content); + break; + case 'model': + $content = str_replace('{{modelModule}}', $this->options['model']['module'], $content); + $content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content); + $content = str_replace('{{table}}', $this->options['table'], $content); + $content = str_replace('{{fillable}}', $this->fillableFields(), $content); + break; + case 'validate': + $content = str_replace('{{validateModule}}', $this->options['validate']['module'], $content); + $content = str_replace('{{validateName}}', $this->rName($this->options['validate']['name']), $content); + break; + } + + file_put_contents($outputPath, $content); + + return "生成文件: {$outputPath}
"; + } + + /** + * 设置字段 + * + * @param array $fields 字段数组 + */ + public function fillableFields() + { + $tableName = $this->options['table']; + $fields = TableInspector::getColumnNames($tableName); + $str = ''; + foreach ($fields as $key => $val) { + if ($key < (count($fields)-1)) { + $str .= "'{$val}', "; + } else { + $str .= "'{$val}'"; + } + } + $fields = trim($str, '"'); + return $fields; + } + + /** + * 如果文件存在,删除它 + * + * @param string $path 文件路径 + */ + protected function deleteFileIfExists($path) + { + if (file_exists($path)) { + // "文件: {$path} 已存在,核对后手动删除。\n"; + // return; + + unlink($path); + echo "删除已存在文件: {$path} \n"; + } + } + + /** + * 设置名称 + */ + public function rName($name = '') + { + return ucfirst(camelize($name)); + } +} + +/** +* 下划线转驼峰 +* 思路: +* step1.原字符串转小写,原字符串中的分隔符用空格替换,在字符串开头加上分隔符 +* step2.将字符串中每个单词的首字母转换为大写,再去空格,去字符串首部附加的分隔符. +*/ +function camelize($uncamelized_words, $separator='_') { + $uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words)); + return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator ); +} + +/** +* 驼峰命名转下划线命名 +* 思路: +* 小写和大写紧挨一起的地方,加上分隔符,然后全部转小写 +*/ +function uncamelize($camelCaps, $separator='_') { + return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps)); +} \ No newline at end of file diff --git a/app/common/utils/scaffold/templates/ControllerStub.stub b/app/common/utils/scaffold/templates/ControllerStub.stub new file mode 100644 index 0000000..a74d97c --- /dev/null +++ b/app/common/utils/scaffold/templates/ControllerStub.stub @@ -0,0 +1,33 @@ +service = new {{serviceName}}Service(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/utils/scaffold/templates/LogicStub.stub b/app/common/utils/scaffold/templates/LogicStub.stub new file mode 100644 index 0000000..da1afdc --- /dev/null +++ b/app/common/utils/scaffold/templates/LogicStub.stub @@ -0,0 +1,17 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/utils/scaffold/templates/ValidateStub.stub b/app/common/utils/scaffold/templates/ValidateStub.stub new file mode 100644 index 0000000..862b6f1 --- /dev/null +++ b/app/common/utils/scaffold/templates/ValidateStub.stub @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/AgreementValidate.php b/app/common/validate/AgreementValidate.php new file mode 100644 index 0000000..b3eb411 --- /dev/null +++ b/app/common/validate/AgreementValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/BannerValidate.php b/app/common/validate/BannerValidate.php new file mode 100644 index 0000000..83c0111 --- /dev/null +++ b/app/common/validate/BannerValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/ConfigValidate.php b/app/common/validate/ConfigValidate.php new file mode 100644 index 0000000..2ff9365 --- /dev/null +++ b/app/common/validate/ConfigValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/MessageNoticeValidate.php b/app/common/validate/MessageNoticeValidate.php new file mode 100644 index 0000000..ab62467 --- /dev/null +++ b/app/common/validate/MessageNoticeValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/NavCategoriesValidate.php b/app/common/validate/NavCategoriesValidate.php new file mode 100644 index 0000000..c8e9f55 --- /dev/null +++ b/app/common/validate/NavCategoriesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/NavItemsValidate.php b/app/common/validate/NavItemsValidate.php new file mode 100644 index 0000000..c309bda --- /dev/null +++ b/app/common/validate/NavItemsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/PostCategoryValidate.php b/app/common/validate/PostCategoryValidate.php new file mode 100644 index 0000000..8f98fb3 --- /dev/null +++ b/app/common/validate/PostCategoryValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/PostCommentValidate.php b/app/common/validate/PostCommentValidate.php new file mode 100644 index 0000000..8aa09e4 --- /dev/null +++ b/app/common/validate/PostCommentValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/PostFavoriteValidate.php b/app/common/validate/PostFavoriteValidate.php new file mode 100644 index 0000000..0660229 --- /dev/null +++ b/app/common/validate/PostFavoriteValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/PostLikeValidate.php b/app/common/validate/PostLikeValidate.php new file mode 100644 index 0000000..4f1af14 --- /dev/null +++ b/app/common/validate/PostLikeValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/PostMediaValidate.php b/app/common/validate/PostMediaValidate.php new file mode 100644 index 0000000..a0da9cf --- /dev/null +++ b/app/common/validate/PostMediaValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/PostValidate.php b/app/common/validate/PostValidate.php new file mode 100644 index 0000000..8a89a98 --- /dev/null +++ b/app/common/validate/PostValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/ResourceValidate.php b/app/common/validate/ResourceValidate.php new file mode 100644 index 0000000..ea2646e --- /dev/null +++ b/app/common/validate/ResourceValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ 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..dec4431 --- /dev/null +++ b/app/common/validate/RoleValidate.php @@ -0,0 +1,32 @@ + 'require|integer', + 'system_id' => 'require|integer', + 'name' => 'require|max:64', + 'desc' => 'max:128', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]; + + protected $message = [ + 'id.require' => '角色ID不能为空', + 'system_id.require' => '系统ID不能为空', + 'name.require' => '角色名称不能为空', + 'name.max' => '角色名称不能超过64个字符', + 'desc.max' => '描述不能超过128个字符', + 'sort.integer' => '排序为数字', + 'status.in' => '状态值必须为0或1', + ]; + + protected $scene = [ + 'store' => ['name', 'desc', 'sort'], + 'update' => ['id', 'name', 'desc', 'sort', 'status'], + ]; +} diff --git a/app/common/validate/SkuAttributeValidate.php b/app/common/validate/SkuAttributeValidate.php new file mode 100644 index 0000000..ce5c697 --- /dev/null +++ b/app/common/validate/SkuAttributeValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/SkuAttributeValueValidate.php b/app/common/validate/SkuAttributeValueValidate.php new file mode 100644 index 0000000..8c7d1c0 --- /dev/null +++ b/app/common/validate/SkuAttributeValueValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/SkuValidate.php b/app/common/validate/SkuValidate.php new file mode 100644 index 0000000..013357c --- /dev/null +++ b/app/common/validate/SkuValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/SystemsValidate.php b/app/common/validate/SystemsValidate.php new file mode 100644 index 0000000..d0783f5 --- /dev/null +++ b/app/common/validate/SystemsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/UserValidate.php b/app/common/validate/UserValidate.php new file mode 100644 index 0000000..d792cb3 --- /dev/null +++ b/app/common/validate/UserValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php new file mode 100644 index 0000000..23305f0 --- /dev/null +++ b/app/controller/IndexController.php @@ -0,0 +1,65 @@ +hi~'; + } + + + + /** + * 生成资源 + */ + public function createResource() + { + $options = [ + 'controller' => [ + 'module' => 'common', + 'name' => 'sku_attribute_value' + ], + 'model' => [ + 'module' => 'common', + 'name' => 'sku_attribute_value' + ], + 'service' => [ + 'module' => 'common', + 'name' => 'sku_attribute_value' + ], + 'logic' => [ + 'module' => 'common', + 'name' => 'sku_attribute_value' + ], + 'validate' => [ + 'module' => 'common', + 'name' => 'sku_attribute_value' + ], + 'table' => 'sku_attribute_value' + ]; + + $result = (new Generate($options))->doGenerate(); + return $result; + } + + + public function test() + { + + $result = Redis::set('test', 'test'); + + var_dump(Redis::get('test')); + + return 'test'; + } +} diff --git a/app/functions.php b/app/functions.php new file mode 100644 index 0000000..5c9c58d --- /dev/null +++ b/app/functions.php @@ -0,0 +1,4 @@ +method() == 'OPTIONS' ? response('') : $handler($request); + + // 给响应添加跨域相关的http头 + $response->withHeaders([ + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Allow-Origin' => $request->header('origin', '*'), + 'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'), + 'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'), + ]); + + return $response; + } +} \ No newline at end of file diff --git a/app/middleware/AuthCheck.php b/app/middleware/AuthCheck.php new file mode 100644 index 0000000..a03e632 --- /dev/null +++ b/app/middleware/AuthCheck.php @@ -0,0 +1,50 @@ +controller); + $properties = $controller->getDefaultProperties(); + $noNeedLogin = $properties['noNeedLogin'] ?? []; + $noNeedAuth = $properties['noNeedAuth'] ?? []; + + if (in_array($request->action, $noNeedLogin)) { + // 白名单 + return $handler($request); + } else { + try { + $authHeader = $request->header('Fdesk-Auth-Token'); + 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); + return $handler($request); + } 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..b8ec3c2 --- /dev/null +++ b/app/middleware/BasicData.php @@ -0,0 +1,45 @@ +controller); + $noNeedHeaderData = $controller->getDefaultProperties()['noNeedHeaderData'] ?? []; + + if (in_array($request->action, $noNeedHeaderData)) { + // 白名单 + return $handler($request); + } else { + try { + $systemCode = $request->header('Fdesk-System-Code'); + $loginType = $request->header('Fdesk-Login-Type'); + $basicDataService = new BasicDataService(); + if ($basicDataService->checkAndCacheBaseParams($loginType, $systemCode)) { + $request->login_type = $loginType; + $request->system_code = $systemCode; + $request->basic_data = [ + 'login_type' => $loginType, + 'system_code' => $systemCode, + ]; + } + return $handler($request); + } catch (\Throwable $th) { + $body = [ + 'status' => 'fail', + 'code' => 403, + 'message' => $th->getMessage(), + 'data' => [], + ]; + return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); + } + } + } +} \ No newline at end of file diff --git a/app/middleware/StaticFile.php b/app/middleware/StaticFile.php new file mode 100644 index 0000000..fa8dbf7 --- /dev/null +++ b/app/middleware/StaticFile.php @@ -0,0 +1,42 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace app\middleware; + +use Webman\MiddlewareInterface; +use Webman\Http\Response; +use Webman\Http\Request; + +/** + * Class StaticFile + * @package app\middleware + */ +class StaticFile implements MiddlewareInterface +{ + public function process(Request $request, callable $handler): Response + { + // Access to files beginning with. Is prohibited + if (strpos($request->path(), '/.') !== false) { + return response('

403 forbidden

', 403); + } + /** @var Response $response */ + $response = $handler($request); + // Add cross domain HTTP header + /*$response->withHeaders([ + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Credentials' => 'true', + ]);*/ + return $response; + } +} diff --git a/app/model/Test.php b/app/model/Test.php new file mode 100644 index 0000000..92d70e3 --- /dev/null +++ b/app/model/Test.php @@ -0,0 +1,29 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace app\process; + +use FilesystemIterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; +use Workerman\Timer; +use Workerman\Worker; + +/** + * Class FileMonitor + * @package process + */ +class Monitor +{ + /** + * @var array + */ + protected array $paths = []; + + /** + * @var array + */ + protected array $extensions = []; + + /** + * @var array + */ + protected array $loadedFiles = []; + + /** + * @var int + */ + protected int $ppid = 0; + + /** + * Pause monitor + * @return void + */ + public static function pause(): void + { + file_put_contents(static::lockFile(), time()); + } + + /** + * Resume monitor + * @return void + */ + public static function resume(): void + { + clearstatcache(); + if (is_file(static::lockFile())) { + unlink(static::lockFile()); + } + } + + /** + * Whether monitor is paused + * @return bool + */ + public static function isPaused(): bool + { + clearstatcache(); + return file_exists(static::lockFile()); + } + + /** + * Lock file + * @return string + */ + protected static function lockFile(): string + { + return runtime_path('monitor.lock'); + } + + /** + * FileMonitor constructor. + * @param $monitorDir + * @param $monitorExtensions + * @param array $options + */ + public function __construct($monitorDir, $monitorExtensions, array $options = []) + { + $this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0; + static::resume(); + $this->paths = (array)$monitorDir; + $this->extensions = $monitorExtensions; + foreach (get_included_files() as $index => $file) { + $this->loadedFiles[$file] = $index; + if (strpos($file, 'webman-framework/src/support/App.php')) { + break; + } + } + if (!Worker::getAllWorkers()) { + return; + } + $disableFunctions = explode(',', ini_get('disable_functions')); + if (in_array('exec', $disableFunctions, true)) { + echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n"; + } else { + if ($options['enable_file_monitor'] ?? true) { + Timer::add(1, function () { + $this->checkAllFilesChange(); + }); + } + } + + $memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null); + if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) { + Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]); + } + } + + /** + * @param $monitorDir + * @return bool + */ + public function checkFilesChange($monitorDir): bool + { + static $lastMtime, $tooManyFilesCheck; + if (!$lastMtime) { + $lastMtime = time(); + } + clearstatcache(); + if (!is_dir($monitorDir)) { + if (!is_file($monitorDir)) { + return false; + } + $iterator = [new SplFileInfo($monitorDir)]; + } else { + // recursive traversal directory + $dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS); + $iterator = new RecursiveIteratorIterator($dirIterator); + } + $count = 0; + foreach ($iterator as $file) { + $count ++; + /** var SplFileInfo $file */ + if (is_dir($file->getRealPath())) { + continue; + } + // check mtime + if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) { + $lastMtime = $file->getMTime(); + if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) { + echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n"; + continue; + } + $var = 0; + exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var); + if ($var) { + continue; + } + // send SIGUSR1 signal to master process for reload + if (DIRECTORY_SEPARATOR === '/') { + if ($masterPid = $this->getMasterPid()) { + echo $file . " updated and reload\n"; + posix_kill($masterPid, SIGUSR1); + } else { + echo "Master process has gone away and can not reload\n"; + } + return true; + } + echo $file . " updated and reload\n"; + return true; + } + } + if (!$tooManyFilesCheck && $count > 1000) { + echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n"; + $tooManyFilesCheck = 1; + } + return false; + } + + /** + * @return int + */ + public function getMasterPid(): int + { + if ($this->ppid === 0) { + return 0; + } + if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) { + echo "Master process has gone away\n"; + return $this->ppid = 0; + } + if (PHP_OS_FAMILY !== 'Linux') { + return $this->ppid; + } + $cmdline = "/proc/$this->ppid/cmdline"; + if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) { + // Process not exist + $this->ppid = 0; + } + return $this->ppid; + } + + /** + * @return bool + */ + public function checkAllFilesChange(): bool + { + if (static::isPaused()) { + return false; + } + foreach ($this->paths as $path) { + if ($this->checkFilesChange($path)) { + return true; + } + } + return false; + } + + /** + * @param $memoryLimit + * @return void + */ + public function checkMemory($memoryLimit): void + { + if (static::isPaused() || $memoryLimit <= 0) { + return; + } + $masterPid = $this->getMasterPid(); + if ($masterPid <= 0) { + echo "Master process has gone away\n"; + return; + } + + $childrenFile = "/proc/$masterPid/task/$masterPid/children"; + if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) { + return; + } + foreach (explode(' ', $children) as $pid) { + $pid = (int)$pid; + $statusFile = "/proc/$pid/status"; + if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) { + continue; + } + $mem = 0; + if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) { + $mem = $match[1]; + } + $mem = (int)($mem / 1024); + if ($mem >= $memoryLimit) { + posix_kill($pid, SIGINT); + } + } + } + + /** + * Get memory limit + * @param $memoryLimit + * @return int + */ + protected function getMemoryLimit($memoryLimit): int + { + if ($memoryLimit === 0) { + return 0; + } + $usePhpIni = false; + if (!$memoryLimit) { + $memoryLimit = ini_get('memory_limit'); + $usePhpIni = true; + } + + if ($memoryLimit == -1) { + return 0; + } + $unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]); + $memoryLimit = (int)$memoryLimit; + if ($unit === 'g') { + $memoryLimit = 1024 * $memoryLimit; + } else if ($unit === 'k') { + $memoryLimit = ($memoryLimit / 1024); + } else if ($unit === 'm') { + $memoryLimit = (int)($memoryLimit); + } else if ($unit === 't') { + $memoryLimit = (1024 * 1024 * $memoryLimit); + } else { + $memoryLimit = ($memoryLimit / (1024 * 1024)); + } + if ($memoryLimit < 50) { + $memoryLimit = 50; + } + if ($usePhpIni) { + $memoryLimit = (0.8 * $memoryLimit); + } + return (int)$memoryLimit; + } + +} diff --git a/app/view/index/view.html b/app/view/index/view.html new file mode 100644 index 0000000..67ebb26 --- /dev/null +++ b/app/view/index/view.html @@ -0,0 +1,14 @@ + + + + + + + + webman + + + +hello + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..37f2ab7 --- /dev/null +++ b/composer.json @@ -0,0 +1,69 @@ +{ + "name": "workerman/webman", + "type": "project", + "keywords": [ + "high performance", + "http service" + ], + "homepage": "https://www.workerman.net", + "license": "MIT", + "description": "High performance HTTP Service Framework.", + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "https://www.workerman.net", + "role": "Developer" + } + ], + "support": { + "email": "walkor@workerman.net", + "issues": "https://github.com/walkor/webman/issues", + "forum": "https://wenda.workerman.net/", + "wiki": "https://workerman.net/doc/webman", + "source": "https://github.com/walkor/webman" + }, + "require": { + "php": ">=8.1", + "workerman/webman-framework": "^2.1", + "monolog/monolog": "^2.0", + "vlucas/phpdotenv": "^5.6", + "webman/console": "^2.1", + "webman/database": "^2.1", + "illuminate/pagination": "^12.31", + "illuminate/events": "^12.31", + "symfony/var-dumper": "^7.3", + "symfony/filesystem": "^7.3", + "topthink/think-validate": "^3.0", + "w7corp/easywechat": "^6.17", + "webman/redis": "^2.1", + "firebase/php-jwt": "^6.11", + "webman/cache": "^2.1", + "shopwwi/webman-filesystem": "1.1.0", + "shopwwi/filesystem-oss": "^1.0" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "autoload": { + "psr-4": { + "": "./", + "app\\": "./app", + "App\\": "./app", + "app\\View\\Components\\": "./app/view/components" + } + }, + "scripts": { + "post-package-install": [ + "support\\Plugin::install" + ], + "post-package-update": [ + "support\\Plugin::install" + ], + "pre-package-uninstall": [ + "support\\Plugin::uninstall" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..a640ebc --- /dev/null +++ b/composer.lock @@ -0,0 +1,6623 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cfcedd2ce8245f797ac93d1eb374abf6", + "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": "" + }, + "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.14.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-08-29T12:40:03+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "illuminate/bus", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/bus.git", + "reference": "d239bd36ddb5b5c0996018528cbfe043fc1b5fea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/bus/zipball/d239bd36ddb5b5c0996018528cbfe043fc1b5fea", + "reference": "d239bd36ddb5b5c0996018528cbfe043fc1b5fea", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/pipeline": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/queue": "Required to use closures when chaining jobs (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Bus\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Bus package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-02T07:28:44+00:00" + }, + { + "name": "illuminate/collections", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "d47aaf15c55dd1c252688fdc7adbee129bd2ff0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/d47aaf15c55dd1c252688fdc7adbee129bd2ff0b", + "reference": "d47aaf15c55dd1c252688fdc7adbee129bd2ff0b", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "php": "^8.2", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "illuminate/http": "Required to convert collections to API resources (^12.0).", + "symfony/var-dumper": "Required to use the dump method (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-28T12:52:25+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/ec677967c1f2faf90b8428919124d2184a4c9b49", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/container", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", + "reference": "d6eaa8afd48dbe16b6b3c412a87479cad67eeb12", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-12T14:35:11+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "0bdbf0cdb5dd5739b2c8e6caf881a4114399ab15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/0bdbf0cdb5dd5739b2c8e6caf881a4114399ab15", + "reference": "0bdbf0cdb5dd5739b2c8e6caf881a4114399ab15", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-12T14:35:11+00:00" + }, + { + "name": "illuminate/database", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "cce8bca069820c32b51abbd54f732956b872cc6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/cce8bca069820c32b51abbd54f732956b872cc6d", + "reference": "cce8bca069820c32b51abbd54f732956b872cc6d", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "ext-pdo": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^12.0).", + "illuminate/events": "Required to use the observers with Eloquent (^12.0).", + "illuminate/filesystem": "Required to use the migrations (^12.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^12.0).", + "illuminate/pagination": "Required to paginate the result set (^12.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-30T19:12:33+00:00" + }, + { + "name": "illuminate/events", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/events.git", + "reference": "ba94fc7c734864e1eba802e75930725fd8074fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/events/zipball/ba94fc7c734864e1eba802e75930725fd8074fce", + "reference": "ba94fc7c734864e1eba802e75930725fd8074fce", + "shasum": "" + }, + "require": { + "illuminate/bus": "^12.0", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Events\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Events package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-30T10:20:25+00:00" + }, + { + "name": "illuminate/filesystem", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/filesystem.git", + "reference": "c7c3bbcd05c0836af89f1b49bf70c3170c011c13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/c7c3bbcd05c0836af89f1b49bf70c3170c011c13", + "reference": "c7c3bbcd05c0836af89f1b49bf70c3170c011c13", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0" + }, + "suggest": { + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-hash": "Required to use the Filesystem class.", + "illuminate/http": "Required for handling uploaded files (^12.0).", + "league/flysystem": "Required to use the Flysystem local driver (^3.25.1).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/mime": "Required to enable support for guessing extensions (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Filesystem package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-30T17:35:38+00:00" + }, + { + "name": "illuminate/http", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/http.git", + "reference": "9f2fbdc2a8288f7b276514d0257c797da86f8358" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/http/zipball/9f2fbdc2a8288f7b276514d0257c797da86f8358", + "reference": "9f2fbdc2a8288f7b276514d0257c797da86f8358", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "illuminate/collections": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/session": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Http\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Http package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-06T22:01:13+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e862e5648ee34004fa56046b746f490dfa86c613", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-07-23T16:31:01+00:00" + }, + { + "name": "illuminate/pagination", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pagination.git", + "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/1d95e70671177108b202e6ceb61bc7bc9924bdbf", + "reference": "1d95e70671177108b202e6ceb61bc7bc9924bdbf", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pagination\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pagination package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-02T15:31:06+00:00" + }, + { + "name": "illuminate/pipeline", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-08-20T13:36:50+00:00" + }, + { + "name": "illuminate/redis", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/redis.git", + "reference": "ce8129e486563b944d80ae159023d9e970bacd04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/redis/zipball/ce8129e486563b944d80ae159023d9e970bacd04", + "reference": "ce8129e486563b944d80ae159023d9e970bacd04", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Redis\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Redis package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-10T14:35:16+00:00" + }, + { + "name": "illuminate/session", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "5f42c194f3b43ad72fbf1ec5ee0874cbc0dbc146" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/5f42c194f3b43ad72fbf1ec5ee0874cbc0dbc146", + "reference": "5f42c194f3b43ad72fbf1ec5ee0874cbc0dbc146", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-session": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0" + }, + "suggest": { + "illuminate/console": "Required to use the session:table command (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Session\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Session package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-03T21:09:42+00:00" + }, + { + "name": "illuminate/support", + "version": "v12.33.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "21016aede3dbeed1fccd4478dfbd9f10114456ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/21016aede3dbeed1fccd4478dfbd9f10114456ce", + "reference": "21016aede3dbeed1fccd4478dfbd9f10114456ce", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^12.0", + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "nesbot/carbon": "^3.8.4", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^12.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.2).", + "symfony/uid": "Required to use Str::ulid() (^7.2).", + "symfony/var-dumper": "Required to use the dd function (^7.2).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-10-06T14:47:22+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3832547db6e0e2f8bb03d4093857b378c66eceed", + "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-09-22T17:29:40+00:00" + }, + { + "name": "league/flysystem", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "shasum": "" + }, + "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|^2", + "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|^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.30.0" + }, + "time": "2025-06-25T13:29:59+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "shasum": "" + }, + "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.30.0" + }, + "time": "2025-05-21T10:34:19+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "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.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "5cf826f2991858b54d5c3809bee745560a1042a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7", + "reference": "5cf826f2991858b54d5c3809bee745560a1042a7", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-11-12T12:43:37+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.10.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-09-06T13:39:36+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "nyholm/psr7-server", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7-server.git", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc", + "reference": "4335801d851f554ca43fa6e7d2602141538854dc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7-server/issues", + "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-08T09:30:43+00:00" + }, + { + "name": "overtrue/socialite", + "version": "4.12.0", + "source": { + "type": "git", + "url": "https://github.com/overtrue/socialite.git", + "reference": "b32353d0ee21e3e52f43ff2ddf26e8c6661eddd6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/socialite/zipball/b32353d0ee21e3e52f43ff2ddf26e8c6661eddd6", + "reference": "b32353d0ee21e3e52f43ff2ddf26e8c6661eddd6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^7.0", + "php": ">=8.0.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.0", + "laravel/pint": "^1.2", + "mockery/mockery": "^1.3", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "autoload": { + "files": [ + "src/Contracts/FactoryInterface.php", + "src/Contracts/UserInterface.php", + "src/Contracts/ProviderInterface.php" + ], + "psr-4": { + "Overtrue\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "A collection of OAuth 2 packages.", + "keywords": [ + "Feishu", + "login", + "oauth", + "qcloud", + "qq", + "social", + "wechat", + "weibo" + ], + "support": { + "issues": "https://github.com/overtrue/socialite/issues", + "source": "https://github.com/overtrue/socialite/tree/4.12.0" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "time": "2025-08-20T22:55:33+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.4", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-08-21T11:53:16+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "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": "" + }, + "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": "" + }, + "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.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f", + "reference": "bf8afc8ffd4bfd3d9c373e417f041d9f1e5b863f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-22T15:31:00+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-13T11:49:31+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-07T08:17:47+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-29T11:18:49+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6", + "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-16T08:38:17+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b796dffea7821f035047235e076b60ca2446e3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf", + "reference": "b796dffea7821f035047235e076b60ca2446e3cf", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-27T12:32:17+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", + "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-16T08:38:17+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-26T08:57:56+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f96476035142921000338bad71e5247fbc138872" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T14:36:48+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", + "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-07T11:39:36+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "thenorthmemory/xml", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/TheNorthMemory/xml.git", + "reference": "6f50c63450a0b098772423f8bdc3c4ad2c4c30bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TheNorthMemory/xml/zipball/6f50c63450a0b098772423f8bdc3c4ad2c4c30bb", + "reference": "6f50c63450a0b098772423f8bdc3c4ad2c4c30bb", + "shasum": "" + }, + "require": { + "ext-libxml": "*", + "ext-simplexml": "*", + "php": ">=7.1.2" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.89 || ^1.0", + "phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "TheNorthMemory\\Xml\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "James ZHANG", + "homepage": "https://github.com/TheNorthMemory" + } + ], + "description": "A wrapper of the XML parser and builder", + "homepage": "https://github.com/TheNorthMemory/xml", + "keywords": [ + "xml-builder", + "xml-parser" + ], + "support": { + "issues": "https://github.com/TheNorthMemory/xml/issues", + "source": "https://github.com/TheNorthMemory/xml/tree/1.1.1" + }, + "time": "2023-01-15T06:01:13+00:00" + }, + { + "name": "topthink/think-container", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-container.git", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-container/zipball/b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "psr/container": "^2.0", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "PHP Container & Facade Manager", + "support": { + "issues": "https://github.com/top-think/think-container/issues", + "source": "https://github.com/top-think/think-container/tree/v3.0.2" + }, + "time": "2025-04-07T03:21:51+00:00" + }, + { + "name": "topthink/think-helper", + "version": "v3.1.11", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", + "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP6 Helper Package", + "support": { + "issues": "https://github.com/top-think/think-helper/issues", + "source": "https://github.com/top-think/think-helper/tree/v3.1.11" + }, + "time": "2025-04-07T06:55:59+00:00" + }, + { + "name": "topthink/think-validate", + "version": "v3.0.7", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-validate.git", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-validate/zipball/85063f6d4ef8ed122f17a36179dc3e0949b30988", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "topthink/think-container": ">=3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "think validate", + "support": { + "issues": "https://github.com/top-think/think-validate/issues", + "source": "https://github.com/top-think/think-validate/tree/v3.0.7" + }, + "time": "2025-06-11T05:51:40+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "w7corp/easywechat", + "version": "6.18.0", + "source": { + "type": "git", + "url": "https://github.com/w7corp/easywechat.git", + "reference": "de9a1e3ec2ea357dcf29fc53af5dd7ea2454a06e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/w7corp/easywechat/zipball/de9a1e3ec2ea357dcf29fc53af5dd7ea2454a06e", + "reference": "de9a1e3ec2ea357dcf29fc53af5dd7ea2454a06e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-fileinfo": "*", + "ext-libxml": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "nyholm/psr7": "^1.5", + "nyholm/psr7-server": "^1.0", + "overtrue/socialite": "^3.5.4|^4.0.1", + "php": ">=8.0.2", + "psr/http-client": "^1.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/polyfill-php81": "^1.25", + "symfony/psr-http-message-bridge": "^2.1.2|^6.4.0|^7.1", + "thenorthmemory/xml": "^1.0" + }, + "conflict": { + "overtrue/wechat": "*" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.0", + "laravel/pint": "^1.2", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.4.4", + "phpstan/phpstan": "^1.0 | ^2", + "phpunit/phpunit": "^9.5", + "symfony/var-dumper": "^5.2|^6|^7" + }, + "type": "library", + "autoload": { + "psr-4": { + "EasyWeChat\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "微信SDK", + "keywords": [ + "easywechat", + "sdk", + "wechat", + "weixin", + "weixin-sdk" + ], + "support": { + "issues": "https://github.com/w7corp/easywechat/issues", + "source": "https://github.com/w7corp/easywechat/tree/6.18.0" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "time": "2025-08-06T14:42:12+00:00" + }, + { + "name": "webman/cache", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/webman-php/cache.git", + "reference": "35cd8c1b3143fd792e2f50a7e6274292a122aed3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/cache/zipball/35cd8c1b3143fd792e2f50a7e6274292a122aed3", + "reference": "35cd8c1b3143fd792e2f50a7e6274292a122aed3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/simple-cache": "^3.0", + "symfony/cache": "^6.0 || ^7.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/cache/issues", + "source": "https://github.com/webman-php/cache/tree/v2.1.0" + }, + "time": "2025-01-20T10:54:32+00:00" + }, + { + "name": "webman/console", + "version": "v2.1.8", + "source": { + "type": "git", + "url": "https://github.com/webman-php/console.git", + "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/console/zipball/2d4ce527810f91e1b4f583ec90eae24a0334ab10", + "reference": "2d4ce527810f91e1b4f583ec90eae24a0334ab10", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "php": ">=8.1", + "symfony/console": ">=6.0" + }, + "require-dev": { + "workerman/webman": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "Webman console", + "homepage": "http://www.workerman.net", + "keywords": [ + "webman console" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://www.workerman.net/questions", + "issues": "https://github.com/webman-php/console/issues", + "source": "https://github.com/webman-php/console", + "wiki": "http://www.workerman.net/doc/webman" + }, + "time": "2025-06-02T00:42:16+00:00" + }, + { + "name": "webman/database", + "version": "v2.1.6", + "source": { + "type": "git", + "url": "https://github.com/webman-php/database.git", + "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/database/zipball/943c5a0f92d8959d9160c2ee3925112b15c308c8", + "reference": "943c5a0f92d8959d9160c2ee3925112b15c308c8", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0 || ^11.0 || ^12.0", + "illuminate/http": "^10.0 || ^11.0 || ^12.0", + "laravel/serializable-closure": "^1.0 || ^2.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Database\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman database", + "support": { + "issues": "https://github.com/webman-php/database/issues", + "source": "https://github.com/webman-php/database/tree/v2.1.6" + }, + "time": "2025-04-04T13:23:25+00:00" + }, + { + "name": "webman/redis", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/webman-php/redis.git", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/redis/zipball/559eb1692d39c6fef5cf526223fff728be6c0fb9", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9", + "shasum": "" + }, + "require": { + "illuminate/redis": "^10.0 || ^11.0 || ^12.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Redis\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman redis", + "support": { + "issues": "https://github.com/webman-php/redis/issues", + "source": "https://github.com/webman-php/redis/tree/v2.1.3" + }, + "time": "2025-03-14T03:52:14+00:00" + }, + { + "name": "workerman/coroutine", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/workerman-php/coroutine.git", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "psr/log": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\": "src", + "Workerman\\Coroutine\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Workerman coroutine", + "support": { + "issues": "https://github.com/workerman-php/coroutine/issues", + "source": "https://github.com/workerman-php/coroutine/tree/v1.1.4" + }, + "time": "2025-10-11T15:09:08+00:00" + }, + { + "name": "workerman/webman-framework", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/walkor/webman-framework.git", + "reference": "f803bd867f07bb0929faef060b59a19a44186bfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/webman-framework/zipball/f803bd867f07bb0929faef060b59a19a44186bfc", + "reference": "f803bd867f07bb0929faef060b59a19a44186bfc", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": ">=8.1", + "psr/container": ">=1.0", + "psr/log": "^3.0", + "workerman/workerman": "^5.1 || dev-master" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "type": "library", + "autoload": { + "files": [ + "./src/support/helpers.php" + ], + "psr-4": { + "Webman\\": "./src", + "Support\\": "./src/support", + "support\\": "./src/support", + "Support\\View\\": "./src/support/view", + "Support\\Bootstrap\\": "./src/support/bootstrap", + "Support\\Exception\\": "./src/support/exception" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "https://www.workerman.net", + "role": "Developer" + } + ], + "description": "High performance HTTP Service Framework.", + "homepage": "https://www.workerman.net", + "keywords": [ + "High Performance", + "http service" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "https://wenda.workerman.net/", + "issues": "https://github.com/walkor/webman/issues", + "source": "https://github.com/walkor/webman-framework", + "wiki": "https://doc.workerman.net/" + }, + "time": "2025-03-10T11:52:22+00:00" + }, + { + "name": "workerman/workerman", + "version": "v5.1.3", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5", + "reference": "371f3a5decb28f1bd3464ae26d47ea1a4cf0a3c5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "workerman/coroutine": "^1.1 || dev-main" + }, + "conflict": { + "ext-swow": "=8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..f26e358 --- /dev/null +++ b/config/app.php @@ -0,0 +1,26 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use support\Request; + +return [ + 'debug' => true, + 'error_reporting' => E_ALL, + 'default_timezone' => 'Asia/Shanghai', + 'request_class' => Request::class, + 'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public', + 'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime', + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, +]; diff --git a/config/autoload.php b/config/autoload.php new file mode 100644 index 0000000..69a8135 --- /dev/null +++ b/config/autoload.php @@ -0,0 +1,21 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'files' => [ + base_path() . '/app/functions.php', + base_path() . '/support/Request.php', + base_path() . '/support/Response.php', + ] +]; diff --git a/config/bootstrap.php b/config/bootstrap.php new file mode 100644 index 0000000..95d2e87 --- /dev/null +++ b/config/bootstrap.php @@ -0,0 +1,17 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + support\bootstrap\Session::class, +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..74775dc --- /dev/null +++ b/config/cache.php @@ -0,0 +1,31 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'default' => 'file', + 'stores' => [ + 'file' => [ + 'driver' => 'file', + 'path' => runtime_path('cache') + ], + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default' + ], + 'array' => [ + 'driver' => 'array' + ] + ] +]; \ No newline at end of file diff --git a/config/container.php b/config/container.php new file mode 100644 index 0000000..106b7b4 --- /dev/null +++ b/config/container.php @@ -0,0 +1,15 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return new Webman\Container; \ No newline at end of file diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..93308ad --- /dev/null +++ b/config/database.php @@ -0,0 +1,29 @@ + 'mysql', + 'connections' => [ + 'mysql' => [ + 'driver' => 'mysql', + 'host' => getenv('DB_HOST'), + 'port' => getenv('DB_PORT'), + 'database' => getenv('DB_NAME'), + 'username' => getenv('DB_USER'), + 'password' => getenv('DB_PASSWORD'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'prefix' => getenv('DB_PREFIX'), + 'strict' => true, + 'engine' => null, + 'options' => [ + PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers. + ], + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ], + ], +]; \ No newline at end of file diff --git a/config/dependence.php b/config/dependence.php new file mode 100644 index 0000000..5b00ae9 --- /dev/null +++ b/config/dependence.php @@ -0,0 +1,42 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'default' => 'mysql', + 'connections' => [ + 'mysql' => [ + 'driver' => 'mysql', + 'host' => getenv('DB_HOST'), + 'port' => getenv('DB_PORT'), + 'database' => getenv('DB_NAME'), + 'username' => getenv('DB_USER'), + 'password' => getenv('DB_PASSWORD'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'prefix' => getenv('DB_PREFIX'), + 'strict' => true, + 'engine' => null, + 'options' => [ + PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers. + ], + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ], + ], +]; \ No newline at end of file diff --git a/config/exception.php b/config/exception.php new file mode 100644 index 0000000..f2aede3 --- /dev/null +++ b/config/exception.php @@ -0,0 +1,17 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + '' => support\exception\Handler::class, +]; \ No newline at end of file diff --git a/config/jwt.php b/config/jwt.php new file mode 100644 index 0000000..c7f09e6 --- /dev/null +++ b/config/jwt.php @@ -0,0 +1,9 @@ + getenv('JWT_SECRET_KEY') ?: 'default_secret_key', + 'jwt_algorithm' => getenv('JWT_ALGORITHM') ?: 'HS256', + 'jwt_issuer' => getenv('JWT_ISSUER') ?: 'domain.com', + 'jwt_expiration' => (int)(getenv('JWT_EXPIRATION') ?: 7200), + 'jwt_refresh_token_expiration' => (int)(getenv('JWT_REFRESH_TOKEN_EXPIRATION') ?: 2592000) +]; \ No newline at end of file diff --git a/config/log.php b/config/log.php new file mode 100644 index 0000000..7f05de5 --- /dev/null +++ b/config/log.php @@ -0,0 +1,32 @@ + + * @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/webman.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/middleware.php b/config/middleware.php new file mode 100644 index 0000000..967929d --- /dev/null +++ b/config/middleware.php @@ -0,0 +1,21 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + '' => [ + \app\middleware\AccessControl::class, + \app\middleware\BasicData::class, + \app\middleware\AuthCheck::class, + ] +]; \ 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/console/app.php b/config/plugin/webman/console/app.php new file mode 100644 index 0000000..074e986 --- /dev/null +++ b/config/plugin/webman/console/app.php @@ -0,0 +1,24 @@ + true, + + 'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build', + + 'phar_filename' => 'webman.phar', + + 'bin_filename' => 'webman.bin', + + 'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL. + + 'private_key_file' => '', // The file path for certificate or OpenSSL private key file. + + 'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#', + + 'exclude_files' => [ + '.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin' + ], + + 'custom_ini' => ' +memory_limit = 256M + ', +]; diff --git a/config/process.php b/config/process.php new file mode 100644 index 0000000..736593a --- /dev/null +++ b/config/process.php @@ -0,0 +1,62 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use support\Log; +use support\Request; +use app\process\Http; + +global $argv; + +return [ + 'webman' => [ + 'handler' => Http::class, + 'listen' => 'http://0.0.0.0:'.getenv('PROCESS_LISTEN'), + 'count' => getenv('PROCESS_COUNT'), + 'user' => '', + 'group' => '', + 'reusePort' => false, + 'eventLoop' => '', + 'context' => [], + 'constructor' => [ + 'requestClass' => Request::class, + 'logger' => Log::channel('default'), + 'appPath' => app_path(), + 'publicPath' => public_path() + ] + ], + // File update detection and automatic reload + 'monitor' => [ + 'handler' => app\process\Monitor::class, + 'reloadable' => false, + 'constructor' => [ + // Monitor these directories + 'monitorDir' => array_merge([ + app_path(), + config_path(), + base_path() . '/process', + base_path() . '/support', + base_path() . '/resource', + base_path() . '/.env', + ], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')), + // Files with these suffixes will be monitored + 'monitorExtensions' => [ + 'php', 'html', 'htm', 'env' + ], + 'options' => [ + 'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/', + 'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/', + ] + ] + ] +]; diff --git a/config/redis.php b/config/redis.php new file mode 100644 index 0000000..0508ef6 --- /dev/null +++ b/config/redis.php @@ -0,0 +1,29 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'default' => [ + 'password' => getenv('REDIS_PASSWORD'), + 'host' => getenv('REDIS_HOST'), + 'port' => getenv('REDIS_PORT'), + 'database' => getenv('REDIS_DATABASE'), + '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 new file mode 100644 index 0000000..b846ddc --- /dev/null +++ b/config/route.php @@ -0,0 +1,215 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Route; + + +Route::any('/c', [app\controller\IndexController::class, 'createResource']); + +Route::any('/qrcode/url', [app\common\controller\UserController::class, 'getQrUrl']); +Route::any('/user/info', [app\common\controller\UserController::class, 'getUserInfo']); +Route::any('/bind/user', [app\common\controller\UserController::class, 'bindUser']); + +//Route::post('/wechat/login', [app\common\controller\UserController::class, 'wechatLogin']); +// 登录 +Route::post('/miniapp/login', [app\common\controller\UserController::class, 'miniappLogin']); +// 手机号 +Route::post('/miniapp/phone', [app\common\controller\UserController::class, 'miniappPhoneNumber']); + +// 代理oss文件地址 +Route::any('/proxy/ossurl', [app\common\controller\UploadController::class, 'proxy']); +Route::any('/upload', [app\common\controller\UploadController::class, 'uploadFile']); +Route::any('/upload/image', [app\common\controller\UploadController::class, 'uploadImage']); +Route::any('/upload/delete', [app\common\controller\UploadController::class, 'deleteFile']); + +// 公共 +Route::group('/common', function () { + Route::group('/auth', function () { + Route::any('/login', [app\common\controller\AuthController::class, 'login']); + Route::any('/refresh/token', [app\common\controller\AuthController::class, 'refresh']); + }); + + Route::group('/user', function () { + Route::any('/info', [app\common\controller\UserController::class, 'info']); + Route::any('/update/password', [app\common\controller\UserController::class, 'savePassword']); + Route::any('/update', [app\common\controller\UserController::class, 'save']); + }); + + Route::group('/preset', function () { + Route::any('/systems', [app\common\controller\PresetController::class, 'systems']); + Route::any('/roles', [app\common\controller\PresetController::class, 'roles']); + Route::any('/cache/clear', [app\common\controller\PresetController::class, 'cacheClear']); + Route::any('/menus', [app\common\controller\PresetController::class, 'menus']); + }); + + // 帖子 + Route::group('/post', function () { + Route::any('/list', [app\common\controller\PostController::class, 'index']); + Route::any('/show', [app\common\controller\PostController::class, 'show']); + Route::any('/interaction', [app\common\controller\PostController::class, 'interaction']); + Route::any('/store', [app\admin\controller\PostController::class, 'store']); + + // 评论 + Route::group('/comment', function () { + Route::any('/list', [app\common\controller\PostCommentController::class, 'index']); + Route::any('/store', [app\common\controller\PostCommentController::class, 'store']); + Route::any('/destroy', [app\common\controller\PostCommentController::class, 'delete']); + }); + + // 点赞 + Route::group('/like', function () { + Route::any('/list', [app\common\controller\PostLikeController::class, 'index']); + Route::any('/store', [app\common\controller\PostLikeController::class, 'store']); + Route::any('/destroy', [app\common\controller\PostLikeController::class, 'delete']); + }); + + // 收藏 + Route::group('/favorite', function () { + Route::any('/self', [app\common\controller\PostFavoriteController::class, 'self']); + Route::any('/store', [app\common\controller\PostFavoriteController::class, 'store']); + Route::any('/destroy', [app\common\controller\PostFavoriteController::class, 'delete']); + }); + + // 消息 + Route::group('/messageNotice', function () { + Route::any('/list', [app\common\controller\MessageNoticeController::class, 'index']); + }); + + // 帖子分类 + Route::group('/category', function () { + Route::any('/list', [app\admin\controller\PostCategoryController::class, 'index']); + }); + }); + + //协议 + Route::any('/agreement', [app\admin\controller\AgreementController::class, 'show']); + + // 轮播 + Route::group('/banner', function () { + Route::any('/list', [app\common\controller\BannerController::class, 'getAll']); + }); +}); + + +// 后台管理 +Route::group('/admin', function () { + // 角色 + Route::group('/role', function () { + Route::any('/list', [app\admin\controller\RoleController::class, 'index']); + Route::any('/show', [app\admin\controller\RoleController::class, 'show']); + Route::any('/store', [app\admin\controller\RoleController::class, 'store']); + Route::any('/update', [app\admin\controller\RoleController::class, 'update']); + Route::any('/destroy', [app\admin\controller\RoleController::class, 'destroy']); + Route::any('/menu/resource', [app\admin\controller\RoleController::class, 'menuResource']); + }); + + // 系统 + Route::group('/systems', function () { + Route::any('/list', [app\admin\controller\SystemsController::class, 'index']); + Route::any('/show', [app\admin\controller\SystemsController::class, 'show']); + Route::any('/store', [app\admin\controller\SystemsController::class, 'store']); + Route::any('/update', [app\admin\controller\SystemsController::class, 'update']); + Route::any('/destroy', [app\admin\controller\SystemsController::class, 'destroy']); + }); + + // 资源 + Route::group('/resources', function () { + Route::any('/list/tree', [app\common\controller\ResourceController::class, 'listTree']); + Route::any('/show', [app\common\controller\ResourceController::class, 'show']); + Route::any('/store', [app\common\controller\ResourceController::class, 'store']); + Route::any('/update', [app\common\controller\ResourceController::class, 'update']); + Route::any('/destroy', [app\common\controller\ResourceController::class, 'destroy']); + Route::any('/list/menu', [app\common\controller\ResourceController::class, 'menuList']); + Route::any('/list/menu/tree', [app\common\controller\ResourceController::class, 'menuTreeList']); + }); + + // 用户 + Route::group('/user', function () { + Route::any('/list', [app\admin\controller\UserController::class, 'index']); + Route::any('/show', [app\admin\controller\UserController::class, 'show']); + Route::any('/store', [app\admin\controller\UserController::class, 'store']); + Route::any('/update', [app\admin\controller\UserController::class, 'update']); + Route::any('/edit', [app\admin\controller\UserController::class, 'edit']); + Route::any('/authorize', [app\admin\controller\UserController::class, 'authorize']); + Route::any('/set/resources', [app\admin\controller\UserController::class, 'updateResources']); + Route::any('/update/password', [app\admin\controller\UserController::class, 'savePassword']); + }); + // 帖子 + Route::group('/post', function () { + Route::any('/list', [app\admin\controller\PostController::class, 'index']); + Route::any('/show', [app\admin\controller\PostController::class, 'show']); + Route::any('/store', [app\admin\controller\PostController::class, 'store']); + Route::any('/update', [app\admin\controller\PostController::class, 'update']); + Route::any('/destroy', [app\admin\controller\PostController::class, 'destroy']); + + // 评论 + Route::group('/comment', function () { + Route::any('/list', [app\admin\controller\PostCommentController::class, 'index']); + Route::any('/show', [app\admin\controller\PostCommentController::class, 'show']); + Route::any('/store', [app\admin\controller\PostCommentController::class, 'store']); + Route::any('/update', [app\admin\controller\PostCommentController::class, 'update']); + Route::any('/destroy', [app\admin\controller\PostCommentController::class, 'destroy']); + }); + + // 帖子分类 + Route::group('/category', function () { + Route::any('/list', [app\admin\controller\PostCategoryController::class, 'index']); + Route::any('/tree', [app\admin\controller\PostCategoryController::class, 'tree']); + Route::any('/show', [app\admin\controller\PostCategoryController::class, 'show']); + Route::any('/store', [app\admin\controller\PostCategoryController::class, 'store']); + Route::any('/update', [app\admin\controller\PostCategoryController::class, 'update']); + Route::any('/destroy', [app\admin\controller\PostCategoryController::class, 'destroy']); + }); + + // 点赞 + Route::group('/like', function () { + Route::any('/list', [app\admin\controller\PostLikeController::class, 'index']); + }); + + // 收藏 + Route::group('/favorite', function () { + Route::any('/list', [app\admin\controller\PostFavoriteController::class, 'index']); + }); + }); + +// // 分类下内容 +// Route::group('/nav/item', function () { +// Route::any('/list', [app\admin\controller\NavItemsController::class, 'index']); +// Route::any('/show', [app\admin\controller\NavItemsController::class, 'show']); +// Route::any('/store', [app\admin\controller\NavItemsController::class, 'store']); +// Route::any('/update', [app\admin\controller\NavItemsController::class, 'update']); +// Route::any('/destroy', [app\admin\controller\NavItemsController::class, 'destroy']); +// }); + + // 设置 + Route::group('/banner', function () { + Route::any('/list', [app\admin\controller\BannerController::class, 'index']); + Route::any('/show', [app\admin\controller\BannerController::class, 'show']); + Route::any('/store', [app\admin\controller\BannerController::class, 'store']); + Route::any('/update', [app\admin\controller\BannerController::class, 'update']); + Route::any('/destroy', [app\admin\controller\BannerController::class, 'destroy']); + Route::any('/module', [app\admin\controller\BannerController::class, 'getBannerModule']); + }); + + // 设置 + Route::group('/agreement', function () { + Route::any('/list', [app\admin\controller\AgreementController::class, 'index']); + Route::any('/show', [app\admin\controller\AgreementController::class, 'show']); + Route::any('/store', [app\admin\controller\AgreementController::class, 'store']); + Route::any('/update', [app\admin\controller\AgreementController::class, 'update']); + Route::any('/destroy', [app\admin\controller\AgreementController::class, 'destroy']); + }); +}); + + diff --git a/config/server.php b/config/server.php new file mode 100644 index 0000000..054d01f --- /dev/null +++ b/config/server.php @@ -0,0 +1,23 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'event_loop' => '', + 'stop_timeout' => 2, + 'pid_file' => runtime_path() . '/webman.pid', + 'status_file' => runtime_path() . '/webman.status', + 'stdout_file' => runtime_path() . '/logs/stdout.log', + 'log_file' => runtime_path() . '/logs/workerman.log', + 'max_package_size' => 10 * 1024 * 1024 +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..043f8c4 --- /dev/null +++ b/config/session.php @@ -0,0 +1,65 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Webman\Session\FileSessionHandler; +use Webman\Session\RedisSessionHandler; +use Webman\Session\RedisClusterSessionHandler; + +return [ + + 'type' => 'file', // or redis or redis_cluster + + 'handler' => FileSessionHandler::class, + + 'config' => [ + 'file' => [ + 'save_path' => runtime_path() . '/sessions', + ], + 'redis' => [ + 'host' => '127.0.0.1', + 'port' => 6379, + 'auth' => '', + 'timeout' => 2, + 'database' => '', + 'prefix' => 'redis_session_', + ], + 'redis_cluster' => [ + 'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'], + 'timeout' => 2, + 'auth' => '', + 'prefix' => 'redis_session_', + ] + ], + + 'session_name' => 'PHPSID', + + 'auto_update_timestamp' => false, + + 'lifetime' => 7*24*60*60, + + 'cookie_lifetime' => 365*24*60*60, + + 'cookie_path' => '/', + + 'domain' => '', + + 'http_only' => true, + + 'secure' => false, + + 'same_site' => '', + + 'gc_probability' => [1, 1000], + +]; diff --git a/config/static.php b/config/static.php new file mode 100644 index 0000000..6313679 --- /dev/null +++ b/config/static.php @@ -0,0 +1,23 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * Static file settings + */ +return [ + 'enable' => true, + 'middleware' => [ // Static file Middleware + //app\middleware\StaticFile::class, + ], +]; \ No newline at end of file diff --git a/config/translation.php b/config/translation.php new file mode 100644 index 0000000..96589b2 --- /dev/null +++ b/config/translation.php @@ -0,0 +1,25 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * Multilingual configuration + */ +return [ + // Default language + 'locale' => 'zh_CN', + // Fallback language + 'fallback_locale' => ['zh_CN', 'en'], + // Folder where language files are stored + 'path' => base_path() . '/resource/translations', +]; \ No newline at end of file diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..e3a7b85 --- /dev/null +++ b/config/view.php @@ -0,0 +1,22 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use support\view\Raw; +use support\view\Twig; +use support\view\Blade; +use support\view\ThinkPHP; + +return [ + 'handler' => Raw::class +]; diff --git a/config/wechat.php b/config/wechat.php new file mode 100644 index 0000000..8b80aa7 --- /dev/null +++ b/config/wechat.php @@ -0,0 +1,20 @@ + [ + 'app_id' => getenv('WECHAT_WEB_APPID', ''), + 'app_secret' => getenv('WECHAT_WEB_SECRET', ''), + 'redirect' => getenv('WECHAT_WEB_REDIRECT', ''), + ], + 'mini' => [ + 'app_id' => getenv('WECHAT_MINI_APPID', ''), + 'app_secret' => getenv('WECHAT_MINI_SECRET', ''), + ], + + // 登录态、幂等等 + 'login' => [ + 'state_ttl' => 300, // state 有效期 5 分钟 + 'result_ttl'=> 300, // 扫码结果缓存 5 分钟 + ], +]; diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..b9f722e Binary files /dev/null and b/public/favicon.ico differ diff --git a/start.php b/start.php new file mode 100644 index 0000000..41ad7ef --- /dev/null +++ b/start.php @@ -0,0 +1,5 @@ +#!/usr/bin/env php + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace support; + +/** + * Class Request + * @package support + */ +class Request extends \Webman\Http\Request +{ + +} \ No newline at end of file diff --git a/support/Response.php b/support/Response.php new file mode 100644 index 0000000..9bc4e1e --- /dev/null +++ b/support/Response.php @@ -0,0 +1,24 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace support; + +/** + * Class Response + * @package support + */ +class Response extends \Webman\Http\Response +{ + +} \ No newline at end of file diff --git a/support/bootstrap.php b/support/bootstrap.php new file mode 100644 index 0000000..d913def --- /dev/null +++ b/support/bootstrap.php @@ -0,0 +1,139 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use Dotenv\Dotenv; +use support\Log; +use Webman\Bootstrap; +use Webman\Config; +use Webman\Middleware; +use Webman\Route; +use Webman\Util; +use Workerman\Events\Select; +use Workerman\Worker; + +$worker = $worker ?? null; + +if (empty(Worker::$eventLoopClass)) { + Worker::$eventLoopClass = Select::class; +} + +set_error_handler(function ($level, $message, $file = '', $line = 0) { + if (error_reporting() & $level) { + throw new ErrorException($message, 0, $level, $file, $line); + } +}); + +if ($worker) { + register_shutdown_function(function ($startTime) { + if (time() - $startTime <= 0.1) { + sleep(1); + } + }, time()); +} + +if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) { + if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) { + Dotenv::createUnsafeMutable(base_path(false))->load(); + } else { + Dotenv::createMutable(base_path(false))->load(); + } +} + +Config::clear(); +support\App::loadAllConfig(['route']); +if ($timezone = config('app.default_timezone')) { + date_default_timezone_set($timezone); +} + +foreach (config('autoload.files', []) as $file) { + include_once $file; +} +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + foreach ($project['autoload']['files'] ?? [] as $file) { + include_once $file; + } + } + foreach ($projects['autoload']['files'] ?? [] as $file) { + include_once $file; + } +} + +Middleware::load(config('middleware', [])); +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project) || $name === 'static') { + continue; + } + Middleware::load($project['middleware'] ?? []); + } + Middleware::load($projects['middleware'] ?? [], $firm); + if ($staticMiddlewares = config("plugin.$firm.static.middleware")) { + Middleware::load(['__static__' => $staticMiddlewares], $firm); + } +} +Middleware::load(['__static__' => config('static.middleware', [])]); + +foreach (config('bootstrap', []) as $className) { + if (!class_exists($className)) { + $log = "Warning: Class $className setting in config/bootstrap.php not found\r\n"; + echo $log; + Log::error($log); + continue; + } + /** @var Bootstrap $className */ + $className::start($worker); +} + +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + foreach ($project['bootstrap'] ?? [] as $className) { + if (!class_exists($className)) { + $log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n"; + echo $log; + Log::error($log); + continue; + } + /** @var Bootstrap $className */ + $className::start($worker); + } + } + foreach ($projects['bootstrap'] ?? [] as $className) { + /** @var string $className */ + if (!class_exists($className)) { + $log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n"; + echo $log; + Log::error($log); + continue; + } + /** @var Bootstrap $className */ + $className::start($worker); + } +} + +$directory = base_path() . '/plugin'; +$paths = [config_path()]; +foreach (Util::scanDir($directory) as $path) { + if (is_dir($path = "$path/config")) { + $paths[] = $path; + } +} +Route::load($paths); + diff --git a/webman b/webman new file mode 100644 index 0000000..3f55062 --- /dev/null +++ b/webman @@ -0,0 +1,71 @@ +#!/usr/bin/env php +load(); + } else { + Dotenv::createMutable(run_path())->load(); + } +} + +$appConfig = require $appConfigFile; +if ($timezone = $appConfig['default_timezone'] ?? '') { + date_default_timezone_set($timezone); +} + +if ($errorReporting = $appConfig['error_reporting'] ?? '') { + error_reporting($errorReporting); +} + +if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) { + require_once __DIR__ . '/support/bootstrap.php'; +} else { + if (class_exists('Support\App')) { + Support\App::loadAllConfig(['route']); + } else { + Config::reload(config_path(), ['route', 'container']); + } +} + +$cli = new Command(); +$cli->setName('webman cli'); +$cli->installInternalCommands(); +if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { + $cli->installCommands($command_path); +} + +foreach (config('plugin', []) as $firm => $projects) { + if (isset($projects['app'])) { + foreach (['', '/app'] as $app) { + if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { + $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; + $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); + } + } + } + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + $project['command'] ??= []; + array_walk($project['command'], [$cli, 'createCommandInstance']); + } +} + +$cli->run(); diff --git a/windows.bat b/windows.bat new file mode 100644 index 0000000..f07ce53 --- /dev/null +++ b/windows.bat @@ -0,0 +1,3 @@ +CHCP 65001 +php windows.php +pause \ No newline at end of file diff --git a/windows.php b/windows.php new file mode 100644 index 0000000..f37a72c --- /dev/null +++ b/windows.php @@ -0,0 +1,136 @@ +load(); + } else { + Dotenv::createMutable(base_path())->load(); + } +} + +App::loadAllConfig(['route']); + +$errorReporting = config('app.error_reporting'); +if (isset($errorReporting)) { + error_reporting($errorReporting); +} + +$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows'; +$paths = [ + $runtimeProcessPath, + runtime_path('logs'), + runtime_path('views') +]; +foreach ($paths as $path) { + if (!is_dir($path)) { + mkdir($path, 0777, true); + } +} + +$processFiles = []; +if (config('server.listen')) { + $processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php'; +} +foreach (config('process', []) as $processName => $config) { + $processFiles[] = write_process_file($runtimeProcessPath, $processName, ''); +} + +foreach (config('plugin', []) as $firm => $projects) { + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + foreach ($project['process'] ?? [] as $processName => $config) { + $processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name"); + } + } + foreach ($projects['process'] ?? [] as $processName => $config) { + $processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm); + } +} + +function write_process_file($runtimeProcessPath, $processName, $firm): string +{ + $processParam = $firm ? "plugin.$firm.$processName" : $processName; + $configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']"; + $fileContent = << true]); + if (!$resource) { + exit("Can not execute $cmd\r\n"); + } + return $resource; +} + +$resource = popen_processes($processFiles); +echo "\r\n"; +while (1) { + sleep(1); + if (!empty($monitor) && $monitor->checkAllFilesChange()) { + $status = proc_get_status($resource); + $pid = $status['pid']; + shell_exec("taskkill /F /T /PID $pid"); + proc_close($resource); + $resource = popen_processes($processFiles); + } +}