├── init-proxy.sh ├── app ├── Model │ ├── Permission.php │ ├── Role.php │ ├── Model.php │ └── User.php ├── Exception │ ├── AppBadRequestException.php │ ├── AppNotAllowedException.php │ ├── AppNotFoundException.php │ └── Handler │ │ ├── AppValidationExceptionHandler.php │ │ ├── UnauthorizedExceptionHandler.php │ │ ├── AppTokenValidExceptionHandler.php │ │ ├── AppExceptionHandler.php │ │ └── AppClientExceptionHandler.php ├── Request │ ├── BaseRequest.php │ ├── LoginRequest.php │ ├── PermissionRequest.php │ ├── RoleRequest.php │ └── UserRequest.php ├── Middleware │ ├── CorsMiddleware.php │ ├── PermissionMiddleware.php │ └── JwtAuthMiddleware.php ├── Controller │ ├── AbstractController.php │ ├── PermissionController.php │ ├── RoleController.php │ ├── UserController.php │ └── IndexController.php ├── Listener │ └── DbQueryExecutedListener.php ├── Helpers │ ├── Code.php │ └── Helper.php └── JsonRpc │ └── UserService.php ├── .gitignore ├── config ├── autoload │ ├── aspects.php │ ├── commands.php │ ├── dependencies.php │ ├── listeners.php │ ├── processes.php │ ├── translation.php │ ├── middlewares.php │ ├── annotations.php │ ├── cache.php │ ├── exceptions.php │ ├── aliyun_acm.php │ ├── redis.php │ ├── permission.php │ ├── devtool.php │ ├── jwt.php │ ├── databases.php │ ├── logger.php │ └── server.php ├── config.php ├── container.php └── routes.php ├── phpstan.neon ├── .env.example ├── test ├── Cases │ └── ExampleTest.php ├── bootstrap.php └── HttpTestCase.php ├── deploy.test.yml.bak ├── seeders ├── user_table_seeder.php └── permission_table_seeder.php ├── phpunit.xml ├── bin └── hyperf.php ├── README.md ├── Jenkinsfile ├── migrations ├── 2019_10_31_100153_create_wx_user_table.php ├── 2019_10_31_100142_create_user_table.php └── 2019_12_06_164358_create_permission_tables.php ├── .gitlab-ci.yml ├── Dockerfile ├── .php_cs ├── composer.json ├── watch └── storage └── languages ├── zh_CN └── validation.php └── en └── validation.php /init-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | php /opt/www/bin/hyperf.php di:init-proxy 4 | 5 | echo Started. -------------------------------------------------------------------------------- /app/Model/Permission.php: -------------------------------------------------------------------------------- 1 | code); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/Exception/AppNotAllowedException.php: -------------------------------------------------------------------------------- 1 | code); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/Exception/AppNotFoundException.php: -------------------------------------------------------------------------------- 1 | code); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /config/autoload/translation.php: -------------------------------------------------------------------------------- 1 | 'zh_CN', 15 | 'fallback_locale' => 'en', 16 | 'path' => BASE_PATH . '/storage/languages', 17 | ]; 18 | -------------------------------------------------------------------------------- /config/autoload/middlewares.php: -------------------------------------------------------------------------------- 1 | [ 14 | App\Middleware\CorsMiddleware::class, 15 | Hyperf\Validation\Middleware\ValidationMiddleware::class 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /config/autoload/annotations.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'paths' => [ 16 | BASE_PATH . '/app', 17 | ], 18 | 'ignore_annotations' => [ 19 | 'mixin', 20 | ], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /config/autoload/cache.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 16 | 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 17 | 'prefix' => 'c:', 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=user-center 2 | 3 | DB_DRIVER=mysql 4 | DB_HOST=mysql8 5 | DB_PORT=3306 6 | DB_DATABASE=user_center 7 | DB_USERNAME=root 8 | DB_PASSWORD=123456 9 | DB_CHARSET=utf8mb4 10 | DB_COLLATION=utf8mb4_unicode_ci 11 | DB_PREFIX= 12 | 13 | REDIS_HOST=redis 14 | REDIS_AUTH=(null) 15 | REDIS_PORT=6379 16 | REDIS_DB=0 17 | 18 | LOG_CHANNEL=elasticsearch 19 | ELASTIC_HOST=es01:9200 20 | 21 | ALIYUN_ACM_ENABLE=false 22 | ALIYUN_ACM_INTERVAL=5 23 | ALIYUN_ACM_NAMESPACE=ef3948fa-d0d5-4119-bc75-33a5b76126fe 24 | ALIYUN_ACM_DATA_ID=hyperf.env 25 | ALIYUN_ACM_GROUP=USER_CENTER 26 | ALIYUN_ACM_AK= 27 | ALIYUN_ACM_SK= -------------------------------------------------------------------------------- /test/Cases/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 26 | $this->assertTrue(is_array($this->get('/'))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Request/BaseRequest.php: -------------------------------------------------------------------------------- 1 | getAttribute(Dispatched::class); 21 | if (is_null($route)) { 22 | return $default; 23 | } 24 | return array_key_exists($key, $route->params) ? $route->params[$key] : $default; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /deploy.test.yml.bak: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | hyperf: 4 | image: $REGISTRY_URL/$PROJECT_NAME:test 5 | environment: 6 | - "APP_PROJECT=hyperf" 7 | - "APP_ENV=test" 8 | ports: 9 | - 9501:9501 10 | deploy: 11 | replicas: 1 12 | restart_policy: 13 | condition: on-failure 14 | delay: 5s 15 | max_attempts: 5 16 | update_config: 17 | parallelism: 2 18 | delay: 5s 19 | order: start-first 20 | networks: 21 | - hyperf_net 22 | configs: 23 | - source: hyperf_v1.0 24 | target: /opt/www/.env 25 | configs: 26 | hyperf_v1.0: 27 | external: true 28 | networks: 29 | hyperf_net: 30 | external: true 31 | -------------------------------------------------------------------------------- /seeders/user_table_seeder.php: -------------------------------------------------------------------------------- 1 | hash = new BcryptHasher(); 24 | Model\User::create([ 25 | 'username' => 'admin', 26 | 'password' => $this->hash->make('123456'), 27 | 'nick_name' => '超级管理员', 28 | 'real_name' => '超级管理员' 29 | ]); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /config/autoload/exceptions.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'http' => [ 15 | App\Exception\Handler\AppClientExceptionHandler::class, 16 | App\Exception\Handler\AppValidationExceptionHandler::class, 17 | App\Exception\Handler\AppTokenValidExceptionHandler::class, 18 | App\Exception\Handler\UnauthorizedExceptionHandler::class, 19 | App\Exception\Handler\AppExceptionHandler::class, 20 | ], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./test 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /bin/hyperf.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(\Hyperf\Contract\ApplicationInterface::class); 21 | $application->run(); 22 | })(); 23 | -------------------------------------------------------------------------------- /config/autoload/aliyun_acm.php: -------------------------------------------------------------------------------- 1 | env('ALIYUN_ACM_ENABLE', false), 6 | // 配置更新间隔(秒) 7 | 'interval' => env('ALIYUN_ACM_INTERVAL', 5), 8 | // 阿里云 ACM 断点地址,取决于您的可用区 9 | 'endpoint' => env('ALIYUN_ACM_ENDPOINT', 'acm.aliyun.com'), 10 | // 当前应用需要接入的 Namespace 11 | 'namespace' => env('ALIYUN_ACM_NAMESPACE', 'ef3948fa-d0d5-4119-bc75-33a5b76126fe'), 12 | // 您的配置对应的 Data ID 13 | 'data_id' => env('ALIYUN_ACM_DATA_ID', 'hyperf.env'), 14 | // 您的配置对应的 Group 15 | 'group' => env('ALIYUN_ACM_GROUP', 'USER_CENTER'), 16 | // 您的阿里云账号的 Access Key 17 | 'access_key' => env('ALIYUN_ACM_AK', ''), 18 | // 您的阿里云账号的 Secret Key 19 | 'secret_key' => env('ALIYUN_ACM_SK', ''), 20 | ]; 21 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'skeleton'), 18 | StdoutLoggerInterface::class => [ 19 | 'log_level' => [ 20 | LogLevel::ALERT, 21 | LogLevel::CRITICAL, 22 | LogLevel::DEBUG, 23 | LogLevel::EMERGENCY, 24 | LogLevel::ERROR, 25 | LogLevel::INFO, 26 | LogLevel::NOTICE, 27 | LogLevel::WARNING, 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /app/Model/Role.php: -------------------------------------------------------------------------------- 1 | with($params['with']); 14 | } else { 15 | $query = $this->query(); 16 | } 17 | if (!isset($params['sort_name']) || empty($params['sort_name'])) { 18 | $params['sort_name'] = $this->primaryKey; 19 | } 20 | $params['sort_value'] = isset($params['sort_value']) ? ($params['sort_value'] == 'descend' ? 'desc' : 'asc') : 'desc'; 21 | $list = $query->orderBy($params['sort_name'], $params['sort_value'])->paginate($pageSize); 22 | return $list; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/Request/LoginRequest.php: -------------------------------------------------------------------------------- 1 | ['bail', 'required'], 17 | 'password' => ['bail', 'required'] 18 | ]; 19 | } 20 | 21 | public function attributes(): array 22 | { 23 | return [ 24 | 'username' => '用户名', 25 | 'password' => '密码' 26 | ]; 27 | } 28 | 29 | public function messages(): array 30 | { 31 | return [ 32 | 'username.required' => '请填写用户名', 33 | 'password.required' => '请填写密码', 34 | ]; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/Request/PermissionRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'bail', 18 | 'required', 19 | Rule::unique('permissions')->ignore($this->routeParam('id', 0)), 20 | ], 21 | ]; 22 | } 23 | 24 | public function attributes(): array 25 | { 26 | return [ 27 | 'name' => '权限名称' 28 | ]; 29 | } 30 | 31 | public function messages(): array 32 | { 33 | return [ 34 | 'name.unique' => '权限名称已存在', 35 | ]; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 29 | -------------------------------------------------------------------------------- /app/Request/RoleRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'bail', 18 | 'required', 19 | Rule::unique('roles')->ignore($this->routeParam('id', 0)), 20 | ], 21 | 'permissions' => 'Required|Array', 22 | ]; 23 | } 24 | 25 | public function attributes(): array 26 | { 27 | return [ 28 | 'name' => '角色名称' 29 | ]; 30 | } 31 | 32 | public function messages(): array 33 | { 34 | return [ 35 | 'name.unique' => '角色名称已存在', 36 | ]; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /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' => 1, 21 | 'max_connections' => 10, 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 | -------------------------------------------------------------------------------- /app/Model/Model.php: -------------------------------------------------------------------------------- 1 | '禁用', 21 | 1 => '正常' 22 | ]; 23 | 24 | const STATUS_DELETED = -1; //status 为-1表示删除 25 | const STATUS_DISABLE = 0; //status 为0表示未启用 26 | const STATUS_ENABLE = 1; //status 为1表示正常 27 | 28 | function getFormatState($key = 0, $enum = array(), $default = '') 29 | { 30 | return array_key_exists($key, $enum) ? $enum[$key] : $default; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/autoload/permission.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'user' => App\Model\User::class, 7 | 'permission' => App\Model\Permission::class, 8 | 'role' => App\Model\Role::class, 9 | ], 10 | //表名设置 11 | 'table_names' => [ 12 | 'roles' => 'roles', 13 | 'permissions' => 'permissions', 14 | 'model_has_permissions' => 'model_has_permissions', 15 | 'model_has_roles' => 'model_has_roles', 16 | 'role_has_permissions' => 'role_has_permissions', 17 | ], 18 | 'column_names' => [ 19 | 'model_morph_key' => 'model_id', //关联模板的主键 20 | ], 21 | 'display_permission_in_exception' => false, 22 | 'cache' => [ 23 | 'expiration_time' => 86400, //\DateInterval::createFromDateString('24 hours') 86400 已向hyperf提交该PR 24 | 'key' => 'donjan.permission.cache', 25 | 'model_key' => 'name', 26 | 'store' => 'default', 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Client::class); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | return $this->client->{$name}(...$arguments); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 用户中心 2 | 3 | 基于Hyperf框架的用户中心,《PHP微服务练兵》系列教程源码 4 | 5 | ## 安装 6 | 7 | > 注意,使用的mysql未启用sql_model的ONLY_FULL_GROUP_BY,STRICT_ALL_TABLES 8 | 9 | ### 已有环境 10 | ``` 11 | git clone https://github.com/donjan-deng/la-user-center.git 12 | cd la-user-center 13 | composer install 14 | 15 | 复制.env.example为.env,并编辑好配置 16 | 17 | # 运行数据库迁移 18 | php bin/hyperf.php migrate 19 | 20 | # 运行数据填充 21 | php bin/hyperf.php db:seed --path=seeders/user_table_seeder.php 22 | php bin/hyperf.php db:seed --path=seeders/permission_table_seeder.php 23 | # 启动 24 | php bin/hyperf.php start 25 | 默认管理员账号admin,密码123456 26 | ``` 27 | ### Docker安装 28 | 29 | ``` 30 | docker run -d --name user-center \ 31 | --restart=always \ 32 | -p 9501:9501 -p 9504:9504 \ 33 | -it --entrypoint /bin/sh \ 34 | donjan/la-user-center 35 | 36 | docker exec -it user-center bash 37 | 38 | cd /opt/www 39 | 40 | 复制.env.example为.env,并编辑好配置 41 | 42 | # 运行数据库迁移 43 | php bin/hyperf.php migrate 44 | 45 | # 运行数据填充 46 | php bin/hyperf.php db:seed --path=seeders/user_table_seeder.php 47 | php bin/hyperf.php db:seed --path=seeders/permission_table_seeder.php 48 | 49 | ``` -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent none 3 | stages { 4 | stage('Build') { //这是一个标准的流水线,由于PHP不需要编译,其实不需要构建阶段 5 | steps { 6 | echo "${env.BRANCH_NAME}" 7 | echo "${env.GIT_COMMIT}" 8 | } 9 | } 10 | stage('Test') { //执行测试 11 | agent { dockerfile true } 12 | steps { 13 | // sh '/opt/www/vendor/bin/co-phpunit -c /opt/www/phpunit.xml --colors=always' 14 | echo 'test' 15 | } 16 | } 17 | stage('Deploy') { //发布到仓库 18 | agent { 19 | docker { 20 | image 'docker:latest' 21 | } 22 | } 23 | steps { 24 | script{ 25 | docker.withRegistry('https://registry.cn-chengdu.aliyuncs.com','aliyun'){ //这个aliyun是我们全局凭据的ID 26 | def customImage = docker.build("donjan/user-center:${env.BRANCH_NAME}-${env.GIT_COMMIT}") 27 | customImage.push() //推送镜像 28 | customImage.push('latest') //推送一个latest的镜像 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/Exception/Handler/AppValidationExceptionHandler.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 24 | /** @var \Hyperf\Validation\ValidationException $throwable */ 25 | $message = $throwable->validator->errors()->first(); 26 | $errors = $throwable->validator->errors(); 27 | $result = $this->helper->error(Code::VALIDATE_ERROR, $message, $errors); 28 | return $response->withStatus($throwable->status) 29 | ->withAddedHeader('content-type', 'application/json') 30 | ->withBody(new SwooleStream($this->helper->jsonEncode($result))); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/Exception/Handler/UnauthorizedExceptionHandler.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 25 | $result = $this->helper->error($throwable->getCode(), $throwable->getMessage()); 26 | return $response->withStatus($throwable->getCode()) 27 | ->withAddedHeader('content-type', 'application/json') 28 | ->withBody(new SwooleStream($this->helper->jsonEncode($result))); 29 | } 30 | public function isValid(Throwable $throwable): bool 31 | { 32 | return $throwable instanceof UnauthorizedException; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Middleware/CorsMiddleware.php: -------------------------------------------------------------------------------- 1 | withHeader('Access-Control-Allow-Origin', '*') 20 | ->withHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS,PATCH') 21 | ->withHeader('Access-Control-Allow-Credentials', 'true') 22 | ->withHeader('Access-Control-Allow-Headers', 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization'); 23 | Context::set(ResponseInterface::class, $response); 24 | if ($request->getMethod() == 'OPTIONS') { 25 | return $response; 26 | } 27 | return $handler->handle($request); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/Exception/Handler/AppTokenValidExceptionHandler.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 26 | $result = $this->helper->error(Code::UNAUTHENTICATED, $throwable->getMessage()); 27 | return $response->withStatus($throwable->getCode()) 28 | ->withAddedHeader('content-type', 'application/json') 29 | ->withBody(new SwooleStream($this->helper->jsonEncode($result))); 30 | } 31 | 32 | public function isValid(Throwable $throwable): bool { 33 | return $throwable instanceof TokenValidException; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /migrations/2019_10_31_100153_create_wx_user_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('wx_user_id'); 15 | $table->bigInteger('user_id')->comment('user表user_id'); 16 | $table->string('nick_name', 50); 17 | $table->string('avatar', 100)->comment('头像'); 18 | $table->string('open_id', 50); 19 | $table->string('union_id', 50); 20 | $table->string('access_token', 100); 21 | $table->timestamp('access_token_expire_time')->nullable(); 22 | $table->string('refresh_token', 100); 23 | $table->timestamp('refresh_token_expire_time')->nullable(); 24 | $table->unique('open_id'); 25 | $table->unique('union_id'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void { 33 | Schema::dropIfExists('wx_user'); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /config/autoload/jwt.php: -------------------------------------------------------------------------------- 1 | env('JWT_LOGIN_TYPE', 'mpop'), 8 | 9 | # 单点登录自定义数据中必须存在uid的键值,这个key你可以自行定义,只要自定义数据中存在该键即可 10 | 'sso_key' => 'user_id', 11 | 12 | # 非对称加密使用字符串,请使用自己加密的字符串 13 | 'secret' => env('JWT_SECRET', 'phper666'), 14 | 15 | /* 16 | * JWT 权限keys 17 | * 对称算法: HS256, HS384 & HS512 使用 `JWT_SECRET`. 18 | * 非对称算法: RS256, RS384 & RS512 / ES256, ES384 & ES512 使用下面的公钥私钥. 19 | */ 20 | 'keys' => [ 21 | # 公钥,例如:'file://path/to/public/key' 22 | 'public' => env('JWT_PUBLIC_KEY'), 23 | 24 | # 私钥,例如:'file://path/to/private/key' 25 | 'private' => env('JWT_PRIVATE_KEY'), 26 | ], 27 | 28 | # token过期时间,单位为秒 29 | 'ttl' => env('JWT_TTL', 7200), 30 | 31 | # jwt的hearder加密算法 32 | 'alg' => env('JWT_ALG', 'HS256'), 33 | 34 | # 是否开启黑名单,单点登录和多点登录的注销、刷新使原token失效,必须要开启黑名单,目前黑名单缓存只支持hyperf缓存驱动 35 | 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), 36 | 37 | # 黑名单的宽限时间 单位为:秒,注意:如果使用单点登录,该宽限时间无效 38 | 'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0), 39 | 40 | # 黑名单缓存token时间,注意:该时间一定要设置比token过期时间要大,默认为1天 41 | 'blacklist_cache_ttl' => env('JWT_BLACKLIST_CACHE_TTL', 86400), 42 | ]; 43 | -------------------------------------------------------------------------------- /app/Controller/AbstractController.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' => 1, 26 | 'max_connections' => 10, 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 | 'db:model' => [ 34 | 'path' => 'app/Model', 35 | 'force_casts' => true, 36 | 'inheritance' => 'Model', 37 | ], 38 | ], 39 | ], 40 | ]; 41 | -------------------------------------------------------------------------------- /app/Exception/Handler/AppExceptionHandler.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 31 | } 32 | 33 | public function handle(Throwable $throwable, ResponseInterface $response) 34 | { 35 | $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); 36 | $this->logger->error($throwable->getTraceAsString()); 37 | return $response->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); 38 | } 39 | 40 | public function isValid(Throwable $throwable): bool 41 | { 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Exception/Handler/AppClientExceptionHandler.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 28 | $result = $this->helper->error($throwable->getCode(), $throwable->getMessage(), $throwable->getMessage()); 29 | return $response->withStatus($throwable->getCode()) 30 | ->withAddedHeader('content-type', 'application/json') 31 | ->withBody(new SwooleStream($this->helper->jsonEncode($result))); 32 | } 33 | 34 | public function isValid(Throwable $throwable): bool 35 | { 36 | return ($throwable instanceof Exception\AppBadRequestException) || 37 | ($throwable instanceof Exception\AppNotFoundException) || 38 | ($throwable instanceof Exception\AppNotAllowedException); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/Request/UserRequest.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'bail', 18 | 'required', 19 | 'alpha_dash', 20 | Rule::unique('user')->ignore($this->routeParam('id', 0), 'user_id'), 21 | ], 22 | 'phone' => [ 23 | 'bail', 24 | 'required', 25 | Rule::unique('user')->ignore($this->routeParam('id', 0), 'user_id'), 26 | ], 27 | 'real_name' => 'required', 28 | 'password' => 'sometimes|same:confirm_password', 29 | ]; 30 | } 31 | 32 | public function attributes(): array 33 | { 34 | return [ 35 | 'username' => '用户名', 36 | 'real_name' => '姓名' 37 | ]; 38 | } 39 | 40 | public function messages(): array 41 | { 42 | return [ 43 | 'username.required' => '用户名必填', 44 | 'username.unique' => '用户名已存在', 45 | 'username.alpha_dash' => '用户名只能包含字母和数字,破折号和下划线', 46 | 'real_name.required' => '姓名必填', 47 | 'password.same' => '两次输入的密码不一致', 48 | ]; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /migrations/2019_10_31_100142_create_user_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('user_id'); 15 | $table->string('username', 50); 16 | $table->string('password', 100); 17 | $table->string('nick_name', 50); 18 | $table->string('real_name', 10); 19 | $table->tinyInteger('sex'); 20 | $table->string('phone', 15); 21 | $table->string('avatar', 100)->comment('头像'); 22 | $table->timestamp('last_login_at')->nullable(); 23 | $table->timestamp('created_at')->nullable(); 24 | $table->timestamp('updated_at')->nullable(); 25 | $table->string('remember_token', 100); 26 | $table->tinyInteger('status')->default(1)->comment('0为禁用,1为正常'); 27 | // 28 | $table->unique('username'); 29 | $table->unique('phone'); 30 | $table->index('status'); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | */ 37 | public function down(): void { 38 | Schema::dropIfExists('user'); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # usermod -aG docker gitlab-runner 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | variables: 8 | PROJECT_NAME: hyperf 9 | REGISTRY_URL: registry-docker.org 10 | 11 | build_test_docker: 12 | stage: build 13 | before_script: 14 | # - git submodule sync --recursive 15 | # - git submodule update --init --recursive 16 | script: 17 | - docker build . -t $PROJECT_NAME 18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test 19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test 20 | only: 21 | - test 22 | tags: 23 | - builder 24 | 25 | deploy_test_docker: 26 | stage: deploy 27 | script: 28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME 29 | only: 30 | - test 31 | tags: 32 | - test 33 | 34 | build_docker: 35 | stage: build 36 | before_script: 37 | # - git submodule sync --recursive 38 | # - git submodule update --init --recursive 39 | script: 40 | - docker build . -t $PROJECT_NAME 41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest 43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest 45 | only: 46 | - tags 47 | tags: 48 | - builder 49 | 50 | deploy_docker: 51 | stage: deploy 52 | script: 53 | - echo SUCCESS 54 | only: 55 | - tags 56 | tags: 57 | - builder 58 | -------------------------------------------------------------------------------- /app/Middleware/PermissionMiddleware.php: -------------------------------------------------------------------------------- 1 | getAttribute('Hyperf\HttpServer\Router\Dispatched'); 30 | $route = $dispatcher->handler->route; 31 | $path = '/' . $this->config->get('app_name') . $route . '/' . $request->getMethod(); 32 | $path = strtolower($path); 33 | $permission = Permission::getPermissions(['name' => $path])->first(); 34 | $user = $request->getAttribute('user'); 35 | if ($user && (!$permission || ($permission && $user->checkPermissionTo($permission)))) { 36 | return $handler->handle($request); 37 | } 38 | throw new UnauthorizedException('无权进行该操作', 403); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/Controller/PermissionController.php: -------------------------------------------------------------------------------- 1 | all(); 23 | $result = Model\Permission::create($data); 24 | return $result; 25 | } 26 | 27 | public function show($id) 28 | { 29 | $result = Model\Permission::find($id); 30 | if (!$result) { 31 | throw new Exception\AppNotFoundException("请求资源不存在"); 32 | } 33 | $result->roles; 34 | return $result; 35 | } 36 | 37 | public function update(Request\PermissionRequest $request, $id) 38 | { 39 | $data = $request->all(); 40 | $result = Model\Permission::find($id); 41 | if (!$result) { 42 | throw new Exception\AppNotFoundException("请求资源不存在"); 43 | } 44 | $result->update($data); 45 | return $result; 46 | } 47 | 48 | public function destroy($id) 49 | { 50 | $result = Model\Permission::find($id); 51 | if (!$result) { 52 | throw new Exception\AppNotFoundException("请求资源不存在"); 53 | } 54 | return $result->delete(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/Middleware/JwtAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | jwt->getTokenObj(); 37 | if ($this->jwt->checkToken()) { 38 | $userId = $token->getClaim('user_id'); 39 | $user = User::where('user_id', $userId)->where('status', User::STATUS_ENABLE)->first(); 40 | if (!$user) { 41 | throw new TokenValidException('Token未验证通过', 401); 42 | } 43 | $request = $request->withAttribute('user', $user); 44 | Context::set(ServerRequestInterface::class, $request); 45 | } 46 | } catch (\Exception $e) { 47 | throw new TokenValidException('Token未验证通过', 401); 48 | } 49 | return $handler->handle($request); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /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/Helpers/Code.php: -------------------------------------------------------------------------------- 1 | request->all(); 17 | $pageSize = $this->request->input('per_page', 15); 18 | $params['with'] = ['permissions']; 19 | $list = Model\Role::getList($params, (int) $pageSize); 20 | return $list; 21 | } 22 | 23 | public function store(Request\RoleRequest $request) 24 | { 25 | $data = $request->all(); 26 | $permissions = $request->input('permissions', []); 27 | unset($data['permissions']); 28 | $result = Model\Role::create($data); 29 | $result->permissions()->sync($permissions); 30 | return $result; 31 | } 32 | 33 | public function show($id) 34 | { 35 | $result = Model\Role::find($id); 36 | if (!$result) { 37 | throw new Exception\AppNotFoundException("请求资源不存在"); 38 | } 39 | $result->permissions; 40 | return $result; 41 | } 42 | 43 | public function update(Request\RoleRequest $request, $id) 44 | { 45 | $data = $request->all(); 46 | $permissions = $request->input('permissions', []); 47 | $result = Model\Role::find($id); 48 | if (!$result) { 49 | throw new Exception\AppNotFoundException("请求资源不存在"); 50 | } 51 | unset($data['permissions']); 52 | $result->update($data); 53 | $result->syncPermissions($permissions); 54 | return $result; 55 | } 56 | 57 | public function destroy($id) 58 | { 59 | $result = Model\Role::find($id); 60 | if (!$result) { 61 | throw new Exception\AppNotFoundException("请求资源不存在"); 62 | } 63 | return $result->delete(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Default Dockerfile 2 | # 3 | # @link https://www.hyperf.io 4 | # @document https://doc.hyperf.io 5 | # @contact group@hyperf.io 6 | # @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE 7 | 8 | FROM hyperf/hyperf:7.3-alpine-cli 9 | LABEL maintainer="Hyperf Developers " version="1.0" license="MIT" 10 | 11 | ## 12 | # ---------- env settings ---------- 13 | ## 14 | # --build-arg timezone=Asia/Shanghai 15 | ARG timezone 16 | 17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ 18 | COMPOSER_VERSION=1.8.6 \ 19 | APP_ENV=prod 20 | 21 | # update 22 | RUN set -ex \ 23 | && apk update \ 24 | # install composer 25 | && cd /tmp \ 26 | && wget https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ 27 | && chmod u+x composer.phar \ 28 | && mv composer.phar /usr/local/bin/composer \ 29 | && composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ \ 30 | # show php version and extensions 31 | && php -v \ 32 | && php -m \ 33 | # ---------- some config ---------- 34 | && cd /etc/php7 \ 35 | # - config PHP 36 | && { \ 37 | echo "upload_max_filesize=100M"; \ 38 | echo "post_max_size=108M"; \ 39 | echo "memory_limit=1024M"; \ 40 | echo "date.timezone=${TIMEZONE}"; \ 41 | } | tee conf.d/99-overrides.ini \ 42 | # - config timezone 43 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 44 | && echo "${TIMEZONE}" > /etc/timezone \ 45 | # ---------- clear works ---------- 46 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 47 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 48 | 49 | COPY . /opt/www 50 | 51 | WORKDIR /opt/www 52 | 53 | RUN chmod u+x ./init-proxy.sh 54 | 55 | RUN composer install \ 56 | && composer dump-autoload -o \ 57 | && ./init-proxy.sh 58 | 59 | EXPOSE 9501 60 | EXPOSE 9504 61 | 62 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] 63 | -------------------------------------------------------------------------------- /app/Helpers/Helper.php: -------------------------------------------------------------------------------- 1 | result(Code::SUCCESS, Code::getMessage(Code::SUCCESS), $data); 16 | } 17 | 18 | //返回错误 19 | public function error($code = 422, $message = '', $data = []) 20 | { 21 | if (empty($message)) { 22 | return $this->result($code, Code::getMessage($code), $data); 23 | } else { 24 | return $this->result($code, $message, $data); 25 | } 26 | } 27 | 28 | public function result($code, $message, $data) 29 | { 30 | return ['code' => $code, 'message' => $message, 'data' => $data]; 31 | } 32 | 33 | public function jsonEncode($data) 34 | { 35 | return json_encode($data, JSON_UNESCAPED_UNICODE); 36 | } 37 | 38 | /** 39 | * 生成随机数 40 | * @param number $length 41 | * @return number 42 | */ 43 | public function generateNumber($length = 6) 44 | { 45 | return rand(pow(10, ($length - 1)), pow(10, $length) - 1); 46 | } 47 | 48 | /** 49 | * 生成随机字符串 50 | * @param number $length 51 | * @param string $chars 52 | * @return string 53 | */ 54 | public function generateString($length = 6, $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') 55 | { 56 | $chars = str_split($chars); 57 | 58 | $chars = array_map(function($i) use($chars) { 59 | return $chars[$i]; 60 | }, array_rand($chars, $length)); 61 | 62 | return implode($chars); 63 | } 64 | 65 | /** 66 | * xml to array 转换 67 | * @param type $xml 68 | * @return type 69 | */ 70 | public function xml2array($xml) 71 | { 72 | return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /config/autoload/logger.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' => null, 27 | 'allowInlineLineBreaks' => true, 28 | ], 29 | ], 30 | ], 31 | // 'elasticsearch' => [ 32 | // 'handler' => [ 33 | // 'class' => Monolog\Handler\ElasticsearchHandler::class, 34 | // 'constructor' => [ 35 | // 'client' => Hyperf\Utils\ApplicationContext::getContainer()->get(Hyperf\Elasticsearch\ClientBuilderFactory::class)->create() 36 | // ->setHosts(explode(',', env('ELASTIC_HOST'))) 37 | // ->build(), 38 | // 'options' => [ 39 | // 'index' => 'user-center-log', // Elastic index name 40 | // 'type' => '_doc', // Elastic document type 41 | // 'ignore_error' => false, // Suppress Elasticsearch exceptions 42 | // ], 43 | // ], 44 | // ], 45 | // 'formatter' => [ 46 | // 'class' => Monolog\Formatter\ElasticsearchFormatter::class, 47 | // 'constructor' => [ 48 | // 'index' => 'user-center-log', 49 | // 'type' => '_doc', 50 | // ], 51 | // ], 52 | // ], 53 | ]; 54 | -------------------------------------------------------------------------------- /config/autoload/server.php: -------------------------------------------------------------------------------- 1 | SWOOLE_PROCESS, 18 | 'servers' => [ 19 | [ 20 | 'name' => 'http', 21 | 'type' => Server::SERVER_HTTP, 22 | 'host' => '0.0.0.0', 23 | 'port' => 9501, 24 | 'sock_type' => SWOOLE_SOCK_TCP, 25 | 'callbacks' => [ 26 | SwooleEvent::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 27 | ], 28 | ], 29 | [ 30 | 'name' => 'jsonrpc-http', 31 | 'type' => Server::SERVER_HTTP, 32 | 'host' => '0.0.0.0', 33 | 'port' => 9504, 34 | 'sock_type' => SWOOLE_SOCK_TCP, 35 | 'callbacks' => [ 36 | SwooleEvent::ON_REQUEST => [Hyperf\JsonRpc\HttpServer::class, 'onRequest'], 37 | ], 38 | ], 39 | ], 40 | 'settings' => [ 41 | 'enable_coroutine' => true, 42 | 'worker_num' => swoole_cpu_num(), 43 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid', 44 | 'open_tcp_nodelay' => true, 45 | 'max_coroutine' => 100000, 46 | 'open_http2_protocol' => true, 47 | 'max_request' => 100000, 48 | 'socket_buffer_size' => 2 * 1024 * 1024, 49 | ], 50 | 'callbacks' => [ 51 | SwooleEvent::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], 52 | SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 53 | SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 54 | ], 55 | ]; 56 | -------------------------------------------------------------------------------- /app/Model/User.php: -------------------------------------------------------------------------------- 1 | 1, 25 | ]; 26 | public static $sex = [ 27 | 0 => '未知', 28 | 1 => '男', 29 | 2 => '女', 30 | ]; 31 | 32 | public function getSexTextAttribute() 33 | { 34 | return $this->attributes['sex_text'] = $this->getFormatState($this->attributes['sex'], self::$sex); 35 | } 36 | 37 | public function getStatusTextAttribute() 38 | { 39 | return $this->attributes['status_text'] = $this->getFormatState($this->attributes['status'], self::$status); 40 | } 41 | 42 | protected function getList(array $params, int $pageSize) 43 | { 44 | $query = $this->with('roles')->where('user_id', '<>', config('app.super_admin')); 45 | (isset($params['status']) && $params['status'] !== "") && $query->where('status', '=', $params['status']); 46 | (isset($params['username']) && !empty($params['username'])) && $query->where('username', 'like', "%{$params['username']}%"); 47 | (isset($params['phone']) && !empty($params['phone'])) && $query->where('phone', 'like', "%{$params['phone']}%"); 48 | if (isset($params['start_time']) && isset($params['end_time']) && !empty($params['start_time']) && !empty($params['end_time'])) { 49 | $query->where('created_at', '>=', $params['start_time'])->where('created_at', '<=', $params['end_time']); 50 | } 51 | if (!isset($params['sort_name']) || empty($params['sort_name'])) { 52 | $params['sort_name'] = $this->primaryKey; 53 | } 54 | $params['sort_value'] = isset($params['sort_value']) ? ($params['sort_value'] == 'descend' ? 'desc' : 'asc') : 'desc'; 55 | $list = $query->orderBy($params['sort_name'], $params['sort_value'])->paginate($pageSize); 56 | foreach ($list as &$value) { 57 | $value->sex_text; 58 | $value->status_text; 59 | } 60 | return $list; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | [App\Middleware\JwtAuthMiddleware::class]]); 24 | Router::addRoute(['GET', 'POST'], '/logout', 'App\Controller\IndexController@logout'); 25 | //User 26 | Router::get('/users', 'App\Controller\UserController@index', ['middleware' => $middleware]); 27 | Router::post('/users', 'App\Controller\UserController@store', ['middleware' => $middleware]); 28 | Router::put('/users/{id:\d+}', 'App\Controller\UserController@update', ['middleware' => $middleware]); 29 | Router::get('/users/{id:\d+}', 'App\Controller\UserController@show', ['middleware' => $middleware]); 30 | Router::delete('/users/{id:\d+}', 'App\Controller\UserController@delete', ['middleware' => $middleware]); 31 | Router::put('/users/{id:\d+}/roles', 'App\Controller\UserController@roles', ['middleware' => $middleware]); 32 | //Role 33 | Router::get('/roles', 'App\Controller\RoleController@index', ['middleware' => $middleware]); 34 | Router::post('/roles', 'App\Controller\RoleController@store', ['middleware' => $middleware]); 35 | Router::put('/roles/{id:\d+}', 'App\Controller\RoleController@update', ['middleware' => $middleware]); 36 | Router::get('/roles/{id:\d+}', 'App\Controller\RoleController@show', ['middleware' => $middleware]); 37 | Router::delete('/roles/{id:\d+}', 'App\Controller\RoleController@delete', ['middleware' => $middleware]); 38 | //Permission 39 | Router::get('/permissions', 'App\Controller\PermissionController@index', ['middleware' => $middleware]); 40 | Router::post('/permissions', 'App\Controller\PermissionController@store', ['middleware' => $middleware]); 41 | Router::put('/permissions/{id:\d+}', 'App\Controller\PermissionController@update', ['middleware' => $middleware]); 42 | Router::get('/permissions/{id:\d+}', 'App\Controller\PermissionController@show', ['middleware' => $middleware]); 43 | Router::delete('/permissions/{id:\d+}', 'App\Controller\PermissionController@delete', ['middleware' => $middleware]); 44 | -------------------------------------------------------------------------------- /app/Controller/UserController.php: -------------------------------------------------------------------------------- 1 | request->all(); 26 | $pageSize = $this->request->input('per_page', 15); 27 | $list = Model\User::getList($params, (int) $pageSize); 28 | return $list; 29 | } 30 | 31 | //post create 32 | public function store(Request\UserRequest $request) 33 | { 34 | $data = $request->all(); 35 | if (empty($data['password'])) { 36 | throw new Exception\AppBadRequestException('请填写密码'); 37 | } 38 | $data['password'] = $this->hash->make($data['password']); 39 | $user = Model\User::create($data); 40 | return $user; 41 | } 42 | 43 | // get 44 | public function show($id) 45 | { 46 | $user = Model\User::where('user_id', '<>', config('app.super_admin'))->find($id); 47 | if (!$user) { 48 | throw new Exception\AppNotFoundException("用户ID:{$id}不存在"); 49 | } 50 | return $user; 51 | } 52 | 53 | // put 54 | public function update(Request\UserRequest $request, $id) 55 | { 56 | $data = $request->all(); 57 | $user = Model\User::where('user_id', '<>', config('app.super_admin'))->find($id); 58 | if (!$user) { 59 | throw new Exception\AppNotFoundException("用户ID:{$id}不存在"); 60 | } 61 | if (isset($data['username'])) { 62 | unset($data['username']); 63 | } 64 | if (isset($data['password']) && empty($data['password'])) { 65 | unset($data['password']); 66 | } elseif (isset($data['password']) && !empty($data['password'])) { 67 | $data['password'] = $this->hash->make($data['password']); 68 | } 69 | $user->update($data); 70 | return $user; 71 | } 72 | 73 | // delete 74 | public function destroy($id) 75 | { 76 | 77 | } 78 | 79 | public function roles($id) 80 | { 81 | $roles = $this->request->input('roles', []); 82 | $model = Model\User::where('user_id', '<>', config('app.super_admin'))->find($id); 83 | if (!$model) { 84 | throw new Exception\AppNotFoundException("用户ID:{$id}不存在"); 85 | } 86 | $model->syncRoles($roles); 87 | return $model; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /app/JsonRpc/UserService.php: -------------------------------------------------------------------------------- 1 | getUser($token); 36 | $this->checkPermission($user, $permission); 37 | return $user; 38 | } 39 | 40 | protected function getUser($token) 41 | { 42 | try { 43 | $token = $this->jwt->getParser()->parse($token); 44 | if ($this->jwt->enalbed) { 45 | $claims = $this->jwt->claimsToArray($token->getClaims()); 46 | // 验证token是否存在黑名单 47 | if ($this->jwt->blacklist->has($claims)) { 48 | throw new TokenValidException('Token authentication does not pass', 401); 49 | } 50 | } 51 | if (!$this->jwt->validateToken($token)) { 52 | throw new TokenValidException('Token authentication does not pass', 401); 53 | } 54 | if (!$this->jwt->verifyToken($token)) { 55 | throw new TokenValidException('Token authentication does not pass', 401); 56 | } 57 | $userId = $token->getClaim('user_id'); 58 | $user = User::where('user_id', $userId)->where('status', User::STATUS_ENABLE)->first(); 59 | if ($user) { 60 | return $user; 61 | } else { 62 | throw new TokenValidException('用户已禁用', 401); 63 | } 64 | } catch (\Exception $e) { 65 | throw new TokenValidException('Token未验证通过', 401); 66 | } 67 | throw new TokenValidException('Token未验证通过', 401); 68 | } 69 | 70 | protected function checkPermission($user, $permission) 71 | { 72 | $allPermissions = Permission::getPermissions(); 73 | $permissions = $allPermissions->filter(function ($value, $key)use($permission) { 74 | return in_array($value->name, $permission); 75 | })->all(); 76 | if (count($permissions) > 0 && !$user->hasAnyPermission($permissions)) { 77 | throw new UnauthorizedException('无权进行该操作', 403); 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | '@Symfony' => true, 17 | '@DoctrineAnnotation' => true, 18 | '@PhpCsFixer' => true, 19 | 'header_comment' => [ 20 | 'commentType' => 'PHPDoc', 21 | 'header' => $header, 22 | 'separate' => 'none', 23 | 'location' => 'after_declare_strict', 24 | ], 25 | 'array_syntax' => [ 26 | 'syntax' => 'short' 27 | ], 28 | 'list_syntax' => [ 29 | 'syntax' => 'short' 30 | ], 31 | 'concat_space' => [ 32 | 'spacing' => 'one' 33 | ], 34 | 'blank_line_before_statement' => [ 35 | 'statements' => [ 36 | 'declare', 37 | ], 38 | ], 39 | 'general_phpdoc_annotation_remove' => [ 40 | 'annotations' => [ 41 | 'author' 42 | ], 43 | ], 44 | 'ordered_imports' => [ 45 | 'imports_order' => [ 46 | 'class', 'function', 'const', 47 | ], 48 | 'sort_algorithm' => 'alpha', 49 | ], 50 | 'single_line_comment_style' => [ 51 | 'comment_types' => [ 52 | ], 53 | ], 54 | 'yoda_style' => [ 55 | 'always_move_variable' => false, 56 | 'equal' => false, 57 | 'identical' => false, 58 | ], 59 | 'phpdoc_align' => [ 60 | 'align' => 'left', 61 | ], 62 | 'multiline_whitespace_before_semicolons' => [ 63 | 'strategy' => 'no_multi_line', 64 | ], 65 | 'class_attributes_separation' => true, 66 | 'combine_consecutive_unsets' => true, 67 | 'declare_strict_types' => true, 68 | 'linebreak_after_opening_tag' => true, 69 | 'lowercase_constants' => true, 70 | 'lowercase_static_reference' => true, 71 | 'no_useless_else' => true, 72 | 'no_unused_imports' => true, 73 | 'not_operator_with_successor_space' => true, 74 | 'not_operator_with_space' => false, 75 | 'ordered_class_elements' => true, 76 | 'php_unit_strict' => false, 77 | 'phpdoc_separation' => false, 78 | 'single_quote' => true, 79 | 'standardize_not_equals' => true, 80 | 'multiline_comment_opening_closing' => true, 81 | ]) 82 | ->setFinder( 83 | PhpCsFixer\Finder::create() 84 | ->exclude('public') 85 | ->exclude('runtime') 86 | ->exclude('vendor') 87 | ->in(__DIR__) 88 | ) 89 | ->setUsingCache(false); 90 | -------------------------------------------------------------------------------- /app/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | request->getMethod(); 38 | return [ 39 | 'method' => $method, 40 | 'message' => 'hyperf', 41 | ]; 42 | } 43 | 44 | //获取token 45 | public function token(LoginRequest $request) 46 | { 47 | $username = $request->input('username'); 48 | $password = $request->input('password'); 49 | $user = User::where('username', '=', $username)->first(); 50 | if (!$user) { 51 | throw new Exception\AppNotFoundException("用户{$username}不存在"); 52 | } 53 | if (!$this->hash->check($password, $user->password)) { 54 | throw new Exception\AppBadRequestException("用户名或者密码错误"); 55 | } 56 | if ($user->status != User::STATUS_ENABLE) { 57 | throw new Exception\AppNotAllowedException("用户{$username}已禁用"); 58 | } 59 | $user->last_login_at = Carbon::now(); 60 | $user->save(); 61 | $token = (string) $this->jwt->getToken(['user_id' => $user->user_id]); 62 | $user['menu'] = $user->getMenu(); 63 | $user['all_permissions'] = $user->getAllPermissions(); 64 | return ['user' => $user, 'access_token' => $token, 'expires_in' => $this->jwt->getTTL()]; 65 | } 66 | 67 | //刷新token 68 | public function refreshToken() 69 | { 70 | $token = $this->jwt->refreshToken(); 71 | return ['access_token' => (string) $token, 'expires_in' => $this->jwt->getTTL()]; 72 | } 73 | 74 | //退出登录 75 | public function logout() 76 | { 77 | try { 78 | $this->jwt->logout(); 79 | } catch (\Exception $e) { 80 | 81 | } 82 | return $this->helper->success('success'); 83 | } 84 | 85 | //验证码 86 | public function captcha() 87 | { 88 | $length = $this->request->input('length', 4); 89 | $width = $this->request->input('width', 80); 90 | $height = $this->request->input('height', 35); 91 | $phraseBuilder = new PhraseBuilder($length); 92 | $builder = new CaptchaBuilder(null, $phraseBuilder); 93 | $builder->build($width, $height); 94 | $phrase = $builder->getPhrase(); 95 | $captchaId = uniqid(); 96 | $this->cache->set($captchaId, $phrase, 300); 97 | $cookie = new Cookie('captcha', $captchaId); 98 | $output = $builder->get(); 99 | return $this->response 100 | ->withCookie($cookie) 101 | ->withAddedHeader('content-type', 'image/jpeg') 102 | ->withBody(new SwooleStream($output)); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/hyperf-skeleton", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "framework", 8 | "hyperf", 9 | "microservice", 10 | "middleware" 11 | ], 12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", 13 | "license": "Apache-2.0", 14 | "require": { 15 | "php": ">=7.2", 16 | "ext-swoole": ">=4.4", 17 | "hyperf/cache": "~1.1.0", 18 | "hyperf/command": "~1.1.0", 19 | "hyperf/config": "~1.1.0", 20 | "hyperf/contract": "~1.1.0", 21 | "hyperf/database": "~1.1.0", 22 | "hyperf/db-connection": "^1.1", 23 | "hyperf/devtool": "~1.1.0", 24 | "hyperf/di": "~1.1.0", 25 | "hyperf/dispatcher": "~1.1.0", 26 | "hyperf/event": "~1.1.0", 27 | "hyperf/exception-handler": "~1.1.0", 28 | "hyperf/framework": "~1.1.0", 29 | "hyperf/guzzle": "~1.1.0", 30 | "hyperf/http-server": "~1.1.0", 31 | "hyperf/logger": "^1.1", 32 | "hyperf/memory": "~1.1.0", 33 | "hyperf/paginator": "~1.1.0", 34 | "hyperf/pool": "~1.1.0", 35 | "hyperf/process": "~1.1.0", 36 | "hyperf/redis": "~1.1.0", 37 | "hyperf/utils": "~1.1.0", 38 | "hyperf/config-aliyun-acm": "^1.1", 39 | "illuminate/hashing": "^6.5", 40 | "gregwar/captcha": "^1.1", 41 | "phper666/jwt-auth": "~2.0.1", 42 | "hyperf/validation": "^1.1", 43 | "hyperf/translation": "^1.1", 44 | "hyperf/constants": "^1.1", 45 | "hyperf/json-rpc": "^1.1", 46 | "hyperf/rpc-server": "^1.1", 47 | "hyperf/elasticsearch": "^1.1", 48 | "donjan-deng/hyperf-permission": "dev-master" 49 | }, 50 | "require-dev": { 51 | "swoft/swoole-ide-helper": "^4.2", 52 | "phpmd/phpmd": "^2.6", 53 | "friendsofphp/php-cs-fixer": "^2.14", 54 | "mockery/mockery": "^1.0", 55 | "doctrine/common": "^2.9", 56 | "phpstan/phpstan": "^0.11.2", 57 | "hyperf/testing": "~1.1.0" 58 | }, 59 | "suggest": { 60 | "ext-openssl": "Required to use HTTPS.", 61 | "ext-json": "Required to use JSON.", 62 | "ext-pdo": "Required to use MySQL Client.", 63 | "ext-pdo_mysql": "Required to use MySQL Client.", 64 | "ext-redis": "Required to use Redis Client." 65 | }, 66 | "autoload": { 67 | "psr-4": { 68 | "App\\": "app/" 69 | }, 70 | "files": [] 71 | }, 72 | "autoload-dev": { 73 | "psr-4": { 74 | "HyperfTest\\": "./test/" 75 | } 76 | }, 77 | "minimum-stability": "dev", 78 | "prefer-stable": true, 79 | "extra": [], 80 | "scripts": { 81 | "post-root-package-install": [ 82 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 83 | ], 84 | "test": "co-phpunit -c phpunit.xml --colors=always", 85 | "cs-fix": "php-cs-fixer fix $1", 86 | "analyze": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", 87 | "start": "php ./bin/hyperf.php start" 88 | }, 89 | "repositories": { 90 | "donjan-deng/hyperf-permission": { 91 | "type": "git", 92 | "url": "https://github.com/donjan-deng/hyperf-permission.git" 93 | }, 94 | "packagist": { 95 | "type": "composer", 96 | "url": "https://mirrors.aliyun.com/composer" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /seeders/permission_table_seeder.php: -------------------------------------------------------------------------------- 1 | getTable())->insert([ 20 | [ 21 | 'id' => 1, 22 | 'parent_id' => 0, 23 | 'url' => 'auth', 24 | 'name' => '系统管理', 25 | 'display_name' => '系统管理', 26 | 'guard_name' => 'web' 27 | ], 28 | [ 29 | 'id' => 2, 30 | 'parent_id' => 1, 31 | 'url' => '/users', 32 | 'name' => '/user-center/users/get', 33 | 'display_name' => '用户管理', 34 | 'guard_name' => 'web' 35 | ], 36 | [ 37 | 'id' => 3, 38 | 'parent_id' => 1, 39 | 'url' => '/roles', 40 | 'name' => '/user-center/roles/get', 41 | 'display_name' => '角色管理', 42 | 'guard_name' => 'web' 43 | ], 44 | [ 45 | 'id' => 4, 46 | 'parent_id' => 1, 47 | 'url' => '/permissions', 48 | 'name' => '/user-center/permissions/get', 49 | 'display_name' => '节点管理', 50 | 'guard_name' => 'web' 51 | ], 52 | [ 53 | 'id' => 5, 54 | 'parent_id' => 2, 55 | 'url' => '', 56 | 'name' => '/user-center/users/post', 57 | 'display_name' => '新建用户', 58 | 'guard_name' => 'web' 59 | ], [ 60 | 'id' => 6, 61 | 'parent_id' => 2, 62 | 'url' => '', 63 | 'name' => '/user-center/users/{id:\d+}/put', 64 | 'display_name' => '编辑用户', 65 | 'guard_name' => 'web' 66 | ], 67 | [ 68 | 'id' => 7, 69 | 'parent_id' => 3, 70 | 'url' => '', 71 | 'name' => '/user-center/roles/post', 72 | 'display_name' => '新建角色', 73 | 'guard_name' => 'web' 74 | ], [ 75 | 'id' => 8, 76 | 'parent_id' => 3, 77 | 'url' => '', 78 | 'name' => '/user-center/roles/{id:\d+}/put', 79 | 'display_name' => '编辑角色', 80 | 'guard_name' => 'web' 81 | ], 82 | [ 83 | 'id' => 9, 84 | 'parent_id' => 4, 85 | 'url' => '', 86 | 'name' => '/user-center/permissions/post', 87 | 'display_name' => '新建节点', 88 | 'guard_name' => 'web' 89 | ], 90 | [ 91 | 'id' => 10, 92 | 'parent_id' => 4, 93 | 'url' => '', 94 | 'name' => '/user-center/permissions/{id:\d+}/put', 95 | 'display_name' => '编辑节点', 96 | 'guard_name' => 'web' 97 | ], 98 | [ 99 | 'id' => 11, 100 | 'parent_id' => 4, 101 | 'url' => '', 102 | 'name' => '/user-center/users/{id:\d+}/roles/put', 103 | 'display_name' => '分配角色', 104 | 'guard_name' => 'web' 105 | ] 106 | ]); 107 | $role = Model\Role::create([ 108 | 'name' => '管理员', 109 | 'guard_name' => 'web' 110 | ]); 111 | $role->permissions()->sync([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); 112 | $user = Model\User::where('user_id', 1)->first(); 113 | $user->assignRole($role); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /watch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | false]); 39 | $hashes = []; 40 | $serve = null; 41 | echo "🚀 Start @ " . date('Y-m-d H:i:s') . PHP_EOL; 42 | start(); 43 | state(); 44 | Timer::tick(SCAN_INTERVAL, 'watch'); 45 | function start() 46 | { 47 | global $serve; 48 | $serve = new Process('serve', true); 49 | $serve->start(); 50 | if (false === $serve->pid) { 51 | echo swoole_strerror(swoole_errno()) . PHP_EOL; 52 | exit(1); 53 | } 54 | Event::add($serve->pipe, function ($pipe) use (&$serve) { 55 | $message = @$serve->read(); 56 | if (!empty($message)) { 57 | echo $message; 58 | } 59 | }); 60 | } 61 | function watch() 62 | { 63 | global $hashes; 64 | foreach ($hashes as $pathname => $current_hash) { 65 | if (!file_exists($pathname)) { 66 | unset($hashes[$pathname]); 67 | continue; 68 | } 69 | $new_hash = file_hash($pathname); 70 | if ($new_hash != $current_hash) { 71 | change(); 72 | state(); 73 | break; 74 | } 75 | } 76 | } 77 | function state() 78 | { 79 | global $hashes; 80 | $files = php_files(WATCH_DIR); 81 | $hashes = array_combine($files, array_map('file_hash', $files)); 82 | $count = count($hashes); 83 | echo "📡 Watching $count files..." . PHP_EOL; 84 | } 85 | function change() 86 | { 87 | global $serve; 88 | echo "🔄 Restart @ " . date('Y-m-d H:i:s') . PHP_EOL; 89 | Process::kill($serve->pid); 90 | start(); 91 | } 92 | function serve(Process $serve) 93 | { 94 | $opt = getopt('c'); 95 | if (isset($opt['c'])) echo exec(PHP . ' ' . ENTRY_POINT_FILE . ' di:init-proxy') . '..' . PHP_EOL; 96 | $serve->exec(PHP, [ENTRY_POINT_FILE, 'start']); 97 | } 98 | function file_hash(string $pathname): string 99 | { 100 | $contents = file_get_contents($pathname); 101 | if (false === $contents) { 102 | return 'deleted'; 103 | } 104 | return md5($contents); 105 | } 106 | function php_files(string $dirname): array 107 | { 108 | $directory = new RecursiveDirectoryIterator($dirname); 109 | $filter = new Filter($directory); 110 | $iterator = new RecursiveIteratorIterator($filter); 111 | return array_map(function ($fileInfo) { 112 | return $fileInfo->getPathname(); 113 | }, iterator_to_array($iterator)); 114 | } 115 | class Filter extends RecursiveFilterIterator 116 | { 117 | public function accept() 118 | { 119 | if ($this->current()->isDir()) { 120 | if (preg_match('/^\./', $this->current()->getFilename())) { 121 | return false; 122 | } 123 | return !in_array($this->current()->getFilename(), EXCLUDE_DIR); 124 | } 125 | $list = array_map(function (string $item): string { 126 | return "\.$item"; 127 | }, explode(',', WATCH_EXT)); 128 | $list = implode('|', $list); 129 | return preg_match("/($list)$/", $this->current()->getFilename()); 130 | } 131 | } -------------------------------------------------------------------------------- /migrations/2019_12_06_164358_create_permission_tables.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 21 | $table->integer('parent_id'); 22 | $table->string('name'); 23 | $table->string('display_name', 50)->comment('名称'); 24 | $table->string('url', 255); 25 | $table->string('guard_name'); 26 | $table->smallInteger('sort')->comment('排序,数字越大越在前面'); 27 | $table->timestamps(); 28 | }); 29 | 30 | Schema::create($tableNames['roles'], function (Blueprint $table) { 31 | $table->bigIncrements('id'); 32 | $table->string('name'); 33 | $table->string('description', 200); 34 | $table->string('guard_name'); 35 | $table->timestamps(); 36 | }); 37 | 38 | Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) { 39 | $table->unsignedBigInteger('permission_id'); 40 | 41 | $table->string('model_type'); 42 | $table->unsignedBigInteger($columnNames['model_morph_key']); 43 | $table->index([$columnNames['model_morph_key'], 'model_type', ], 'model_has_permissions_model_id_model_type_index'); 44 | 45 | $table->foreign('permission_id') 46 | ->references('id') 47 | ->on($tableNames['permissions']) 48 | ->onDelete('cascade'); 49 | 50 | $table->primary(['permission_id', $columnNames['model_morph_key'], 'model_type'], 51 | 'model_has_permissions_permission_model_type_primary'); 52 | }); 53 | 54 | Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) { 55 | $table->unsignedBigInteger('role_id'); 56 | 57 | $table->string('model_type'); 58 | $table->unsignedBigInteger($columnNames['model_morph_key']); 59 | $table->index([$columnNames['model_morph_key'], 'model_type', ], 'model_has_roles_model_id_model_type_index'); 60 | 61 | $table->foreign('role_id') 62 | ->references('id') 63 | ->on($tableNames['roles']) 64 | ->onDelete('cascade'); 65 | 66 | $table->primary(['role_id', $columnNames['model_morph_key'], 'model_type'], 67 | 'model_has_roles_role_model_type_primary'); 68 | }); 69 | 70 | Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) { 71 | $table->unsignedBigInteger('permission_id'); 72 | $table->unsignedBigInteger('role_id'); 73 | 74 | $table->foreign('permission_id') 75 | ->references('id') 76 | ->on($tableNames['permissions']) 77 | ->onDelete('cascade'); 78 | 79 | $table->foreign('role_id') 80 | ->references('id') 81 | ->on($tableNames['roles']) 82 | ->onDelete('cascade'); 83 | 84 | $table->primary(['permission_id', 'role_id'], 'role_has_permissions_permission_id_role_id_primary'); 85 | }); 86 | Hyperf\Utils\ApplicationContext::getContainer()->get(Psr\SimpleCache\CacheInterface::class)->delete(config('permission.cache.key')); 87 | } 88 | 89 | /** 90 | * Reverse the migrations. 91 | * 92 | * @return void 93 | */ 94 | public function down() 95 | { 96 | $tableNames = config('permission.table_names'); 97 | 98 | Schema::drop($tableNames['role_has_permissions']); 99 | Schema::drop($tableNames['model_has_roles']); 100 | Schema::drop($tableNames['model_has_permissions']); 101 | Schema::drop($tableNames['roles']); 102 | Schema::drop($tableNames['permissions']); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------