Write the Code. Change the World.

分类目录
1月 31

web 页面,以及后台接口,权限基础架构已经完成。现在再多增点东西。比如发短信,比如模型事件。

先看模型事件,用模型事件一方面是减少耦合,另一方面如果模型多出 Create ,并在 Created 后要做一系列逻辑。这个时候用模型事件就比较好。否则有重复代码,或你用 trait 去处理。反正就是模型事件好。

模型事件文档

# 我们先给 User 用户创建一个观察者
php artisan make:observer UserObserver --model=Models/User

# 创建成功后,删除掉其他的事件,只保留 created 
# 在你希望观察的模型上使用 observe 方法注册观察者。也可以在服务提供者的 boot 方法注册观察者。这里在 AppServiceProvider 中注册观察者。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Models\User;
use App\Observers\UserObserver;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        User::observe(UserObserver::class);
    }
}

# 这样,当 User created 的时候,就会调用到事件侦听

再来处理短信相关的。

短信提供商有很多,我们选择用阿里云的。因为短信价格都差不多,服务器,域名都在阿里下。所以短信也选择阿里的。这里使用 overtrue 的 easy-sms 来做。

composer require overtrue/easy-sms

vim config/easysms.php

<?php

return [
    // HTTP 请求的超时时间(秒)
    'timeout' => 10.0,

    // 默认发送配置
    'default' => [
        // 网关调用策略,默认:顺序调用
        'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,

        // 默认可用的发送网关
        'gateways' => [
            'aliyun',
        ],
    ],
    // 可用的网关配置
    'gateways' => [
        'errorlog' => [
            'file' => storage_path('logs/easy-sms/easy-sms.log'),
        ],
        'aliyun' => [
            'access_key_id' => env('SMS_ALIYUN_ACCESS_KEY_ID'),
            'access_key_secret' => env('SMS_ALIYUN_ACCESS_KEY_SECRET'),
            'sign_name' => '约拍宝',
            'templates' => [
                'register' => env('SMS_ALIYUN_TEMPLATE_REGISTER'),
            ]
        ],
    ],
];

# 再来创建 serverprovider

php artisan make:provider EasySmsServiceProvider

<?php

namespace App\Providers;

use Overtrue\EasySms\EasySms;
use Illuminate\Support\ServiceProvider;

class EasySmsServiceProvider extends ServiceProvider
{
    public function boot()
    {

    }

    public function register()
    {
        $this->app->singleton(EasySms::class, function ($app) {
            return new EasySms(config('easysms'));
        });

        $this->app->alias(EasySms::class, 'easysms');
    }
}

# 在 config/app.php 中加入 
App\Providers\EasySmsServiceProvider::class,

# 最后,env 中配置好对应的值即可

使用

use Overtrue\EasySms\EasySms;

public function send(EasySms $easySms)
    {
        $phone = 13888888888;
            // 生成4位随机数,左侧补0
        $code = str_pad(random_int(1, 9999), 4, 0, STR_PAD_LEFT);

        try {
            $result = $easySms->send($phone, [
                'template' => config('easysms.gateways.aliyun.templates.register'),
                'data' => [
                    'code' => $code
                ],
            ]);
        } catch (\Overtrue\EasySms\Exceptions\NoGatewayAvailableException $exception) {
            $message = $exception->getException('aliyun')->getMessage();
            abort(500, $message ?: '短信发送异常');
        }
    }

先到这里,真实场景再使用。下一步就是开始后台了。这里使用阿里的 ant design pro。

顺便再说一下。env 中加入的配置,我们先要在 env example
中先加一份。 .env 是不加入版本控制的。掉了就没了。 .env.example 是加入了版本控制,但敏感数据空着就好,空留key即可。

1月 27

完成了 web 的布局以及权限配置,现在要着手接口了。因为使用 ant design pro,进行前后端分离的项目。之前做了 User 模型的权限控制,这里想改成 Admin 模型,然后 jwt 基于 User 模型和 Admin 模型实现两套接口。这样之前的模型需要修改,以及需要创建 Admin 模型。

实现过程

# 创建模型,迁移以及控制器
php artisan make:model Models/Admin -mc

# 制作迁移文件,执行迁移,给 Admin 加白名单,并把之前 User 模型中的 hasRole 移到 Admin 模型中。最终的数据见下边

# CreateAdminsTable 迁移
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAdminsTable extends Migration
{
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id')->unique();
            $table->string('nickname', 128);
            $table->string('phone', 16);
            $table->string('address', 128)->nullable();
            $table->string('remark', 128)->nullable();
            $table->unsignedBigInteger('operator_user_id');
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->nullableTimestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('admins');
    }
}

# Admin 模型
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasRoles;

class Admin extends Model
{
    use HasRoles;

    protected $guard_name = 'admin';

    protected $fillable = ['user_id', 'nickname', 'phone', 'address', 'remark', 'operator_user_id'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

虽然上边这么做了。但是细节还是蛮多的。下边一一说明。

  1. 在 Admin 模型中,加入 `protected $guard_name = 'admin';

  2. 在 config/permission.php 中加入 'guard_name' => 'admin',。seeder 的时候会从这里取值。

  3. 在 config/auth.php 中 加入 guard 配置。

# 复制一份 web,修改 provider
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],

# 定义 provider
'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],

# 暂时先这样,然后执行迁移回滚再seeder。调用 tinker 看 role 就晓得了。

php artisan migrate:refresh --seed

php artisan tinker

$user = User::first();
# 这里就可以看到全部权限了
$user->admin->getAllPermissions ();

# 在 seeder 文件中,我们创建 role 以及 permision 的时候,都指定了具体的 guard_name 为 admin。

$guard_name = config('permission.guard_name') ?? 'web';

# 先这样吧,下一步, 我们安装 jwt,然后修改 guard。 这个时候 jwt 后台会使用到,将来接口也使用到。想要两者兼容,可以看下边的链接。

https://learnku.com/articles/28881

安装 jwt

这里操作。

# 安装 jwt
composer require tymon/jwt-auth:1.0.0-rc.5

# 生成 jwt secret
php artisan jwt:secret

# 修改 config/auth.php 中的 guard,指定 admin 的 driver 为jwt

        'admin' => [
            'driver' => 'jwt',
            'provider' => 'admins',
        ],

# 然后修改 Admin 模型,实现 JWTSubject

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasRoles;
use Tymon\JWTAuth\Contracts\JWTSubject;

class Admin extends Model implements JWTSubject
{
    use HasRoles;

    protected $guard_name = 'admin';

    protected $fillable = ['user_id', 'nickname', 'phone', 'address', 'remark', 'operator_user_id'];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return ['role' => 'admin'];
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

# 将来用到接口的时候 User 模型一样要这波操作。

看起来好像好了,那么我们到 tinker 中操作一波试试。

php artisan tinker;

use App\Models\Admin;
use Auth;

$admin = Admin::first();
$token = Auth::guard('admin')->login($admin);

# 这个时候,发现报错了。原来是 Admin 这个模型模型没有继承 Auth,我们复制 User 模型这的 Auth 过来,然后继承一下。完整如下。

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
use Tymon\JWTAuth\Contracts\JWTSubject;

class Admin extends Authenticatable implements JWTSubject
{
    use HasRoles;

    protected $guard_name = 'admin';

    protected $fillable = ['user_id', 'nickname', 'phone', 'address', 'remark', 'operator_user_id'];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return ['role' => 'admin'];
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}


# 修改好之后,我们再一波操作。发现生成的 token 是 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9hZG1pbi5jb20iLCJpYXQiOjE1ODAzMTI1MTUsImV4cCI6MTU4MDMxNjExNSwibmJmIjoxNTgwMzEyNTE1LCJqdGkiOiJsR25sZmlmcW9iSWFwQnc5Iiwic3ViIjoxLCJwcnYiOiJkZjg4M2RiOTdiZDA1ZWY4ZmY4NTA4MmQ2ODZjNDVlODMyZTU5M2E5Iiwicm9sZSI6ImFkbWluIn0.AOU3iiIgGHE6XuaioCDo3Ce8mZmRe63WLYKL_BDj1_w

# 我们复制 token 到 https://jwt.io/ 中,进行 decode,就可以还原出具体的数据 

    {
 typ: "JWT",
 alg: "HS256"
}.
{
 iss: "http:\/\/admin.com",
 iat: 1580312515,
 exp: 1580316115,
 nbf: 1580312515,
 jti: "lGnlfifqobIapBw9",
 sub: 1,
 prv: "df883db97bd05ef8ff85082d686c45e832e593a9",
 role: "admin"
}.
[signature]

# jwt 组成的 三部分都在

到了这里,部分细节要说一遍。

  1. 建立 Admin 模型。安装 jwt ,以及之前已经安装的 laravel permission。让 Admin 模型适应 jwt 以及 laravel permission。
  2. 新建 中间件。以区分 token 是授权 api 的 还是 admin 的。后边会列出来。
  3. 建立路由。默认有 web 和 api 两个路由。可以建一个 admin 的路由。然后在 RouteServiceProvider 中复制一份 api 的修改成 admin 的即可。

列出来

# 新增路由配置
    // 新增 admin 路由
    protected function mapAdminRoutes()
    {
        Route::prefix('admin')
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/admin.php'));
    }

# 中间件
<?php

namespace App\Http\Middleware;

use Closure;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;

class JWTRoleAuth extends BaseMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param $request
     * @param Closure $next
     * @param null $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role = null)
    {
        try {
            // 解析token角色
            $token_role = $this->auth->parseToken()->getClaim('role');
        } catch (JWTException $e) {
            /**
             * token解析失败,说明请求中没有可用的token。
             * 为了可以全局使用(不需要token的请求也可通过),这里让请求继续。
             * 因为这个中间件的责职只是校验token里的角色。
             */
            return $next($request);
        }

        // 判断token角色。
        if ($token_role != $role) {
            throw new UnauthorizedHttpException('jwt-auth', 'User role error');
        }

        return $next($request);
    }
}

# 并且要在 Kernel 的 routeMiddleware 中进行注册。
'jwt.role' => \App\Http\Middleware\JWTRoleAuth::class,

上边这些弄好了,就开始处理路由。

# 路由 admin.php
<?php

Route::namespace('Admin')->group(function(){

    Route::post('authorizations', 'AuthorizationsController@store')->name('api.authorizations.store');

    Route::group([
        'middleware' => ['jwt.role:admin', 'jwt.auth'],
    ], function () {
        // 刷新token
        Route::put('authorizations/current', 'AuthorizationsController@update')->name('authorizations.update');

        // 删除token
        Route::delete('authorizations/current', 'AuthorizationsController@destroy')->name('authorizations.destroy');
    });
});

测试发现,返回的是 html代码,并非是 json 数据。这个时候需要手动添加 Head 头 指定 Accept 为 application/json 才返回 json 格式。既然这样,就加一个中间件来返回 json 数据。

中间件

<?php

namespace App\Http\Middleware;

use Closure;

class AcceptHeader
{
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');

        return $next($request);
    }
}

并添加到 api 中间件组中,

        'api' => [
            \App\Http\Middleware\AcceptHeader::class,
            'throttle:60,1',
            'bindings',
        ],

# admin 路由 和 api 都将会用到这个中间件组。

这样在请求中,就不必每次指定 Header 头的 Accept 值。

1月 27

做好之前的功能,现在开始做权限管理以及后台。权限管理这里使用第三方扩展包 laravel-permission

laravel-permission github

https://docs.spatie.be/laravel-permission/v3/installation-laravel/

按照文档,进行常规操作。

安装使用 laravel-permission

基本步骤如下

composer require spatie/laravel-permission

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

为了更好的使用权限,在 权限表中加了一些额外的字段

# permissions 迁移中加入下边字段
$table->string('display_name');
$table->string('route')->nullable();
$table->integer('icon_id')->nullable();
$table->integer('parent_id')->default(0);
$table->integer('sort')->default(0);

# roles 迁移中加入下边字段
$table->string('display_name');

因为会用到 icon,所以得额外加一个 icons 表。生成一个额外的迁移文件

php artisan make:migration create_icons_tabel --create=icons

# 填充迁移文件
        Schema::create('icons', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('unicode')->nullable();
            $table->string('class')->nullable();
            $table->string('name')->nullable();
            $table->integer('sort')->default(0);
            $table->nullableTimestamps();
        });

# 执行迁移
php artisan migrate

怎么使用呢,看 wiki https://docs.spatie.be/laravel-permission/v3/basic-usage/basic-usage/

  1. 在模型中加入 Spatie\Permission\Traits\HasRoles trait,可以是 User 模型也可以是其他,看你怎么用。
  2. 虽然可以给单独模型赋予权限,不建议这样做。建议通过 role 来实现。先给 role 赋予权限,再给用户赋予角色。这样更好管理和控制权限。

完成了上边的操作,我们还需要做两个数据填充(Seeders),一个 IconSeeder 和 PermissionSeeder 。

php artisan make:seeder IconSeeder

php artisan make:seeder PermissionSeeder

既然要整 icon ,那我们就用阿里的 iconfonts 。去 https://www.iconfont.cn/ 操作一波。

制作好了之后,先创建 json 文件:

vim public/backstage/json/icons.json

# 填充以下内容(以3个icon为例)
[{"unicode": "", "name": "控制台", "class": "icon-kongzhitai"},
 {"unicode": "", "name": "权限", "class": "icon-quanxian"},
 {"unicode": "", "name": "仪表盘", "class": "icon-yibiaopan"}]

再去编辑 IconSeeder。

# 创建 icon
php artisan make:model Models/Icon

# 白名单
protected $fillable = ['unicode','class','name','sort'];

# IconSeeder
<?php

use Illuminate\Database\Seeder;
use App\Models\Icon;

class IconSeeder extends Seeder
{
    public function run()
    {
        Icon::truncate();
        $file = file_get_contents(public_path().'/backstage/json/icons.json');
        $icons = json_decode($file, true);
        foreach ($icons as $icon){
            Icon::create($icon);
        }
    }
}

PermissionSeeder 的迁移文件可参考: https://docs.spatie.be/laravel-permission/v3/advanced-usage/seeding/

根据需求,先暂时建立几个权限以及角色。迁移文件如下:

php artisan make:model Models/Permission

php artisan make:model Models/Role

# Permission Model
<?php

namespace App\Models;

use Spatie\Permission\Models\Permission as SPermission;

class Permission extends SPermission
{
    //菜单图标
    public function icon()
    {
        return $this->belongsTo('App\Models\Icon', 'icon_id', 'id');
    }

    //子权限
    public function childs()
    {
        return $this->hasMany('App\Models\Permission', 'parent_id', 'id');
    }
}

# Role Model
<?php

namespace App\Models;

use Spatie\Permission\Models\Role as SRole;

class Role extends SRole
{

}

然后,permission seeder。

<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\PermissionRegistrar;
use App\Models\User;
use App\Models\Permission;
use App\Models\Role;
use DB;
use Hash;

class PermissionSeeder extends Seeder
{
    public function run()
    {
        app(PermissionRegistrar::class)->forgetCachedPermissions();

        $tableNames = config('permission.table_names');
        Model::unguard();
        foreach ($tableNames as $tableName) {
            DB::table($tableName)->delete();
        }
        Model::reguard();

        $user = User::where('account', '13671638524')->first();
        if (!$user) {
            $user = User::create([
                'account' => '13671638524',
                'password' => Hash::make('w123456'),
                'email' => 'lichking_lin86@qq.com',
                'phone' => '13671638524'
            ]);
        }

        $role = Role::create([
            'name' => 'root',
            'display_name' => '站长'
        ]);

        $user->assignRole($role);

        $permissions = [
            [
                'name' => 'home.manager',
                'display_name' => '主页',
                'route' => '',
                'icon_id' => '1',
                'child' => [
                    [
                        'name' => 'home',
                        'display_name' => '控制台',
                        'route' => 'admin.home',
                        'icon_id' => '12',
                        'child' => []
                    ]
                ]
            ],
            [
                'name' => 'system.manage',
                'display_name' => '系统管理',
                'route' => '',
                'icon_id' => '2',
                'child' => [
                    [
                        'name' => 'system.permission',
                        'display_name' => '权限管理',
                        'route' => 'admin.system.permission',
                        'icon_id' => '2',
                        'child' => [
                            ['name' => 'system.permission.create', 'display_name' => '添加权限', 'route' => 'admin.system.permission.create'],
                            ['name' => 'system.permission.edit', 'display_name' => '编辑权限', 'route' => 'admin.system.permission.edit'],
                            ['name' => 'system.permission.destroy', 'display_name' => '删除权限', 'route' => 'admin.system.permission.destroy'],
                        ]
                    ],
                    [
                        'name' => 'system.role',
                        'display_name' => '角色管理',
                        'route' => 'admin.system.role',
                        'icon_id' => '3',
                        'child' => [
                            ['name' => 'system.role.create', 'display_name' => '添加角色', 'route' => 'admin.system.role.create'],
                            ['name' => 'system.role.edit', 'display_name' => '编辑角色', 'route' => 'admin.system.role.edit'],
                            ['name' => 'system.role.destroy', 'display_name' => '删除角色', 'route' => 'admin.system.role.destroy'],
                            ['name' => 'system.role.permission', 'display_name' => '分配权限', 'route' => 'admin.system.role.permission'],
                        ]
                    ],
                    [
                        'name' => 'system.manager',
                        'display_name' => '管理员',
                        'route' => 'admin.system.manager',
                        'icon_id' => '123',
                        'child' => [
                            ['name' => 'system.manager.create', 'display_name' => '添加管理', 'route' => 'admin.system.manager.create'],
                            ['name' => 'system.manager.edit', 'display_name' => '编辑管理', 'route' => 'admin.system.manager.edit'],
                            ['name' => 'system.manager.destroy', 'display_name' => '删除管理', 'route' => 'admin.system.manager.destroy'],
                            ['name' => 'system.manager.role', 'display_name' => '分配角色', 'route' => 'admin.system.manager.role'],
                            ['name' => 'system.manager.permission', 'display_name' => '分配权限', 'route' => 'admin.system.manager.permission'],
                        ]
                    ]
                ]
            ]
        ];

        foreach ($permissions as $pem1) {
            //生成一级权限
            $p1 = Permission::create([
                'name' => $pem1['name'],
                'display_name' => $pem1['display_name'],
                'route' => $pem1['route'] ?? '',
                'icon_id' => $pem1['icon_id'] ?? 1,
            ]);
            //为角色添加权限
            $role->givePermissionTo($p1);
            if (isset($pem1['child'])) {
                foreach ($pem1['child'] as $pem2) {
                    //生成二级权限
                    $p2 = Permission::create([
                        'name' => $pem2['name'],
                        'display_name' => $pem2['display_name'],
                        'parent_id' => $p1->id,
                        'route' => $pem2['route'] ?? 1,
                        'icon_id' => $pem2['icon_id'] ?? 1,
                    ]);
                    //为角色添加权限
                    $role->givePermissionTo($p2);
                    if (isset($pem2['child'])) {
                        foreach ($pem2['child'] as $pem3) {
                            //生成三级权限
                            $p3 = \App\Models\Permission::create([
                                'name' => $pem3['name'],
                                'display_name' => $pem3['display_name'],
                                'parent_id' => $p2->id,
                                'route' => $pem3['route'] ?? '',
                                'icon_id' => $pem3['icon_id'] ?? 1,
                            ]);
                            //为角色添加权限
                            $role->givePermissionTo($p3);
                        }
                    }
                }
            }
        }

        //初始化的角色
        $roles = [
            ['name' => 'manager', 'display_name' => '管理员']
        ];

        foreach ($roles as $role) {
            Role::create($role);
        }
    }
}

再在 DatabaseSeeder 中加入:

        $this->call(IconSeeder::class);
        $this->call(PermissionSeeder::class);

# 然后迁移回滚操作
php artisan migrate:refresh --seed -v

添加中间件。在 app/Http/Kernel.php 中的 routeMiddleware 增加映射

        'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
        'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
1月 26

完成了注册和登录,现在来完成完整邮箱验证

参考

https://blog.vini123.com/288

https://blog.vini123.com/333

按照上述参考操作,先将 User 模型实现 MustVerifyEmail,再修改路由,加入中间件 verified

Route::namespace('Web')->middleware(['verified'])->group(function() {

    Route::get('/', 'PageController@home')->name('home');

    Route::get('search', 'PageController@search')->name('search');

    Route::get('/users/{user}', 'UserController@index')->name('users.show');

    Route::group(['middleware' => 'auth'], function() {
        Route::get('/users/{user}/edit', 'UserController@edit')->name('users.edit');
    });
});

访问首页,发现会自动跳转到验证邮箱页面。然后点击重新发送验证。会发现有下边的报错:

Expected response code 250 but got code "530", with message "530 5.7.1 Authentication required
 "

这个是 email 相关的配置没好,我们可以使用 qq 邮箱、163邮箱等进行 SMTP 发送邮件。以 qq 邮箱来说,首先得去自己的 qq 邮箱里去申请开通。这里直接列出邮箱的配置数据。

vim .env

# 填充好下边的配置
MAIL_DRIVER=smtp
MAIL_HOST=smtp.qq.com
MAIL_PORT=465
MAIL_USERNAME=xxx@xxxx.com
MAIL_PASSWORD=xxxxx
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=xxxx@xxxx.com
MAIL_FROM_NAME=xxxxxx

当这些配置好了,再发送时,提示 新的验证链接已发送到您的 E-mail。 邮件已经发送。点击,跳转验证。

如果想验证成功后出现提示,可以在 EventServiceProvider中增加:

        \Illuminate\Auth\Events\Verified::class => [
            \App\Listeners\EmailVerified::class,
        ],

然后,自定义 mailVerified 类。添加好上边的,再执行 php artisan event:generate 会自动生成,然后填充方法。

    public function handle(Verified $event)
    {
        // 会话里闪存认证成功后的消息提醒
        session()->flash('success', '邮箱验证成功 ^_^');
    }

之前,弄的样式以及 layouts/app.blade.php 有问题,需要稍微修改。这里懒的弄了。下一步,权限管理,使用 spatie/laravel-permission

1月 25

上边完成了基本布局,发现某些 js,css缺失。这个时候,需要补进来。然后,完成服务端的注册登录逻辑,以及 gee 验证。

修补

加入以下文件

static/style/css/nprogress.css
static/style/css/nprogress.min.css

static/style/js/fn.js
static/style/js/gt.js
static/style/js/nprogress.min.js

部分调整

  1. 将 nprogress.min.js 和 app.js 的加载顺序更换一下。因为 app.js 中会使用到 nprogress 的封装
git add .
git commit -m '修复引入js,css不存在的bug'

认证脚手架

php artisan ui:auth

# 因为之前已经有了 layouts 相关的文件,这个时候会提示是否要覆盖,输入 no 回车。这个时候,会补入和修改相关的文件

# 查看变更的文件
git status 

# 删掉其他新增文件,只保留 resources/views/auth/ 下的文件
# web.php  路由也处理一下。之前就已经好了,这次注入是重复的,可以删掉重复的部分。

做好上边这些,然后开始修改 user 表的迁移文件,以及将 User模型移动到 app\Models\ 下。
继续阅读

1月 19

构建 bootstrap 框架,并定制好基本布局

composer require laravel/ui --dev

php artisan ui bootstrap

以上命令做了以下事情:

  1. 在 npm 依赖配置文件 package.json 中引入 bootstrap、jquery、popper.js 作为依赖;
  2. 修改 resources/js/bootstrap.js ,在此文件中初始化 Bootstrap UI 框架的 JS 部分;
  3. 修改 resources/sass/app.scss 以加载 Bootstrap 的样式文件;
  4. 新增 resources/sass/_variables.scss 样式配置文件。

运行 Laravel Mix

Laravel Mix 一款前端任务自动化管理工具,使用了工作流的模式对制定好的任务依次执行。Mix 提供了简洁流畅的 API,让你能够为你的 Laravel 应用定义 Webpack 编译任务。Mix 支持许多常见的 CSS 与 JavaScript 预处理器,通过简单的调用,你可以轻松地管理前端资源。

使用 Mix 很简单,首先你需要使用以下命令安装 npm 依赖即可。我们将使用 Yarn 来安装依赖,在这之前,因为国内的网络原因,我们还需为 NPM 和 Yarn 配置安装加速:

npm config set registry=https://registry.npm.taobao.org
yarn config set registry https://registry.npm.taobao.org

# 查看镜像
npm get registry 
yarn config get registry

yarn install

yarn run watch-poll

下边进行基础布局
继续阅读

1月 19

从零开始构建 laravel 项目

包裹以下功能:

  1. web 页面(可能是商城,可能是论坛,可能是其他)
  2. 接口功能(一套接口(jwt),提供给 app,小程序 或其他)
  3. 后台功能 (不再使用 layui admin ,使用 ant design pro。权限控制,laravel 自己的权限控制很好,ant design pro 不熟,不知两边的权限能否融洽。就是只使用服务端的权限,不去耦合 antdesignpro 的权限)
  4. 额外。 微信授权、站点地图、elastsearch、定时任务、swoole等。

开始

开发环境依然是 homestead。虽然 docker 很火热, 但总用不好。还有,如果真是线上环境,宁愿一个一个去编译安装所用软件(个数不多)。因为不熟悉,所以不敢放心去使用。

homestead 配置域名:admin.com

laravel 版本为最新版本 6.8.*

# 创建 laravel 6.8 项目
composer create-project laravel/laravel --prefer-dist admin.com "6.8.*"

cd admin.com

# 去掉原本的 readme,添加简单的 readme
rm -rf README.md

echo 'laravel 6.8 initialize' >> Readme.md

# 初始化 git
git init

# 添加,并 commit
git add .

git commit -m 'laravel 6.8 initialize'

为了保证代码风格,安装使用 editorcofig 。配置文件 laravel 项目里边已经有了再稍微改一点点。如下,保存提交版本。

修改根目录下的 .editorconfig

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.yml]
indent_size = 2

[*.{js,html,blade.php,css,scss}]
indent_style = space
indent_size = 2

然后提交版本:

git add .
git commit -m '增加 editorconfig 配置'

修改时区以及语言版本

# vim config/app.php

'timezone' => 'Asia/Shanghai',

'locale' => 'zh-CN',

# git
git add .

git commit -m '设置时区以及默认语言版本'

添加辅助函数

# 在 app 目录下,添加 helpers.php 文件,辅助函数都放在该文件里。
touch app/helpers.php

# 修改 composer.json,让 autoload 的时候载入辅助函数

{
    ...

    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "classmap": [
            "database/seeds",
            "database/factories"
        ],
        "files": [
            "app/helpers.php"
        ]
    }
    ...
}

# 重新加载文件
composer dump-autoload

git提交

git add .

git commit -m '新增辅助函数文件'

现在 helpers.php 文件还是空的,但后续可以添加你需要的。比如 curl 等。

1月 18

站点地图有助于 seo 优化,一般站点都会制作站点地图。 Laravel 项目,有制作站点地图的插件 Laravelium-sitemap。拿来用即可。

laravelium-sitemap wiki

看文档,根据不同的 laravel 版本安装不同的包。

了解点

  1. 可以直接展示站点地图或分包展示地图。也就是直接 xml 或 sitemap index。

做一做

composer require laravelium/sitemap

php artisan vendor:publish --provider="Laravelium\Sitemap\SitemapServiceProvider"
  1. 可以缓存。不是每次请求都要生一个。

  2. 可以添加图片,视频等资源。

实践产品

逻辑里边,两种展示方式都添加了。只是选择了直接使用 xml
https://www.yuepaibao.net/sitemap.xml

1月 10

准备好基础框架后,就可以开始准备起接口了。准备最最基础的登录注册接口。

需要做以下处理:

  1. 准备 user 表的迁移文件。
  2. 准备 jwt
  3. 准备好路由以及文件结构
  4. 相关注册逻辑。

先准备 user 表的迁移文件

默认就有了 user 表的迁移,我们直接修改它就好。

$table->bigIncrements('id');
$table->string('account', 16)->unique();
$table->string('password');
$table->unsignedInteger('viewid')->unique();
$table->string('phone', 16)->unique();
$table->string('email')->nullable();
$table->string('nickname');
$table->string('avatar')->default('app/public/upload/image/avatar/default.jpg');
$table->string('signture')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->rememberToken();
$table->nullableTimestamps();

# 执行迁移
php artisan migrate

创建 Models 目录。

# App 目录下创建 Models 文件夹,并将 User 模型移动到该文件夹下
mkdir App/Models
mv App/User.php App/Models/User.php

再修改 User 的命名空间,并将整个框架的 User 的命名空间变更。

安装 jwt

# 安装
composer require tymon/jwt-auth:1.0.0-rc.5

# 生成 jwt 的 secret
php artisan jwt:secret

jwt-auth 有两个重要的参数,可以在 .env 中进行设置
JWT_TTL 生成的 token 在多少分钟后过期,默认 60 分钟
JWT_REFRESH_TTL 生成的 token,在多少分钟内,可以刷新获取一个新 token,默认 20160 分钟,14 天。

这里需要理解一下 JWT 的过期和刷新机制,过期很好理解,超过了这个时间,token 就无效了。刷新时间一般比过期时间长,只要在这个刷新时间内,即使 token 过期了, 依然可以换取一个新的 token,以达到应用长期可用,不需要重新登录的目的。

# 编辑 .env 增加 jwt 的过期配置和刷新配置
vim .env
# 增加下边两个配置
JWT_TTL= 10080
JWT_REFRESH_TTL=20160

修改完善 User 模型,使其继承 Tymon\JWTAuth\Contracts\JWTSubject 接口,并实现接口的两个方法 getJWTIdentifier()getJWTCustomClaims()

于是 User 模型中的类容为:

<?php

namespace App\Models;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;


    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }

    protected $fillable = [
        'account', 'password', 'viewid', 'phone', 'email', 'nickname', 'avatar', 'signture'
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

我们启动 tinker,先创建一个用户,再尝试获取 jwt token。

# 启动 tinker
php artisan tinker

use App\Models\User;
use Hash;
use Auth;

$user = [
    'account' => '1367163xxxx',
    'password' => Hash::make('123456'),
    'phone' => '1367163xxxx',
    'nickname' => '七月羽歌',
    'signture' => '美的事物是永恒的喜悦'
];

# 创建用户
User::create($data);

# 获取用户
$user = User::first();

# 获取 token
Auth::guard('api')->login($user);

新建用户登录注册控制器

# 创建控制器
php artisan make:controller Api/AuthorizationsController

# 创建 request
php artisan make:request Api/AuthorizationRequest

然后编辑 request

<?php

namespace App\Http\Requests\Api;

use Illuminate\Foundation\Http\FormRequest;

class AuthorizationRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'account' => 'required|regex:/^1[1-9]\d{9}$/',
            'password' => 'required|alpha_dash|min:6'
        ];
    }
}

控制器代码

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\AuthorizationRequest;
use Illuminate\Auth\AuthenticationException;
use Auth;

class AuthorizationsController extends Controller
{
    public function login(AuthorizationRequest $request)
    {
        $data = $request->only(['account', 'password']);

        if (!$token = Auth::guard('api')->attempt($data)) {
            throw new AuthenticationException('用户名或密码错误');
        }

        return $this->respondWithToken($token)->setStatusCode(200);
    }

    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'Bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60
        ]);
    }
}

路由

Route::namespace('Api')->group(function () {
    Route::post('authorizations/login', 'AuthorizationsController@login')->name('api.authorizations.login');
});

然后使用 postman 测试下

登录好了,下一步就处理 ant design pro 框架。构建基础的功能以及去掉不需要的功能,美化页面。

有用的参考

如果多端,多表授权可以点这里

https://www.jianshu.com/p/344c5e540eaa

https://www.cnblogs.com/freely/p/10874297.html

https://www.cnblogs.com/lihuijuan/p/11242976.html

1月 09

laravel 好用, ant design pro 不仅好用还好看。那么,laravel + ant design pro 做出来的项目也会很好用,很好看。

创建初始化 laravel 项目以及 ant design pro 项目

先创建 laravel 项目

# 创建 最新版本的 laravel 项目,并且指定项目文件名为 lreact.com
composer create-project laravel/laravel=6.* --prefer-dist lreact.com

# 删除原本的 readme
rm -rf README.md

# 使用新的 readme,简洁
echo '### Laravel 6.* initialize' >> Readme.md

# 初始化 git 仓库
git init
git add .
git commit -m 'Laravel 6.* initialize'

再创建 ant design pro 项目

在 `laravel` 的 `resources` 目录下,新建文件夹 `antdesign`
cd resources

mkdir antdesign

npm create umi
# 依次选择 ant design pro -> javascript

# 当前版本 v4

git add .

git commit -m 'ant design pro v4 initialize'

npm install

安装完成后,需要稍微修改,并打包

部署请参考 https://pro.ant.design/docs/deploy-cn

# 修改输出目录(antdesign 目录下)
vim config/config.js

# 添加指定目录 base 以及 publicPath

export default {
  base: '/dashboard/',
  publicPath: '/dashboard/',
  plugins,
  hash: false,

# 编译
npm run build

# 将打包好的文件复制到 laravel 的 public 目录
mv dist ../../public/dashboard

# 访问站点
https://www.lrect.com/dashboard

能正常访问,但是授权登录错误。先到这来。

如果你不想每次都移来移去,可以在 config/config.js 里追加配置 outputPath
参考

于是,配置为

export default {
  base: '/dashboard/',
  publicPath: '/dashboard/',
  outputPath: '../../public/dashboard',
  plugins,
  hash: false,

再打包。文件就会打包到 laravel 项目下的 public 下的 dashboard 目录下。

修改 request

ant design pro 的请求都放在 services 下。在这里你可以定义和修改请求。但,这里的请求用的是 umi-request, 而这里对 umi-request 的引用放在 utils/request.js 中。这里你可以添加 header,加入拦截器等等。请看 umi-request 文档。

https://github.com/umijs/umi-request/blob/master/README_zh-CN.md

这里添加空的拦截器(request,response),还有添加请求前缀。完整的 request.js 如下。后边,会对拦截器进一步完善。

/**
 * request 网络请求工具
 * 更详细的 api 文档: https://github.com/umijs/umi-request
 */
import { extend } from 'umi-request';
import { notification } from 'antd';
const codeMessage = {
  200: '服务器成功返回请求的数据。',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)。',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器。',
  502: '网关错误。',
  503: '服务不可用,服务器暂时过载或维护。',
  504: '网关超时。',
};
/**
 * 异常处理程序
 */

const errorHandler = error => {
  const { response } = error;

  if (response && response.status) {
    const errorText = codeMessage[response.status] || response.statusText;
    const { status, url } = response;
    notification.error({
      message: `请求错误 ${status}: ${url}`,
      description: errorText,
    });
  } else if (!response) {
    notification.error({
      description: '您的网络发生异常,无法连接服务器',
      message: '网络异常',
    });
  }

  return response;
};

/**
 * 配置request请求时的默认参数
 */
const request = extend({
  prefix: '/dashboard',
  errorHandler,
  // 默认错误处理
  credentials: 'include', // 默认请求是否带上cookie
});

// request拦截器, 改变url 或 options.
request.interceptors.request.use(async (url, options) => {
  const headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  };

  return (
    {
      url: url,
      options: { ...options, headers: headers },
    }
  );
})

// response拦截器, 处理response
request.interceptors.response.use((response, options) => {
  return response;
});

export default request;

到目前为止,输出目录,接口前缀,都是手动写的。分别写在了三个地方。如果想修改,必须得每个地方改一次。如果定义一个常量,直接使用常量不就好了么。下边进行常量定义。不过,定义了常量,那就必须保证你的需求里这三个地方的值是一样的。

定义常量,方便输出以及接口调用

在 config/config.js 中,定义常量 BASE , 设置好值。

const BASE = '/dashboard';

然后在 define 中进行注册该值。只有注册了,其他 js 才能访问的到,并且不需要加 window,请看这里文档。
https://pro.ant.design/docs/environment-variables-cn#%E5%A4%84%E7%90%86%E5%9C%A8-lint-%E4%B8%AD%E7%9A%84%E6%8A%A5%E9%94%99

于是 config/config.js 是这样

const BASE = '/dashboard';
…
export default {
  base: BASE,
  publicPath: `${BASE}/`,
  outputPath: `../../public/${BASE}/`,
  plugins,
  hash: false,
  targets: {
    ie: 11,
  },

…

utils/request.js 是这样

const request = extend({
  prefix: BASE,
  errorHandler,
  // 默认错误处理
  credentials: 'include', // 默认请求是否带上cookie
});

好了,提交 git 暂时先到这一步。

git add .

git commit -m '定义BASE作为项的输出路径和Api前缀'

最后又多一句,还是可以使用 npm start 进行奔跑的。

可参考

https://pro.ant.design/docs/getting-started-cn

https://www.yuque.com/yuxuanbeishui/zog1rm

https://learnku.com/articles/32218

https://umijs.org/zh/config/#uglifyjsoptions

https://www.codercto.com/a/26106.html