commit a9c2455857abfe9c88df594c36f0c5563bbbf88f Author: u2nyakim Date: Fri Aug 22 10:11:22 2025 +0800 init. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5054a67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.log +.env +composer.phar +composer.lock +.DS_Store +Thumbs.db +/.idea +/.vscode +/vendor +/.settings +/.buildpath +/.project \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..36f7b6f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,42 @@ +sudo: false + +language: php + +branches: + only: + - stable + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - composer self-update + +install: + - composer install --no-dev --no-interaction --ignore-platform-reqs + - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip . + - composer require --update-no-dev --no-interaction "topthink/think-image:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0" + - composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0" + - composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0" + - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip . + +script: + - php think unit + +deploy: + provider: releases + api_key: + secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw= + file: + - ThinkPHP_Core.zip + - ThinkPHP_Full.zip + skip_cleanup: true + on: + tags: true diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8d94897 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2025 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..05c7b83 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +![](https://www.thinkphp.cn/uploads/images/20230630/300c856765af4d8ae758c503185f8739.png) + +ThinkPHP 8 +=============== + +## 特性 + +* 基于PHP`8.0+`重构 +* 升级`PSR`依赖 +* 依赖`think-orm`3.0+版本 +* 全新的`think-dumper`服务,支持远程调试 +* 支持`6.0`/`6.1`无缝升级 + +> ThinkPHP8的运行环境要求PHP8.0+ + +现在开始,你可以使用官方提供的[ThinkChat](https://chat.topthink.com/),让你在学习ThinkPHP的旅途中享受私人AI助理服务! + +![](https://www.topthink.com/uploads/assistant/20230630/4d1a3f0ad2958b49bb8189b7ef824cb0.png) + +ThinkPHP生态服务由[顶想云](https://www.topthink.com)(TOPThink Cloud)提供,为生态提供专业的开发者服务和价值之选。 + +## 文档 + +[完全开发手册](https://doc.thinkphp.cn) + + +## 赞助 + +全新的[赞助计划](https://www.thinkphp.cn/sponsor)可以让你通过我们的网站、手册、欢迎页及GIT仓库获得巨大曝光,同时提升企业的品牌声誉,也更好保障ThinkPHP的可持续发展。 + +[![](https://www.thinkphp.cn/sponsor/special.svg)](https://www.thinkphp.cn/sponsor/special) + +[![](https://www.thinkphp.cn/sponsor.svg)](https://www.thinkphp.cn/sponsor) + +## 安装 + +~~~ +composer create-project topthink/think tp +~~~ + +启动服务 + +~~~ +cd tp +php think run +~~~ + +然后就可以在浏览器中访问 + +~~~ +http://localhost:8000 +~~~ + +如果需要更新框架使用 +~~~ +composer update topthink/framework +~~~ + +## 命名规范 + +`ThinkPHP`遵循PSR-2命名规范和PSR-4自动加载规范。 + +## 参与开发 + +直接提交PR或者Issue即可 + +## 版权信息 + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 + +本项目包含的第三方源码和二进制文件之版权信息另行标注。 + +版权所有Copyright © 2006-2024 by ThinkPHP (http://thinkphp.cn) All rights reserved。 + +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +更多细节参阅 [LICENSE.txt](LICENSE.txt) diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/app/AppService.php b/app/AppService.php new file mode 100644 index 0000000..fa69acd --- /dev/null +++ b/app/AppService.php @@ -0,0 +1,18 @@ +app->middleware->unshift(ContextMiddleware::class, 'route'); + } +} diff --git a/app/BaseController.php b/app/BaseController.php new file mode 100644 index 0000000..c7646bb --- /dev/null +++ b/app/BaseController.php @@ -0,0 +1,145 @@ +app = $app; + $this->request = $this->app->request; + $this->auth = $this->app->request->getAuth(); + + + // 控制器初始化 + $this->initialize(); + } + + // 初始化 + protected function initialize() + { + } + + /** + * 验证数据 + * @access protected + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @return array|string|true + * @throws ValidateException + */ + protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false) + { + if (is_array($validate)) { + $v = new Validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + [$validate, $scene] = explode('.', $validate); + } + $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate); + $v = new $class(); + if (!empty($scene)) { + $v->scene($scene); + } + } + + $v->message($message); + + // 是否批量验证 + if ($batch || $this->batchValidate) { + $v->batch(true); + } + + return $v->failException(true)->check($data); + } + + /** + * 接口调用成功 + * @param string $message + * @param mixed $data + * @return Json + */ + protected function writeSuccess(string $message = '', mixed $data = new stdClass()): Json + { + return $this->writeJson(HttpStatus::API_SUCCESS->value, $message ?: HttpStatus::message(HttpStatus::API_SUCCESS), $data); + } + + /** + * 接口调用失败 + * @param string $message + * @param mixed $data + * @return Json + */ + protected function writeError(string $message = '', mixed $data = new stdClass()): Json + { + return $this->writeJson(HttpStatus::API_ERROR->value, $message ?: HttpStatus::message(HttpStatus::API_ERROR), $data); + } + + /** + * 状态码 + * @param int $code + * @param string $message + * @param mixed $data + * @return Json + */ + protected function writeJson(int $code = 200, string $message = '', mixed $data = null): Json + { + if($data instanceof Paginator) { + $data = [ + 'count'=> $data->total(), + 'list' => $data->items() + ]; + } + return json(array( + "code" => $code, + "data" => $data, + "message" => $message, + )); + } +} diff --git a/app/BaseModel.php b/app/BaseModel.php new file mode 100644 index 0000000..64f22f2 --- /dev/null +++ b/app/BaseModel.php @@ -0,0 +1,19 @@ + true + ]; + } +} \ No newline at end of file diff --git a/app/ExceptionHandle.php b/app/ExceptionHandle.php new file mode 100644 index 0000000..453d126 --- /dev/null +++ b/app/ExceptionHandle.php @@ -0,0 +1,58 @@ +auth = $auth; + return $this; + } + + public function getAuth(): ?HttpAuth + { +// var_dump($this->auth); + return $this->auth ?? null; + } + + + public function getBrowser(): string + { + return get_agent_browser($this->server('HTTP_USER_AGENT')); + } + + public function getDevice(): string + { + return get_agent_device($this->server('HTTP_USER_AGENT')); + } + + public function getOs(): string + { + return get_agent_os($this->server('HTTP_USER_AGENT')); + } + +} diff --git a/app/RequestClient.php b/app/RequestClient.php new file mode 100644 index 0000000..b0cb744 --- /dev/null +++ b/app/RequestClient.php @@ -0,0 +1,53 @@ +name = $clientName; // 客户端名称(PAdmin/IAdmin) + $this->id = $clientId; // 客户端ID(前端生成的唯一标识) + $this->version = $clientVersion; // 客户端版本号 + $this->sign = $sign; // 携带签名数据 + try { + $this->validate(); + } catch (Exception $e) { + throw new ValidateException($e->getMessage()); + } + } + + private function validate(): void + { + $data = []; + if ($this->sign) { + $encryptedData = base64_decode($this->sign); + $dataStr = openssl_decrypt($encryptedData, 'AES-128-ECB', RequestClient::CLIENT_KEY, OPENSSL_RAW_DATA); + $data = json_decode($dataStr, true); + } + $this->data = $data; + validate(ClientValidate::class)->check([ + 'name' => $this->name, + 'id' => $this->id, + 'version' => $this->version, + ]); + } +} diff --git a/app/common.php b/app/common.php new file mode 100644 index 0000000..83b7ca5 --- /dev/null +++ b/app/common.php @@ -0,0 +1,221 @@ +toArray(); + } + if ($data instanceof Collection) { + $data = $data->toArray(); + } + + Event::trigger('sys:securityLogRecord', [$event, $message, $data]); +} + + +/** + * 获取字典内容 + * @param string $name + * @param $default + * @return mixed + */ +function dict_get(string $name, $default = null): mixed +{ + return dict()->get($name, $default); +} + +/** + * 获取字典服务 + * @return DictionaryService + */ +function dict(): DictionaryService +{ + return app()->dict; +} + +/** + * 将扁平数组列表转换为树形结构 + * @param array $items 原始数组 + * @param string $idKey ID字段名 + * @param string $parentKey 父ID字段名 + * @param string $childrenKey 子节点字段名 + * @return array 树形结构 + */ +function list_build_tree(array $items, string $idKey = 'id', string $parentKey = 'pid', string $childrenKey = 'children'): array +{ + // 创建ID映射和结果树 + $tree = []; + $map = []; + + // 创建ID到数组元素的引用映射 + foreach ($items as &$item) { + $map[$item[$idKey]] = &$item; + $item[$childrenKey] = []; // 初始化children + } + + // 构建树结构 + foreach ($items as &$item) { + $parentId = $item[$parentKey]; + + if (isset($map[$parentId])) { + // 当前项有父节点,添加到父节点的children + $map[$parentId][$childrenKey][] = &$item; + } else { + // 当前项是根节点,添加到树 + $tree[] = &$item; + } + } + + // 清理引用 + unset($item); + + return $tree; +} + + +/** + * @param int $minHashLength + * @return Hashids + */ +function hashids(int $minHashLength = 6): Hashids +{ + $salt = (string)config('app.default_salt', ''); + return new Hashids($salt, $minHashLength); +} + +function snowflake_id(int $machineId = null): int +{ + // 时间戳 42字节 + $time = floor(microtime(true) * 1000); + // 当前时间 与 开始时间 差值 + $time -= 1641862815726; + $base = decbin(1099511627775 + $time); + if ($machineId) { + $machineId = str_pad(decbin($machineId), 10, '0', STR_PAD_LEFT); + } + $random = str_pad(decbin(mt_rand(0, 4095)), 12, '0', STR_PAD_LEFT); + return bindec($base . $machineId . $random); +} + +function guid($namespace = ''): string +{ + + $uid = uniqid("", true); + $data = $namespace; + $data .= $_SERVER['REQUEST_TIME'] ?? ''; + $data .= $_SERVER['HTTP_USER_AGENT'] ?? ''; + $data .= $_SERVER['LOCAL_ADDR'] ?? ''; + $data .= $_SERVER['LOCAL_PORT'] ?? ''; + $data .= $_SERVER['REMOTE_ADDR'] ?? ''; + $data .= $_SERVER['REMOTE_PORT'] ?? ''; + $hash = strtoupper(hash('ripemd128', $uid . md5($data))); + return + substr($hash, 0, 8) . + '-' . + substr($hash, 8, 4) . + '-' . + substr($hash, 12, 4) . + '-' . + substr($hash, 16, 4) . + '-' . + substr($hash, 20, 12); +} + +function unique_str(): string +{ + return md5(uniqid()); +} + +function get_agent_browser($userAgent = ''): string +{ + if (str_contains($userAgent, 'MicroMessenger')) { + return 'Wechat'; + } elseif (str_contains($userAgent, 'QQBrowser')) { + return 'QQ'; + } elseif (str_contains($userAgent, 'MSIE') || str_contains($userAgent, 'Trident/7.0;') || str_contains($userAgent, 'rv:11.0')) { + return 'Explorer'; + } elseif (str_contains($userAgent, 'Firefox')) { + return 'Firefox'; + } elseif (str_contains($userAgent, 'Edg')) { + return 'Edge'; + } elseif (str_contains($userAgent, 'Chrome')) { + return 'Chrome'; + } elseif (str_contains($userAgent, 'Safari')) { + return 'Safari'; + } elseif (str_contains($userAgent, 'Opera Mini') || str_contains($userAgent, 'Opera')) { + return 'Opera'; + } else { + return 'Unknown'; + } +} + +function get_agent_device($agent = ''): string +{ + if (stristr($agent, 'iPad')) { + $fb_fs = "iPad"; + } else if (preg_match('/Android (([0-9_.]{1,3})+)/i', $agent, $version)) { + $fb_fs = "手机(Android " . $version[1] . ")"; + } else if (stristr($agent, 'Linux')) { + $fb_fs = "电脑(Linux)"; + } else if (preg_match('/iPhone OS (([0-9_.]{1,3})+)/i', $agent, $version)) { + $fb_fs = "手机(iPhone " . $version[1] . ")"; + } else if (preg_match('/Mac OS X (([0-9_.]{1,5})+)/i', $agent, $version)) { + $fb_fs = "电脑(OS X " . $version[1] . ")"; + } else if (preg_match('/unix/i', $agent)) { + $fb_fs = "Unix"; + } else if (preg_match('/windows/i', $agent)) { + $fb_fs = "电脑(Windows)"; + } else { + $fb_fs = "Unknown"; + } + return $fb_fs; +} + +function get_agent_os($userAgent): string +{ + if (preg_match('/windows/i', $userAgent)) { + return 'Windows'; + } elseif (preg_match('/iPhone/i', $userAgent)) { + return 'iOS'; + } elseif (preg_match('/HarmonyOS/i', $userAgent)) { + return 'Android'; + } elseif (preg_match('/Android/i', $userAgent)) { + return 'Android'; + } elseif (preg_match('/mac/i', $userAgent)) { + return 'Mac'; + } else { + return 'Unknown'; + } +} + +function agent_is_crawler($userAgent): bool +{ + $crawlers = [ + 'googlebot', 'bingbot', 'slurp', 'baidu', 'duckduckbot', + 'yandexbot', 'sogou', 'exabot', 'ia_archiver', 'facebot', + 'facebookexternalhit' + ]; + + foreach ($crawlers as $crawler) { + if (str_contains($userAgent, $crawler)) { + return true; + } + } + return false; +} diff --git a/app/controller/Index.php b/app/controller/Index.php new file mode 100644 index 0000000..80d42ba --- /dev/null +++ b/app/controller/Index.php @@ -0,0 +1,30 @@ + function($query) { +// $query->where('deleted', 0); +// }])->find("1"); +// +// $menus = $user->getMenus(); +// dump($user); +// dump($menus); + +// dd($user->getMenus(true)); + + + return ''; + return ''; + } + + public function hello($name = 'ThinkPHP8') + { + return 'hello,' . $name; + } +} diff --git a/app/controller/admin/auth/AuthController.php b/app/controller/admin/auth/AuthController.php new file mode 100644 index 0000000..a751023 --- /dev/null +++ b/app/controller/admin/auth/AuthController.php @@ -0,0 +1,40 @@ +auth->userId); + $data = $user->append(['authorities', 'roles'])->toArray(); + + return $this->writeSuccess('', $data); + } + + /** + * 退出登录 + * @return Json + */ + public function logout(): Json + { + return $this->writeSuccess('退出成功'); + } +} diff --git a/app/controller/admin/auth/LoginController.php b/app/controller/admin/auth/LoginController.php new file mode 100644 index 0000000..4d45eed --- /dev/null +++ b/app/controller/admin/auth/LoginController.php @@ -0,0 +1,37 @@ +writeSuccess('登录成功', [ + 'base64'=> $captchaData['data']['base64'] ?? '', + 'text'=> $captchaData['data']['text'] ?? '', + ]); + } + public function index() + { + $username = $this->request->post('username', ''); + $password = $this->request->post('password', ''); + $remember = $this->request->post('remember', false); + $code = $this->request->post('code', ''); + + $loginService = new LoginService(); + try { + $result = $loginService->login($this->request, $this->request->getClient(), $username, $password, $code, $remember); + } catch (ValidateException $e) { + return $this->writeError($e->getError()); + } + + return $this->writeSuccess('登录成功', ['user' => $result['user'], 'access_token' => $result['access_token']]); + } +} diff --git a/app/controller/admin/system/DictionaryController.php b/app/controller/admin/system/DictionaryController.php new file mode 100644 index 0000000..2a42521 --- /dev/null +++ b/app/controller/admin/system/DictionaryController.php @@ -0,0 +1,135 @@ +request, new SysDictionary(), ['sort_number' => 'desc']); + + return $this->writeSuccess('success', $lists); + } + + public function update() + { + $data = $this->request->put([ + 'dictId' => 0, + 'dictCode' => null, + 'dictName' => null, + 'sortNumber' => 100, + 'comments' => '' + ]); + SysDictionary::findOrFail($data['dictId'])->save([ + 'dict_code' => $data['dictCode'], + 'dict_name' => $data['dictName'], + 'sort_number' => $data['sortNumber'], + 'comments' => $data['comments'] + ]); + return $this->writeSuccess('修改成功'); + } + + public function remove(SysDictionary $model) + { + $model->delete(); + + return $this->writeSuccess('删除成功'); + } + + public function add() + { + $data = $this->request->post([ + 'dictCode' => null, + 'dictName' => null, + 'sortNumber' => 100, + 'comments' => '' + ]); + $sysDictionary = new SysDictionary(); + $sysDictionary->save([ + 'dict_code' => $data['dictCode'], + 'dict_name' => $data['dictName'], + 'sort_number' => $data['sortNumber'], + 'comments' => $data['comments'] + ]); + return $this->writeSuccess('添加成功'); + } + + public function dataAdd() + { + $data = $this->request->post([ + 'dictId' => null, + 'dictDataCode' => null, + 'dictDataName' => null, + 'sortNumber' => 100, + 'comments' => '' + ]); + $sysDictionary = new SysDictionaryData(); + $sysDictionary->save([ + 'dict_id' => $data['dictId'], + 'dict_data_code' => $data['dictDataCode'], + 'dict_data_name' => $data['dictDataName'], + 'sort_number' => $data['sortNumber'], + 'comments' => $data['comments'] + ]); + return $this->writeSuccess('添加成功'); + } + + public function dataUpdate() + { + $data = $this->request->put([ + 'dictDataId' => 0, + 'dictId' => null, + 'dictDataCode' => null, + 'dictDataName' => null, + 'sortNumber' => 100, + 'comments' => '' + ]); + SysDictionaryData::findOrFail($data['dictDataId'])->save([ + 'dict_id' => $data['dictId'], + 'dict_data_code' => $data['dictDataCode'], + 'dict_data_name' => $data['dictDataName'], + 'sort_number' => $data['sortNumber'], + 'comments' => $data['comments'] + ]); + return $this->writeSuccess('修改成功'); + } + + public function dataBatchRemove() + { + $data = $this->request->delete(); + SysDictionaryData::destroy($data); + return $this->writeSuccess('删除成功'); + } + + public function dataLists(): Json + { + $dictCode = $this->request->param('dictCode/s', ''); + + $data = SysDictionary::dictCodeData($dictCode); + + return $this->writeSuccess('success', $data); + } + + /** + * 查询字典集列表 + * @return Json + * @throws DbException + */ + public function dataPage(): Json + { + $paginate = CurdService::getPaginate($this->request, SysDictionaryData::withSearch(['dictId', 'dictDataName', 'dictDataCode'], [ + 'dictId' => $this->request->get('dictId/d', 0), + 'dictDataName' => $this->request->get('dictDataName/s', ''), + 'dictDataCode' => $this->request->get('dictDataCode/s', ''), + ])); + + return $this->writeSuccess('success', $paginate); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/DictionaryDataController.php b/app/controller/admin/system/DictionaryDataController.php new file mode 100644 index 0000000..5d8d2a5 --- /dev/null +++ b/app/controller/admin/system/DictionaryDataController.php @@ -0,0 +1,85 @@ +request->post([ + 'dictId' => null, + 'dictDataCode' => null, + 'dictDataName' => null, + 'sortNumber' => 100, + 'comments' => '' + ]); + $sysDictionary = new SysDictionaryData(); + $sysDictionary->save([ + 'dict_id' => $data['dictId'], + 'dict_data_code' => $data['dictDataCode'], + 'dict_data_name' => $data['dictDataName'], + 'sort_number' => $data['sortNumber'], + 'comments' => $data['comments'] + ]); + return $this->writeSuccess('添加成功'); + } + + public function update() + { + $data = $this->request->put([ + 'dictDataId' => 0, + 'dictId' => null, + 'dictDataCode' => null, + 'dictDataName' => null, + 'sortNumber' => 100, + 'comments' => '' + ]); + SysDictionaryData::findOrFail($data['dictDataId'])->save([ + 'dict_id' => $data['dictId'], + 'dict_data_code' => $data['dictDataCode'], + 'dict_data_name' => $data['dictDataName'], + 'sort_number' => $data['sortNumber'], + 'comments' => $data['comments'] + ]); + return $this->writeSuccess('修改成功'); + } + + public function batchRemove() + { + $data = $this->request->delete(); + SysDictionaryData::destroy($data); + return $this->writeSuccess('删除成功'); + } + + public function lists(): Json + { + $dictCode = $this->request->param('dictCode/s', ''); + + $data = SysDictionary::dictCodeData($dictCode); + + return $this->writeSuccess('success', $data); + } + + /** + * 查询字典集列表 + * @return Json + * @throws DbException + */ + public function page(): Json + { + $paginate = CurdService::getPaginate($this->request, SysDictionaryData::withSearch(['dictId', 'dictDataName', 'dictDataCode'], [ + 'dictId' => $this->request->get('dictId/d', 0), + 'dictDataName' => $this->request->get('dictDataName/s', ''), + 'dictDataCode' => $this->request->get('dictDataCode/s', ''), + ])); + + return $this->writeSuccess('success', $paginate); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/FileController.php b/app/controller/admin/system/FileController.php new file mode 100644 index 0000000..40a3861 --- /dev/null +++ b/app/controller/admin/system/FileController.php @@ -0,0 +1,41 @@ +withSearch(['name','path','createNickname'], [ + 'name'=> $this->request->param('name/s',''), + 'path'=> $this->request->param('path/s',''), + 'createNickname'=> $this->request->param('createNickname/s',''), + 'createTime' => [ + $this->request->get('createTimeStart/s', ''), + $this->request->get('createTimeEnd/s', '') + ], + ]); + + $paginate = CurdService::getPaginate($this->request, $model); + + return $this->writeSuccess('ok', $paginate); + } + + public function batchRemove() + { + $data = $this->request->delete(); + SysFileRecord::destroy($data); + return $this->writeSuccess('删除成功'); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/LoginRecordController.php b/app/controller/admin/system/LoginRecordController.php new file mode 100644 index 0000000..acb1866 --- /dev/null +++ b/app/controller/admin/system/LoginRecordController.php @@ -0,0 +1,34 @@ + $this->request->get('username/s', ''), + 'nickname' => $this->request->get('nickname/s', ''), + 'loginType' => $this->request->get('loginType/d', 0), + 'createTime' => [ + $this->request->get('createTimeStart/s', ''), + $this->request->get('createTimeEnd/s', '') + ], + ]); + + $paginate = CurdService::getPaginate($this->request, $model); + + return $this->writeSuccess('ok', $paginate); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/MenuController.php b/app/controller/admin/system/MenuController.php new file mode 100644 index 0000000..563eeef --- /dev/null +++ b/app/controller/admin/system/MenuController.php @@ -0,0 +1,84 @@ + $this->request->get('title/s', ''), + 'path' => $this->request->get('path/s', ''), + 'authority' => $this->request->get('authority/s', ''), + ]); + + $lists = CurdService::getList($this->request, $model, ['sort_number' => 'asc']); + + return $this->writeSuccess('ok', $lists); + } + + /** + * 添加菜单列表 + * @return Json + */ + public function add(): Json + { + $data = $this->request->post([ + 'parentId' => 0, + 'title' => '', + 'menuType' => '', + 'openType' => '', + 'icon' => '', + 'path' => '', + 'component' => '', + 'authority' => '', + 'sortNumber' => 0, + 'hide' => 0, + 'meta' => '', + ]); + + $user = new SysMenu(); + $user->save([ + 'parent_id' => $data['parentId'], + 'title' => $data['title'], + 'menu_type' => $data['menuType'], + 'open_type' => $data['openType'], + 'icon' => $data['icon'], + 'path' => $data['path'], + 'component' => $data['component'], + 'authority' => $data['authority'], + 'sortNumber' => $data['sortNumber'], + 'hide' => $data['hide'], + 'meta' => $data['meta'], + + ]); + return $this->writeSuccess('添加成功'); + } + + /** + * 删除菜单 + * @param SysMenu $model + * @return Json + */ + public function remove(SysMenu $model) + { + $model->delete(); + + return $this->writeSuccess('删除成功'); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/OperateRecordController.php b/app/controller/admin/system/OperateRecordController.php new file mode 100644 index 0000000..7ff0533 --- /dev/null +++ b/app/controller/admin/system/OperateRecordController.php @@ -0,0 +1,34 @@ + $this->request->get('username/s', ''), + 'nickname' => $this->request->get('nickname/s', ''), + 'loginType' => $this->request->get('loginType/d', 0), + 'createTime' => [ + $this->request->get('createTimeStart/s', ''), + $this->request->get('createTimeEnd/s', '') + ], + ]); + + $paginate = CurdService::getPaginate($this->request, $model); + + return $this->writeSuccess('ok', $paginate); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/OrganizationController.php b/app/controller/admin/system/OrganizationController.php new file mode 100644 index 0000000..20646a4 --- /dev/null +++ b/app/controller/admin/system/OrganizationController.php @@ -0,0 +1,80 @@ + $this->request->get('organizationType/d', 0), + 'organizationName' => $this->request->get('organizationName/s', ''), + ])->append(['organizationTypeName']); + + $lists = CurdService::getList($this->request, $model); + + return $this->writeSuccess('ok', $lists); + } + + public function update() + { + $data = $this->request->post([ + 'parentId' => 0, + 'organizationType' => 0, + 'sortNumber' => 100, + 'organizationName' => '', + 'organizationFullName' => '', + 'organizationCode' => '', + 'comments' => '', + 'organizationId' => '' + ]); + $sysDictionary = SysOrganization::findOrFail($data['organizationId']); + $sysDictionary->save([ + 'parent_id' => $data['parentId'], + 'sort_number' => $data['sortNumber'], + 'organization_type' => $data['organizationType'], + 'organization_name' => $data['organizationName'], + 'organization_full_name' => $data['organizationFullName'], + 'organization_code' => $data['organizationCode'], + 'comments' => $data['comments'], + ]); + return $this->writeSuccess('更新成功'); + } + + public function add() + { + $data = $this->request->post([ + 'parentId' => 0, + 'organizationType' => 0, + 'sortNumber' => 100, + 'organizationName' => '', + 'organizationFullName' => '', + 'organizationCode' => '', + 'comments' => '', + + ]); + $sysDictionary = new SysOrganization(); + $sysDictionary->save([ + 'parent_id' => $data['parentId'], + 'sort_number' => $data['sortNumber'], + 'organization_type' => $data['organizationType'], + 'organization_name' => $data['organizationName'], + 'organization_full_name' => $data['organizationFullName'], + 'organization_code' => $data['organizationCode'], + 'comments' => $data['comments'], + ]); + return $this->writeSuccess('添加成功'); + } + + public function remove(SysOrganization $model) + { + $model->delete(); + + return $this->writeSuccess('删除成功'); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/RequestRecordController.php b/app/controller/admin/system/RequestRecordController.php new file mode 100644 index 0000000..c182dc8 --- /dev/null +++ b/app/controller/admin/system/RequestRecordController.php @@ -0,0 +1,31 @@ + [ + $this->request->get('createTimeStart/s', ''), + $this->request->get('createTimeEnd/s', '') + ], + ]); + + $paginate = CurdService::getPaginate($this->request, $model); + + return $this->writeSuccess('ok', $paginate); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/RoleController.php b/app/controller/admin/system/RoleController.php new file mode 100644 index 0000000..603390a --- /dev/null +++ b/app/controller/admin/system/RoleController.php @@ -0,0 +1,90 @@ +request, new SysRole()); + + return $this->writeSuccess('ok', $lists); + } + + /** + * 查询角色菜单 + * @param $role_id + * @return Json + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getMenu($role_id) + { + $role = SysRole::findOrFail($role_id); + $menuIds = $role->menus->column('menu_id'); + $menus = SysMenu::select()->each(function (SysMenu $menu) use ($menuIds) { + $menu->checked = in_array($menu['menu_id'], $menuIds); + }); + return $this->writeSuccess('ok', $menus); + } + + /** + * 更新角色菜单 + * @param $role_id + * @return Json + * @throws DataNotFoundException + * @throws ModelNotFoundException + */ + public function updateMenu($role_id) + { + $menus = $this->request->put(); + $role = SysRole::findOrFail($role_id); + $role->menus()->attach($menus); + return $this->writeSuccess('操作成功'); + } + + /** + * 分页查询角色列表 + * @return Json + * @throws DbException + */ + public function page(): Json + { + $model = SysRole::withSearch(['roleName', 'roleCode'], [ + 'roleName' => $this->request->get('roleName/s', ''), + 'roleCode' => $this->request->get('roleCode/s', ''), + ]); + + $paginate = CurdService::getPaginate($this->request, $model); + + return $this->writeSuccess('ok', $paginate); + } + + /** + * 批量删除角色 + * @return Json + */ + public function removeRoles(): Json + { + $data = $this->request->delete(); + SysRole::destroy($data); + return $this->writeSuccess('删除成功'); + } +} \ No newline at end of file diff --git a/app/controller/admin/system/UserController.php b/app/controller/admin/system/UserController.php new file mode 100644 index 0000000..fb70347 --- /dev/null +++ b/app/controller/admin/system/UserController.php @@ -0,0 +1,170 @@ +withSearch(['organizationId', 'username', 'nickname', 'sex'], [ + 'organizationId' => $this->request->get('organizationId/d', 0), + 'username' => $this->request->get('username/s', ''), + 'nickname' => $this->request->get('nickname/s', ''), + 'sex' => $this->request->get('sex/d', 0), + ])->append(['sexName']); + + $paginate = CurdService::getPaginate($this->request, $model); + + return $this->writeSuccess('success', $paginate); + } + + /** + * 新增用户 + * @return Json + */ + public function add(): Json + { + $data = $this->request->post([ + 'birthday' => null, + 'username' => '', + 'email' => '', + 'nickname' => '', + 'introduction' => '', + 'organizationId' => 0, + 'phone' => '', + 'sex' => '', + 'status' => 0, + 'roles' => [], + ]); + $data['roles'] = array_column((array)$data['roles'], 'roleId'); + + $user = new SysUser(); + $user->save([ + 'birthday' => $data['birthday'], + 'username' => $data['username'], + 'nickname' => $data['nickname'], + 'introduction' => $data['introduction'], + 'organization_id' => $data['organizationId'], + 'phone' => $data['phone'], + 'sex' => $data['sex'], + 'status' => $data['status'], + 'email' => $data['email'], + 'password' => password_hash($data['password'], PASSWORD_DEFAULT), + ]); + $user->roles()->saveAll($data['roles']); + + return $this->writeSuccess('添加成功'); + } + + /** + * 更新用户信息 + * @return Json + * @throws DataNotFoundException + * @throws ModelNotFoundException + */ + public function update(): Json + { + $data = $this->request->put([ + 'userId' => 0, + 'birthday' => null, + 'username' => '', + 'email' => '', + 'nickname' => '', + 'introduction' => '', + 'organizationId' => 0, + 'phone' => '', + 'sex' => '', + 'status' => 0, + 'roles' => [], + ]); + $data['roles'] = array_column((array)$data['roles'], 'roleId'); + + $user = SysUser::findOrFail($data['userId']); + $updateOk = $user->save([ + 'birthday' => $data['birthday'], + 'username' => $data['username'], + 'nickname' => $data['nickname'], + 'introduction' => $data['introduction'], + 'organization_id' => $data['organizationId'], + 'phone' => $data['phone'], + 'sex' => $data['sex'], + 'status' => $data['status'], + 'email' => $data['email'], + ]); + if (!$updateOk) { + return $this->writeError('修改失败'); + } + $user->roles()->attach($data['roles']); + + return $this->writeSuccess('修改成功'); + } + + /** + * 批量删除用户 + * @return Json + */ + public function batchRemove(): Json + { + $data = $this->request->delete(); + SysUser::destroy($data); + return $this->writeSuccess('删除成功'); + } + + /** + * 修改用户状态 + * @return Json + * @throws DataNotFoundException + * @throws ModelNotFoundException + */ + public function updateStatus(): Json + { + $userId = $this->request->put('userId/d', 0); + $status = $this->request->put('status/d', 0); + SysUser::findOrfail($userId)->save(['status' => $status]); + return $this->writeSuccess('操作成功'); + } + + /** + * 修改用户密码 + * @return Json + * @throws DataNotFoundException + * @throws ModelNotFoundException + */ + public function updatePassword(): Json + { + $userId = $this->request->put('userId/d', 0); + $password = $this->request->put('password/s', 0); + + SysUser::findOrfail($userId)->save(['password' => password_hash($password, PASSWORD_DEFAULT)]); + + return $this->writeSuccess('操作成功'); + } + + /** + * 判断数据是否存在 + * @return Json + */ + public function existence(): Json + { + $field = $this->request->get('field/s', ''); + $value = $this->request->get('value/s', ''); + try { + SysUser::field('user_id')->where([$field => $value])->findOrFail(); + } catch (ModelNotFoundException|DataNotFoundException) { + return $this->writeError("用户不存在"); + } + return $this->writeSuccess('用户已存在'); + } +} \ No newline at end of file diff --git a/app/entity/SysDictionary.php b/app/entity/SysDictionary.php new file mode 100644 index 0000000..1e0b03e --- /dev/null +++ b/app/entity/SysDictionary.php @@ -0,0 +1,17 @@ +findOrFail()->dictData->order('sortNumber', 'desc'); + } +} \ No newline at end of file diff --git a/app/entity/SysDictionaryData.php b/app/entity/SysDictionaryData.php new file mode 100644 index 0000000..7554a29 --- /dev/null +++ b/app/entity/SysDictionaryData.php @@ -0,0 +1,11 @@ +authorities, 'menuId', 'parentId') : $this->authorities; + } + + /** + * 生成AccessToken + * @return string + */ + public function getAccessToken(): string + { + $jwtConfig = (array)config('admin.jwt_config', []); + $jwtKey = (string)config('admin.jwt_key', ''); + $hashids = new Hashids($jwtKey, 8); + return JWT::encode([ + ...$jwtConfig, + 'iat' => time(), + 'exp' => time() + 3600, + 'uid' => $hashids->encode($this->user_id), + ], $jwtKey, 'HS256'); + } +} \ No newline at end of file diff --git a/app/enum/ClientEnum.php b/app/enum/ClientEnum.php new file mode 100644 index 0000000..986ff32 --- /dev/null +++ b/app/enum/ClientEnum.php @@ -0,0 +1,8 @@ + [ + ], + + 'listen' => [ + 'AppInit' => [], + 'HttpRun' => [], + 'HttpEnd' => [], + 'LogLevel' => [], + 'LogWrite' => [], + ], + + 'subscribe' => [ + 'sysUser' => SysUserSubscribe::class + ], +]; diff --git a/app/http/HttpAuth.php b/app/http/HttpAuth.php new file mode 100644 index 0000000..434886b --- /dev/null +++ b/app/http/HttpAuth.php @@ -0,0 +1,43 @@ +0 已登录用户 + * =0 未登录 + * <0 访客 + * @var int + */ + public int $userId; + + /** + * 用户类型 + * @var UserTypeEnum + * USER 标准用户 + * VISITOR 未登录访客 + */ + public UserTypeEnum $userType; + + public function __construct($userId, $userType) + { + $this->userId = $userId; + $this->userType = $userType; + } + + public function isLogin(): bool + { + return $this->userId > 0; + } +} \ No newline at end of file diff --git a/app/http/HttpClient.php b/app/http/HttpClient.php new file mode 100644 index 0000000..72ae8e0 --- /dev/null +++ b/app/http/HttpClient.php @@ -0,0 +1,53 @@ +name = $clientName; // 客户端名称(PAdmin/IAdmin) + $this->id = $clientId; // 客户端ID(前端生成的唯一标识) + $this->version = $clientVersion; // 客户端版本号 + $this->sign = $sign; // 携带签名数据 + try { + $this->validate(); + } catch (Exception $e) { + throw new ValidateException($e->getMessage()); + } + } + + private function validate(): void + { + $data = []; + if ($this->sign) { + $encryptedData = base64_decode($this->sign); + $dataStr = openssl_decrypt($encryptedData, 'AES-128-ECB', static::CLIENT_KEY, OPENSSL_RAW_DATA); + $data = json_decode($dataStr, true); + } + $this->data = $data; + validate(ClientValidate::class)->check([ + 'name' => $this->name, + 'id' => $this->id, + 'version' => $this->version, + ]); + } +} diff --git a/app/http/HttpStatus.php b/app/http/HttpStatus.php new file mode 100644 index 0000000..37093b6 --- /dev/null +++ b/app/http/HttpStatus.php @@ -0,0 +1,24 @@ + '操作成功', + self::API_ERROR => '操作失败', + self::UNAUTHORIZED => '未授权访问', + self::FORBIDDEN => '禁止访问', + }; + } +} + + diff --git a/app/http/middleware/AuthMiddleware.php b/app/http/middleware/AuthMiddleware.php new file mode 100644 index 0000000..cdaef93 --- /dev/null +++ b/app/http/middleware/AuthMiddleware.php @@ -0,0 +1,60 @@ +header('authorization', ''); + $authorization = str_replace('Bearer ', '', $authorization); + $loginSrv = new LoginService(); + if (!in_array($request->pathinfo(), $this->whiteList, true)) { + try{ + $auth = $loginSrv->checkUserAccessToken($authorization); + }catch (ValidateException $e){ + return json(['code' => 401, 'message' => $e->getMessage()]); + } + } else { + $auth = $loginSrv->getVisitor($request); + return json(['code' => 401, 'message' => '禁止访问']); + } + /* + * 注入获取用户信息的function + */ + $auth::macro('getUser', function () use ($auth) { + return SysUser::cache("sysUserInfo:{$auth->userId}", 180, 'sysUserInfoLists')->findOrFail($auth->userId); + }); + + return $next($request->setAuth($auth)); + } + +// /** +// * 结束调度 +// * @中间件支持定义请求结束前的回调机制,你只需要在中间件类中添加end方法。 +// * @param Response $response +// */ +// public function end(Response $response) +// { +// // 回调行为 +// } +} \ No newline at end of file diff --git a/app/http/middleware/ClientMiddleware.php b/app/http/middleware/ClientMiddleware.php new file mode 100644 index 0000000..c09eec9 --- /dev/null +++ b/app/http/middleware/ClientMiddleware.php @@ -0,0 +1,36 @@ +header('client', ''); + if(empty($clientName)) { + return response('设备未授权',200); + } + /* + * 客户端信息 + */ + $client = new HttpClient( + $clientName, + $request->header('clientId', ''), + $request->header('clientVersion', ''), + ); + /* + * 获取客户端 + */ + \app\Request::macro('getClient', function () use ($client) { + return $client; + }); + + // Next. + return $next($request); + } +} \ No newline at end of file diff --git a/app/http/middleware/ContextMiddleware.php b/app/http/middleware/ContextMiddleware.php new file mode 100644 index 0000000..f21d337 --- /dev/null +++ b/app/http/middleware/ContextMiddleware.php @@ -0,0 +1,135 @@ +contextId = unique_str(); + /** + * @var Response $response + */ + $response = $next($request); + $response->header([ + 'R-Context-Id' => $request->contextId, + ]); + return $response; + } + + /** + * 结束 + * @param Response $response + * @return void + */ + public function end(Response $response): void + { + $request = $this->app->request; + $log = (array)$this->app->config->get('admin.httpLog'); + + $logOpen = $log['open'] ?? false; + + if (!$logOpen) { + return; + } + $request_start_time = $request->time(true); + $request_end_time = microtime(true); + $running_time = ($request_end_time - $request_start_time) * 1000; + + $openRecordStartTime = $log['open_start_time'] ?? 0; + $openRecordEndTime = $log['open_end_time'] ?? 0; + if ($openRecordStartTime && $openRecordEndTime) { + if ($request_end_time < $openRecordStartTime || $openRecordEndTime < $request_end_time) { + return; + } + } + + $rules = array_merge([ + 'include_methods' => [], + 'include_responses' => [], + 'include_codes' => [], + 'exclude_codes' => [], + ], $log['recordRules'] ?? []); + + // 请求类型(GET/POST/PUT/DELETE) + $request_method = $request->method(); + if (!in_array($request_method, $rules['include_methods'])) { + return; + } + // 返回类型(JSON/HTML/XML) + $response_type = get_class($response); + if (!in_array($response_type, $rules['include_responses'])) { + return; + } + // 返回状态码(Http Status Code) + $response_code = $response->getCode(); + if (in_array($response_type, $rules['exclude_codes'])) { + return; + } + if (count($rules['include_codes']) > 0) { + if (!in_array($response_type, $rules['include_codes'])) { + return; + } + } + $request_start_time = explode('.', $request_start_time); + $request_end_time = explode('.', $request_end_time); + $request_date = date('Y-m-d H:i:s', $request_start_time[0]); + $response_data = $response->getContent(); + + // 限制response_data记录长度 + $response_max = $log['response_max'] ?? 0; + if ($response_max) { + $response_data = mb_substr($response_data, 0, $response_max, 'utf-8'); + } + $request_body = $request->param(); + + try { + // 对文件类型的数据做处理 + $request_file = $request->file(); + if ($request_file && count($request_file) > 0) { + /** + * @var UploadedFile $file + */ + foreach ($request->file() as $key => $file) { + $request_body[$key] = [ + 'md5' => $file->md5(), + 'name' => $file->getOriginalName(), + 'size' => $file->getSize(), + ]; + } + } + $route = $request->rule()->getRoute(); + + (new SysRequestRecord)->save([ + 'context_id' => $request->contextId, + 'request_date' => $request_date, + 'request_time' => $request_date . '.' . ($request_start_time[1] ?? 0), + 'request_method' => $request_method, + 'request_end_time' => date('Y-m-d H:i:s', $request_end_time[0]) . '.' . ($request_end_time[1] ?? 0), + 'request_headers' => json_encode($request->header(), JSON_UNESCAPED_UNICODE), + 'request_body' => json_encode($request_body, JSON_UNESCAPED_UNICODE), + 'request_path' => $request->url(), + 'request_domain' => $request->domain(), + 'response_type' => $response_type, + 'response_code' => $response_code, + 'response_data' => $response_data, + 'running_time' => (int)$running_time, + 'running_memory_limit' => ini_get('memory_limit'), + 'running_memory_usage' => memory_get_peak_usage(), + 'request_ip' => $request->ip(), + 'rule_name' => $request->rule()->getName(), + 'rule_route' => is_array($route) ? implode('@', $route) : @json_encode($route), + ]); + } catch (\Throwable $e) { + $this->app->log->error("[ContextMiddleware::end] 日志写入失败->{$e->getMessage()}"); + } + } +} \ No newline at end of file diff --git a/app/middleware.php b/app/middleware.php new file mode 100644 index 0000000..d2112c7 --- /dev/null +++ b/app/middleware.php @@ -0,0 +1,12 @@ +hasMany(SysDictionaryData::class, 'dict_id', 'dict_id'); + } + +// +// public static function onAfterInsert(Model|SysDictionary $model): void +// { +// security_log_record([SysDictionary::class, 'onAfterInsert'], "添加了新字典集", $model); +// } +// +// public static function onAfterDelete(Model|SysDictionary $model): void +// { +// security_log_record([SysDictionary::class, 'onAfterDelete'], "字典集{$model->dict_name}被删除", $model); +// } +// +// public static function onBeforeUpdate(Model|SysDictionary $model): void +// { +// security_log_record([SysDictionary::class, 'onBeforeUpdate'], "修改了{$model->dict_name}字典集", $model); +// } + + public static function getByDictCode(string $dictCode) + { + return self::with(['dictData'])->where('dict_code', $dictCode)->findOrEmpty(); + } + + public function scopeDictCode($query, $dictCode) + { + $query->with(['dictData'])->where('dict_code', $dictCode); + } + + public function getDictDataCodesAttr() + { + return $this->dictData->column('dict_data_code'); + } + + public function getByDictDataCode(string $dictDataCode, mixed $default = null) + { + $dictDataMap = $this->dictData->column('dict_data_name', 'dict_data_code'); + + return $dictDataMap[$dictDataCode] ?? $default; + } + + public function searchGroupAttr($query, $value): void + { + $query->where('group_tag', '=', $value); + } + + public function searchDictNameAttr($query, $value): void + { + $query->where('dict_name', 'like', "%$value%"); + } +// public function searchDictCodeAttr($query, $value): void +// { +// $query->where('dict_code', '=', $value); +// } +} \ No newline at end of file diff --git a/app/model/SysDictionaryData.php b/app/model/SysDictionaryData.php new file mode 100644 index 0000000..fdc8dbe --- /dev/null +++ b/app/model/SysDictionaryData.php @@ -0,0 +1,83 @@ +set('deleted', 0); +// return true; +// } +// +// public static function onBeforeDelete(Model|SysDictionaryData $model) +// { +// $model->set('deleted', 1); +// return true; +// } +// +// public static function onAfterInsert(Model|SysDictionaryData $model): void +// { +// security_log_record([SysDictionaryData::class, 'onAfterInsert'], "添加了新字典", $model); +// } +// +// public static function onAfterDelete(Model|SysDictionaryData $model): void +// { +// security_log_record([SysDictionaryData::class, 'onAfterDelete'], "字典{$model->dict_data_name}被删除", $model); +// } +// +// public static function onBeforeUpdate(Model|SysDictionaryData $model): void +// { +// security_log_record([SysDictionaryData::class, 'onBeforeUpdate'], "修改了{$model->dict_data_name}字典", $model->getChangedDataDA()); +// } + +// public function searchDictCodeAttr($query, $value): void +// { +// $query->where('dict_id', function (Query $query) use ($value) { +// return $query->table(SysDictionary::getTable())->where('dict_code', $value)->field('dict_id'); +// }); +// } + + + public function searchDictDataNameAttr($query, string $value, array $data = []): void + { + $value != "" && $query->where('dict_data_name', 'like', '%' . $value . '%'); + } + + public function searchDictDataCodeAttr($query, string $value, array $data = []): void + { + $value != "" && $query->where('dict_data_code', 'like', '%' . $value . '%'); + } + + public function searchDictIdAttr($query, string|int $value, array $data = []): void + { + (is_numeric($value) && $value > 0) && $query->where('dict_id', $value); + } +} \ No newline at end of file diff --git a/app/model/SysFileRecord.php b/app/model/SysFileRecord.php new file mode 100644 index 0000000..780104b --- /dev/null +++ b/app/model/SysFileRecord.php @@ -0,0 +1,44 @@ +hasOne(SysUser::class, 'user_id', 'create_user_id')->field('user_id,nickname,avatar,username,status'); + } + + public function searchNameAttr(Query $query, string $value): void + { + $value != "" && $query->where('name', 'like', '%' . $value . '%'); + } + + public function searchPathAttr(Query $query, string $value): void + { + $value != "" && $query->where('path', 'like', '%' . $value . '%'); + } + + public function searchCreateNicknameAttr(Query $query, string $value): void + { + $value != "" && $query->whereIn('create_user_id', SysUser::where('username', $value)->column('user_id')); + } + + public function searchCreateTimeAttr(Query $query, array $value, array $data): void + { + $value = array_filter($value); + count($value) >= 2 && $query->whereBetweenTime('request_time', $value[0], $value[1]); + } + + +} \ No newline at end of file diff --git a/app/model/SysFileRule.php b/app/model/SysFileRule.php new file mode 100644 index 0000000..e0f83cf --- /dev/null +++ b/app/model/SysFileRule.php @@ -0,0 +1,27 @@ +hasOne(SysUser::class, 'username', 'username')->field('user_id,username,nickname'); + } + + public function searchUsernameAttr(Query $query, string $value, array $data): void + { + $value != '' && $query->where('username', $value); + } + + public function searchLoginTypeAttr(Query $query, int $value, array $data): void + { + $value > 0 && $query->where('login_type', $value); + } + + public function searchNicknameAttr(Query $query, string $value, array $data): void + { + $value != '' && $query->whereIn('username', SysUser::where('username', 'like', "%{$value}%")->column('username')); + } + + public function searchCreateTimeAttr(Query $query, array $value, array $data): void + { + $value = array_filter($value); + count($value) >= 2 && $query->whereBetweenTime('create_time', $value[0], $value[1]); + } + +} \ No newline at end of file diff --git a/app/model/SysMenu.php b/app/model/SysMenu.php new file mode 100644 index 0000000..b112560 --- /dev/null +++ b/app/model/SysMenu.php @@ -0,0 +1,79 @@ +belongsToMany(SysRole::class, SysRoleMenu::class, 'menu_id', 'role_id'); + } + + // 事件定义 + public static function onBeforeRestore(Model|SysMenu $model) + { + $model->set('deleted', 0); + return true; + } + + public static function onBeforeDelete(Model|SysMenu $model): bool + { + $model->set('deleted', 1); + return true; + } + + public static function onAfterInsert(Model|SysMenu $model): void + { + security_log_record([SysMenu::class, 'onAfterInsert'], "添加了新菜单", $model); + } + + public static function onAfterDelete(Model|SysMenu $model): void + { + security_log_record([SysMenu::class, 'onAfterDelete'], "菜单{$model->title}被删除", $model); + } + + // 搜索定义 + public function searchTitleAttr($query, $value): void + { + $value !== '' && $query->where('title', 'like', '%' . $value . '%'); + } + + public function searchAuthorityAttr($query, $value): void + { + $value !== '' && $query->where('authority', 'like', '%' . $value . '%'); + } + + public function searchPathAttr($query, $value): void + { + $value !== '' && $query->where('path', 'like', '%' . $value . '%'); + } +} \ No newline at end of file diff --git a/app/model/SysOperateRecord.php b/app/model/SysOperateRecord.php new file mode 100644 index 0000000..d3a040a --- /dev/null +++ b/app/model/SysOperateRecord.php @@ -0,0 +1,34 @@ +hasOne(SysUser::class, 'user_id', 'operate_user_id')->field('user_id,nickname,avatar,username,status'); + } + + public function request(): HasOne + { + return $this->hasOne(SysRequestRecord::class, 'context_id', 'context_id'); + } + + + + public function searchCreateTimeAttr(Query $query, array $value, array $data): void + { + $value = array_filter($value); + count($value) >= 2 && $query->whereBetweenTime('create_time', $value[0], $value[1]); + } +} \ No newline at end of file diff --git a/app/model/SysOrganization.php b/app/model/SysOrganization.php new file mode 100644 index 0000000..4056d36 --- /dev/null +++ b/app/model/SysOrganization.php @@ -0,0 +1,70 @@ +organization_full_name}", $model); + } + + public static function onAfterDelete(Model|SysOrganization $model): void + { + security_log_record([SysOrganization::class, 'onAfterDelete'], "机构{$model->organization_full_name}被删除", $model); + } + + // 搜索定义 + public function searchOrganizationNameAttr(Query $query, string $value): void + { + $value != '' && $query->where('organization_name', 'like', '%' . $value . '%'); + } + + public function searchOrganizationFullNameAttr(Query $query, string $value): void + { + $value != '' && $query->where('organization_full_name', 'like', '%' . $value . '%'); + } + + public function searchOrganizationCodeAttr(Query $query, string $value): void + { + $value != '' && $query->where('organization_code', 'like', '%' . $value . '%'); + } + + public function searchOrganizationTypeAttr(Query $query, int $value): void + { + $value > 0 && $query->where('organization_type', $value); + } + + public function getOrganizationTypeNameAttr($value, $data): string + { + return dict_get('organization_type.' . $data['organization_type']); + } +} \ No newline at end of file diff --git a/app/model/SysRequestRecord.php b/app/model/SysRequestRecord.php new file mode 100644 index 0000000..9f93c20 --- /dev/null +++ b/app/model/SysRequestRecord.php @@ -0,0 +1,22 @@ += 2 && $query->whereBetweenTime('request_time', $value[0], $value[1]); + } +} \ No newline at end of file diff --git a/app/model/SysRole.php b/app/model/SysRole.php new file mode 100644 index 0000000..84c0c6e --- /dev/null +++ b/app/model/SysRole.php @@ -0,0 +1,71 @@ +belongsToMany(SysRole::class, SysUserRole::class, 'role_id', 'user_id'); + } + + public function menus(): BelongsToMany + { + return $this->belongsToMany(SysMenu::class, SysRoleMenu::class, 'menu_id', 'role_id'); + } + + // 搜索定义 + public function searchRoleNameAttr($query, $value): void + { + $value != '' && $query->where('role_name', 'like', '%' . $value . '%'); + } + + public function searchRoleCodeAttr($query, $value): void + { + $value != '' && $query->where('role_code', 'like', '%' . $value . '%'); + } + + public function searchCommentsAttr($query, $value): void + { + $value != '' && $query->where('comments', 'like', '%' . $value . '%'); + } + + // 事件定义 + + public static function onAfterInsert(Model|SysRole $model): void + { + security_log_record([SysRole::class, 'onAfterInsert'], "添加了新角色", $model); + } + + public static function onAfterDelete(SysRole $model): void + { + security_log_record([SysRole::class, 'onAfterDelete'], "角色{$model->role_name}被删除", $model); + } +} \ No newline at end of file diff --git a/app/model/SysRoleMenu.php b/app/model/SysRoleMenu.php new file mode 100644 index 0000000..f5286f4 --- /dev/null +++ b/app/model/SysRoleMenu.php @@ -0,0 +1,23 @@ +hasOne(SysRole::class); + } + + public function menu(): HasOne + { + return $this->hasOne(SysMenu::class); + } +} \ No newline at end of file diff --git a/app/model/SysUser.php b/app/model/SysUser.php new file mode 100644 index 0000000..8c86827 --- /dev/null +++ b/app/model/SysUser.php @@ -0,0 +1,126 @@ +belongsToMany(SysRole::class, SysUserRole::class, 'role_id', 'user_id'); + } + + public function getAuthoritiesAttr(): array + { + $menus = []; + $this->roles->load(['menus'], true); + foreach ($this->roles as $role) { + $menus = array_merge($menus, $role->menus->hidden(['pivot'])->where('deleted', 0)->toArray()); + } + $uniqueMenus = []; + foreach ($menus as $menu) { + $uniqueMenus[$menu['menuId']] = $menu; + } + + $array = array_values($uniqueMenus); + usort($array, function($a, $b) { + return $a['sortNumber'] <=> $b['sortNumber']; + }); + return $array; + } + + /** + * Attr@性别 + * @param $value + * @param $data + * @return string + */ + public function getSexNameAttr($value, $data): string + { + return (string)dict_get('sex.' . $data['sex'], '未知'); + } + + + public function searchOrganizationIdAttr(Query $query, int $value, array $data): void + { + ($value > 0) && $query->where('organization_id', $value); + } + + + public function searchCreateTimeAttr($query, array $value): void + { + $query->whereBetweenTime('create_time', $value[0], $value[1]); + } + + public function searchNicknameAttr(Query $query, string $value): void + { + $value != "" && $query->where('nickname', 'like', '%' . $value . '%'); + } + + public function searchUsernameAttr(Query $query, string $value): void + { + $value != "" && $query->where('username', 'like', '%' . $value . '%'); + } + + public function searchSexAttr(Query $query, int $value): void + { + ($value > 0) && $query->where('sex', '=', $value); + } + + public function searchStatusAttr(Query $query, int $value): void + { + ($value > 0) && $query->where('status', '=', $value); + } + + public function searchEmailAttr(Query $query, string $value): void + { + $value != "" && $query->where('email', 'like', '%' . $value . '%'); + } + + public function searchPhoneAttr(Query $query, string $value): void + { + $value != "" && $query->where('phone', 'like', '%' . $value . '%'); + } + + public function searchIdCardAttr(Query $query, string $value): void + { + $value != "" && $query->where('id_card', 'like', '%' . $value . '%'); + } + +} \ No newline at end of file diff --git a/app/model/SysUserFile.php b/app/model/SysUserFile.php new file mode 100644 index 0000000..ebcbf47 --- /dev/null +++ b/app/model/SysUserFile.php @@ -0,0 +1,63 @@ +hasOne(SysUser::class, 'user_id', 'user_id')->field('user_id,username,nickname,avatar'); + } + + public function searchNameAttr($query, $value): void + { + $query->whereLike('name', '%' . $value . '%'); + } + + public function searchContentTypeAttr($query, $value): void + { + $query->whereLike('content_type', $value . '%'); + } + + public function searchParentIdAttr($query, $value): void + { + $query->where('parent_id', $value); + } + + public function searchUserIdAttr($query, $value): void + { + $query->where('user_id', $value); + } + +} \ No newline at end of file diff --git a/app/model/SysUserRole.php b/app/model/SysUserRole.php new file mode 100644 index 0000000..c34c92d --- /dev/null +++ b/app/model/SysUserRole.php @@ -0,0 +1,40 @@ +hasOne(SysRole::class); + } + + /** + * 用户 + * @return HasOne + */ + public function user(): HasOne + { + return $this->hasOne(SysUser::class); + } +} \ No newline at end of file diff --git a/app/observer/DictionaryObserver.php b/app/observer/DictionaryObserver.php new file mode 100644 index 0000000..e2f31cf --- /dev/null +++ b/app/observer/DictionaryObserver.php @@ -0,0 +1,45 @@ +clearCache(); + } + + /** + * 删除数据事件 + * @param SysDictionary|SysDictionaryData $model + * @return void + */ + public function onAfterDelete(SysDictionary|SysDictionaryData $model) + { + $this->clearCache(); + } + + /** + * 恢复数据事件 + * @param SysDictionary|SysDictionaryData $model + * @return void + */ + public function onAfterRestore(SysDictionary|SysDictionaryData $model) + { + $this->clearCache(); + } + + public function clearCache() + { + dict()->clearCache(); + } +} \ No newline at end of file diff --git a/app/provider.php b/app/provider.php new file mode 100644 index 0000000..73d99fa --- /dev/null +++ b/app/provider.php @@ -0,0 +1,9 @@ + Request::class, + 'think\exception\Handle' => ExceptionHandle::class, +]; diff --git a/app/service.php b/app/service.php new file mode 100644 index 0000000..8400c2a --- /dev/null +++ b/app/service.php @@ -0,0 +1,15 @@ +cache(60) +// ->value('value'); + } +} \ No newline at end of file diff --git a/app/service/CurdService.php b/app/service/CurdService.php new file mode 100644 index 0000000..d93acf4 --- /dev/null +++ b/app/service/CurdService.php @@ -0,0 +1,65 @@ +param('offset', 0); + $limit = (int)$request->param('limit', 30); + $limit = min($limit, self::$MAX_LIMIT); + + return $model->order(!is_null($order) ? $order : (string)$model->getPk(), 'desc') + ->limit($offset, $limit) + ->select(); + } + + /** + * @param Request $request + * @param BaseModel $model + * @param null $order + * @return Paginator + * @throws DbException + */ + public static function getPaginate(Request $request, mixed $model, $order = null) + { + $page = (int)$request->param('page', 1); + $page = max(1, $page); + $limit = (int)$request->param('limit', 30); + $limit = min($limit, self::$MAX_LIMIT); + // 前端自定义排序条件 + $sort = $request->param('sort'); + $sort_order = $request->param('order'); + if (!empty($sort) && in_array($sort_order, ['desc', 'asc'])) { + $order = [Str::snake($sort) => $sort_order]; + } + return $model->order(!is_null($order) ? $order : (string)$model->getPk(), 'desc') + ->limit($page, $limit) + ->paginate([ + 'list_rows' => $limit, + 'page' => $page, + ]); + } +} \ No newline at end of file diff --git a/app/service/DictionaryService.php b/app/service/DictionaryService.php new file mode 100644 index 0000000..2ad2c26 --- /dev/null +++ b/app/service/DictionaryService.php @@ -0,0 +1,83 @@ +app->bind('dict', $this); + /* + * 添加应用事件初始化字典内容 + */ + $this->app->event->listen("AppInit", function () { + $this->sysDictData = Cache::remember('sysDict', function () { + $array = []; + SysDictionary::with(['dictData'])->select()->each(function (SysDictionary|\app\model\SysDictionary $dict) use (&$array) { + $array[$dict->dict_code] = $dict->dictData->column('dict_data_name', 'dict_data_code'); + }); + return $array; + }, 3600 * 4); + }); + } + + /** + * 获取字典 + * @param string $name + * @param $default + * @param $cache + * @return mixed + */ + public function get(string $name, $default = null, $cache = true) + { + if (!$cache) { + return $this->getValue($name, $default); + } else { + /* + * 通过缓存判断减少计算操作(使用内存换取性能) + */ + static $cacheValue = []; + if (!isset($cacheValue[$name]) || $cacheValue[$name]['0'] !== $default) { + $cacheValue[$name] = ['v' => $this->getValue($name, $default), '0' => $default]; + } + return $cacheValue[$name]['v']; + } + } + + /** + * 获取字典元素 + * @param string $name + * @param $default + * @return mixed + */ + private function getValue(string $name, $default = null): mixed + { + $keys = explode('.', $name); + if (count($keys) == 1) { + return $this->sysDictData[$name] ?? (is_array($default) ? $default : []); + } + $code = array_pop($keys); + $key = implode('.', $keys); + if (isset($this->sysDictData[$key])) { + return $this->sysDictData[$key][$code] ?? $default; + } + return $default; + } + + public function clearCache() + { + Cache::delete('sysDict'); + } +} \ No newline at end of file diff --git a/app/service/SecurityService.php b/app/service/SecurityService.php new file mode 100644 index 0000000..128b1f8 --- /dev/null +++ b/app/service/SecurityService.php @@ -0,0 +1,50 @@ +app->event->listen("sys:securityLogRecord", function ($trigger) { + + [[$location, $event], $message, $data] = $trigger; + $model = new SysOperateRecord(); + $model->setOption('convertNameToCamel', false); + $model->location = $location; + $model->event = $event; + $model->description = $message; + $model->create_time = date('Y-m-d H:i:s'); + $model->data = json_encode($data, JSON_UNESCAPED_UNICODE); + $this->securityLogRecords[] = $model->toArray(); + }); + + $this->app->event->listen("HttpEnd", function () { + $auth = $this->app->request->getAuth(); + $context_id = $this->app->request->contextId; + $operate_user_id = null; + if ($auth) { + $operate_user_id = $auth->userId; + } + /** + * 保存接口操作记录到日志表 + */ + (new SysOperateRecord)->insertAll(array_map(function ($item) use ($operate_user_id, $context_id) { + $item['operate_user_id'] = $operate_user_id; + $item['context_id'] = $context_id; + return $item; + }, $this->securityLogRecords)); + }); + } +} \ No newline at end of file diff --git a/app/service/admin/LoginService.php b/app/service/admin/LoginService.php new file mode 100644 index 0000000..20af87a --- /dev/null +++ b/app/service/admin/LoginService.php @@ -0,0 +1,120 @@ +name; + validate(LoginValidate::class) + ->scene($clientName . 'Login') + ->check(compact('username', 'password', 'remember', 'code')); + $user = SysUser::where(['username' => $username])->find(); + if (empty($user)) { + $this->saveLoginRecord($request, $username, UserLoginEnum::USERNAME_ERROR, "用户名错误"); + throw new ValidateException("用户名或密码错误"); + } + if (!password_verify($password, $user->password)) { + $this->saveLoginRecord($request, $username, UserLoginEnum::PASSWORD_ERROR, "密码错误"); + throw new ValidateException("用户名或密码错误"); + } + if ($user->status == UserStatusEnum::Disable->value) { + $this->saveLoginRecord($request, $username, UserLoginEnum::STATUS_DISABLED, "账号被禁用"); + throw new ValidateException("账户不可用"); + } + try { + $access_token = $user->getAccessToken(); + $user = $user->append(['authorities', 'roles'])->toArray(); + $this->saveLoginRecord($request, $username, UserLoginEnum::LOGIN_SUCCESS, "登录成功"); + } catch (Exception $e) { + $this->saveLoginRecord($request, $username, UserLoginEnum::LOGIN_EXCEPTION, $e->getMessage()); + throw $e; + } + return compact('user', 'access_token'); + } + + /** + * 保存登录记录 + * @param Request $request + * @param string $username + * @param UserLoginEnum $loginType + * @param string $comments + * @return void + */ + private function saveLoginRecord(Request $request, string $username, UserLoginEnum $loginType, string $comments): void + { + $sysLoginRecord = new SysLoginRecord(); + $sysLoginRecord->save([ + 'username' => $username, + 'os' => $request->getOs(), + 'device' => $request->getDevice(), + 'browser' => $request->getBrowser(), + 'ip' => $request->ip(), + 'comments' => $comments, + 'login_type' => $loginType->value, + ]); + } + + + public function checkUserAccessToken(string $token): HttpAuth + { + if (empty($token)) { + throw new ValidateException('凭证错误'); + } + try { + $jwtKey = (string)config('admin.jwt_key', ''); + $headers = new stdClass(); + $data = JWT::decode($token, new Key($jwtKey, 'HS256'), $headers); + } catch (UnexpectedValueException $e) { + if($e->getMessage() == "Expired token") { + throw new ValidateException('凭证过期'); + } + throw new ValidateException('校验失败' . $e->getMessage()); + } + if ($data->exp <= (time() - 180)) { + // 还要三分钟就要过期 + throw new ValidateException('凭证过期'); + } + $hashids = new Hashids($jwtKey, 8); + + $user_id = $hashids->decode($data->uid)[0] ?? 0; + if (empty($user_id)) { + throw new ValidateException('凭证错误'); + } + return new HttpAuth($user_id, UserTypeEnum::USER); + } + + public function getVisitor(\think\Request $request): HttpAuth + { + return new HttpAuth(-1, UserTypeEnum::VISITOR); + } +} \ No newline at end of file diff --git a/app/subscribe/SysUserSubscribe.php b/app/subscribe/SysUserSubscribe.php new file mode 100644 index 0000000..3fc21a7 --- /dev/null +++ b/app/subscribe/SysUserSubscribe.php @@ -0,0 +1,18 @@ + ClientEnum::class, // 客户端类型(PAdmin:PC管理端,IAdmin:手机管理端) + 'uuid' => 'max:64', // 客户端UUID + 'version' => 'max:10', // 客户端版本号 + ]; + protected $message = [ + 'name.enum' => '客户端类型错误', + 'uuid.max' => '客户端UUID错误', + 'version.max' => '客户端版本号错误', + ]; +} \ No newline at end of file diff --git a/app/validate/auth/LoginValidate.php b/app/validate/auth/LoginValidate.php new file mode 100644 index 0000000..42491e9 --- /dev/null +++ b/app/validate/auth/LoginValidate.php @@ -0,0 +1,15 @@ + 'max:20', + 'password' => 'min:5|max:64' + ]; +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..63b5ced --- /dev/null +++ b/composer.json @@ -0,0 +1,54 @@ +{ + "name": "topthink/think", + "description": "the new thinkphp framework", + "minimum-stability": "dev", + "type": "project", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "https://www.thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=8.0.0", + "topthink/framework": "^8.0", + "topthink/think-orm": "^4.0", + "topthink/think-filesystem": "^3.0", + "firebase/php-jwt": "dev-main", + "hashids/hashids": "5.0.x-dev", + "phpmailer/phpmailer": "dev-master", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "topthink/think-dumper": "^1.0", + "topthink/think-trace": "^2.0" + }, + "autoload": { + "psr-4": { + "app\\": "app" + }, + "psr-0": { + "": "extend/" + } + }, + "config": { + "preferred-install": "dist" + }, + "scripts": { + "post-autoload-dump": [ + "@php think service:discover", + "@php think vendor:publish" + ] + } +} diff --git a/config/admin.php b/config/admin.php new file mode 100644 index 0000000..270de0d --- /dev/null +++ b/config/admin.php @@ -0,0 +1,22 @@ + 'oasdasdasd', + 'httpLog' => [ + 'open' => true, + 'open_start_time' => 0, + 'open_end_time' => 0, + 'recordRules' => [ + 'include_methods' => ['GET', 'POST', 'PUT', 'DELETE'], + 'include_responses' => [Json::class], + 'include_codes' => [], + 'exclude_codes' => [], + ] + ], + 'cls' => [ + 'secretId' => 'AKIDz6HXnBZ0Pm5UtEXlv5BThrwvsmcM0a5e', + 'secretKey' => 'eGoIj6QiYSLBFNTsIdv8GxZOWaWWcn8R' + ] +]; \ No newline at end of file diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..63285d2 --- /dev/null +++ b/config/app.php @@ -0,0 +1,30 @@ + '', + // 是否启用路由 + 'with_route' => true, + // 默认应用 + 'default_app' => 'index', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + + // 应用映射(自动多应用模式有效) + 'app_map' => [], + // 域名绑定(自动多应用模式有效) + 'domain_bind' => [], + // 禁止URL访问的应用列表(自动多应用模式有效) + 'deny_app_list' => [], + + // 异常页面的模板文件 + 'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..6b72dc8 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,29 @@ + 'file', + + // 缓存连接方式配置 + 'stores' => [ + 'file' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => '', + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + // 缓存标签前缀 + 'tag_prefix' => 'tag:', + // 序列化机制 例如 ['serialize', 'unserialize'] + 'serialize' => [], + ], + // 更多的缓存连接 + ], +]; diff --git a/config/console.php b/config/console.php new file mode 100644 index 0000000..a818a98 --- /dev/null +++ b/config/console.php @@ -0,0 +1,9 @@ + [ + ], +]; diff --git a/config/cookie.php b/config/cookie.php new file mode 100644 index 0000000..d3b3aab --- /dev/null +++ b/config/cookie.php @@ -0,0 +1,20 @@ + 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => false, + // 是否使用 setcookie + 'setcookie' => true, + // samesite 设置,支持 'strict' 'lax' + 'samesite' => '', +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..9d3f0d0 --- /dev/null +++ b/config/database.php @@ -0,0 +1,63 @@ + env('DB_DRIVER', 'mysql'), + + // 自定义时间查询规则 + 'time_query_rule' => [], + + // 自动写入时间戳字段 + // true为自动识别类型 false关闭 + // 字符串则明确指定时间字段类型 支持 int timestamp datetime date + 'auto_timestamp' => true, + + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + + // 时间字段配置 配置格式:create_time,update_time + 'datetime_field' => '', + + // 数据库连接配置信息 + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => env('DB_TYPE', 'mysql'), + // 服务器地址 + 'hostname' => env('DB_HOST', '127.0.0.1'), + // 数据库名 + 'database' => env('DB_NAME', ''), + // 用户名 + 'username' => env('DB_USER', 'root'), + // 密码 + 'password' => env('DB_PASS', ''), + // 端口 + 'hostport' => env('DB_PORT', '3306'), + // 数据库连接参数 + 'params' => [], + // 数据库编码 + 'charset' => env('DB_CHARSET', 'utf8mb4'), + // 数据库表前缀 + 'prefix' => env('DB_PREFIX', ''), + + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 是否需要断线重连 + 'break_reconnect' => false, + // 监听SQL + 'trigger_sql' => env('APP_DEBUG', true), + // 开启字段缓存 + 'fields_cache' => false, + ], + + // 更多的数据库配置信息 + ], +]; diff --git a/config/filesystem.php b/config/filesystem.php new file mode 100644 index 0000000..582a8f8 --- /dev/null +++ b/config/filesystem.php @@ -0,0 +1,24 @@ + 'local', + // 磁盘列表 + 'disks' => [ + 'local' => [ + 'type' => 'local', + 'root' => app()->getRuntimePath() . 'storage', + ], + 'public' => [ + // 磁盘类型 + 'type' => 'local', + // 磁盘路径 + 'root' => app()->getRootPath() . 'public/storage', + // 磁盘路径对应的外部URL路径 + 'url' => '/storage', + // 可见性 + 'visibility' => 'public', + ], + // 更多的磁盘配置信息 + ], +]; diff --git a/config/lang.php b/config/lang.php new file mode 100644 index 0000000..ccad14a --- /dev/null +++ b/config/lang.php @@ -0,0 +1,29 @@ + env('DEFAULT_LANG', 'zh-cn'), + // 自动侦测浏览器语言 + 'auto_detect_browser' => true, + // 允许的语言列表 + 'allow_lang_list' => [], + // 多语言自动侦测变量名 + 'detect_var' => 'lang', + // 是否使用Cookie记录 + 'use_cookie' => true, + // 多语言cookie变量 + 'cookie_var' => 'think_lang', + // 多语言header变量 + 'header_var' => 'think-lang', + // 扩展语言包 + 'extend_list' => [], + // Accept-Language转义为对应语言包名称 + 'accept_language' => [ + 'zh-hans-cn' => 'zh-cn', + ], + // 是否支持语言分组 + 'allow_group' => false, +]; diff --git a/config/log.php b/config/log.php new file mode 100644 index 0000000..0d406f8 --- /dev/null +++ b/config/log.php @@ -0,0 +1,45 @@ + 'file', + // 日志记录级别 + 'level' => [], + // 日志类型记录的通道 ['error'=>'email',...] + 'type_channel' => [], + // 关闭全局日志写入 + 'close' => false, + // 全局日志处理 支持闭包 + 'processor' => null, + + // 日志通道列表 + 'channels' => [ + 'file' => [ + // 日志记录方式 + 'type' => 'File', + // 日志保存目录 + 'path' => '', + // 单文件日志写入 + 'single' => false, + // 独立日志级别 + 'apart_level' => [], + // 最大日志文件数量 + 'max_files' => 0, + // 使用JSON格式记录 + 'json' => false, + // 日志处理 + 'processor' => null, + // 关闭通道日志写入 + 'close' => false, + // 日志输出格式化 + 'format' => '[%s][%s] %s', + // 是否实时写入 + 'realtime_write' => false, + ], + // 其它日志通道配置 + ], + +]; diff --git a/config/middleware.php b/config/middleware.php new file mode 100644 index 0000000..7e1972f --- /dev/null +++ b/config/middleware.php @@ -0,0 +1,8 @@ + [], + // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行 + 'priority' => [], +]; diff --git a/config/route.php b/config/route.php new file mode 100644 index 0000000..729d7be --- /dev/null +++ b/config/route.php @@ -0,0 +1,55 @@ + '/', + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 是否区分大小写 + 'url_case_sensitive' => false, + // 自动扫描子目录分组 + 'route_auto_group' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => false, + // 去除斜杠 + 'remove_slash' => false, + // 默认的路由变量规则 + 'default_route_pattern' => '[\w\.]+', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // 访问控制器层名称 + 'controller_layer' => 'controller', + // 空控制器名 + 'empty_controller' => 'Error', + // 是否使用控制器后缀 + 'controller_suffix' => false, + // 默认模块名(开启自动多模块有效) + 'default_module' => 'index', + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 操作方法后缀 + 'action_suffix' => '', + // 非路由变量是否使用普通参数方式(用于URL生成) + 'url_common_param' => true, + // 操作方法的参数绑定方式 route get param + 'action_bind_param' => 'get', + // 请求缓存规则 true为自动规则 + 'request_cache_key' => true, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + // 请求缓存的Tag + 'request_cache_tag' => '', + // API版本header变量 + 'api_version' => 'Api-Version', +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..c1ef6e1 --- /dev/null +++ b/config/session.php @@ -0,0 +1,19 @@ + 'PHPSESSID', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // 驱动方式 支持file cache + 'type' => 'file', + // 存储连接标识 当type使用cache的时候有效 + 'store' => null, + // 过期时间 + 'expire' => 1440, + // 前缀 + 'prefix' => '', +]; diff --git a/config/trace.php b/config/trace.php new file mode 100644 index 0000000..fad2392 --- /dev/null +++ b/config/trace.php @@ -0,0 +1,10 @@ + 'Html', + // 读取的日志通道名 + 'channel' => '', +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..01259a0 --- /dev/null +++ b/config/view.php @@ -0,0 +1,25 @@ + 'Think', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 模板目录名 + 'view_dir_name' => 'view', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', +]; diff --git a/extend/.gitignore b/extend/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/extend/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..cbc7868 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,8 @@ + + Options +FollowSymlinks -Multiviews + RewriteEngine On + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..3f2817f Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..c3b7842 --- /dev/null +++ b/public/index.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- + +use think\App; + +// [ 应用入口文件 ] + +require __DIR__ . '/../vendor/autoload.php'; + +// 执行HTTP应用并响应 +$http = (new App())->http; + +$response = $http->run(); + +$response->send(); + +$http->end($response); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/router.php b/public/router.php new file mode 100644 index 0000000..9b39a62 --- /dev/null +++ b/public/router.php @@ -0,0 +1,19 @@ + +// +---------------------------------------------------------------------- +// $Id$ + +if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) { + return false; +} else { + $_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php'; + + require __DIR__ . "/index.php"; +} diff --git a/public/static/.gitignore b/public/static/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/public/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/route/api.php b/route/api.php new file mode 100644 index 0000000..98c8ccd --- /dev/null +++ b/route/api.php @@ -0,0 +1,112 @@ +name("admin.LoginCaptcha"); + + Route::post("login", [auth\LoginController::class, "index"])->name("admin.SysUserLogin")->middleware([ClientMiddleware::class]); + Route::group(function () { + + + Route::group("auth", function () { + Route::post("logout", [auth\AuthController::class, "logout"])->name("admin.SysUserLogout"); + Route::get("user", [auth\AuthController::class, "user"])->name("admin.SysUserInfo"); + }); + + /* + * 系统文件管理 + */ + Route::get('file/page', [FileController::class, "page"])->name("system.pageFiles"); + Route::delete('file/remove/batch', [FileController::class, "batchRemove"])->name("system.batchRemoveFile"); + + + Route::group("system", function () { + Route::get('login-record/page', [LoginRecordController::class, "page"])->name("system.pageLoginRecords"); + Route::get('operate-record/page', [OperateRecordController::class, "page"])->name("system.pageOperateRecords"); + Route::get('request-record/page', [RequestRecordController::class, "page"])->name("system.pageRequestRecords"); + + /* + * 用户管理 + */ + Route::get("user/page", [UserController::class, "page"])->name("system.pageUsers"); + Route::delete("user/batch", [UserController::class, "batchRemove"])->name("system.batchRemoveUser"); + Route::post("user", [UserController::class, "add"])->name("system.addUser"); + Route::put("user$", [UserController::class, "update"])->name("system.updateUser"); + Route::get("user/existence", [UserController::class, "existence"])->name("system.userExistence"); + Route::put("user/status", [UserController::class, "updateStatus"])->name("system.updateUserStatus"); + Route::put("user/password", [UserController::class, "updatePassword"])->name("system.updateUserPassword"); + /* + * 角色管理 + */ + Route::get("role$", [RoleController::class, "list"])->name("system.listRoles"); + Route::get("role/page", [RoleController::class, "page"])->name("system.pageRoles"); + Route::delete("role/batch", [RoleController::class, "removeRoles"])->name("system.removeRoles"); + Route::get("role-menu/:role_id", [RoleController::class, "getMenu"])->name("system.getRoleMenu"); + Route::put("role-menu/:role_id", [RoleController::class, "updateMenu"])->name("system.updateRoleMenu"); + /* + * 菜单管理 + */ + Route::get("menu$", [MenuController::class, "list"])->name("system.listMenus"); + Route::post("menu", [MenuController::class, "add"])->name("system.addMenu"); + Route::delete("menu/:menu_id", [MenuController::class, "remove"]) + ->model('menu_id', SysMenu::class) + ->name("system.removeMenu"); + + /* + * 机构管理 + */ + Route::get("organization", [OrganizationController::class, "lists"])->name("system.getOrganizationList"); + Route::post("organization", [OrganizationController::class, "add"])->name("system.addOrganization"); + Route::put("organization", [OrganizationController::class, "update"])->name("system.updateOrganization"); + Route::delete("organization/:organization_id", [OrganizationController::class, "remove"]) + ->model('organization_id', SysOrganization::class) + ->name("system.removeOrganization"); + /* + * 字典管理 + */ + Route::get("dictionary$", [DictionaryController::class, "lists"])->name("system.getDictionaryList"); + Route::post("dictionary", [DictionaryController::class, "add"])->name("system.addDictionary"); + Route::put("dictionary", [DictionaryController::class, "update"])->name("system.updateDictionary"); + Route::delete("dictionary/:dict_id", [DictionaryController::class, "remove"])->model('dict_id', SysDictionary::class)->name("system.removeDictionary"); + /* + * 字典数据管理 + */ + Route::get("dictionary-data$", [DictionaryDataController::class, "lists"])->name("system.getDictionaryData"); + Route::get("dictionary-data/page", [DictionaryDataController::class, "page"])->name("system.pageDictionaryData"); + Route::post("dictionary-data", [DictionaryDataController::class, "add"])->name("system.addDictionaryData"); + Route::put("dictionary-data", [DictionaryDataController::class, "update"])->name("system.updateDictionaryData"); + Route::delete("dictionary-data/batch", [DictionaryDataController::class, "batchRemove"])->name("system.batchRemoveDictionaryData"); + + })->name('系统接口'); + + })->middleware([ClientMiddleware::class, AuthMiddleware::class]); + +})->layer('admin') + ->middleware([AllowCrossDomain::class], [ + 'Access-Control-Allow-Headers' => 'Client, Client-Version, Client-Id, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With' + ]); +// AllowCrossDomain 缺一个预检检查. +//if($request->isOptions()) { +// return \response()->header($header)->code(204); +//} \ No newline at end of file diff --git a/route/app.php b/route/app.php new file mode 100644 index 0000000..69071f7 --- /dev/null +++ b/route/app.php @@ -0,0 +1,17 @@ + +// +---------------------------------------------------------------------- +use think\facade\Route; + +Route::get('think', function () { + return 'hello,ThinkPHP8!'; +}); + +Route::get('hello/:name', 'index/hello'); diff --git a/runtime/.gitignore b/runtime/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/think b/think new file mode 100644 index 0000000..979ac35 --- /dev/null +++ b/think @@ -0,0 +1,11 @@ +#!/usr/bin/env php +console->run(); diff --git a/view/README.md b/view/README.md new file mode 100644 index 0000000..360eb24 --- /dev/null +++ b/view/README.md @@ -0,0 +1 @@ +如果不使用模板,可以删除该目录 \ No newline at end of file