前言
前面章节没看过的朋友请先从第一章开始看 。这章主要写标签相关功能。
后端
创建迁移文件
php artisan make:migration create_tags_table
编辑迁移文件
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->id(); // 主键,自增ID
$table->string('name')->unique(); // 标签名称,唯一,如“Laravel”
$table->string('slug')->unique(); // 标签别名,URL 友好,如“laravel”
$table->timestamps(); // 创建时间和更新时间
});
}
运行迁移
php artisan migrate
编辑种子文件database/seeders/DatabaseSeeder.php
// 创建8个标签
use App\Models\Tag;
$tags = [
Tag::create(['name' => 'Laravel', 'slug' => 'laravel']),
Tag::create(['name' => 'Vue.js', 'slug' => 'vue']),
Tag::create(['name' => 'PHP', 'slug' => 'php']),
Tag::create(['name' => 'JavaScript', 'slug' => 'javascript']),
Tag::create(['name' => 'MySQL', 'slug' => 'mysql']),
Tag::create(['name' => 'CSS', 'slug' => 'css']),
Tag::create(['name' => 'Git', 'slug' => 'git']),
Tag::create(['name' => 'Docker', 'slug' => 'docker']),
];
执行迁移命令,注意:先创建模型
php artisan migrate:fresh
php artisan db:seed
创建模型命令
php artisan make:model Tag
编辑文件app/Models/Tag.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasUniqueSlug;
class Tag extends Model
{
use HasFactory, HasUniqueSlug;
protected $fillable = ['name'];
/**
* 定义标签与文章之间的多对多关系
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function articles()
{
// 这里的 'article_tag' 是中间表名
return $this->belongsToMany(Article::class, 'article_tag');
}
}
创建控制器命令
php artisan make:controller TagController
编辑控制器app/Http/Controller/TagController.php
<?php
namespace App\Http\Controllers;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\Rule;
use Illuminate\Database\QueryException;
class TagController extends Controller
{
/**
* 标签列表,支持分页和搜索,并附带文章计数
*
* @param Request $request
* @return JsonResponse
*/
public function index(Request $request): JsonResponse
{
// 验证请求参数
$validated = $request->validate([
'per_page' => 'integer|min:1|max:100',
'page' => 'integer|min:1',
'search' => 'nullable|string|max:255'
], [
'per_page.integer' => '每页数量必须为整数',
'per_page.min' => '每页数量至少为1',
'per_page.max' => '每页数量最多为100',
'page.integer' => '页码必须为整数',
'page.min' => '页码至少为1',
'search.max' => '搜索关键词不能超过255个字符',
]);
$perPage = $validated['per_page'] ?? 10;
$page = $validated['page'] ?? 1;
$search = $validated['search'] ?? null;
// 构建查询,并使用 withCount 统计文章数量
$query = Tag::withCount('articles')->orderBy('created_at', 'desc');
// 搜索过滤
if ($search) {
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('slug', 'like', "%{$search}%");
});
}
// 执行分页查询,并让 paginate() 自动生成标准的 JSON 结构
// 包含 articles_count 字段
$tags = $query->paginate($perPage, ['id', 'name', 'slug', 'articles_count'], 'page', $page);
return response()->json($tags, 200);
}
/**
* 前台展示:获取最新10条标签数据
*
* @return JsonResponse
*/
public function latest(): JsonResponse
{
// 简单查询最新10条标签,与 CategoryController 的 latest 方法一致
$tags = Tag::select('id', 'name', 'slug')
->orderBy('created_at', 'desc')
->take(10)
->get();
return response()->json([
'data' => $tags,
'message' => '成功获取最新标签'
], 200);
}
/**
* 创建新标签
*
* @param Request $request
* @return JsonResponse
*/
public function store(Request $request): JsonResponse
{
// 验证请求数据,只验证 name 字段,slug 由 Trait 自动生成
$validated = $request->validate([
'name' => 'required|string|max:50|unique:tags,name',
], [
'name.required' => '标签名称不能为空',
'name.max' => '标签名称不能超过50个字符',
'name.unique' => '标签名称已存在',
]);
// 创建标签,HasUniqueSlug Trait 会在 creating 事件中自动生成 slug
$tag = Tag::create($validated);
return response()->json([
'data' => $tag,
'message' => '标签创建成功',
], 201);
}
/**
* 更新标签
*
* @param Request $request
* @param Tag $tag
* @return JsonResponse
*/
public function update(Request $request, Tag $tag): JsonResponse
{
// 验证请求数据,只验证 name 字段,并忽略当前标签
$validated = $request->validate([
'name' => [
'required',
'string',
'max:50',
Rule::unique('tags', 'name')->ignore($tag->id),
],
], [
'name.required' => '标签名称不能为空',
'name.max' => '标签名称不能超过50个字符',
'name.unique' => '标签名称已存在',
]);
// 更新标签,HasUniqueSlug Trait 会在 updating 事件中自动更新 slug
$tag->update($validated);
return response()->json([
'data' => $tag,
'message' => '标签更新成功',
], 200);
}
/**
* 删除标签
*
* @param Tag $tag
* @return JsonResponse
*/
public function destroy(Tag $tag): JsonResponse
{
try {
// 删除标签,会自动处理关联关系
$tag->delete();
// 返回 204 No Content,表示成功但无返回体
return response()->json(null, 204);
} catch (QueryException $e) {
// 检查是否是外键约束异常(例如该标签下存在文章)
if ($e->getCode() === '23000') {
return response()->json([
'message' => '该标签下存在文章,无法删除。'
], 409); // 409 Conflict
}
// 处理其他异常
return response()->json([
'message' => '删除标签失败',
], 500);
}
}
}
编辑路由文件routes/api.php
受保护接口和公开接口分开放,不要直接复制
use App\Http\Controllers\TagController;
// 分类、标签、文章公开接口
Route::get('/latest-categories', [CategoryController::class, 'latest']);
Route::get('/latest-tag', [TagController::class, 'latest']);
// 标签管理(受保护接口)
Route::prefix('tags')->group(function () {
Route::get('/', [TagController::class, 'index']);
Route::post('/', [TagController::class, 'store']);
Route::put('/{tag}', [TagController::class, 'update']);
Route::delete('/{tag}', [TagController::class, 'destroy']);
});
551

被折叠的 条评论
为什么被折叠?



