1. 简介
什么是Migration呢?
- 是对数据表操作的一套机制
- 将sql语句转换成PHP语言去执行
- 可做到对数据库结构的版本控制
为什么需要Migration呢?
解决团队合作下数据库结构不统一的问题
2. 传统操作数据库的方式
开启数据库服务,使用命令行连接数据库。
mysql -uroot -p
建立数据库
create database zhihu
使用数据库
use zhihu
创建数据表文件database.sql并创建表结构。
DROP TABLE IF EXISTS `user`;
CREATE TABLE IF NOT EXISTS `user`(
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '用户名',
`password` VARCHAR(64) NOT NULL DEFAULT '' UNIQUE COMMENT '密码',
PRIMARY KEY `id`(`id`),
UNIQUE KEY `user_username_unique`(`username`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 COMMENT='用户表';
导出数据库
mysqldump -uroot -p zhihu > zhihu.sql
3. 使用Migration操作数据库
数据库表明约定使用复数形式
使用migration创建数据表users
php artisan make:migration create_table_users --create=users
# laravel5.4 中此语句语法报错
[ErrorException]
include(/Users/junchow/Code/laravel/vendor/composer/../../database/migrations/2017_06_22_073036_create_table_users.php): failed to open stream: No such file or directory
# create_table_users 修改语法为 create_users_table
php artisan make:migration create_users_table --create=uesrs
创建文件位于
\database\migrations\2017_03_02_030628_create_table_users.php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTableUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// 创建数user据库表
Schema::create('users',function(Blueprint $table){
$table->unsignedInteger('id',10)->autoIncrement();// 创建数据表自增主键
// $table->increments('id');
$table->string('username',20)->nullable()->unique();//字符串类型为空唯一
$table->string('password',64);
$table->text('intro')->nullable();
$table->string('email',128)->unique()->nullable();
$table->string('country_code',10)->default('+86');
$table->string('phone',20)->unique()->nullable();
$table->text('avatar')->nullable();
$table->timestamps();
});
//修改表名
//Schema::rename('users','admins');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//删除表
Schema::drop('users');
}
}
increments('id');
$table->string('username',20)->default('')->comment('账户')->unique();
$table->string('password',255)->default('')->comment('密码')->unique();
$table->string('email',128)->default('')->comment('邮箱')->unique();
$table->string('phone',30)->default('')->comment('手机')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
执行migration创建表类的up()内的操作
// 查看执行的SQL
$ php artisan migrate --pretend [15:49:09]
CreateUsersTable: create table `users` (`id` int unsigned not null auto_increment primary key, `username` varchar(20) not null default '' comment '账户', `password` varch(255) not null default '' comment '密码', `email` varchar(128) not null default '' comment '邮箱', `phone` varchar(30) not null default '' comment '手机', `created_at` tip null, `updated_at` timestamp null) default character set utf8mb4 collate utf8mb4_unicode_ci
CreateUsersTable: alter table `users` add unique `users_username_unique`(`username`)
CreateUsersTable: alter table `users` add unique `users_password_unique`(`password`)
CreateUsersTable: alter table `users` add unique `users_email_unique`(`email`)
CreateUsersTable: alter table `users` add unique `users_phone_unique`(`phone`)
// 执行SQL
php artisan migrate
执行Migration创建表类的down()内的操作
// 查看执行的SQL
php artisan migrate:rollback --pretend
// 回滚到上一次执行的SQL
php artisan migrate:rollback
数据填充
# 创建数据填充
php artisan make:seeder UsersTableSeeder
# app/database/seeds/UsersTableSeeder.php
create()->each(function($u){
//$u->posts()->save(factory('App\Models\Post')->make());
});
}
}
# app/database/factories/ModelFactory.php
define(User::class, function (Faker\Generator $faker) {
static $password;
return [
'username' => $faker->username,
'password' => $password ?: $password = bcrypt('secret'),
'email' => $faker->unique()->safeEmail,
'phone' => str_random(10),
//'remember_token' => str_random(10),
];
});
# 批量填充数据
php artisan db:seed --class=UsersTableSeeder
# 若出现错误
[PDOException]
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '$2y$10$Hni.nH7IseLz/ifwgoj5qe7p8klDdbMui1pW1cQUGguuI0tXEu.Ha' for key 'users_password_unique'
问题原因是 password 设置了 unique 索引,而批量填充的时候统一使用了 secret 密码。可暂时删除索引,或修改随机种子。
创建用户资料表
php artisan make:migration craete_profiles_table --create=profiles
increments('id');
$table->unsignedInteger('user_id',10)->default('0')->comment('用户编号');
$table->foreign('user_id')->references('users')->on('id')->onDelete('cascade');
$table->string('nickname',20)->default('')->comment('昵称');
$table->string('realname',20)->default('')->comment('真名');
$table->string('avatar',255)->default('')->comment('头像');
$table->text('introduce')->nullable()->comment('简介');
$table->boolean('gender')->default(0)->comment('性别');
$table->date('birth')->default('0000-00-00')->comment('出生日期');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('profiles');
}
}
执行迁移后出现错误1
$ php artisan migrate [16:14:49]
[Illuminate\Database\QueryException]
SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value for 'user_id' (SQL: create table `profiles` (`id` int unsigned not null auto_increment
primary key, `user_id` int unsigned not null default '0' auto_increment primary key comment '用户编号', `nickname` varchar(20) not null default '' comment '昵称', `r
ealname` varchar(20) not null default '' comment '真名', `avatar` varchar(255) not null default '' comment '头像', `introduce` text null comment '简介', `gender` tin
yint(1) not null default '0' comment '性别', `birth` date not null default '0000-00-00' comment '出生日期', `created_at` timestamp null, `updated_at` timestamp null)
default character set utf8mb4 collate utf8mb4_unicode_ci)
[PDOException]
SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value for 'user_id'
FAIL: 1
错误定位:
`user_id` int unsigned not null default '0' auto_increment primary key comment '用户编号',
解决方案:
Remove the second parameter in the integer method. It sets the column as auto-increment. Check the Laravel API for more detials.
整型中不能加入字段长度,否则会出现 auto_increment primary
数据迁移错误问题2:
SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value for 'birth'
错误定位:
$table->date('birth')->default('0000-00-00')->comment('出生日期');
解决方案:
Mysql配置的问题,查证后确定是mysql配置项sql_mode中的NO_ZERO_IN_DATE和NO_ZERO_DATE导致的问题。
NO_ZERO_DATE:在非严格模式下,可以插入形如“0000-00-00 00:00:00”的非法日期,MySQL数据库仅抛出一个警告。而启用该选项后,MySQL数据库不允许插入零日期,插入零日期会抛出错误而非警告。
NO_ZERO_IN_DATE:在严格模式下,不允许日期和月份为零。如“2011-00-01”和“2011-01-00”这样的格式是不允许的。采用日期或月份为零的格式时MySQL都会直接抛出错误而非警告。
mysql 执行
SHOW VARIABLES LIKE 'sql_mode';
SELECT @@sql_mode;
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
mysql5.0以上版本支持三种sql_mode模式:ANSI、TRADITIONAL和STRICT_TRANS_TABLES。
- ANSI模式:宽松模式,对插入数据进行校验,如果不符合定义类型或长度,对数据类型调整或截断保存,报warning警告。
- TRADITIONAL模式:严格模式,当向mysql数据库插入数据时,进行数据的严格校验,保证错误数据不能插入,报error错误。用于事物时,会进行事物的回滚。
- STRICT_TRANS_TABLES模式:严格模式,进行数据的严格校验,错误数据不能插入,报error错误。
解决方案2:
$table->date('birth')->nullable()->comment('出生日期');
数据迁移问题3:
SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint
解决方案:暂时先屏蔽外键连接
4. 用户注册API
- Route的建立
- Model的建立
- 注册方法的建立
接口返回值约定:err表示返回状态,err=1表示出错,err=0表示正确。
Route的建立
function userInstance(){
return new App\Model\User;
}
Route::any('/api/signup',function(){
return userInstance()->signup();
});
Model的建立
php artisan make:model Model\User
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
use Request;
use Hash;
class User extends Model
{
//定义表名
protected $table = 'user';
}
注册方法的建立
/*用户注册API*/
public function signup()
{
$username = Request::get('username');
$password = Request::get('password');
//检查用户名密码是否为空
if(!($username && $password)){
return ['err'=>1, 'msg'=>'用户名和密码必填'];
}
//检查用户名是否存在
if($this->where('username',$username)->exists()){
return ['err'=>1, 'msg'=>'用户名已存在'];
}
//加密密码
// $hash_password = Hash::make($password);
$hash_password = bcrypt($password);//快捷写法
//新增数据
$this->username = $username;
$this->password = $hash_password;
if($this->save()){
return ['err'=>0, 'id'=>$this->id];
}else{
return ['err'=>1, 'msg'=>'数据库新增失败'];
}
// 插入数据并返回
// return $this->save() ? ['err'=>0, 'id'=>$this->id] : ['err'=>1, 'msg'=>'注册失败'];
}
用户注册 API v2
# 路由提取公共部分
// 返回用户实例
function user(){
return new User();
}
//新用户注册
Route::any('api/user', function(){
return user()->signup();
});
# 接口提取公共部分
/*api common : 检测账户和密码*/
// 注意,虽然是公共方法,但在其他模型中使用,此处不要使用 protected
public function has_username_password()
{
//接收请求参数并检查账户和密码是否同时存在
$username = Request::get('username');
$password = Request::get('password');
return $username&&$password ? ['username'=>$username, 'password'=>$password] : false;
}
/*api 用户注册*/
public function signup()
{
//判断参数
$user = $this->has_username_password();
if(!$user){
return ['err'=>1, 'msg'=>'账户或密码为空'];
}
//密码加密
$this->password = bcrypt($user['password']);//bcrypt()是 Hash::make() 的快捷写法
$this->username = $user['username'];
// 插入数据并返回
return $this->save() ? ['err'=>0, 'id'=>$this->id] : ['err'=>1, 'msg'=>'注册失败'];
}
5. 用户登录API
Route的建立
Route::any('/api/login',function(){
return userInstance()->login();
});
登录方法实现
/*用户登录API*/
public function login()
{
$username = Request::get('username');
$password = Request::get('password');
//检测用户名和密码是否为空
if(!($username && $password)){
return ['err'=>1, 'msg'=>'用户名和密码必填'];
}
//判断数据库是否存在
$user = $this->where('username',$username)->first();
if(!$user){
return ['err'=>1, 'msg'=>'用户不存在'];
}
//检查密码是否匹配
$hash_password = $user->password;
if(!Hash::check($password, $hash_password)){
return ['err'=>1, 'msg'=>'密码有误'];
}
//登录成功保存入session
session()->put('user.id',$user->id);
session()->put('user.username',$user->username);
//dd(session()->all());
return ['err'=>0, 'id'=>$user->id];
}
用户注册和登录中存在公共的部分,检测用户名和密码是否为空,可提取出来作为公方法调用。
/*判断用户名与密码*/
public function checkUser()
{
$username = Request::get('username');
$password = Request::get('password');
//检测用户名和密码是否为空
if($username && $password){
return [$username,$password];
}else{
return false;
}
}
用户登陆 API v2
操作流程
- 判断参数是否正确 has_username_password()
- 判断唯一性账户是否存在 first()
- 比对密码是否正确 Hash::check()
- 保存回话 session()->put()
session使用
dd(session()->all());
array:6 [▼
"_token" => "49xoCJDqzefW729KZykmfTfc3vZJUORq6YtAYFau"
"_previous" => array:1 [▼
"url" => "http://laravel.app/api/login?password=secret&username=junchow52"
]
"_flash" => array:2 [▼
"old" => []
"new" => []
]
"url" => [] "login_admin_59ba36addc2b2f9401580f014c7f58ea4e30989d" => 1 "PHPDEBUGBAR_STACK_DATA" => []
]
接口实现
/*api 用户登陆*/
public function login()
{
//参数判断
$user = $this->has_username_password();
if(!$user){
return ['err'=>1, 'msg'=>'账户或密码为空'];
}
//检查账户是否存在
$dbuser = $this->where('username',$user['username'])->first();//获取唯一一条
if(!$dbuser){
return ['err'=>1, 'msg'=>'账户不存在'];
}
//检查密码
if(!Hash::check($user['password'], $dbuser['password'])){
return ['err'=>1, 'msg'=>'密码错误'];
}
//回话记录
session()->put('user_id', $dbuser->id);
session()->put('username', $dbuser->username);
//dd(session()->all());
return ['err'=>0, 'id'=>$dbuser->id];
}
6. 用户退出API
- 选择性清除session信息
- 页面跳转
Route::any('/api/logout',function(){
return userInstance()->logout();
});
/*用户退出API*/
public function logout()
{
//session()->flush();//清空所有session信息
//设置值为null
//session()->put('user.id',null);
//session()->put('user.username',null);
//删除变量(推荐)
session()->forget('user.id');
session()->forget('user.username');
// dd(session()->all());
return redirect('/');//跳转到首页
//return ['err'=>0];//SPA应用返回数据转换为json
}
用户退出 API v2
操作流程
- 清空 session 中保存的数据
//小应用清空所用 session
//session()->flush();
//设置 session 中变量
//session()->put('user_id',null);
//session()->put('username',null);
//session 嵌套
//session()->put('user.friend.name','alice');
//dd(session()->all());
接口实现
/*api 用户退出*/
public function logout()
{
session()->forget('user_id');
session()->forget('username');
return ['err'=>0, 'msg'=>'安全退出'];
}
7. 是否登录
此函数作为公共方法调用,不直接使用接口调用。返回的 false 若在接口中会直接报错。
创建路由
Route::any('test',function(){
dd(user()->islogin());
});
实现方法
/*判断用户是否登录*/
// 此处不要使用 protected,由于其他模型会直接使用。
public function islogin()
{
return session('user.id')?:false;
}
注意:此函数为公共函数,返回 false页面会出现错误。仅用于接口内部使用。
8. 修改密码API
创建路由
Route::any('/api/change_password',function(){
return userInstance()->changePassword();
});
实现方法
/*修改密码API*/
public function changePassword()
{
//判断用户是否登录
if(!session('user.id')){
return ['err'=>1,'msg'=>'请先登录'];
}
//判断新旧密码
if(!rq('old_password') || !rq('new_password')){
return ['err'=>1,'msg'=>'旧密码或旧密码为空'];
}
//判断旧密码是否正确
$user = $this->find(session('user.id'));
if(!Hash::check(rq('old_password'),$user->password)){
return ['err'=>1, 'msg'=>'旧密码错误'];
}
//更改密码
$user->password = bcrypt(rq('new_password'));
return $user->save()?['err'=>0,'msg'=>'修改成功']:['err'=>1,'msg'=>'修改失败'];
}
# 版本2
/*api 修改密码*/
public function resetpwd()
{
//判断是否登录
if(!$this->islogin()){
return ['err'=>1, 'msg'=>'尚未登录'];
}
//判断新旧密码
if(!rq('oldpwd') || !rq('newpwd')){
return ['err'=>1, 'msg'=>'缺少新旧密码'];
}
//判断旧密码
$user = $this->find(session('user_id'));
if(!Hash::check(rq('oldpwd'), $user->password)){
return ['err'=>1, 'msg'=>'旧密码错误'];
}
//更新密码
$user->password = bcrypt(rq('newpwd'));
return $user->save() ? ['err'=>0,'msg'=>'设置成功'] : ['err'=>0,'msg'=>'设置失败'];
}
9.找回密码API
创建路由
Route::any('/api/getback_password',function(){
return userInstance()->getbackPassword();
});
通过手机短信找回密码,此时需要手机短信服务商,并需要在users表中新增phone_captcha字段用于存储短信验证码,以便于密码找回时使用。
创建Migration
php artisan make:migration add_field_phone_captcha -- table=users
添加字段
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddFieldPhoneCaptcha extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
//添加字段
$table->string('phone_captcha');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone_captcha');
});
}
}
执行Migration
php artisan migrate --pretend
php artisan migrate
php artisan migrate:rollback
实现接口
/*获取手机验证码*/
public function getPhoneCaptcha()
{
//测试暂时使用随机数
return rand(1000,9999);
}
/*发送短信验证码*/
public function sendSms($data)
{
return true;
}
/*找回密码API*/
public function getbackPassword()
{
//使用手机找回密码
if(!rq('phone')){
return ['err'=>1, 'msg'=>'请填写手机号码'];
}
//判断手机号码是否存在数据库中
$user = $this->where('phone',rq('phone'))->first();
if(!$user){
return ['err'=>1, 'msg'=>'手机号码不存在'];
}
//获取手机验证码
$phone_captcha = $this->getPhoneCaptcha();
//若保存成功则发送短信否则给出提示;
$user->phone_captcha = $phone_captcha;
if($user->save()){
//发送短信验证码
$this->sendSms($phone_captcha);
return ['err'=>0, 'msg'=>'操作成功'];
}else{
return ['err'=>1,'msg'=>'操作失败'];
}
}
此时存在问题是,若用户使用短信轰炸的方式发送大量垃圾短信,此此处接口调用会出现问题,如何预防此种方式呢?
可采用时间判断和黑名单的方式来对此进行约束,此处使用两次发送时间间隔判断的方式来避免机器人发送短信。
/*找回密码API*/
public function getbackPassword()
{
//预防短信轰炸
$curtime = time();
$smstime = session('send_sms_time');
//若10秒内调用两次接口
if($curtime-$smstime > 10){
return ['err'=>1,'msg'=>'操作频繁,请间隔10秒后操作!'];
}
//使用手机找回密码
if(!rq('phone')){
return ['err'=>1, 'msg'=>'请填写手机号码'];
}
//判断手机号码是否存在数据库中
$user = $this->where('phone',rq('phone'))->first();
if(!$user){
return ['err'=>1, 'msg'=>'手机号码不存在'];
}
//获取手机验证码
$phone_captcha = $this->getPhoneCaptcha();
//若保存成功则发送短信否则给出提示;
$user->phone_captcha = $phone_captcha;
if($user->save()){
//发送短信验证码
$this->sendSms($phone_captcha);
//检查两次短信发送时间
session('validate_time',time());
return ['err'=>0, 'msg'=>'操作成功'];
}else{
return ['err'=>1,'msg'=>'操作失败'];
}
}
10. 验证找回密码API
创建路由
Route::any('/api/validate_password',function(){
return userInstance()->validatePassword();
});
实现方法
/*验证找回密码*/
public function validatePassword()
{
//检查是否传入电话号码
if(!rq('phone') || !rq('phone_captcha') || !rq('password')){
return ['err'=>1, 'msg'=>'请填写手机号码和短信验证码'];
}
//根据手机号码获取用户
$user = $this->where(['phone'=>rq('phone'),'phone_captcha'=>rq('phone_captcha')])->first();
if(!$user){
return ['err'=>1,'msg'=>'用户不存在或短信验证码错误'];
}
//修改密码
$user->password = bcrypt(rq('password'));
return $user->save() ? ['err'=>0,'msg'=>'操作成功'] : ['err'=>1,'msg'=>'操作失败'];
}
此处存在的安全漏洞是,当找回密码接口频繁调用时,有可能是采用机器人不断进行暴力破解的方式,来对密码进行猜测。
封装公共方法用于判断是否为机器人提交
/*判断是否为机器人提交*/
public function isRobot($second)
{
if(!session('validate_time')){
return false;
}
//若两次时间间隔小于n秒则断定为机器人访问
return time()-session('validate_time') < $second;
}
/*更新机器人时间*/
public function setRobotTime()
{
// session()->set('validate_time',time());
session(['validate_time'=>time()]);
}
使用时间间隔判断来进行约束
/*验证找回密码*/
public function validatePassword()
{
//判断两次调用接口的时间间隔来防止机器人暴力破解,同时用户可能存在误操作。
if($this->isRobot(3)){
return ['err'=>1,'msg'=>'操作频繁,请间隔3秒后操作。'];
}
//检查是否传入电话号码
if(!rq('phone') || !rq('phone_captcha') || !rq('password')){
return ['err'=>1, 'msg'=>'请填写手机号码和短信验证码'];
}
//根据手机号码获取用户
$user = $this->where(['phone'=>rq('phone'),'phone_captcha'=>rq('phone_captcha')])->first();
if(!$user){
return ['err'=>1,'msg'=>'用户不存在或短信验证码错误'];
}
//修改密码
$user->password = bcrypt(rq('password'));
if($user->save()){
$this->setRobotTime();//更新机器人行为时间
return ['err'=>0,'msg'=>'操作成功'];
}else{
return ['err'=>1,'msg'=>'操作失败'];
}
}
10. 个人资料API
创建路由
Route::any('/api/profile',function(){
return userInstance()->profile();
});
实现方法
/*获取用户个人资料API*/
public function profile()
{
//获取用户编号,游客也可查看个人信息。
if(!rq('id')){
return ['err'=>1,'msg'=>'参数错误'];
}
//获取指定用户信息
$fields = ['username','avatar','intro'];
$user = $this->find(rq('id'),$fields);
if(!$user){
return ['err'=>1,'msg'=>'用户不存在'];
}
//获取用户的提问数
$question_count = questionInstance()->where('user_id',rq('id'))->count();
//获取用户的回答数
$answer_count = $user->answers()->count();//多表查询必须提前建立多表关系
//返回数据
$data = $user->toArray();
$data['question_count'] = $question_count;
$data['answer_count'] = $answer_count;
return ['err'=>0,'data'=>$data];
}