Laravel Question

  • 问答系统

用户API

  • Migration

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');
    }
}
<?php

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

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->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

<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{

    public function run()
    {
      // 使用工厂批量生成数据
        factory('App\Models\User',10)->create()->each(function($u){
            //$u->posts()->save(factory('App\Models\Post')->make());
        });
    }
}

# app/database/factories/ModelFactory.php
<?php
use App\Models\User;

$factory->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

<?php

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

class CreateProfilesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('profiles', function (Blueprint $table) {
            $table->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];
}

问题API

1. 创建问题的Migration

php artisan make:migration create_questions_table --create=questions

出现错误

[ErrorException]                                                                                                                                                       
  include(/Users/junchow/Code/laravel/vendor/composer/../../database/migrations/2017_06_22_150313_create_questions_table.php): failed to open stream: No such file or directory                                                                                                                                                               

解决方案

composer update

composer dumpautoload

database/migrations/2017_03_02_065319_create_questions_table.php
注意:创建表时表名使用复数形式

2. 创建表结构与操作

<?php

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

class CreateTableQuestions extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('questions', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            // 自定义字段
            $table->string('title',64)->comment('问题标题');//标题非空可重复
            $table->text('desc')->nullable()->comment('问题描述');//描述为空
            $table->unsignedInteger('user_id')->comment('用户编号');
            $table->tinyInteger('status')->default(0)->comment('审核状态');
            //外键
            $table->foreign('user_id')->references('id')->on('users');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('questions');
    }
}

# 版本2

Schema::create('questions', function (Blueprint $table) {
    $table->increments('id');

    $table->string('title',128)->default('')->comment('标题');
    $table->text('description')->nullable()->comment('描述');

    $table->unsignedInteger('user_id')->default('0')->comment('评论人');
    $table->foreign('user_id')->references('id')->on('users');

    $table->unsignedInteger('admin_id')->default('0')->comment('审核人');

    $table->unsignedInteger('status')->default('0')->comment('审核状态');

    $table->timestamps();
});

创建表结构

php artisan migrate --pretend
php artisan migrate:rollback
php artisan migrate

3. 创建问题模型

php artisan make:model Model\Question
namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class Question extends Model
{
    //
}

4.问题API的实现

在路由文件中为对公共方法进行抽取

/*获取URL地址参数*/
function rq($key=null, $default=null){
    if(!$key){
        return Request::all();
    }
    return Request::get($key,$default);
}

/*实例化问题对象*/
function questionInstance(){
    return new App\Model\Question;
}

版本2

# 路由文件 route.php 或 web.php 中提取公共方法

use Illuminate\Support\Facades\Request;

/*Common API Function*/
function rq($key=null, $default=null){
    return $key ? Request::get($key,$default) : Request::all();
}
//获取 question 模型实例
function question(){
    return new Question();
}

接口返回值约定:err为错误状态,err=1表示出错,err=0表示正确。

4.1 增加问题API

建立路由


Route::any('/api/question/add',function(){
    return questionInstance()->add();
});

实现方法

/*添加问题API*/
public function add()
{
    //判断用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //判断必填字段
    if(!rq('title')){
        return ['err'=>1, 'msg'=>'请填写标题'];
    }
    //判断可选字段
    if(!rq('desc')){
        $this->desc = rq('desc');
    }

    $this->user_id = session('user.id');
    $this->title = rq('title');
    return $this->save() ? ['err'=>0, 'id'=>$this->id] : ['err'=>1,'msg'=>'添加失败'];
}

接口实现 v2

//API 添加问题
public function add()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }
    $this->user_id = session('user_id');
    //判断标题是否传入(必选)
    if(!rq('title')){
        return ['err'=>1, 'msg'=>'标题不存在'];
    }
    $this->title = rq('title');
    //判断描述是否传入(可选)
    if(rq('description')){
        $this->description = rq('description');
    }

    return $this->save() ? ['err'=>0, 'msg'=>'添加成功','id'=>$this->id] : ['err'=>1,'添加失败'];
}
遗留问题

接口中字段的验证,需使用 validator 完善。

4.2 修改问题API

建立路由

Route::any('/api/question/edit',function(){
  return questionInstance()->edit();
});

实现方法

/*修改问题API*/
public function edit()
{
    //判断用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //判断必填字段
    if(!rq('id')){
        return ['err'=>1, 'msg'=>'参数错误'];
    }

    //获取记录
    $question = $this->find(rq('id'));
    //若数据库不存在问题记录
    if(!$question){
        return ['err'=>1, 'msg'=>'暂无数据'];
    }
    //问题仅限创建者可修改
    if($question->user_id != session('user.id')){
        return ['err'=>1, 'msg'=>'权限不足'];
    }
    //获取用户修改的数据
    if(rq('title')){
        $question->title = rq('title');
    }
    if(rq('desc')){
        $question->desc = rq('desc');
    }
    return $question->save() ? ['err'=>0] : ['err'=>1,'msg'=>'修改失败'];
}

实现版本2

// API 编辑问题
public function edit()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }
    //判断问题编号
    if(!rq('id')){
        return ['err'=>1,'msg'=>'参数错误'];
    }
    //仅发布人可修改
    $question = $this->find(rq('id'));
    if(!$question){
        return ['err'=>1,'msg'=>'问题不存在'];
    }
    //仅发布人可修改
    if($question['user_id'] != session('user_id')){
        return ['err'=>1, 'msg'=>'仅发布人可编辑'];
    }
    //获取更新字段
    if(rq('title')){
        $question->title = rq('title');
    }
    if(rq('description')){
        $question->description = rq('description');
    }
    //数据库更新
    return $question->save()?['err'=>0,'msg'=>'编辑成功']:['err'=>1,'编辑失败'];
}

4.3 查看问题API

建立路由

Route::any('/api/question/read',function(){
    return questionInstance()->read();
});

实现方法

/*查看问题API*/
public function read()
{
    if(rq('id')){
        $data = $this->find(rq('id'),['id','title','desc']);
        return ['err'=>0, 'data'=>$data];
    }
    //默认分页查询
    $limit = rq('limit')?:15;//每页显示条数
    $skip = (rq('page')?:0)*$limit;//页码从0开始
    $data = $this->orderBy('created_at')->limit($limit)->skip($skip)->get(['id','title','desc'])->keyBy('id');
    return ['err'=>0, 'data'=>$data];
}

实现版本2

//API 查看问题
public function read()
{
    //指定ID查询
    if(rq('id')){
        return ['err'=>0,'data'=>$this->find(rq('id'))];
    }

    //默认查看多条
    $limit = rq('limit') ? : 3;// 每页条数
    $skip = (rq('page')?rq('page')-1:0) * $limit;// 页码求位移
    $field = ['id','title','description','created_at'];
    $data = $this->orderBy('created_at')->limit($limit)->skip($skip)->get($field)->keyBy('id');
    
    //返回 collection 数据
    return $data ? ['err'=>0, 'data'=>$data] : ['err'=>1,'暂无数据'];
}

4.4 删除问题API

建立路由

Route::any('/api/question/remove',function(){
    return questionInstance()->remove();
});

实现方法

/*删除问题API*/
public function remove()
{
    //判断用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //判断必填字段
    if(!rq('id')){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //判断问题是否存在
    $question = $this->find(rq('id'));
    if(!$question){
        return ['err'=>1, 'msg'=>'暂无数据'];
    }
    //仅有问题所有者才有权限删除
    if($question->user_id != session('user.id')){
        return ['err'=>1, 'msg'=>'权限不足'];
    }
    //删除记录
    return $question->delete() ? ['err'=>0] : ['err'=>1,'msg'=>'删除失败'];
}

实现版本2

//API 删除问题
public function del()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }
    //判断问题编号是否存在
    if(!rq('id')){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //判断问题是否存在
    $question = $this->find(rq('id'));
    if(!$question){
        return ['err'=>1, 'msg'=>'数据不存在'];
    }
    //发布者可删除,审核人可删除(未实现)
    if(session('user_id') != $question['user_id']){
        return ['err'=>1, 'msg'=>'权限不足'];
    }
    
    //返回数据
    return $question->delete() ? ['err'=>0,'msg'=>'删除成功'] : ['err'=>1,'msg'=>'删除失败'];
}

回答API

  • 创建回答Migration
  • 创建回答表结构
  • 创建回答表模型
  • 实现回答表操作

1. 创建回答Migration

php artisan make:migration create-table-answers --create=answers

# Lavarel 5.4
php artisan make:migration create_answers_table --create=answers

2. 创建回答表结构

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

class CreateTableAnswers extends Migration
{

    public function up()
    {
        Schema::create('answers', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();

            //自定义字段
            $table->text('content')->comment('回答内容');
            $table->unsignedInteger('user_id')->comment('回答用户');
            $table->unsignedInteger('question_id')->comment('问题编号');
            //外键连接
            $table->foreign('user_id')->references('id')->on('users');
            $table->foreign('question_id')->references('id')->on('questions');
        });
    }
    
    public function down()
    {
        Schema::dropIfExists('answers');
    }
}
php artisan migrate --pretend

php artisan migrate

php artisan migrate:rollback

3. 创建回答表模型

php artisan make:model Model\Answer

MAC Laravel5.4
php artian make:model Models\\Answer
namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class Answer extends Model
{
    
}

4. 实现回答表操作

路由文件中提取公共函数

function answerInstance(){
    return new App\Model\Answer;
}
# 版本2
use App\Models\Answer;

function answer(){
    return new Answer();
}

4.1 添加回答API

创建路由

Route::any('/api/answer/add',function(){
    return answerInstance()->add();
});

实现方法

/*添加回答API*/
public function add()
{
    //检查用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //检查问题是否存在
    if(!(rq('question_id') && rq('content'))){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //检查问题是否存在
    $question = questionInstance()->find(rq('question_id'));
    if(!$question){
        return ['err'=>1, 'msg'=>'问题不存在'];
    }
    //相同问题进行回答一次
    $count = $this->where(['question_id'=>rq('question_id'), 'user_id'=>session('user.id')])->count();
    if($count){
        return ['err'=>1, 'msg'=>'您已回答'];
    }
    //添加数据库
    $this->user_id = session('user.id');
    $this->question_id = rq('question_id');
    $this->content = rq('content');
    return $this->save() ? ['err'=>0,'id'=>$this->id] : ['err'=>1,'msg'=>'添加失败'];
}

# 版本2
/*API 添加回答*/
public function add()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }
    $this->user_id = session('user_id');

    //判断问题和回答
    if(!rq('question_id') || !rq('content')){
        return ['err'=>1, 'msg'=>'缺少问题编号和回答内容'];
    }

    //判断问题是否存在
    $question = question()->find(rq('question_id'));
    if(!$question){
        return ['err'=>1, 'msg'=>'问题不存在'];
    }
    $this->question_id = $question->id;

    //同一问题仅作答一次
    $count = $this->where(['question_id'=>rq('question_id'), 'user_id'=>session('user_id')])->count();
    if($count){
        return ['err'=>1, 'msg'=>'已回答过'];
    }

    //数据库添加
    $this->content=rq('content');
    return $this->save() ? ['err'=>0,'msg'=>'回答成功','id'=>$this->id] : ['err'=>0,'msg'=>'回答失败'];

}

4.2 更新回答API

创建路由

Route::any('/api/answer/edit',function(){
    return answerInstance()->edit();
});


实现方法

/*更新回答API*/
public function edit()
{
    //检查用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //检查答案是否存在
    if(!(rq('id') && rq('content')) ){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //检查当答案的拥有者是否为当前用户
    $answer = $this->find(rq('id'));
    if($answer->user_id != session('user.id')){
        return ['err'=>1, 'msg'=>'权限不足'];
    }
    //更新答案
    if(rq('content')){
        $answer->content = rq('content');
    }
    return $answer->save() ? ['err'=>0] : ['err'=>1,'msg'=>'更新失败'];
}

# 版本2
/*API 编辑回答*/
public function edit()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }

    //判断回答编号是否存在
    if(!rq('id') || !rq('content')){
        return ['err'=>1, 'msg'=>'缺少回答编号和回答内容'];
    }

    //判断回答是否存在
    $answer = answer()->find(rq('id'));
    if(!$answer){
        return ['err'=>1, 'msg'=>'回答不存在'];
    }

    //作答人可编辑
    if($answer['user_id'] != session('user_id')){
        return ['err'=>1, 'msg'=>'权限不足'];
    }

    //数据库更新
    $answer->content=rq('content');

    return $answer->save() ? ['err'=>0,'msg'=>'编辑成功'] : ['err'=>0,'msg'=>'编辑失败'];

}

4.3 查看回答API

创建路由

Route::any('/api/answer/read',function(){
    return answerInstance()->read();
});

实现方法

/*查看回答API*/
public function read()
{
    //当任意一个参数不存在时均报错
    if(!rq('id') && !rq('question_id')){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //根据回答编号查看回答
    if(rq('id')){
        $data = $this->find(rq('id'));
        if(!$data){
            return ['err'=>1, 'msg'=>'暂无数据'];
        }
        return ['err'=>0, 'data'=>$data];
    }
    //根据问题编号查看回答
    if(rq('question_id')){
        $data = $this->where('question_id',rq('question_id'))->get(['id','content'])->keyBy('id');
        if(!$data){
            return ['err'=>1, 'msg'=>'暂无数据'];
        }
        return ['err'=>0, 'data'=>$data];
    }
}

# 版本2
/*API 查看回答*/
public function read()
{
    if(!rq('id') && !rq('question_id')){
        return ['err'=>1, 'msg'=>'缺少回答编号或问题编号'];
    }

    //查看单条回答
    if(rq('id')){
        $answer = $this->find(rq('id'));
        if(!$answer){
            return ['err'=>1,'msg'=>'回答不存在'];
        }
        return ['err'=>0,'data'=>$answer];
    }

    //判断问题是否存在
    $question = question()->find(rq('question_id'));
    if(!$question){
        return ['err'=>1, 'msg'=>'问题不存在'];
    }

    //查看指定问题下的回答
    $answers = $this->where('question_id',rq('question_id'))->get()->keyBy('id');
    return ['err'=>0,'data'=>$answers];
}

评论API


  • 创建评论Migration
  • 创建评论表结构
  • 创建评论表模型
  • 实现评论API
    • 添加评论API
    • 查看评论API
    • 删除评论API

1. 创建评论Migration

php artisan make:migration create_table_comments --create=comments

2. 创建评论表结构

<?php

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

class CreateTableComments extends Migration
{
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            //自定义字段
            $table->text('content')->comment('评论内容');
            $table->unsignedInteger('user_id')->comment('评论人');
            $table->unsignedInteger('question_id')->nullable()->comment('问题编号');//可为空,由于无法确认是针对问题评论
            $table->unsignedInteger('answer_id')->nullable()->comment('回答编号');//可为空,由于无法确认是针对回答评论
            $table->unsignedInteger('reply_to')->nullable()->comment('回复评论');
            //外键关系
            $table->foreign('user_id')->references('id')->on('users');
            $table->foreign('question_id')->references('id')->on('questions');
            $table->foreign('answer_id')->references('id')->on('answers');
            $table->foreign('reply_to')->references('id')->on('comments');

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

版本2
public function up()
{
    Schema::create('comments', function (Blueprint $table) {
        $table->increments('id');

        $table->text('content')->comment('内容');

        $table->unsignedInteger('user_id')->default('0')->comment('评论人');
        $table->foreign('user_id')->references('id')->on('users');

        $table->unsignedInteger('question_id')->nullable()->comment('问题');
        $table->foreign('question_id')->references('id')->on('questions');

        $table->unsignedInteger('answer_id')->nullable()->comment('回答');
        $table->foreign('answer_id')->references('id')->on('answers');

        $table->unsignedInteger('reply_id')->nullable()->comment('回复评论');
        $table->foreign('reply_id')->references('id')->on('comments');

        $table->timestamps();
    });
}

注意:reply_id 是针对评论而发表的评论,因此外键连接的位置仍然是 comments 表。

php artisan migrate --pretend
php artisan migrate:rollback
php artisan migrate

3. 创建评论表模型

php artisan make:model Model\Comment
namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    
}

4.实现评论API

路由文件中提取公共函数

function commentInstance(){
    return new App\Model\Comment;
}

4.1 添加评论API

创建路由

Route::any('/api/comment/add',function(){
    return commentInstance()->add();
});

实现方法

/*添加评论API*/
public function add()
{
    //检查用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //判断是否具有评论内容
    if(!rq('content')){
        return ['err'=>1, 'msg'=>'请先评论'];
    }
    //判断是对问题评论或是对回答评论
    if(!rq('question_id') && !rq('answer_id')){
        return ['err'=>1, 'msg'=>'请针对问题或回答进行评论'];
    }
    //判断是否同时对问题或答案评论
    if(rq('questioin_id') && rq('answer_id')){
        return ['err'=>1, 'msg'=>'请勿针对问题和答案同时评论'];
    }
    //若针对问题评论
    if(rq('question_id')){
        //判断问题是否存在
        $question = questionInstance()->find(rq('question_id'));
        if(!$question){
            return ['err'=>1, 'msg'=>'问题不存在'];
        }
        $this->question_id = rq('question_id');
    }
    //若针对回答评论
    if(rq('answer_id')){
        //判断回答是否存在
        $answer = answerInstance()->find(rq('answer_id'));
        if(!$answer){
            return ['err'=>1, 'msg'=>'回答不存在'];
        }
        $this->answer_id = rq('answer_id');
    }
    //判断是否对自身评论进行回复
    if(rq('reply_to')){
        $target = $this->find(rq('reply_to'));
        if(!$target){
            return ['err'=>1, 'msg'=>'目录评论不存在'];
        }
        //禁止自己回复自己
        if($target->user_id == session('user.id')){
            return ['err'=>1, 'msg'=>'禁止回复自身评论'];
        }
    }
    $this->content = rq('content');
    $this->user_id = session('user.id');
    $this->reply_to = rq('reply_to');
    //插入数据库并返回结果
    return $this->save() ? ['err'=>0,'id'=>$this->id] : ['err'=>1, 'msg'=>'新增失败'];
}

# 版本2
/*API 添加评论*/
public function add()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }
    $this->user_id = session('user_id');

    //判断评论内容
    if(!rq('content')){
        return ['err'=>1, 'msg'=>'缺少评论内容'];
    }
    $this->content = rq('content');

    //针对问题或回答评论
    if(!rq('question_id') && !rq('answer_id')){
        return ['err'=>1, 'msg'=>'缺少问题编号或回答编号'];
    }
    if(rq('question_id') && rq('answer_id')){
        return ['err'=>1, 'msg'=>'禁止同时评论问题和回答'];
    }

    //针对问题评论
    if(rq('question_id')){
        //判断问题是否存在
        $question = question()->find(rq('question_id'));
        if(!$question){
            return ['err'=>1,'msg'=>'问题不存在'];
        }
        $this->question_id = rq('question_id');
    }

    //针对回答评论
    if(rq('answer_id')){
        //判断回答是否存在
        $answer = answer()->find(rq('answer_id'));
        if(!$answer){
            return ['err'=>1, 'msg'=>'回答不存在'];
        }
        $this->answer_id = rq('answer_id');
    }

    //针对评论而评论
    if(rq('reply_id')){
        //判断评论是否存在
        $comment = $this->find(rq('reply_id'));
        if(!$comment){
            return ['err'=>1, 'msg'=>'评论不存在'];
        }

        //禁止对自己评论进行评论
        if($comment->user_id == session('user_id')){
            return ['err'=>1, 'msg'=>'禁止自身评论'];
        }

        $this->reply_id = rq('reply_id');
    }
    

    //保存数据
    return $this->save()?['err'=>0,'msg'=>'评论成功','id'=>$this->id]:['err'=>1,'msg'=>'评论失败'];

}

4.2 查看评论API

创建路由

Route::any('/api/comment/read',function(){
    return commentInstance()->read();
});

实现方法

public function read()
{
    //判断参数是否存在
    if(!rq('question_id') && !rq('answer_id')){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //根据问题查看评论
    if(rq('question_id')){
        //判断问题是否存在
        $question = questionInstance()->find(rq('question_id'));
        if(!$question){
            return ['err'=>1, 'msg'=>'问题不存在'];
        }
        //获取问题下所有评论
        $data = $this->where('question_id',rq('question_id'))->get();

    }
    //根据回答查看评论
    if(rq('answer_id')){
        //判断回答是否存在
        $answer = answerInstance()->find(rq('answer_id'));
        if(!$answer){
            return ['err'=>1, 'msg'=>'回答不存在'];
        }
        //获取回答下所有评论
        $data = $this->where('answer_id',rq('answer_id'))->get();
    }
    return $data->isEmpty() ? ['err'=>1, 'msg'=>'暂无评论'] : ['err'=>0,'data'=>$data->keyBy('id')];
}

# 版本2
/*API 查看评论*/
public function read()
{
    //判断参数
    if(!rq('question_id') && !rq('answer_id')){
        return ['err'=>1, 'msg'=>'缺少问题编号或回答编号'];
    }

    //查看问题的评论
    if(rq('question_id')){
        //判断问题是否存在
       $question = question()->find(rq('question_id'));
       if(!$question){
           return ['err'=>1, 'msg'=>'问题不存在'];
       }
        //获取问题下所有评论
       $comment = $this->where('question_id',rq('question_id'));
    }

    //查看回答下的评论
    if(rq('answer_id')){
        //判断回答是否存在
        $answer = answer()->find(rq('answer_id'));
        if(!$answer){
            return ['err'=>1, 'msg'=>'回答不存在'];
        }
        //获取回答下所有评论
        $comment = $this->where('answer_id', rq('answer_id'));
    }

    $data = $comment->get()->keyBy('id');
    //返回评论
    return $data->isEmpty() ? ['err'=>1, 'msg'=>'暂无评论'] : ['err'=>0, 'data'=>$data];
}

4.3 删除评论API

创建路由

Route::any('/api/comment/remove',function(){
    return commentInstance()->remove();
});

实现方法

/*删除评论API*/
public function remove()
{
    //检查用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //根据评论编号删除
    if(!rq('id')){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //判断评论是否存在
    $comment = $this->find(rq('id'));
    if(!$comment){
        return ['err'=>1, 'msg'=>'评论不存在'];
    }
    //判断是否为本人评论
    if($comment->user_id != session('user.id')){
        return ['err'=>1, 'msg'=>'权限错误'];
    }
    //删除所有回复过评论的评论
    $this->where('reply_to',rq('id'))->delete();
    //删除评论
    return $comment->delete() ? ['err'=>0, 'msg'=>'删除成功'] : ['err'=>1, 'msg'=>'删除失败'];
}

# 版本2
/*API 删除评论*/
public function del()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }

    //判断评论编号
    if(!rq('id')){
        return ['err'=>1, 'msg'=>'缺少评论编号'];
    }

    //判断评论是否存在
    $comment = $this->find(rq('id'));
    if(!$comment){
        return ['err'=>1, 'msg'=>'评论不存在'];
    }

    //自身可删除
    if($comment->user_id != session('user_id')){
        return ['err'=>1, 'msg'=>'权限不足'];
    }

    //删除评论下的回复
    $this->where('reply_id',rq('id'))->delete();

    //删除评论
    return $comment->delete()?['err'=>0,'msg'=>'删除成功']:['err'=>1,'msg'=>'删除失败'];
}

顶踩投票API

简介

当对某用户的回答做点赞或点差时,此时用户、回答两个实体之间存在多对多的关联关系,而用户、回答、投票三者组合必须是唯一的,即用户不能为回答投多票。而类似功能均可作为通用API来实现。

通用API应用场景

  • 当涉及到多表查询时需用通用API
  • 零碎功能点所需要的API

1.创建多对多关联关系

回答表和用户表之间隶属于多对多的关联关系,laravel中称这种中间表为轴心表。
(1)在模型中创建多对多的关联关系
app\Model\Answer.php

/*答案和用户之间隶属于多对多的关联关系*/
public function users()
{
    //默认中间表仅保存关联字段,新增字段需使用withPivot注明
    return $this
        ->belongsToMany('App\Model\User')
        ->withPivot('vote') //执行自定义新增字段(轴心)
        ->withTimestamps();//同时更新时间
}

app\Model\User.php

/*答案和用户之间隶属于多对多的关联关系*/
public function answers()
{
    //默认中间表仅保存关联字段,新增字段需使用withPivot注明
    return $this
        ->belongsToMany('App\Model\Answer')
        ->withPivot('vote') //执行自定义新增字段
        ->withTimestamps();//同时更新时间
}

(2)创建多对多中间表

php artisan make:migration create_table_answer_user --create=answer_user

注意:中间表表名不使用复数形式,必须使用单数。

(3)创建中间关联表结构

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

class CreateTableAnswerUser extends Migration
{
    public function up()
    {
        Schema::create('answer_user', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            //自定义字段
            $table->unsignedInteger('user_id');//用户表主键
            $table->unsignedInteger('answer_id');//回答表主键
            $table->unsignedSmallInteger('vote');//票数
            //外键
            $table->foreign('user_id')->references('id')->on('user');
            $table->foreign('answer_id')->references('id')->on('answer');
            //唯一键设置
            $table->unique(['user_id','answer_id','vote']);
        });
    }
    public function down()
    {
        Schema::dropIfExists('answer_user');
    }
}

# 版本2
Schema::create('answer_user', function (Blueprint $table) {
    $table->increments('id');

    $table->unsignedInteger('user_id')->default('0')->comment('用户');
    $table->foreign('user_id')->references('id')->on('users');

    $table->unsignedInteger('answer_id')->default('0')->comment('回答');
    $table->foreign('answer_id')->references('id')->on('answers');
    
    $table->unsignedTinyInteger('vote')->default('0')->comment('投票:0踩1赞');

    $table->unique(['user_id','answer_id','vote']);//一个用户只能对一个问题投一次票(顶或踩)

    $table->timestamps();
});

(4)生成表结构

php artisan migrate:pretend
php artisan migrate
php artisan migrate:rollback

2. 为回答投票

(1)创建路由 routes\web.php

Route::any('/api/answer/vote',function(){
    return answerInstance()->vote();
});

(2)实现方法 app\Model\Answer.php

/*使用中间表为问题投票*/
public function vote()
{
    //检查用户是否登录
    if(!userInstance()->isLogin()){
        return ['err'=>1, 'msg'=>'请先登录'];
    }
    //判断必填参数
    if(!rq('id') || !rq('vote')){
        return ['err'=>1, 'msg'=>'参数错误'];
    }
    //检查回答是否存在
    $answer = $this->find(rq('id'));
    if(!$answer){
        return ['err'=>1, 'msg'=>'回答不存在'];
    }
    //检查当前用户是否在回答上重复投票,若中间表已存在数据则先存在后添加
    $answer->users()->newPivotStatement()
        ->where('user_id',session('user.id'))
        ->where('answer_id',rq('id'))
        ->delete();
    //向中间表添加
    $vote = rq('vote')<=1 ? 1 : 2;//1赞同 2反对
    $answer->users()->attach(session('user.id'),['vote'=>$vote]);
    return ['err'=>0];
}

# 版本2
/*API 用户投票*/
public function vote()
{
    //判断用户是否登录
    if(!user()->islogin()){
        return ['err'=>1, 'msg'=>'尚未登录'];
    }

    //判断回答编号和投票是否存在
    if(!rq('id')){
        return ['err'=>1, 'msg'=>'缺少回答编号'];
    }
    if(!Request::has('vote')){
        return ['err'=>1, 'msg'=>'缺少投票'];
    }

    //判断投票类型
    $vote = rq('vote') ? 1 : 0;//1顶0踩

    //判断问题是否存在
    $answer = $this->find(rq('id'));
    if(!$answer){
        return ['err'=>1, 'msg'=>'回答不存在'];
    }

    //判断用户在相同回答下是否已投票,注意 answer_user 中 answer_id+user_id+vote 必须唯一,即一个用户对一个问题只能投一票。
    //获取中间表
    $answer_user = $answer->user()->newPivotStatement();
    //删除投票记录
    $answer_user->where(['answer_id'=>rq('id'), 'user_id'=>session('user_id')])->delete();
    //添加投票
    $answer->user()->attach(session('user_id'), ['vote'=>$vote]);

    return ['err'=>0,'msg'=>'投票成功'];

}

时间线API

简介

首页需要获取时间线的数据,时间线的数据是问题和回答的组合,作为一个综合模型调用的功能,可作为通用API。

创建路由
由于时间线调用多张表的数据,因此将其放置在单独的控制器中实现。

Route::any('/api/timeline','CommonController@timeline');

路由文件中提取公共分页函数

function paginate($page=1,$limit=15){
    $page = $page?$page-1:0;
    $limit = $limit?:15;
    return [$limit, $page*$limit];
}

创建控制器

php artisan make:controller CommonController

实现接口

/*获取时间线API*/
public function timeline()
{
    //获取页码和每页显示条数
    list($limit, $skip)=paginate(rq('page'),rq('limit'));
    //获取所有问题与答案
    $questions = questionInstance()->limit($limit)->skip($skip)->orderBy('created_at','desc')->get();
    $answers = answerInstance()->limit($limit)->skip($skip)->orderBy('created_at','desc')->get();
    //将问题和答案合并
    $data = $questions->merge($answers);//merge()合并集合存在bug
    //根据创建时间排序
    $data = $data->sortBy(function($item){
        return $item->created_at;
    });
    return $data->values()->all();
}

# 版本2
/*API 时间线*/
public function timeline()
{
    list($limit,$skip) = pager(rq('page'), rq('limit'));

    //获取所有问题和回答
    $questions = question()->limit($limit)->skip($skip)->orderBy('created_at', 'DESC')->get();
    $answers = answer()->limit($limit)->skip($skip)->orderBy('created_at', 'DESC')->get();

    //合并数据后按时间降序排列
    $data = $questions->merge($answers);
    $data->sortByDesc(function($item){
       return $item->created_at;
    });

    //去除默认的 key
    $data  = $data->values()->all();

    return $data;
}

遗留问题
当前时间线中糅合了问题和回答,若种类比较多时,此时为区分每条记录所代表的类型,需添加类型字段以指明具体数据所属模块。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 1.MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品。My...
    黄花菜已凉阅读 4,568评论 3 60
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,067评论 25 707
  • 刚好遇见你 留下足迹才美丽 第一次知道你的名字还是小学 只闻其名 不见其人 再见到你已经是初中了 带着一脸质疑 听...
    辞笙阅读 101评论 0 1
  • 又是一个周五,跟往常不一样的是今天的风格外大,下着雨,虽然气温不是很低,却也让人感到刺骨的凉意。 没有去图书馆,吃...
    疏棠阅读 607评论 3 7