├── 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 |
--------------------------------------------------------------------------------