咖啡店标签后端 API 接口功能实现
接下来的两篇教程中,我们会咖啡店实现打标签功能,这个标签与咖啡店、标签和用户相关联,我们将最终根据用户为每个咖啡店标记的标签数量为排序条件为每个咖啡店构建标签云,由于功能相对而言比较复杂,我们分两篇教程来实现,首先实现后端相关 API 接口,从后端角度来说,这也是一个多对多关联,只不过在中间表中需要引入用户 ID 以便区分不同用户打的标签。
实现标签系统后,我们就可以通过多种维度对咖啡店进行筛选,比如在首页构建一个简单的咖啡店分拣组件。
第一步:创建标签表
在实现标签功能之前,首先需要创建相应的数据表,这里,我们需要创建两张表,一张用于存储标签,一张用于存储标签、咖啡店、用户三者关联关系。在项目根目录下,运行如下命令来创建标签表数据库迁移,指定表名为 tags
:
php artisan make:migration create_tags_table --create=tags
然后编辑新创建的 CreateTagsTable
迁移类的 up
方法,我们只是在这张表中新增一个 tag
字段用于存储标签名:
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->unique();
$table->timestamps();
});
}
需要注意的是,name
字段上定义了唯一索引,意味着标签值在标签表中是唯一的。
第二步:创建中间表
接下来创建存储标签、咖啡店、用户三者关联关系的中间表 cafes_users_tags
:
php artisan make:migration create_cafes_users_tags_table --create=cafes_users_tags
编辑新创建的 CreateCafesUsersTagsTable
迁移类的 up
方法如下:
public function up()
{
Schema::create('cafes_users_tags', function (Blueprint $table) {
$table->integer('cafe_id')->unsigned();
$table->integer('user_id')->unsigned();
$table->integer('tag_id')->unsigned();
$table->primary(['cafe_id', 'user_id', 'tag_id'], 'cafes_users_tags_primary');
$table->timestamps();
});
}
一个用户只能不能重复给一个咖啡店打相同的标签,如果不需要知道谁打的标签,可以去掉 user_id
字段,但这样就无法为每个咖啡店统计同一个标签的数量了。
创建并编写好上面两个迁移类之后,就可以运行迁移命令在数据库中创建数据表了:
php artisan migrate
第三步:创建标签模型类
创建数据表之后,接下来创建相应的模型类并定义关联关系,这里我们需要创建一个 Tag
模型类映射 tags
表:
php artisan make:model Models/Tag
编写新生成的 app/Models/Tag.php
模型类代码如下:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = [
'name'
];
public function cafes()
{
return $this->belongsToMany(Cafe::class, 'cafes_users_tags', 'tag_id', 'user_id');
}
}
我们在 Tag
类中定义了一个 $fillable
属性用于支持批量赋值,以及 cafes
方法用来表示标签与咖啡店之间的多对多关联。
第四步:在咖啡店模型类中定义与标签的关联关系
相对的,我们还需要在咖啡店模型类 app/Models/Cafe.php
中定于咖啡店与标签之间的多对多关联方法 tags
:
public function tags()
{
return $this->belongsToMany(Tag::class, 'cafes_users_tags', 'cafe_id', 'tag_id');
}
这样,我们就可以在查询咖啡店时获取咖啡店的标签了。
第五步:定义咖啡店标签路由
接下来在 routes/api.php
路由文件中定义给咖啡店添加标签和删除标签的路由:
/*
|-------------------------------------------------------------------------------
| 添加标签到指定咖啡店
|-------------------------------------------------------------------------------
| 请求URL: /api/v1/cafes/{id}/tags
| 控制器方法: API\CafesController@postAddTags
| 请求方式: POST
| 功能描述: 用户为某个咖啡店添加标签
*/
Route::post('/cafes/{id}/tags', 'API\CafesController@postAddTags');
/*
|-------------------------------------------------------------------------------
| 删除指定咖啡店上的指定标签
|-------------------------------------------------------------------------------
| 请求URL: /api/v1/cafes/{id}/tags/{tagID}
| 控制器方法: API\CafesController@deleteCafeTag
| 请求方式: DELETE
| 功能描述: 用户从某个咖啡店上删除标签
*/
Route::delete('/cafes/{id}/tags/{tagID}', 'API\CafesController@deleteCafeTag');
第六步:控制器方法实现
紧接着我们在控制器 app/Http/Controllers/API/CafesController.php
中创建上述路由定义中的两个控制器方法:
/**
* 给咖啡店添加标签
* @param $request
* @param $cafeID
* @return JsonResponse
*/
public function postAddTags(Request $request, $cafeID)
{
}
/**
* 删除咖啡店上的指定标签
* @param $cafeID
* @param $tagID
* @return Response
*/
public function deleteCafeTag($cafeID, $tagID)
{
}
然后像地理编码一样,我们创建一个 app/Utilities/Tagger.php
类用于处理新增标签逻辑,编写 Tagger
类代码如下:
<?php
namespace App\Utilities;
use App\Models\Tag;
class Tagger
{
public static function tagCafe($cafe, $tags, $userId)
{
// 遍历标签数据,分别存储每个标签,并建立其余咖啡店的关联
foreach ($tags as $tag) {
$name = trim($tag);
// 如果标签已经存在则直接获取其实例
$newCafeTag = Tag::firstOrNew(array('name' => $name));
$newCafeTag->name = $name;
$newCafeTag->save();
// 将标签和咖啡店关联起来
$cafe->tags()->syncWithoutDetaching([$newCafeTag->id => ['user_id' => $userId]]);
}
}
}
我们在 Tagger
类中实现了一个静态方法 tagCafe
方法用于实现标签的插入以及与咖啡店的关联。
然后回到控制器,编写 postAddTags
方法如下:
/**
* 给咖啡店添加标签
* @param $request
* @param $cafeID
* @return JsonResponse
*/
public function postAddTags(Request $request, $cafeID)
{
// 从请求中获取标签信息
$tags = $request->input('tags');
$cafe = Cafe::find($cafeID);
// 处理新增标签并建立标签与咖啡店之间的关联
Tagger::tagCafe($cafe, $tags, Auth::user()->id);
// 返回标签
$cafe = Cafe::where('id', '=', $cafeID)
->with('brewMethods')
->with('userLike')
->with('tags')
->first();
return response()->json($cafe, 201);
}
至于 deleteCafeTag
方法,我们直接删除中间表中的关联记录即可:
public function deleteCafeTag($cafeID, $tagID)
{
DB::table('cafes_users_tags')->where('cafe_id', $cafeID)->where('tag_id', $tagID)->where('user_id', Auth::user()->id)->delete();
return response(null, 204);
}
需要注意的是,上面两个控制器方法都涉及到用户ID,所以这两个方法都需要登录后才能访问,不过由于路由定义在了应用 auth:api
中间件的路由群组中,所以后面实现环节可以忽略这一点。另外,如果你直接拷贝上述代码的话,不要忘了为相关类补足命名空间引用。
这样,我们就已经完成了登录用户为指定咖啡店添加标签和删除标签的后端 API 接口,接下来,为了优化用户体验,我们在前端输入标签时,往往会提供自动提示功能,类似这种:
下面我们就来为输入标签自动提示提供后端 API。
第七步:标签自动完成路由
在路由文件 routes/api.php
中新增一个路由:
/*
|-------------------------------------------------------------------------------
| 搜索标签(自动提示/补全)
|-------------------------------------------------------------------------------
| 请求URL: /api/v1/tags
| 控制器: API\TagsController@getTags
| 请求方式: GET
| 功能描述: 根据输入词提供标签补全功能
*/
Route::get('/tags', 'API\TagsController@getTags');
然后在 app/Http/Controllers/API
目录下创建一个新的控制器 TagsController
:
php artisan make:controller API/TagsController
编写 TagsController
控制器类代码如下,我们编写了一个 getTags
方法用于实现标签的模糊搜索,如果没有提供搜索词,则返回所有标签(如果量大的话,可选择性提供数量最多的若干个标签):
<?php
namespace App\Http\Controllers\API;
use App\Models\Tag;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class TagsController extends Controller
{
public function getTags()
{
$query = Request::get('search');
if ($query == null || $query == '') {
$tags = Tag::all();
} else {
$tags = Tag::where('name', 'LIKE', $query . '%')->get();
}
return response()->json($tags);
}
}
这样,当用户在前端输入标签时,我们就可以通过相应的 JavaScript 事件处理函数将输入字符传递到后端 API 进行查询,如果有结果的话,就可以以下拉列表的方式将查询结果展示给用户进行选择了。
第八步:更新新增咖啡店处理方法
最后,我们更新 app/Http/Controllers/API/CafesController.php
中的新增咖啡店方法 postNewCafe
,因为在下一篇教程中我们将会在新增咖啡店页面里输入并提交标签数据:
// 冲泡方法
$brewMethods = $locations[0]['methodsAvailable'];
// 标签信息
$tags = $locations[0]['tags'];
// 保存与此咖啡店关联的所有冲泡方法(保存关联关系)
$parentCafe->brewMethods()->sync($brewMethods);
// 绑定咖啡店与标签
Tagger::tagCafe($parentCafe, $tags, $request->user()->id);
以上是总店的处理代码,同理,每个分店也有自己的标签数据,在冲泡方法之后添加标签数据处理代码:
$cafe->brewMethods()->sync($locations[$i]['methodsAvailable']);
Tagger::tagCafe($cafe, $locations[$i]['tags'], $request->user()->id);
至此,所有后端 API 接口都已经创建/更新好了,下一篇教程中,我们将实现前端标签输入组件及在咖啡店详情页显示标签功能。
11 Comments
打卡打卡
感谢支持
学院君,在创建标签表的时候你用了
name
字段表示标签的名称,在model里也是name
,但是你在标签控制器查找标签的时候用了tag
,你github中建表用的是tag
,模型是name
,控制器是tag
。。。笔误了 以 name 为准
存储标签、咖啡店、用户三者关联关系的中间表
cafes_users_tags
这张表怎么理解呢,标签和咖啡店之间是多对多关联这个可以理解,但是为什么要用到用户呢?从用户维度可以看到自己给咖啡店打的标签,从咖啡店维度也可以实现类似下面这种效果:
感觉关联关系还是理解的不到位,要去复习啦
cafe模型的tags方法可以理解,tag模型的cafes方法为啥第四个参数是
user_id
,还不是很理解。。。$cafe = Cafe::where('id', '=', $cafeID)->with('tags')->first
是获取当前咖啡店的所有标签,是不是就是你说的咖啡店维度?那用户维度怎么理解呢?用户维护需要通过自己的user_id去过滤
第七步 TagsController中应该修正为: public function getTags(Request $request){ ///$query = Request::get('search'); $query = $request->input('search'); //....