├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app ├── Controller │ ├── Common │ │ ├── MediaLib.php │ │ └── TypeLib.php │ ├── IndexController.php │ └── System │ │ ├── AclController.php │ │ ├── ActivitiesController.php │ │ ├── AdminController.php │ │ ├── AudioController.php │ │ ├── AudioTypeController.php │ │ ├── BaseController.php │ │ ├── ColumnController.php │ │ ├── LogsController.php │ │ ├── MainController.php │ │ ├── PermissionController.php │ │ ├── PictureController.php │ │ ├── PictureTypeController.php │ │ ├── PolicyController.php │ │ ├── ResourceController.php │ │ ├── RoleController.php │ │ ├── SchemaController.php │ │ ├── VideoController.php │ │ └── VideoTypeController.php ├── Exception │ └── Handler │ │ └── AppExceptionHandler.php ├── Job │ ├── ActivitiesJob.php │ └── LogsJob.php ├── Listener │ └── DbQueryExecutedListener.php ├── Middleware │ └── System │ │ ├── AuthVerify.php │ │ ├── RbacVerify.php │ │ └── Spy.php ├── RedisModel │ └── System │ │ ├── AclRedis.php │ │ ├── AdminRedis.php │ │ ├── ResourceRedis.php │ │ └── RoleRedis.php └── Service │ ├── CommonService.php │ ├── CosService.php │ ├── LoggerService.php │ ├── OpenApiService.php │ └── SchemaService.php ├── bin └── hyperf.php ├── composer.json ├── config ├── autoload │ ├── annotations.php │ ├── aspects.php │ ├── async_queue.php │ ├── commands.php │ ├── cookie.php │ ├── cors.php │ ├── curd.php │ ├── databases.php │ ├── dependencies.php │ ├── devtool.php │ ├── exceptions.php │ ├── hashing.php │ ├── listeners.php │ ├── logger.php │ ├── middlewares.php │ ├── processes.php │ ├── qcloud.php │ ├── redis.php │ ├── server.php │ ├── token.php │ └── translation.php ├── config.php ├── container.php └── routes.php ├── docker-compose.yml └── storage └── languages ├── en └── validation.php └── zh_CN └── validation.php /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=skeleton 2 | APP_KEY=(null) 3 | 4 | COOKIE_EXPIRE=0 5 | COOKIE_PATH=/ 6 | COOKIE_DOMAIN= 7 | COOKIE_SECURE=false 8 | COOKIE_HTTPONLY=false 9 | COOKIE_SAMESITE=(null) 10 | 11 | CORS_METHODS=* 12 | CORS_ORIGINS=* 13 | CORS_HEADERS=* 14 | CORS_EXPOSED_HEADERS= 15 | CORS_MAX_AGE=0 16 | CORS_CREDENTIALS=true 17 | 18 | DB_DRIVER=mysql 19 | DB_HOST=localhost 20 | DB_PORT=3306 21 | DB_DATABASE=skeleton 22 | DB_USERNAME=root 23 | DB_PASSWORD= 24 | DB_CHARSET=utf8mb4 25 | DB_COLLATION=utf8mb4_unicode_ci 26 | DB_PREFIX= 27 | 28 | REDIS_HOST=localhost 29 | REDIS_AUTH=(null) 30 | REDIS_PORT=6379 31 | REDIS_DB=0 32 | 33 | QCLOUD_APP_ID= 34 | QCLOUD_SECRET_ID= 35 | QCLOUD_SECRET_KEY= 36 | COS_REGION= 37 | COS_BUCKET= 38 | COS_PREFIX= 39 | COS_UPLOAD_SIZE=5242880 40 | 41 | QCLOUD_API= 42 | QCLOUD_TOKEN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | .phpintel/ 10 | .env 11 | .DS_Store 12 | *.lock 13 | .phpunit* 14 | /package-lock.json 15 | /public -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperf Api Case 2 | 3 | 辅助 Hyperf 框架的工具集合使用案例,构建简洁统一的中后台接口方案 4 | 5 | **开发文档:** https://p.kdocs.cn/s/USXSUBIA7M 6 | -------------------------------------------------------------------------------- /app/Controller/Common/MediaLib.php: -------------------------------------------------------------------------------- 1 | curd->should([ 28 | 'type_id' => 'required', 29 | 'data' => 'required|array', 30 | 'data.*.name' => 'required', 31 | 'data.*.url' => 'required' 32 | ]); 33 | $data = []; 34 | $now = time(); 35 | foreach ($body['data'] as $value) { 36 | $data[] = [ 37 | 'type_id' => $body['type_id'], 38 | 'name' => $value['name'], 39 | 'url' => $value['url'], 40 | 'create_time' => $now, 41 | 'update_time' => $now 42 | ]; 43 | } 44 | Db::table(static::$model)->insert($data); 45 | return [ 46 | 'error' => 0, 47 | 'msg' => 'ok' 48 | ]; 49 | } 50 | 51 | public function bulkEdit(): array 52 | { 53 | $body = $this->curd->should([ 54 | 'type_id' => 'required', 55 | 'ids' => 'required|array', 56 | ]); 57 | Db::transaction(function () use ($body) { 58 | foreach ($body['ids'] as $value) { 59 | Db::table(static::$model) 60 | ->where('id', '=', $value) 61 | ->update(['type_id' => $body['type_id']]); 62 | } 63 | }); 64 | return [ 65 | 'error' => 0, 66 | 'msg' => 'ok' 67 | ]; 68 | } 69 | 70 | public function count(): array 71 | { 72 | $total = Db::table(static::$model)->count(); 73 | $values = Db::table(static::$model) 74 | ->groupBy(['type_id']) 75 | ->get(['type_id', Db::raw('count(*) as size')]); 76 | 77 | return [ 78 | 'error' => 0, 79 | 'data' => [ 80 | 'total' => $total, 81 | 'values' => $values 82 | ] 83 | ]; 84 | } 85 | } -------------------------------------------------------------------------------- /app/Controller/Common/TypeLib.php: -------------------------------------------------------------------------------- 1 | curd->should([ 19 | 'data' => 'required|array', 20 | 'data.*.id' => 'required', 21 | 'data.*.sort' => 'required' 22 | ]); 23 | Db::transaction(function () use ($body) { 24 | foreach ($body['data'] as $value) { 25 | Db::table(static::$model) 26 | ->where('id', '=', $value['id']) 27 | ->update(['sort' => $value['sort']]); 28 | } 29 | }); 30 | return [ 31 | 'error' => 0, 32 | 'msg' => 'ok' 33 | ]; 34 | } 35 | } -------------------------------------------------------------------------------- /app/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | 2.0, 12 | ]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/Controller/System/AclController.php: -------------------------------------------------------------------------------- 1 | 'required|array', 25 | 'key' => 'required', 26 | 'write' => 'sometimes|array', 27 | 'read' => 'sometimes|array' 28 | ]; 29 | protected static array $editValidate = [ 30 | 'name' => 'required_if:switch,false|array', 31 | 'key' => 'required_if:switch,false', 32 | 'write' => 'sometimes|array', 33 | 'read' => 'sometimes|array' 34 | ]; 35 | 36 | /** 37 | * @Inject() 38 | * @var AclRedis 39 | */ 40 | private AclRedis $aclRedis; 41 | /** 42 | * @Inject() 43 | * @var RoleRedis 44 | */ 45 | private RoleRedis $roleRedis; 46 | 47 | public function addBeforeHook(stdClass $ctx): bool 48 | { 49 | $this->before($ctx->body); 50 | return true; 51 | } 52 | 53 | public function addAfterHook(stdClass $ctx): bool 54 | { 55 | $this->clearRedis(); 56 | return true; 57 | } 58 | 59 | public function editBeforeHook(stdClass $ctx): bool 60 | { 61 | if (!$ctx->switch) { 62 | $this->before($ctx->body); 63 | } 64 | return true; 65 | } 66 | 67 | public function editAfterHook(stdClass $ctx): bool 68 | { 69 | $this->clearRedis(); 70 | return true; 71 | } 72 | 73 | private function before(array &$body): void 74 | { 75 | $body['name'] = json_encode($body['name'], JSON_UNESCAPED_UNICODE); 76 | $body['write'] = implode(',', (array)$body['write']); 77 | $body['read'] = implode(',', (array)$body['read']); 78 | } 79 | 80 | public function deleteAfterHook(stdClass $ctx): bool 81 | { 82 | $this->clearRedis(); 83 | return true; 84 | } 85 | 86 | private function clearRedis(): void 87 | { 88 | $this->aclRedis->clear(); 89 | $this->roleRedis->clear(); 90 | } 91 | 92 | /** 93 | * Exists Acl Key 94 | * @return array 95 | */ 96 | public function validedKey(): array 97 | { 98 | $body = $this->request->post(); 99 | if (empty($body['key'])) { 100 | return [ 101 | 'error' => 1, 102 | 'msg' => 'require key' 103 | ]; 104 | } 105 | 106 | $exists = Db::table('acl') 107 | ->where('key', '=', $body['key']) 108 | ->exists(); 109 | 110 | return [ 111 | 'error' => 0, 112 | 'data' => [ 113 | 'exists' => $exists 114 | ] 115 | ]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/Controller/System/ActivitiesController.php: -------------------------------------------------------------------------------- 1 | 'desc']; 14 | } -------------------------------------------------------------------------------- /app/Controller/System/AdminController.php: -------------------------------------------------------------------------------- 1 | [ 36 | 'required', 37 | 'between:4,20' 38 | ], 39 | 'password' => [ 40 | 'required', 41 | 'between:12,20', 42 | 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&-+])(?=.*[0-9])[\w|@$!%*?&-+]+$/' 43 | ], 44 | 'role' => ['required', 'array'], 45 | 'resource' => ['sometimes', 'array'], 46 | 'permission' => ['sometimes', 'array'] 47 | ]; 48 | protected static array $editValidate = [ 49 | 'role' => ['required_if:switch,false', 'array'], 50 | 'resource' => ['sometimes', 'array'], 51 | 'permission' => ['sometimes', 'array'] 52 | ]; 53 | 54 | /** 55 | * @Inject() 56 | * @var AdminRedis 57 | */ 58 | private AdminRedis $adminRedis; 59 | 60 | public function getCustomReturn(array $body, array $result): array 61 | { 62 | $username = Context::get('auth')['user']; 63 | $data = Db::table('admin') 64 | ->where('username', '=', $username) 65 | ->where('status', '=', 1) 66 | ->first(); 67 | if (!empty($data) && $data->id === (int)$body['id']) { 68 | $result['data']->self = true; 69 | } 70 | return $result; 71 | } 72 | 73 | public function addBeforeHook(stdClass $ctx): bool 74 | { 75 | $ctx->role = $ctx->body['role']; 76 | unset($ctx->body['role']); 77 | $ctx->resource = $ctx->body['resource']; 78 | unset($ctx->body['resource']); 79 | $ctx->body['password'] = $this->hash->create($ctx->body['password']); 80 | $ctx->body['permission'] = implode(',', (array)$ctx->body['permission']); 81 | return true; 82 | } 83 | 84 | public function addAfterHook(stdClass $ctx): bool 85 | { 86 | Db::table('admin_role_rel')->insert(array_map(static fn($v) => [ 87 | 'admin_id' => $ctx->id, 88 | 'role_key' => $v 89 | ], $ctx->role)); 90 | if (!empty($ctx->resource)) { 91 | Db::table('admin_resource_rel')->insert(array_map(static fn($v) => [ 92 | 'admin_id' => $ctx->id, 93 | 'resource_key' => $v 94 | ], $ctx->resource)); 95 | } 96 | $this->clearRedis(); 97 | return true; 98 | } 99 | 100 | public function editBeforeHook(stdClass $ctx): bool 101 | { 102 | $username = Context::get('auth')['user']; 103 | $data = Db::table('admin') 104 | ->where('username', '=', $username) 105 | ->where('status', '=', 1) 106 | ->first(); 107 | if (!empty($data) && $data->id === $ctx->body['id']) { 108 | Context::set('error', [ 109 | 'error' => 2, 110 | 'msg' => 'Detected as currently logged in user' 111 | ]); 112 | return false; 113 | } 114 | if (!$ctx->switch) { 115 | $ctx->role = $ctx->body['role']; 116 | unset($ctx->body['role']); 117 | $ctx->resource = $ctx->body['resource']; 118 | unset($ctx->body['resource']); 119 | if (!empty($ctx->body['password'])) { 120 | $validator = $this->validation->make($ctx->body, [ 121 | 'password' => [ 122 | 'between:12,20', 123 | 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&-+])(?=.*[0-9])[\w|@$!%*?&-+]+$/' 124 | ], 125 | ]); 126 | if ($validator->fails()) { 127 | throw new ValidationException($validator); 128 | } 129 | $ctx->body['password'] = $this->hash->create($ctx->body['password']); 130 | } else { 131 | unset($ctx->body['password']); 132 | } 133 | $ctx->body['permission'] = implode(',', (array)$ctx->body['permission']); 134 | } 135 | return true; 136 | } 137 | 138 | public function editAfterHook(stdClass $ctx): bool 139 | { 140 | if (!$ctx->switch) { 141 | if (!empty($ctx->role)) { 142 | Db::table('admin_role_rel') 143 | ->where('admin_id', '=', $ctx->body['id']) 144 | ->delete(); 145 | Db::table('admin_role_rel')->insert(array_map(static fn($v) => [ 146 | 'admin_id' => $ctx->body['id'], 147 | 'role_key' => $v 148 | ], $ctx->role)); 149 | } 150 | if (!empty($ctx->resource)) { 151 | Db::table('admin_resource_rel') 152 | ->where('admin_id', '=', $ctx->body['id']) 153 | ->delete(); 154 | Db::table('admin_resource_rel')->insert(array_map(static fn($v) => [ 155 | 'admin_id' => $ctx->body['id'], 156 | 'resource_key' => $v 157 | ], $ctx->resource)); 158 | } 159 | } 160 | $this->clearRedis(); 161 | return true; 162 | } 163 | 164 | public function deleteBeforeHook(stdClass $ctx): bool 165 | { 166 | $username = Context::get('auth')['user']; 167 | $data = Db::table('admin') 168 | ->where('username', '=', $username) 169 | ->where('status', '=', 1) 170 | ->first(); 171 | if (!empty($data) && in_array($data->id, $ctx->body['id'], true)) { 172 | Context::set('error', [ 173 | 'error' => 1, 174 | 'msg' => 'is self' 175 | ]); 176 | return false; 177 | } 178 | return true; 179 | } 180 | 181 | public function deleteAfterHook(stdClass $ctx): bool 182 | { 183 | $this->clearRedis(); 184 | return true; 185 | } 186 | 187 | /** 188 | * Clear Redis 189 | */ 190 | private function clearRedis(): void 191 | { 192 | $this->adminRedis->clear(); 193 | } 194 | 195 | /** 196 | * Exists Acl Key 197 | * @return array 198 | */ 199 | public function validedUsername(): array 200 | { 201 | $body = $this->request->post(); 202 | if (empty($body['username'])) { 203 | return [ 204 | 'error' => 1, 205 | 'msg' => 'require username' 206 | ]; 207 | } 208 | 209 | $exists = Db::table('admin') 210 | ->where('username', '=', $body['username']) 211 | ->exists(); 212 | 213 | return [ 214 | 'error' => 0, 215 | 'data' => [ 216 | 'exists' => $exists 217 | ] 218 | ]; 219 | } 220 | 221 | } -------------------------------------------------------------------------------- /app/Controller/System/AudioController.php: -------------------------------------------------------------------------------- 1 | 'asc' 15 | ]; 16 | protected static array $addValidate = [ 17 | 'name' => 'required' 18 | ]; 19 | protected static array $editValidate = [ 20 | 'name' => 'required' 21 | ]; 22 | } -------------------------------------------------------------------------------- /app/Controller/System/BaseController.php: -------------------------------------------------------------------------------- 1 | curd->should([ 29 | 'schema' => 'required', 30 | 'columns' => 'array', 31 | 'columns.*.column' => 'required', 32 | 'columns.*.datatype' => 'required', 33 | 'columns.*.name' => 'required|array', 34 | ]); 35 | return Db::transaction(function () use ($body) { 36 | $now = time(); 37 | $data = []; 38 | foreach ($body['columns'] as $value) { 39 | $data[] = [ 40 | 'schema' => $body['schema'], 41 | 'column' => $value['column'], 42 | 'datatype' => $value['datatype'], 43 | 'name' => json_encode($value['name'], JSON_UNESCAPED_UNICODE), 44 | 'description' => json_encode((object)$value['description'], JSON_UNESCAPED_UNICODE), 45 | 'sort' => $value['sort'], 46 | 'extra' => json_encode((object)$value['extra']), 47 | 'create_time' => $now, 48 | 'update_time' => $now 49 | ]; 50 | } 51 | Db::table('column') 52 | ->where('schema', '=', $body['schema']) 53 | ->delete(); 54 | return Db::table('column')->insert($data) > 0; 55 | }) ? [ 56 | 'error' => 0, 57 | 'msg' => 'ok' 58 | ] : [ 59 | 'error' => 1, 60 | 'msg' => 'failed' 61 | ]; 62 | } 63 | } -------------------------------------------------------------------------------- /app/Controller/System/LogsController.php: -------------------------------------------------------------------------------- 1 | 'desc']; 14 | } -------------------------------------------------------------------------------- /app/Controller/System/MainController.php: -------------------------------------------------------------------------------- 1 | curd->should([ 80 | 'username' => ['required', 'between:4,20'], 81 | 'password' => ['required', 'between:12,20'], 82 | ]); 83 | $locker = $this->lock; 84 | $address = $this->common->getClinetIp(); 85 | $ipData = $this->openapi->ip($address); 86 | $ipData['ip'] = $address; 87 | if (!$locker->check('ip:' . $address)) { 88 | $locker->lock('ip:' . $address); 89 | $this->loginRecord($body, $ipData, false, 'IP 登录锁定'); 90 | return $this->response->json([ 91 | 'error' => 2, 92 | 'msg' => '当前尝试登录失败次数上限,请稍后再试' 93 | ]); 94 | } 95 | $user = $body['username']; 96 | $data = $this->adminRedis->get($user); 97 | if (empty($data)) { 98 | $locker->inc('ip:' . $address); 99 | $this->loginRecord($body, $ipData, false, '用户登录失败上限'); 100 | return $this->response->json([ 101 | 'error' => 1, 102 | 'msg' => '当前用户不存在或已被冻结' 103 | ]); 104 | } 105 | $userKey = 'admin:'; 106 | if (!$locker->check($userKey . $user)) { 107 | $locker->lock($userKey . $user); 108 | $this->loginRecord($body, $ipData, false, '用户登录失败上限'); 109 | return $this->response->json([ 110 | 'error' => 2, 111 | 'msg' => '当前用户登录失败次数以上限,请稍后再试' 112 | ]); 113 | } 114 | if (!$this->hash->check($body['password'], $data['password'])) { 115 | $locker->inc($userKey . $user); 116 | $this->loginRecord($body, $ipData, false, '用户登录密码不正确'); 117 | return $this->response->json([ 118 | 'error' => 1, 119 | 'msg' => '当前用户认证不成功' 120 | ]); 121 | } 122 | $locker->remove('ip:' . $address); 123 | $locker->remove($userKey . $user); 124 | $this->loginRecord($body, $ipData, true); 125 | return $this->create('system', [ 126 | 'user' => $data['username'], 127 | ]); 128 | } 129 | 130 | /** 131 | * 登录日志 132 | * @param bool $logged 登录成功 133 | * @param string|null $risk 备注 134 | */ 135 | private function loginRecord(array $body, array $data, bool $logged, ?string $risk = null): void 136 | { 137 | $this->logger->activities([ 138 | 'platform' => 'console', 139 | 'username' => $body['username'], 140 | 'ip' => $data['ip'], 141 | 'country' => $data['country'], 142 | 'region' => $data['region'], 143 | 'province' => $data['province'], 144 | 'city_id' => $data['cityId'], 145 | 'city' => $data['city'], 146 | 'isp' => $data['isp'], 147 | 'logged' => $logged, 148 | 'device' => $this->request->getHeader('user-agent')[0], 149 | 'password' => !$logged ? $body['password'] : null, 150 | 'risk' => $risk, 151 | 'time' => time(), 152 | ]); 153 | } 154 | 155 | /** 156 | * 用户登出 157 | */ 158 | public function logout(): ResponseInterface 159 | { 160 | return $this->destory('system'); 161 | } 162 | 163 | /** 164 | * 用户验证 165 | */ 166 | public function verify(): ResponseInterface 167 | { 168 | return $this->authVerify('system'); 169 | } 170 | 171 | /** 172 | * 获取资源 173 | * @return array 174 | */ 175 | public function resource(): array 176 | { 177 | return [ 178 | 'error' => 0, 179 | 'data' => $this->fetchResource( 180 | $this->resourceRedis, 181 | $this->adminRedis, 182 | $this->roleRedis 183 | ) 184 | ]; 185 | } 186 | 187 | /** 188 | * 个人信息 189 | * @return array 190 | */ 191 | public function information(): array 192 | { 193 | $user = Context::get('auth')['user']; 194 | $data = Db::table('admin') 195 | ->where('username', '=', $user) 196 | ->first(); 197 | 198 | if (empty($data)) { 199 | return [ 200 | 'error' => 1, 201 | 'msg' => '当前用户不存在' 202 | ]; 203 | } 204 | 205 | return [ 206 | 'error' => 0, 207 | 'data' => [ 208 | 'username' => $data->username, 209 | 'email' => $data->email, 210 | 'phone' => $data->phone, 211 | 'call' => $data->call, 212 | 'avatar' => $data->avatar 213 | ] 214 | ]; 215 | } 216 | 217 | /** 218 | * 更新个人信息 219 | * @return array 220 | */ 221 | public function update(): array 222 | { 223 | $body = $this->curd->should([ 224 | 'old_password' => [ 225 | 'sometimes', 226 | 'between:12,20', 227 | 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&-+])(?=.*[0-9])[\w|@$!%*?&-+]+$/' 228 | ], 229 | 'new_password' => [ 230 | 'required_with:old_password', 231 | 'between:12,20', 232 | 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*[@$!%*?&-+])(?=.*[0-9])[\w|@$!%*?&-+]+$/' 233 | ], 234 | ]); 235 | $user = Context::get('auth')['user']; 236 | $data = Db::table('admin') 237 | ->where('username', '=', $user) 238 | ->first(); 239 | 240 | if (empty($data)) { 241 | return [ 242 | 'error' => 1, 243 | 'msg' => '当前用户不存在' 244 | ]; 245 | } 246 | 247 | if (!empty($body['old_password'])) { 248 | if (!$this->hash->check($body['old_password'], $data->password)) { 249 | return [ 250 | 'error' => 2, 251 | 'msg' => '用户密码验证失败' 252 | ]; 253 | } 254 | $body['password'] = $this->hash->create($body['new_password']); 255 | } 256 | 257 | unset($body['old_password'], $body['new_password']); 258 | Db::table('admin') 259 | ->where('username', '=', $user) 260 | ->update($body); 261 | 262 | $this->adminRedis->clear(); 263 | return [ 264 | 'error' => 0, 265 | 'msg' => 'ok' 266 | ]; 267 | } 268 | 269 | /** 270 | * 对象存储签名 271 | * @param ConfigInterface $config 272 | * @return array 273 | * @throws Exception 274 | */ 275 | public function presigned(ConfigInterface $config): array 276 | { 277 | return $this->cos->generatePostPresigned([ 278 | ['content-length-range', 0, $config->get('qcloud.cos.upload_size')] 279 | ]); 280 | } 281 | 282 | /** 283 | * 指定删除对象 284 | * @return array 285 | * @throws Exception 286 | */ 287 | public function objectDelete(): array 288 | { 289 | $body = $this->curd->should([ 290 | 'keys' => 'required|array' 291 | ]); 292 | $this->cos->delete($body['keys']); 293 | return [ 294 | 'error' => 0, 295 | 'msg' => 'ok' 296 | ]; 297 | } 298 | } -------------------------------------------------------------------------------- /app/Controller/System/PermissionController.php: -------------------------------------------------------------------------------- 1 | 'required|array', 22 | 'key' => 'required', 23 | ]; 24 | protected static array $editValidate = [ 25 | 'name' => 'required_if:switch,false|array', 26 | 'key' => 'required_if:switch,false', 27 | ]; 28 | 29 | public function addBeforeHook(stdClass $ctx): bool 30 | { 31 | $this->before($ctx->body); 32 | return true; 33 | } 34 | 35 | public function editBeforeHook(stdClass $ctx): bool 36 | { 37 | if (!$ctx->switch) { 38 | $this->before($ctx->body); 39 | } 40 | return true; 41 | } 42 | 43 | private function before(array &$body): void 44 | { 45 | $body['name'] = json_encode($body['name'], JSON_UNESCAPED_UNICODE); 46 | } 47 | 48 | /** 49 | * Exists Acl Key 50 | * @return array 51 | */ 52 | public function validedKey(): array 53 | { 54 | $body = $this->request->post(); 55 | if (empty($body['key'])) { 56 | return [ 57 | 'error' => 1, 58 | 'msg' => 'require key' 59 | ]; 60 | } 61 | 62 | $exists = Db::table('permission') 63 | ->where('key', '=', $body['key']) 64 | ->exists(); 65 | 66 | return [ 67 | 'error' => 0, 68 | 'data' => [ 69 | 'exists' => $exists 70 | ] 71 | ]; 72 | } 73 | } -------------------------------------------------------------------------------- /app/Controller/System/PictureController.php: -------------------------------------------------------------------------------- 1 | 'asc' 15 | ]; 16 | protected static array $addValidate = [ 17 | 'name' => 'required' 18 | ]; 19 | protected static array $editValidate = [ 20 | 'name' => 'required' 21 | ]; 22 | } -------------------------------------------------------------------------------- /app/Controller/System/PolicyController.php: -------------------------------------------------------------------------------- 1 | 'required', 22 | 'acl_key' => 'required', 23 | 'policy' => 'required' 24 | ]; 25 | 26 | /** 27 | * @Inject() 28 | * @var RoleRedis 29 | */ 30 | private RoleRedis $roleRedis; 31 | 32 | public function addAfterHook(stdClass $ctx): bool 33 | { 34 | $this->clearRedis(); 35 | return true; 36 | } 37 | 38 | public function deleteAfterHook(stdClass $ctx): bool 39 | { 40 | $this->clearRedis(); 41 | return true; 42 | } 43 | 44 | /** 45 | * Clear Cache 46 | */ 47 | private function clearRedis(): void 48 | { 49 | $this->roleRedis->clear(); 50 | } 51 | } -------------------------------------------------------------------------------- /app/Controller/System/ResourceController.php: -------------------------------------------------------------------------------- 1 | 'asc' 25 | ]; 26 | protected static array $addValidate = [ 27 | 'key' => 'required', 28 | 'name' => 'required|array' 29 | ]; 30 | protected static array $editValidate = [ 31 | 'key' => 'required_if:switch,false', 32 | 'name' => 'required_if:switch,false|array' 33 | ]; 34 | 35 | /** 36 | * @Inject() 37 | * @var ResourceRedis 38 | */ 39 | private ResourceRedis $resourceRedis; 40 | /** 41 | * @Inject() 42 | * @var RoleRedis 43 | */ 44 | private RoleRedis $roleRedis; 45 | 46 | public function addBeforeHook(stdClass $ctx): bool 47 | { 48 | $ctx->body['name'] = json_encode($ctx->body['name'], JSON_UNESCAPED_UNICODE); 49 | return true; 50 | } 51 | 52 | public function addAfterHook(stdClass $ctx): bool 53 | { 54 | $this->clearRedis(); 55 | return true; 56 | } 57 | 58 | public function editBeforeHook(stdClass $ctx): bool 59 | { 60 | if (!$ctx->switch) { 61 | $ctx->body['name'] = json_encode($ctx->body['name'], JSON_UNESCAPED_UNICODE); 62 | $data = Db::table('resource') 63 | ->where('id', '=', $ctx->body['id']) 64 | ->first(); 65 | 66 | if (!empty($data)) { 67 | $ctx->key = $data->key; 68 | } 69 | } 70 | return true; 71 | } 72 | 73 | public function editAfterHook(stdClass $ctx): bool 74 | { 75 | if (!$ctx->switch && !empty($ctx->key)) { 76 | Db::table('resource') 77 | ->where('parent', '=', $ctx->key) 78 | ->update([ 79 | 'parent' => $ctx->body['key'] 80 | ]); 81 | } 82 | $this->clearRedis(); 83 | return true; 84 | } 85 | 86 | public function deleteBeforeHook(stdClass $ctx): bool 87 | { 88 | $data = Db::table('resource') 89 | ->whereIn('id', $ctx->body['id']) 90 | ->first(); 91 | if (empty($data)) { 92 | Context::set('error', [ 93 | 'error' => 1, 94 | 'msg' => 'not exist' 95 | ]); 96 | return false; 97 | } 98 | $exists = Db::table('resource') 99 | ->where('parent', '=', $data->key) 100 | ->exists(); 101 | if ($exists) { 102 | Context::set('error', [ 103 | 'error' => 1, 104 | 'msg' => 'not exist' 105 | ]); 106 | return false; 107 | } 108 | return true; 109 | } 110 | 111 | public function deleteAfterHook(stdClass $ctx): bool 112 | { 113 | $this->clearRedis(); 114 | return true; 115 | } 116 | 117 | /** 118 | * Sort Lists 119 | * @return array 120 | */ 121 | public function sort(): array 122 | { 123 | $body = $this->curd->should([ 124 | 'data' => 'required|array', 125 | ]); 126 | return Db::transaction(function () use ($body) { 127 | foreach ($body['data'] as $value) { 128 | Db::table('resource') 129 | ->where('id', '=', $value['id']) 130 | ->update([ 131 | 'sort' => $value['sort'] 132 | ]); 133 | } 134 | $this->clearRedis(); 135 | return true; 136 | }) ? [ 137 | 'error' => 0, 138 | 'msg' => 'success' 139 | ] : [ 140 | 'error' => 1, 141 | 'msg' => 'error' 142 | ]; 143 | } 144 | 145 | private function clearRedis(): void 146 | { 147 | $this->resourceRedis->clear(); 148 | $this->roleRedis->clear(); 149 | } 150 | 151 | /** 152 | * Exists Resources Key 153 | * @return array 154 | */ 155 | public function validedKey(): array 156 | { 157 | $body = $this->request->post(); 158 | if (empty($body['key'])) { 159 | return [ 160 | 'error' => 1, 161 | 'msg' => 'require key' 162 | ]; 163 | } 164 | 165 | $exists = Db::table('resource') 166 | ->where('key', '=', $body['key']) 167 | ->exists(); 168 | 169 | return [ 170 | 'error' => 0, 171 | 'data' => [ 172 | 'exists' => $exists 173 | ] 174 | ]; 175 | } 176 | } -------------------------------------------------------------------------------- /app/Controller/System/RoleController.php: -------------------------------------------------------------------------------- 1 | 'required|array', 33 | 'key' => 'required', 34 | 'resource' => 'required|array' 35 | ]; 36 | protected static array $editValidate = [ 37 | 'name' => 'required_if:switch,false|array', 38 | 'key' => 'required_if:switch,false', 39 | 'resource' => 'required_if:switch,false|array' 40 | ]; 41 | 42 | /** 43 | * @Inject() 44 | * @var RoleRedis 45 | */ 46 | private RoleRedis $roleRedis; 47 | /** 48 | * @Inject() 49 | * @var AdminRedis 50 | */ 51 | private AdminRedis $adminRedis; 52 | 53 | public function addBeforeHook(stdClass $ctx): bool 54 | { 55 | $ctx->body['name'] = json_encode($ctx->body['name'], JSON_UNESCAPED_UNICODE); 56 | $ctx->resource = $ctx->body['resource']; 57 | unset($ctx->body['resource']); 58 | $ctx->body['permission'] = implode(',', (array)$ctx->body['permission']); 59 | return true; 60 | } 61 | 62 | public function addAfterHook(stdClass $ctx): bool 63 | { 64 | $resource = []; 65 | foreach ($ctx->resource as $key => $value) { 66 | $resource[] = [ 67 | 'role_key' => $ctx->body['key'], 68 | 'resource_key' => $value 69 | ]; 70 | } 71 | $result = Db::table('role_resource_rel')->insert($resource); 72 | if (!$result) { 73 | Context::set('error', [ 74 | 'error' => 1, 75 | 'msg' => 'insert resource failed' 76 | ]); 77 | return false; 78 | } 79 | $this->clearRedis(); 80 | return true; 81 | } 82 | 83 | public function editBeforeHook(stdClass $ctx): bool 84 | { 85 | $ctx->resource = []; 86 | if (!$ctx->switch) { 87 | $ctx->body['name'] = json_encode($ctx->body['name'], JSON_UNESCAPED_UNICODE); 88 | $ctx->resource = $ctx->body['resource']; 89 | unset($ctx->body['resource']); 90 | $ctx->body['permission'] = implode(',', (array)$ctx->body['permission']); 91 | } 92 | return true; 93 | } 94 | 95 | public function editAfterHook(stdClass $ctx): bool 96 | { 97 | if (!$ctx->switch) { 98 | $resource = []; 99 | foreach ($ctx->resource as $key => $value) { 100 | $resource[] = [ 101 | 'role_key' => $ctx->body['key'], 102 | 'resource_key' => $value 103 | ]; 104 | } 105 | Db::table('role_resource_rel') 106 | ->where('role_key', '=', $ctx->body['key']) 107 | ->delete(); 108 | $result = Db::table('role_resource_rel') 109 | ->insert($resource); 110 | if (!$result) { 111 | Context::set('error', [ 112 | 'error' => 1, 113 | 'msg' => 'insert resource failed' 114 | ]); 115 | return false; 116 | } 117 | } 118 | $this->clearRedis(); 119 | return true; 120 | } 121 | 122 | public function deleteAfterHook(stdClass $ctx): bool 123 | { 124 | $this->clearRedis(); 125 | return true; 126 | } 127 | 128 | /** 129 | * Clear Cache 130 | */ 131 | private function clearRedis(): void 132 | { 133 | $this->roleRedis->clear(); 134 | $this->adminRedis->clear(); 135 | } 136 | 137 | /** 138 | * Exists Role Key 139 | * @return array 140 | */ 141 | public function validedKey(): array 142 | { 143 | $body = $this->request->post(); 144 | if (empty($body['key'])) { 145 | return [ 146 | 'error' => 1, 147 | 'msg' => 'require key' 148 | ]; 149 | } 150 | 151 | $exists = Db::table('role') 152 | ->where('key', '=', $body['key']) 153 | ->exists(); 154 | 155 | return [ 156 | 'error' => 0, 157 | 'data' => [ 158 | 'exists' => $exists 159 | ] 160 | ]; 161 | } 162 | } -------------------------------------------------------------------------------- /app/Controller/System/SchemaController.php: -------------------------------------------------------------------------------- 1 | 'required|array', 27 | 'table' => 'required', 28 | 'type' => 'required', 29 | ]; 30 | protected static array $editValidate = [ 31 | 'name' => 'required_if:switch,0|array', 32 | 'table' => 'required_if:switch,0', 33 | 'type' => 'required_if:switch,0', 34 | ]; 35 | /** 36 | * @Inject() 37 | * @var SchemaService 38 | */ 39 | private SchemaService $schema; 40 | 41 | public function addBeforeHook(stdClass $ctx): bool 42 | { 43 | $this->before($ctx->body); 44 | return true; 45 | } 46 | 47 | public function addAfterHook(stdClass $ctx): bool 48 | { 49 | $now = time(); 50 | $body = $ctx->body; 51 | $result = Db::table('column')->insert([ 52 | [ 53 | 'schema' => $body['table'], 54 | 'column' => 'id', 55 | 'datatype' => 'system', 56 | 'name' => json_encode([ 57 | 'zh_cn' => '主键', 58 | 'en_us' => 'Primary key' 59 | ], JSON_UNESCAPED_UNICODE), 60 | 'description' => json_encode([ 61 | 'zh_cn' => '系统默认字段', 62 | 'en_us' => '' 63 | ], JSON_UNESCAPED_UNICODE), 64 | 'sort' => 0, 65 | 'create_time' => $now, 66 | 'update_time' => $now 67 | ], 68 | [ 69 | 'schema' => $body['table'], 70 | 'column' => 'create_time', 71 | 'datatype' => 'system', 72 | 'name' => json_encode([ 73 | 'zh_cn' => '创建时间', 74 | 'en_us' => 'Create Time' 75 | ], JSON_UNESCAPED_UNICODE), 76 | 'description' => json_encode([ 77 | 'zh_cn' => '系统默认字段', 78 | 'en_us' => '' 79 | ], JSON_UNESCAPED_UNICODE), 80 | 'sort' => 0, 81 | 'create_time' => $now, 82 | 'update_time' => $now 83 | ], 84 | [ 85 | 'schema' => $body['table'], 86 | 'column' => 'update_time', 87 | 'datatype' => 'system', 88 | 'name' => json_encode([ 89 | 'zh_cn' => '更新时间', 90 | 'en_us' => 'Update Time' 91 | ], JSON_UNESCAPED_UNICODE), 92 | 'description' => json_encode([ 93 | 'zh_cn' => '系统默认字段', 94 | 'en_us' => '' 95 | ], JSON_UNESCAPED_UNICODE), 96 | 'sort' => 0, 97 | 'create_time' => $now, 98 | 'update_time' => $now 99 | ] 100 | ]); 101 | return $result > 0; 102 | } 103 | 104 | public function editBeforeHook(stdClass $ctx): bool 105 | { 106 | if (!$ctx->switch) { 107 | $this->before($ctx->body); 108 | } 109 | return true; 110 | } 111 | 112 | private function before(array &$body): void 113 | { 114 | $body['name'] = json_encode($body['name'], JSON_UNESCAPED_UNICODE); 115 | $body['description'] = json_encode((object)$body['description'], JSON_UNESCAPED_UNICODE); 116 | } 117 | 118 | /** 119 | * 发布数据表 120 | * @return array 121 | * @throws Exception 122 | */ 123 | public function publish(): array 124 | { 125 | $body = $this->curd->should([ 126 | 'table' => 'required', 127 | ]); 128 | 129 | $manager = $this->schema->manager(); 130 | $name = $this->schema->table($body['table']); 131 | if ($manager->tablesExist($name)) { 132 | $version = time(); 133 | $manager->renameTable($name, $name . '_' . $version); 134 | Db::name('schema_history')->insert([ 135 | 'schema' => $body['table'], 136 | 'remark' => $body['remark'] ?? '', 137 | 'version' => $version 138 | ]); 139 | } 140 | $lists = Db::table('column') 141 | ->where('schema', '=', $body['table']) 142 | ->where('status', '=', 1) 143 | ->orderBy('sort') 144 | ->select(); 145 | $table = new Table($name); 146 | foreach ($lists as $value) { 147 | $column = $value['column']; 148 | $datatype = $value['datatype']; 149 | $extra = json_decode($value['extra']); 150 | if ($column === 'id') { 151 | $table->addColumn('id', Types::BIGINT, [ 152 | 'unsigned' => true, 153 | 'autoincrement' => true 154 | ]); 155 | continue; 156 | } 157 | if ($column === 'create_time') { 158 | $table->addColumn('create_time', Types::BIGINT, [ 159 | 'unsigned' => true, 160 | 'default' => 0 161 | ]); 162 | continue; 163 | } 164 | if ($column === 'update_time') { 165 | $table->addColumn('update_time', Types::BIGINT, [ 166 | 'unsigned' => true, 167 | 'default' => 0 168 | ]); 169 | continue; 170 | } 171 | $type = null; 172 | $option = []; 173 | if (!$extra->required) { 174 | $option['notnull'] = false; 175 | } 176 | if (!$extra->default) { 177 | $option['default'] = $extra->default; 178 | } 179 | switch ($datatype) { 180 | case 'string': 181 | $type = Types::TEXT; 182 | if (!$extra->is_text) { 183 | $type = Types::STRING; 184 | $option['length'] = 200; 185 | } 186 | break; 187 | case 'i18n': 188 | $type = Types::JSON; 189 | $option = [ 190 | 'default' => '{}' 191 | ]; 192 | break; 193 | case 'richtext': 194 | $type = Types::TEXT; 195 | break; 196 | case 'number': 197 | $type = Types::BIGINT; 198 | if ($extra->is_sort) { 199 | $type = Types::SMALLINT; 200 | $option['unsigned'] = true; 201 | } 202 | break; 203 | case 'status': 204 | $type = Types::BOOLEAN; 205 | $option['unsigned'] = true; 206 | break; 207 | case 'datetime': 208 | case 'date': 209 | $type = Types::BIGINT; 210 | $option['unsigned'] = true; 211 | break; 212 | case 'picture': 213 | case 'video': 214 | $type = Types::JSON; 215 | break; 216 | case 'assoc': 217 | case 'enmu': 218 | $type = Types::STRING; 219 | $option['length'] = 200; 220 | break; 221 | } 222 | $table->addColumn($column, $type, $option); 223 | } 224 | $table->setPrimaryKey(['id']); 225 | $manager->createTable($table); 226 | return [ 227 | 'error' => 0, 228 | 'msg' => 'ok' 229 | ]; 230 | } 231 | 232 | /** 233 | * 发布历史 234 | * @return array 235 | * @throws Exception 236 | */ 237 | public function history(): array 238 | { 239 | $body = $this->curd->should([ 240 | 'table' => 'required' 241 | ]); 242 | 243 | $lists = Db::table('schema_history') 244 | ->where('schema', '=', $body['table']) 245 | ->orderBy('version', 'desc') 246 | ->select(); 247 | 248 | return [ 249 | 'error' => 0, 250 | 'data' => $lists 251 | ]; 252 | } 253 | 254 | /** 255 | * 表结构详情 256 | * @return array 257 | * @throws Exception 258 | */ 259 | public function table(): array 260 | { 261 | $body = $this->curd->should([ 262 | 'table' => 'required' 263 | ]); 264 | 265 | $manager = $this->schema->manager(); 266 | $name = $this->schema->table($body['table']); 267 | $table = $manager->listTableDetails($name); 268 | 269 | return [ 270 | 'error' => 0, 271 | 'data' => [ 272 | 'columns' => array_values( 273 | array_map(static fn($v) => [ 274 | 'name' => $v->getName(), 275 | 'type' => $v->getType()->getName(), 276 | 'length' => $v->getLength(), 277 | 'autoincrement' => $v->getAutoincrement(), 278 | 'unsigned' => $v->getUnsigned(), 279 | 'notnull' => $v->getNotnull(), 280 | 'default' => $v->getDefault(), 281 | 'comment' => $v->getComment() 282 | ], $table->getColumns()) 283 | ), 284 | 'indexs' => array_values( 285 | array_map(static fn($v) => [ 286 | 'name' => $v->getName(), 287 | 'columns' => $v->getColumns(), 288 | 'unique' => $v->isUnique(), 289 | 'primary' => $v->isPrimary() 290 | ], $table->getIndexes()) 291 | ) 292 | ] 293 | ]; 294 | } 295 | 296 | /** 297 | * 判断表存在 298 | * @return array 299 | */ 300 | public function validedTable(): array 301 | { 302 | $body = $this->request->post(); 303 | if (empty($body['table'])) { 304 | return [ 305 | 'error' => 1, 306 | 'msg' => '请求参数必须存在[table]值' 307 | ]; 308 | } 309 | 310 | $exists = Db::table(static::$model) 311 | ->where('table', '=', $body['table']) 312 | ->exists(); 313 | 314 | return [ 315 | 'error' => 0, 316 | 'data' => [ 317 | 'exists' => $exists 318 | ] 319 | ]; 320 | } 321 | } -------------------------------------------------------------------------------- /app/Controller/System/VideoController.php: -------------------------------------------------------------------------------- 1 | 'asc' 15 | ]; 16 | protected static array $addValidate = [ 17 | 'name' => 'required' 18 | ]; 19 | protected static array $editValidate = [ 20 | 'name' => 'required' 21 | ]; 22 | } -------------------------------------------------------------------------------- /app/Exception/Handler/AppExceptionHandler.php: -------------------------------------------------------------------------------- 1 | 1, 19 | 'msg' => $throwable->errors() 20 | ], JSON_UNESCAPED_UNICODE); 21 | $this->stopPropagation(); 22 | return $response 23 | ->withHeader('Content-Type', 'application/json') 24 | ->withStatus(200) 25 | ->withBody(new SwooleStream($data)); 26 | } 27 | return $response; 28 | } 29 | 30 | public function isValid(Throwable $throwable): bool 31 | { 32 | return $throwable instanceof ValidationException; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Job/ActivitiesJob.php: -------------------------------------------------------------------------------- 1 | params = $params; 18 | } 19 | 20 | public function handle(): void 21 | { 22 | Db::table('activities')->insert($this->params); 23 | } 24 | } -------------------------------------------------------------------------------- /app/Job/LogsJob.php: -------------------------------------------------------------------------------- 1 | params = $params; 18 | } 19 | 20 | public function handle(): void 21 | { 22 | Db::table('logs')->insert($this->params); 23 | } 24 | } -------------------------------------------------------------------------------- /app/Listener/DbQueryExecutedListener.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(LoggerFactory::class)->get('sql'); 37 | } 38 | 39 | public function listen(): array 40 | { 41 | return [ 42 | QueryExecuted::class, 43 | ]; 44 | } 45 | 46 | /** 47 | * @param QueryExecuted $event 48 | */ 49 | public function process(object $event) 50 | { 51 | if ($event instanceof QueryExecuted) { 52 | $sql = $event->sql; 53 | if (!Arr::isAssoc($event->bindings)) { 54 | foreach ($event->bindings as $key => $value) { 55 | $sql = Str::replaceFirst('?', "'{$value}'", $sql); 56 | } 57 | } 58 | 59 | $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Middleware/System/AuthVerify.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 20 | } 21 | 22 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 23 | { 24 | $this->logger->logs([ 25 | 'path' => $request->getUri()->getPath(), 26 | 'username' => Context::get('auth')['user'] ?? 'none', 27 | 'body' => $request->getBody()->getContents(), 28 | 'time' => time(), 29 | ]); 30 | return $handler->handle($request); 31 | } 32 | } -------------------------------------------------------------------------------- /app/RedisModel/System/AclRedis.php: -------------------------------------------------------------------------------- 1 | redis->del($this->getKey()); 20 | } 21 | 22 | /** 23 | * Get Cache 24 | * @param string $key 25 | * @param int $policy 26 | * @return array 27 | */ 28 | public function get(string $key, int $policy): array 29 | { 30 | if (!$this->redis->exists($this->getKey())) { 31 | $this->update(); 32 | } 33 | 34 | $raws = $this->redis->hGet($this->getKey(), $key); 35 | $data = json_decode($raws, true); 36 | 37 | switch ($policy) { 38 | case 0: 39 | return $data['read']; 40 | case 1: 41 | return [ 42 | ...$data['read'], 43 | ...$data['write'] 44 | ]; 45 | default: 46 | return []; 47 | } 48 | } 49 | 50 | /** 51 | * Refresh Cache 52 | */ 53 | private function update(): void 54 | { 55 | $query = Db::table('acl') 56 | ->where('status', '=', 1) 57 | ->get(['key', 'write', 'read']); 58 | 59 | if ($query->isEmpty()) { 60 | return; 61 | } 62 | 63 | $lists = []; 64 | foreach ($query->toArray() as $value) { 65 | $lists[$value->key] = json_encode([ 66 | 'write' => !empty($value->write) ? explode(',', $value->write) : [], 67 | 'read' => !empty($value->read) ? explode(',', $value->read) : [] 68 | ]); 69 | } 70 | $this->redis->hMSet($this->getKey(), $lists); 71 | } 72 | } -------------------------------------------------------------------------------- /app/RedisModel/System/AdminRedis.php: -------------------------------------------------------------------------------- 1 | redis->del($this->getKey()); 20 | } 21 | 22 | /** 23 | * Get Cache 24 | * @param string $username 25 | * @return array 26 | */ 27 | public function get(string $username): array 28 | { 29 | if (!$this->redis->exists($this->getKey())) { 30 | $this->update(); 31 | } 32 | 33 | $raws = $this->redis->hGet($this->getKey(), $username); 34 | return !empty($raws) ? json_decode($raws, true) : []; 35 | } 36 | 37 | /** 38 | * Refresh Cache 39 | */ 40 | private function update(): void 41 | { 42 | $query = Db::table('admin_mix') 43 | ->where('status', '=', 1) 44 | ->get(['id', 'role', 'username', 'password', 'resource', 'acl', 'permission']); 45 | 46 | if ($query->isEmpty()) { 47 | return; 48 | } 49 | 50 | $lists = []; 51 | foreach ($query->toArray() as $value) { 52 | $value->role = explode(',', $value->role); 53 | $value->resource = !empty($value->resource) ? explode(',', $value->resource) : []; 54 | $value->acl = !empty($value->acl) ? explode(',', $value->acl) : []; 55 | $value->permission = !empty($value->permission) ? explode(',', $value->permission) : []; 56 | $lists[$value->username] = json_encode($value); 57 | } 58 | $this->redis->hMSet($this->getKey(), $lists); 59 | } 60 | } -------------------------------------------------------------------------------- /app/RedisModel/System/ResourceRedis.php: -------------------------------------------------------------------------------- 1 | redis->del($this->getKey()); 20 | } 21 | 22 | /** 23 | * Get Cache 24 | * @return array 25 | */ 26 | public function get(): array 27 | { 28 | if (!$this->redis->exists($this->getKey())) { 29 | $this->update(); 30 | } 31 | $raws = $this->redis->get($this->getKey()); 32 | return !empty($raws) ? json_decode($raws, true) : []; 33 | } 34 | 35 | /** 36 | * Refresh Cache 37 | */ 38 | private function update(): void 39 | { 40 | $query = Db::table('resource') 41 | ->where('status', '=', 1) 42 | ->orderBy('sort') 43 | ->get(['key', 'parent', 'name', 'nav', 'router', 'policy', 'icon']); 44 | 45 | if ($query->isEmpty()) { 46 | return; 47 | } 48 | 49 | $this->redis->set($this->getKey(), json_encode($query->toArray())); 50 | } 51 | } -------------------------------------------------------------------------------- /app/RedisModel/System/RoleRedis.php: -------------------------------------------------------------------------------- 1 | redis->del($this->getKey()); 20 | } 21 | 22 | /** 23 | * Get Role 24 | * @param array $keys 25 | * @param string $type 26 | * @return array 27 | */ 28 | public function get(array $keys, string $type): array 29 | { 30 | if (!$this->redis->exists($this->getKey())) { 31 | $this->update(); 32 | } 33 | $raws = $this->redis->hMGet($this->getKey(), $keys); 34 | if (empty($raws)) { 35 | return []; 36 | } 37 | $lists = []; 38 | foreach ($raws as $value) { 39 | $data = json_decode($value, true); 40 | array_push($lists, ...$data[$type]); 41 | } 42 | return $lists; 43 | } 44 | 45 | /** 46 | * Refresh Cache 47 | */ 48 | private function update(): void 49 | { 50 | $query = Db::table('role_mix') 51 | ->where('status', '=', 1) 52 | ->get(['key', 'acl', 'resource', 'permission']); 53 | 54 | if ($query->isEmpty()) { 55 | return; 56 | } 57 | $lists = []; 58 | foreach ($query->toArray() as $value) { 59 | $lists[$value->key] = json_encode([ 60 | 'acl' => !empty($value->acl) ? explode(',', $value->acl) : [], 61 | 'resource' => !empty($value->resource) ? explode(',', $value->resource) : [], 62 | 'permission' => !empty($value->permission) ? explode(',', $value->permission) : [] 63 | ]); 64 | } 65 | 66 | $this->redis->hMSet($this->getKey(), $lists); 67 | } 68 | } -------------------------------------------------------------------------------- /app/Service/CommonService.php: -------------------------------------------------------------------------------- 1 | request->getHeader('X-Real-IP')[0]; 24 | } 25 | } -------------------------------------------------------------------------------- /app/Service/CosService.php: -------------------------------------------------------------------------------- 1 | option = $config->get('qcloud'); 19 | } 20 | 21 | /** 22 | * @param $file 23 | * @return string 24 | * @throws Exception 25 | */ 26 | public function put($file): string 27 | { 28 | $fileName = $this->option['cos']['prefix'] . 29 | date('Ymd') . '/' . 30 | uuid()->toString() . '.' . 31 | $file->getOriginalExtension(); 32 | $object = new ObjectClient([ 33 | 'app_id' => $this->option['app_id'], 34 | 'secret_id' => $this->option['secret_id'], 35 | 'secret_key' => $this->option['secret_key'], 36 | 'bucket' => $this->option['cos']['bucket'], 37 | 'region' => $this->option['cos']['region'], 38 | ]); 39 | $object->putObject( 40 | $fileName, 41 | file_get_contents($file->getRealPath()) 42 | ); 43 | return $fileName; 44 | } 45 | 46 | /** 47 | * @param array $objects 48 | * @return Response 49 | * @throws Exception 50 | */ 51 | public function delete(array $objects): Response 52 | { 53 | $object = new ObjectClient([ 54 | 'app_id' => $this->option['app_id'], 55 | 'secret_id' => $this->option['secret_id'], 56 | 'secret_key' => $this->option['secret_key'], 57 | 'bucket' => $this->option['cos']['bucket'], 58 | 'region' => $this->option['cos']['region'], 59 | ]); 60 | return $object->deleteObjects([ 61 | 'Delete' => [ 62 | 'Quiet' => true, 63 | 'Object' => [...$objects] 64 | ] 65 | ]); 66 | } 67 | 68 | /** 69 | * 生成客户端上传 COS 对象存储签名 70 | * @param array $conditions 71 | * @param int $expired 72 | * @return array 73 | * @throws Exception 74 | */ 75 | public function generatePostPresigned(array $conditions, int $expired = 600): array 76 | { 77 | $date = Carbon::now()->setTimezone('UTC'); 78 | $keyTime = $date->unix() . ';' . ($date->unix() + $expired); 79 | $policy = json_encode([ 80 | 'expiration' => $date->addSeconds($expired)->toISOString(), 81 | 'conditions' => [ 82 | ['q-sign-algorithm' => 'sha1'], 83 | ['q-ak' => $this->option['secret_id']], 84 | ['q-sign-time' => $keyTime], 85 | ...$conditions 86 | ] 87 | ]); 88 | $signKey = hash_hmac('sha1', $keyTime, $this->option['secret_key']); 89 | $stringToSign = sha1($policy); 90 | $signature = hash_hmac('sha1', $stringToSign, $signKey); 91 | return [ 92 | 'filename' => $this->option['cos']['prefix'] . date('Ymd') . '/' . uuid()->toString(), 93 | 'type' => 'cos', 94 | 'option' => [ 95 | 'ak' => $this->option['secret_id'], 96 | 'policy' => base64_encode($policy), 97 | 'key_time' => $keyTime, 98 | 'sign_algorithm' => 'sha1', 99 | 'signature' => $signature 100 | ], 101 | ]; 102 | } 103 | } -------------------------------------------------------------------------------- /app/Service/LoggerService.php: -------------------------------------------------------------------------------- 1 | driver = $driverFactory->get('default'); 21 | } 22 | 23 | public function logs($params): bool 24 | { 25 | return $this->driver->push(new LogsJob($params)); 26 | } 27 | 28 | public function activities($params): bool 29 | { 30 | return $this->driver->push(new ActivitiesJob($params)); 31 | } 32 | } -------------------------------------------------------------------------------- /app/Service/OpenApiService.php: -------------------------------------------------------------------------------- 1 | option = $config->get('qcloud.api'); 18 | $this->client = $clientFactory->create([ 19 | 'base_uri' => $this->option['url'], 20 | 'version' => 2.0, 21 | 'timeout' => 2.0, 22 | ]); 23 | } 24 | 25 | public function ip(string $value): array 26 | { 27 | $response = $this->client->get('/release/ip', [ 28 | 'headers' => [ 29 | 'x-token' => $this->option['token'] 30 | ], 31 | 'query' => [ 32 | 'ip' => $value 33 | ], 34 | ]); 35 | return json_decode($response->getBody()->getContents(), true); 36 | } 37 | } -------------------------------------------------------------------------------- /app/Service/SchemaService.php: -------------------------------------------------------------------------------- 1 | get('databases.default'); 20 | $this->prefix = $cfg['prefix']; 21 | $this->conn = DriverManager::getConnection([ 22 | 'dbname' => $cfg['database'], 23 | 'user' => $cfg['username'], 24 | 'password' => $cfg['password'], 25 | 'host' => $cfg['host'], 26 | 'port' => $cfg['port'], 27 | 'charset' => $cfg['charset'], 28 | 'driver' => 'mysqli', 29 | ]); 30 | } 31 | 32 | /** 33 | * @return AbstractSchemaManager 34 | * @throws Exception 35 | */ 36 | public function manager(): AbstractSchemaManager 37 | { 38 | return $this->conn->createSchemaManager(); 39 | } 40 | 41 | /** 42 | * @param string $name 43 | * @return string 44 | */ 45 | public function table(string $name): string 46 | { 47 | return $this->prefix . $name; 48 | } 49 | } -------------------------------------------------------------------------------- /bin/hyperf.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(\Hyperf\Contract\ApplicationInterface::class); 23 | $application->run(); 24 | })(); 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kain/hyperf-skeleton", 3 | "description": "辅助 Hyperf 框架的工具集合使用案例,构建简洁统一的中后台接口方案", 4 | "type": "project", 5 | "require": { 6 | "php": ">=7.4.0", 7 | "ext-json": "*", 8 | "ext-redis": "*", 9 | "ext-swoole": ">=4.5", 10 | "hyperf/async-queue": "2.1.*", 11 | "hyperf/command": "2.1.*", 12 | "hyperf/config": "2.1.*", 13 | "hyperf/db-connection": "2.1.*", 14 | "hyperf/framework": "2.1.*", 15 | "hyperf/guzzle": "2.1.*", 16 | "hyperf/http-server": "2.1.*", 17 | "hyperf/logger": "2.1.*", 18 | "hyperf/paginator": "2.1.*", 19 | "hyperf/process": "2.1.*", 20 | "hyperf/redis": "2.1.*", 21 | "hyperf/validation": "2.1.*", 22 | "kain/hyperf-curd": "~2.1", 23 | "kain/hyperf-extra": "~2.1" 24 | }, 25 | "require-dev": { 26 | "filp/whoops": "^2.9", 27 | "hyperf/devtool": "2.1.*", 28 | "roave/security-advisories": "dev-latest", 29 | "swoole/ide-helper": "dev-master" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "App\\": "app/" 34 | } 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true, 38 | "extra": [], 39 | "scripts": { 40 | "post-root-package-install": [ 41 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 42 | ], 43 | "post-autoload-dump": [ 44 | "rm -rf runtime/container" 45 | ], 46 | "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", 47 | "cs-fix": "php-cs-fixer fix $1", 48 | "start": "php ./bin/hyperf.php start", 49 | "test": "co-phpunit -c phpunit.xml --colors=always" 50 | }, 51 | "config": { 52 | "optimize-autoloader": true, 53 | "sort-packages": true 54 | }, 55 | "repositories": { 56 | "packagist": { 57 | "type": "composer", 58 | "url": "https://mirrors.aliyun.com/composer/" 59 | } 60 | }, 61 | "license": "MIT" 62 | } 63 | -------------------------------------------------------------------------------- /config/autoload/annotations.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'paths' => [ 16 | BASE_PATH . '/app', 17 | ], 18 | 'ignore_annotations' => [ 19 | 'mixin', 20 | ], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /config/autoload/aspects.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class, 6 | 'redis' => [ 7 | 'pool' => 'default' 8 | ], 9 | 'channel' => 'queue', 10 | 'timeout' => 2, 11 | 'retry_seconds' => 5, 12 | 'handle_timeout' => 10, 13 | 'processes' => 1, 14 | 'concurrent' => [ 15 | 'limit' => 5, 16 | ], 17 | ], 18 | ]; -------------------------------------------------------------------------------- /config/autoload/commands.php: -------------------------------------------------------------------------------- 1 | (int)env('COOKIE_EXPIRE', 0), 5 | 'path' => env('COOKIE_PATH', '/'), 6 | 'domain' => env('COOKIE_DOMAIN', ''), 7 | 'secure' => (bool)env('COOKIE_SECURE', false), 8 | 'httponly' => (bool)env('COOKIE_HTTPONLY', false), 9 | 'raw' => true, 10 | 'samesite' => env('COOKIE_SAMESITE', null), 11 | ]; 12 | -------------------------------------------------------------------------------- /config/autoload/cors.php: -------------------------------------------------------------------------------- 1 | explode(',', env('CORS_METHODS', '*')), 5 | 'allowed_origins' => explode(',', env('CORS_ORIGINS', '*')), 6 | 'allowed_headers' => explode(',', env('CORS_HEADERS', 'CONTENT-TYPE,X-REQUESTED-WITH')), 7 | 'exposed_headers' => explode(',', env('CORS_EXPOSED_HEADERS', '')), 8 | 'max_age' => (int)env('CORS_MAX_AGE', 0), 9 | 'allowed_credentials' => env('CORS_CREDENTIALS', false), 10 | ]; -------------------------------------------------------------------------------- /config/autoload/curd.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'ignore' => [ 6 | '__construct', 7 | 'originListsConditionQuery', 8 | 'originListsCustomReturn', 9 | 'listsCustomReturn', 10 | 'listsConditionQuery', 11 | 'getCustomReturn', 12 | 'getConditionQuery', 13 | 'addBeforeHook', 14 | 'addAfterHook', 15 | 'editBeforeHook', 16 | 'editAfterHook', 17 | 'deleteBeforeHook', 18 | 'deletePrepHook', 19 | 'deleteAfterHook' 20 | ] 21 | ] 22 | ]; -------------------------------------------------------------------------------- /config/autoload/databases.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => env('DB_DRIVER', 'mysql'), 16 | 'host' => env('DB_HOST', 'localhost'), 17 | 'database' => env('DB_DATABASE', 'hyperf'), 18 | 'port' => env('DB_PORT', 3306), 19 | 'username' => env('DB_USERNAME', 'root'), 20 | 'password' => env('DB_PASSWORD', ''), 21 | 'charset' => env('DB_CHARSET', 'utf8'), 22 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 23 | 'prefix' => env('DB_PREFIX', ''), 24 | 'pool' => [ 25 | 'min_connections' => 10, 26 | 'max_connections' => 300, 27 | 'connect_timeout' => 10.0, 28 | 'wait_timeout' => 3.0, 29 | 'heartbeat' => -1, 30 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 31 | ], 32 | 'commands' => [ 33 | 'gen:model' => [ 34 | 'path' => 'app/Model', 35 | 'force_casts' => true, 36 | 'inheritance' => 'Model', 37 | ], 38 | ], 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /config/autoload/dependencies.php: -------------------------------------------------------------------------------- 1 | Hyperf\Curd\CurdService::class, 15 | Hyperf\Extra\Cipher\CipherInterface::class => Hyperf\Extra\Cipher\CipherFactory::class, 16 | Hyperf\Extra\Hash\HashInterface::class => Hyperf\Extra\Hash\HashFactory::class, 17 | Hyperf\Extra\Token\TokenInterface::class => Hyperf\Extra\Token\TokenFactory::class, 18 | Hyperf\Extra\Utils\UtilsInterface::class => Hyperf\Extra\Utils\UtilsFactory::class, 19 | Hyperf\Extra\Cors\CorsInterface::class => Hyperf\Extra\Cors\CorsFactory::class, 20 | ]; 21 | -------------------------------------------------------------------------------- /config/autoload/devtool.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'amqp' => [ 16 | 'consumer' => [ 17 | 'namespace' => 'App\\Amqp\\Consumer', 18 | ], 19 | 'producer' => [ 20 | 'namespace' => 'App\\Amqp\\Producer', 21 | ], 22 | ], 23 | 'aspect' => [ 24 | 'namespace' => 'App\\Aspect', 25 | ], 26 | 'command' => [ 27 | 'namespace' => 'App\\Command', 28 | ], 29 | 'controller' => [ 30 | 'namespace' => 'App\\Controller', 31 | ], 32 | 'job' => [ 33 | 'namespace' => 'App\\Job', 34 | ], 35 | 'listener' => [ 36 | 'namespace' => 'App\\Listener', 37 | ], 38 | 'middleware' => [ 39 | 'namespace' => 'App\\Middleware', 40 | ], 41 | 'Process' => [ 42 | 'namespace' => 'App\\Processes', 43 | ], 44 | ], 45 | ]; 46 | -------------------------------------------------------------------------------- /config/autoload/exceptions.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'http' => [ 16 | Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class, 17 | App\Exception\Handler\AppExceptionHandler::class, 18 | ], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /config/autoload/hashing.php: -------------------------------------------------------------------------------- 1 | 'argon2id', 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Bcrypt Options 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may specify the configuration options that should be used when 25 | | passwords are hashed using the Bcrypt algorithm. This will allow you 26 | | to control the amount of time it takes to hash the given password. 27 | | 28 | */ 29 | 30 | 'bcrypt' => [ 31 | 'rounds' => env('BCRYPT_ROUNDS', 10), 32 | ], 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Argon Options 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Here you may specify the configuration options that should be used when 40 | | passwords are hashed using the Argon algorithm. These will allow you 41 | | to control the amount of time it takes to hash the given password. 42 | | 43 | */ 44 | 45 | 'argon' => [ 46 | 'memory' => 1024, 47 | 'threads' => 2, 48 | 'time' => 2, 49 | ], 50 | 51 | ]; 52 | -------------------------------------------------------------------------------- /config/autoload/listeners.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'handler' => [ 16 | 'class' => Monolog\Handler\StreamHandler::class, 17 | 'constructor' => [ 18 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 19 | 'level' => Monolog\Logger::DEBUG, 20 | ], 21 | ], 22 | 'formatter' => [ 23 | 'class' => Monolog\Formatter\LineFormatter::class, 24 | 'constructor' => [ 25 | 'format' => null, 26 | 'dateFormat' => 'Y-m-d H:i:s', 27 | 'allowInlineLineBreaks' => true, 28 | ], 29 | ], 30 | 'processors' => [ 31 | ], 32 | ], 33 | ]; 34 | -------------------------------------------------------------------------------- /config/autoload/middlewares.php: -------------------------------------------------------------------------------- 1 | [ 15 | Hyperf\Validation\Middleware\ValidationMiddleware::class 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /config/autoload/processes.php: -------------------------------------------------------------------------------- 1 | env('QCLOUD_APP_ID'), 5 | 'secret_id' => env('QCLOUD_SECRET_ID'), 6 | 'secret_key' => env('QCLOUD_SECRET_KEY'), 7 | 'cos' => [ 8 | 'region' => env('COS_REGION'), 9 | 'bucket' => env('COS_BUCKET'), 10 | 'prefix' => env('COS_PREFIX', ''), 11 | 'upload_size' => (int)env('COS_UPLOAD_SIZE', 5242880) 12 | ], 13 | 'api' => [ 14 | 'url' => env('QCLOUD_API'), 15 | 'token' => env('QCLOUD_TOKEN'), 16 | ] 17 | ]; -------------------------------------------------------------------------------- /config/autoload/redis.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'host' => env('REDIS_HOST', 'localhost'), 16 | 'auth' => env('REDIS_AUTH', null), 17 | 'port' => (int) env('REDIS_PORT', 6379), 18 | 'db' => (int) env('REDIS_DB', 0), 19 | 'pool' => [ 20 | 'min_connections' => 10, 21 | 'max_connections' => 100, 22 | 'connect_timeout' => 10.0, 23 | 'wait_timeout' => 3.0, 24 | 'heartbeat' => -1, 25 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), 26 | ], 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /config/autoload/server.php: -------------------------------------------------------------------------------- 1 | SWOOLE_PROCESS, 19 | 'servers' => [ 20 | [ 21 | 'name' => 'http', 22 | 'type' => Server::SERVER_HTTP, 23 | 'host' => '0.0.0.0', 24 | 'port' => 9502, 25 | 'sock_type' => SWOOLE_SOCK_TCP, 26 | 'callbacks' => [ 27 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 28 | ], 29 | ], 30 | ], 31 | 'settings' => [ 32 | 'enable_coroutine' => true, 33 | 'worker_num' => swoole_cpu_num(), 34 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid', 35 | 'open_tcp_nodelay' => true, 36 | 'max_coroutine' => 100000, 37 | 'open_http2_protocol' => true, 38 | 'max_request' => 100000, 39 | 'socket_buffer_size' => 2 * 1024 * 1024, 40 | 'buffer_output_size' => 2 * 1024 * 1024, 41 | ], 42 | 'callbacks' => [ 43 | Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 44 | Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 45 | Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 46 | ], 47 | ]; 48 | -------------------------------------------------------------------------------- /config/autoload/token.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'issuer' => 'api.kainonly.com', 6 | 'audience' => 'console.kainonly.com', 7 | 'expires' => 900 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /config/autoload/translation.php: -------------------------------------------------------------------------------- 1 | 'zh_CN', 15 | 'fallback_locale' => 'en', 16 | 'path' => BASE_PATH . '/storage/languages', 17 | ]; 18 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'skeleton'), 19 | 'app_key' => env('APP_KEY', '123456'), 20 | 'app_env' => env('APP_ENV', 'dev'), 21 | 'scan_cacheable' => env('SCAN_CACHEABLE', false), 22 | StdoutLoggerInterface::class => [ 23 | 'log_level' => [ 24 | LogLevel::ALERT, 25 | LogLevel::CRITICAL, 26 | // LogLevel::DEBUG, 27 | LogLevel::EMERGENCY, 28 | LogLevel::ERROR, 29 | LogLevel::INFO, 30 | LogLevel::NOTICE, 31 | LogLevel::WARNING, 32 | ], 33 | ], 34 | ]; 35 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | [ 11 | App\Middleware\System\AuthVerify::class => [ 12 | 'resource', 'information', 'update', 'uploads', 'cosPresigned' 13 | ], 14 | App\Middleware\System\Spy::class => [ 15 | 'information', 'update' 16 | ] 17 | ] 18 | ]); 19 | $options = [ 20 | 'middleware' => [ 21 | App\Middleware\System\AuthVerify::class, 22 | App\Middleware\System\RbacVerify::class, 23 | App\Middleware\System\Spy::class 24 | ] 25 | ]; 26 | AutoController(App\Controller\System\AclController::class, $options); 27 | AutoController(App\Controller\System\ResourceController::class, $options); 28 | AutoController(App\Controller\System\PolicyController::class, $options); 29 | AutoController(App\Controller\System\PermissionController::class, $options); 30 | AutoController(App\Controller\System\RoleController::class, $options); 31 | AutoController(App\Controller\System\AdminController::class, $options); 32 | AutoController(App\Controller\System\LogsController::class, [ 33 | 'middleware' => [ 34 | App\Middleware\System\AuthVerify::class, 35 | App\Middleware\System\RbacVerify::class 36 | ] 37 | ]); 38 | AutoController(App\Controller\System\ActivitiesController::class, [ 39 | 'middleware' => [ 40 | App\Middleware\System\AuthVerify::class, 41 | App\Middleware\System\RbacVerify::class 42 | ] 43 | ]); 44 | AutoController(App\Controller\System\PictureController::class, $options); 45 | AutoController(App\Controller\System\PictureTypeController::class, $options); 46 | AutoController(App\Controller\System\VideoController::class, $options); 47 | AutoController(App\Controller\System\VideoTypeController::class, $options); 48 | AutoController(App\Controller\System\AudioController::class, $options); 49 | AutoController(App\Controller\System\AudioTypeController::class, $options); 50 | }, [ 51 | 'middleware' => [ 52 | Hyperf\Extra\Cors\CorsMiddleware::class 53 | ] 54 | ]); 55 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | update: 4 | image: composer 5 | command: 'composer update --prefer-dist -o --no-dev --ignore-platform-reqs' 6 | volumes: 7 | - ../.composer:/tmp 8 | - ./:/app -------------------------------------------------------------------------------- /storage/languages/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute must be accepted.', 26 | 'active_url' => 'The :attribute is not a valid URL.', 27 | 'after' => 'The :attribute must be a date after :date.', 28 | 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', 29 | 'alpha' => 'The :attribute may only contain letters.', 30 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 31 | 'alpha_num' => 'The :attribute may only contain letters and numbers.', 32 | 'array' => 'The :attribute must be an array.', 33 | 'before' => 'The :attribute must be a date before :date.', 34 | 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', 35 | 'between' => [ 36 | 'numeric' => 'The :attribute must be between :min and :max.', 37 | 'file' => 'The :attribute must be between :min and :max kilobytes.', 38 | 'string' => 'The :attribute must be between :min and :max characters.', 39 | 'array' => 'The :attribute must have between :min and :max items.', 40 | ], 41 | 'boolean' => 'The :attribute field must be true or false.', 42 | 'confirmed' => 'The :attribute confirmation does not match.', 43 | 'date' => 'The :attribute is not a valid date.', 44 | 'date_format' => 'The :attribute does not match the format :format.', 45 | 'different' => 'The :attribute and :other must be different.', 46 | 'digits' => 'The :attribute must be :digits digits.', 47 | 'digits_between' => 'The :attribute must be between :min and :max digits.', 48 | 'dimensions' => 'The :attribute has invalid image dimensions.', 49 | 'distinct' => 'The :attribute field has a duplicate value.', 50 | 'email' => 'The :attribute must be a valid email address.', 51 | 'exists' => 'The selected :attribute is invalid.', 52 | 'file' => 'The :attribute must be a file.', 53 | 'filled' => 'The :attribute field is required.', 54 | 'image' => 'The :attribute must be an image.', 55 | 'in' => 'The selected :attribute is invalid.', 56 | 'in_array' => 'The :attribute field does not exist in :other.', 57 | 'integer' => 'The :attribute must be an integer.', 58 | 'ip' => 'The :attribute must be a valid IP address.', 59 | 'json' => 'The :attribute must be a valid JSON string.', 60 | 'max' => [ 61 | 'numeric' => 'The :attribute may not be greater than :max.', 62 | 'file' => 'The :attribute may not be greater than :max kilobytes.', 63 | 'string' => 'The :attribute may not be greater than :max characters.', 64 | 'array' => 'The :attribute may not have more than :max items.', 65 | ], 66 | 'mimes' => 'The :attribute must be a file of type: :values.', 67 | 'mimetypes' => 'The :attribute must be a file of type: :values.', 68 | 'min' => [ 69 | 'numeric' => 'The :attribute must be at least :min.', 70 | 'file' => 'The :attribute must be at least :min kilobytes.', 71 | 'string' => 'The :attribute must be at least :min characters.', 72 | 'array' => 'The :attribute must have at least :min items.', 73 | ], 74 | 'not_in' => 'The selected :attribute is invalid.', 75 | 'numeric' => 'The :attribute must be a number.', 76 | 'present' => 'The :attribute field must be present.', 77 | 'regex' => 'The :attribute format is invalid.', 78 | 'required' => 'The :attribute field is required.', 79 | 'required_if' => 'The :attribute field is required when :other is :value.', 80 | 'required_unless' => 'The :attribute field is required unless :other is in :values.', 81 | 'required_with' => 'The :attribute field is required when :values is present.', 82 | 'required_with_all' => 'The :attribute field is required when :values is present.', 83 | 'required_without' => 'The :attribute field is required when :values is not present.', 84 | 'required_without_all' => 'The :attribute field is required when none of :values are present.', 85 | 'same' => 'The :attribute and :other must match.', 86 | 'size' => [ 87 | 'numeric' => 'The :attribute must be :size.', 88 | 'file' => 'The :attribute must be :size kilobytes.', 89 | 'string' => 'The :attribute must be :size characters.', 90 | 'array' => 'The :attribute must contain :size items.', 91 | ], 92 | 'string' => 'The :attribute must be a string.', 93 | 'timezone' => 'The :attribute must be a valid zone.', 94 | 'unique' => 'The :attribute has already been taken.', 95 | 'uploaded' => 'The :attribute failed to upload.', 96 | 'url' => 'The :attribute format is invalid.', 97 | 'max_if' => [ 98 | 'numeric' => 'The :attribute may not be greater than :max when :other is :value.', 99 | 'file' => 'The :attribute may not be greater than :max kilobytes when :other is :value.', 100 | 'string' => 'The :attribute may not be greater than :max characters when :other is :value.', 101 | 'array' => 'The :attribute may not have more than :max items when :other is :value.', 102 | ], 103 | 'min_if' => [ 104 | 'numeric' => 'The :attribute must be at least :min when :other is :value.', 105 | 'file' => 'The :attribute must be at least :min kilobytes when :other is :value.', 106 | 'string' => 'The :attribute must be at least :min characters when :other is :value.', 107 | 'array' => 'The :attribute must have at least :min items when :other is :value.', 108 | ], 109 | 'between_if' => [ 110 | 'numeric' => 'The :attribute must be between :min and :max when :other is :value.', 111 | 'file' => 'The :attribute must be between :min and :max kilobytes when :other is :value.', 112 | 'string' => 'The :attribute must be between :min and :max characters when :other is :value.', 113 | 'array' => 'The :attribute must have between :min and :max items when :other is :value.', 114 | ], 115 | /* 116 | |-------------------------------------------------------------------------- 117 | | Custom Validation Language Lines 118 | |-------------------------------------------------------------------------- 119 | | 120 | | Here you may specify custom validation messages for attributes using the 121 | | convention "attribute.rule" to name the lines. This makes it quick to 122 | | specify a specific custom language line for a given attribute rule. 123 | | 124 | */ 125 | 126 | 'custom' => [ 127 | 'attribute-name' => [ 128 | 'rule-name' => 'custom-message', 129 | ], 130 | ], 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Custom Validation Attributes 135 | |-------------------------------------------------------------------------- 136 | | 137 | | The following language lines are used to swap attribute place-holders 138 | | with something more reader friendly such as E-Mail Address instead 139 | | of "email". This simply helps us make messages a little cleaner. 140 | | 141 | */ 142 | 143 | 'attributes' => [], 144 | 'phone_number' => 'The :attribute must be a valid phone number', 145 | 'telephone_number' => 'The :attribute must be a valid telephone number', 146 | 147 | 'chinese_word' => 'The :attribute must contain valid characters(chinese/english character, number, underscore)', 148 | 'sequential_array' => 'The :attribute must be sequential array', 149 | ]; 150 | -------------------------------------------------------------------------------- /storage/languages/zh_CN/validation.php: -------------------------------------------------------------------------------- 1 | ':attribute 必须接受', 26 | 'active_url' => ':attribute 必须是一个合法的 URL', 27 | 'after' => ':attribute 必须是 :date 之后的一个日期', 28 | 'after_or_equal' => ':attribute 必须是 :date 之后或相同的一个日期', 29 | 'alpha' => ':attribute 只能包含字母', 30 | 'alpha_dash' => ':attribute 只能包含字母、数字、中划线或下划线', 31 | 'alpha_num' => ':attribute 只能包含字母和数字', 32 | 'array' => ':attribute 必须是一个数组', 33 | 'before' => ':attribute 必须是 :date 之前的一个日期', 34 | 'before_or_equal' => ':attribute 必须是 :date 之前或相同的一个日期', 35 | 'between' => [ 36 | 'numeric' => ':attribute 必须在 :min 到 :max 之间', 37 | 'file' => ':attribute 必须在 :min 到 :max kb 之间', 38 | 'string' => ':attribute 必须在 :min 到 :max 个字符之间', 39 | 'array' => ':attribute 必须在 :min 到 :max 项之间', 40 | ], 41 | 'boolean' => ':attribute 字符必须是 true 或 false, 1 或 0', 42 | 'confirmed' => ':attribute 二次确认不匹配', 43 | 'date' => ':attribute 必须是一个合法的日期', 44 | 'date_format' => ':attribute 与给定的格式 :format 不符合', 45 | 'different' => ':attribute 必须不同于 :other', 46 | 'digits' => ':attribute 必须是 :digits 位', 47 | 'digits_between' => ':attribute 必须在 :min 和 :max 位之间', 48 | 'dimensions' => ':attribute 具有无效的图片尺寸', 49 | 'distinct' => ':attribute 字段具有重复值', 50 | 'email' => ':attribute 必须是一个合法的电子邮件地址', 51 | 'exists' => '选定的 :attribute 是无效的', 52 | 'file' => ':attribute 必须是一个文件', 53 | 'filled' => ':attribute 的字段是必填的', 54 | 'image' => ':attribute 必须是 jpg, jpeg, png, bmp 或者 gif 格式的图片', 55 | 'in' => '选定的 :attribute 是无效的', 56 | 'in_array' => ':attribute 字段不存在于 :other', 57 | 'integer' => ':attribute 必须是个整数', 58 | 'ip' => ':attribute 必须是一个合法的 IP 地址', 59 | 'json' => ':attribute 必须是一个合法的 JSON 字符串', 60 | 'max' => [ 61 | 'numeric' => ':attribute 的最大值为 :max', 62 | 'file' => ':attribute 的最大为 :max kb', 63 | 'string' => ':attribute 的最大长度为 :max 字符', 64 | 'array' => ':attribute 至多有 :max 项', 65 | ], 66 | 'mimes' => ':attribute 的文件类型必须是 :values', 67 | 'min' => [ 68 | 'numeric' => ':attribute 的最小值为 :min', 69 | 'file' => ':attribute 大小至少为 :min kb', 70 | 'string' => ':attribute 的最小长度为 :min 字符', 71 | 'array' => ':attribute 至少有 :min 项', 72 | ], 73 | 'not_in' => '选定的 :attribute 是无效的', 74 | 'numeric' => ':attribute 必须是数字', 75 | 'present' => ':attribute 字段必须存在', 76 | 'regex' => ':attribute 格式是无效的', 77 | 'required' => ':attribute 字段是必须的', 78 | 'required_if' => ':attribute 字段是必须的当 :other 是 :value', 79 | 'required_unless' => ':attribute 字段是必须的,除非 :other 是在 :values 中', 80 | 'required_with' => ':attribute 字段是必须的当 :values 是存在的', 81 | 'required_with_all' => ':attribute 字段是必须的当 :values 是存在的', 82 | 'required_without' => ':attribute 字段是必须的当 :values 是不存在的', 83 | 'required_without_all' => ':attribute 字段是必须的当 没有一个 :values 是存在的', 84 | 'same' => ':attribute 和 :other 必须匹配', 85 | 'size' => [ 86 | 'numeric' => ':attribute 必须是 :size', 87 | 'file' => ':attribute 必须是 :size kb', 88 | 'string' => ':attribute 必须是 :size 个字符', 89 | 'array' => ':attribute 必须包括 :size 项', 90 | ], 91 | 'string' => ':attribute 必须是一个字符串', 92 | 'timezone' => ':attribute 必须是个有效的时区', 93 | 'unique' => ':attribute 已存在', 94 | 'uploaded' => ':attribute 上传失败', 95 | 'url' => ':attribute 无效的格式', 96 | 'max_if' => [ 97 | 'numeric' => '当 :other 为 :value 时 :attribute 不能大于 :max', 98 | 'file' => '当 :other 为 :value 时 :attribute 不能大于 :max kb', 99 | 'string' => '当 :other 为 :value 时 :attribute 不能大于 :max 个字符', 100 | 'array' => '当 :other 为 :value 时 :attribute 最多只有 :max 个单元', 101 | ], 102 | 'min_if' => [ 103 | 'numeric' => '当 :other 为 :value 时 :attribute 必须大于等于 :min', 104 | 'file' => '当 :other 为 :value 时 :attribute 大小不能小于 :min kb', 105 | 'string' => '当 :other 为 :value 时 :attribute 至少为 :min 个字符', 106 | 'array' => '当 :other 为 :value 时 :attribute 至少有 :min 个单元', 107 | ], 108 | 'between_if' => [ 109 | 'numeric' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 之间', 110 | 'file' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max kb 之间', 111 | 'string' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 个字符之间', 112 | 'array' => '当 :other 为 :value 时 :attribute 必须只有 :min - :max 个单元', 113 | ], 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Custom Validation Language Lines 117 | |-------------------------------------------------------------------------- 118 | | 119 | | Here you may specify custom validation messages for attributes using the 120 | | convention "attribute.rule" to name the lines. This makes it quick to 121 | | specify a specific custom language line for a given attribute rule. 122 | | 123 | */ 124 | 125 | 'custom' => [ 126 | 'attribute-name' => [ 127 | 'rule-name' => 'custom-message', 128 | ], 129 | ], 130 | 131 | /* 132 | |-------------------------------------------------------------------------- 133 | | Custom Validation Attributes 134 | |-------------------------------------------------------------------------- 135 | | 136 | | The following language lines are used to swap attribute place-holders 137 | | with something more reader friendly such as E-Mail Address instead 138 | | of "email". This simply helps us make messages a little cleaner. 139 | | 140 | */ 141 | 142 | 'attributes' => [], 143 | 'phone_number' => ':attribute 必须为一个有效的电话号码', 144 | 'telephone_number' => ':attribute 必须为一个有效的手机号码', 145 | 146 | 'chinese_word' => ':attribute 必须包含以下有效字符 (中文/英文,数字, 下划线)', 147 | 'sequential_array' => ':attribute 必须是一个有序数组', 148 | ]; 149 | --------------------------------------------------------------------------------