博客项目 laravel vue mysql 第四章 分类功能

前言

前面章节没看过的朋友请先从第一章开始看 。这章主要写分类相关功能。

后端

创建迁移文件命令

php artisan make:migration create_categories_table

编辑database/migrations/2025_07_11_210720_create_categories_table.php

public function up()
 {
     Schema::create('categories', function (Blueprint $table) {
         $table->id(); // 主键,自增ID
         $table->string('name')->unique(); // 分类名称,唯一,如“技术”
         $table->string('slug')->unique(); // 分类别名,URL 友好,如“tech”
         $table->text('description')->nullable(); // 分类描述,可为空
         $table->timestamps(); // 创建时间和更新时间
     });
 }

执行迁移

php artisan migrate

创建模型命令

php artisan make:model Category

编辑模型app/Models/Category.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'slug', 'description'];

}

编辑种子文件database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Category;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            UserSeeder::class
        ]);

        $admin = User::where('name', 'admin')->first();

        // 创建分类
        $categories = [
            Category::create([
                'name' => '技术分享',
                'slug' => 'tech',
                'description' => '分享最新技术、编程经验和开发技巧。',
            ]),
            Category::create([
                'name' => '生活随笔',
                'slug' => 'life',
                'description' => '记录生活点滴、感悟与随想。',
            ]),
            Category::create([
                'name' => '学习笔记',
                'slug' => 'study',
                'description' => '学习过程中的笔记、总结与心得。',
            ]),
        ];
    }
}

这里我执行了

php artisan migrate:refresh --seed

不知道为啥不起作用。有懂的大佬说一下呗。
我这里直接删了数据库,重新运行迁移和种子文件了。以后加文章之类的应该还是这套操作。笨办法,嘻嘻。

php artisan migrate
php artisan db:seed

找到方法了,上面的不用看了,直接看下面两条命令

php artisan migrate:fresh
删除数据库中的所有表,然后重新执行 database/migrations 目录下的所有迁移文件,重建表结构。

php artisan db:seed
运行种子文件

因为需要分类,标签,文章都需要生成slug,所以做成一个公共的trait。这里为什么做成trait而不是继承我解释原因。
如果使用继承,那么 HasUniqueSlug就成了所有子类的共同祖先,这会形成一种强耦合。它暗示着所有子类都必须具备生成 Slug 的能力,这不符合现实世界的模型(比如一个 User 模型通常就不需要 Slug)。
trait不同,你可以把它当成一个插件,它可以把一个独立的功能注入任何类中,而不用考虑复杂的继承关系。
创建app/Traits/HasUniqueSlug.php

<?php

namespace App\Traits;

use Overtrue\Pinyin\Pinyin;
use Illuminate\Database\Eloquent\Model;

trait HasUniqueSlug
{
    /**
     * 在模型创建或更新时自动生成唯一的 slug
     *
     * @return void
     */
    protected static function bootHasUniqueSlug()
    {
        // 监听模型创建事件
        static::creating(function (Model $model) {
            $model->slug = self::generateUniqueSlug($model->name, null);
        });

        // 监听模型更新事件
        static::updating(function (Model $model) {
            // 只有当 name 字段被修改时才重新生成 slug
            if ($model->isDirty('name')) {
                $model->slug = self::generateUniqueSlug($model->name, $model->id);
            }
        });
    }

    /**
     * 根据给定名称生成唯一的 slug
     *
     * @param string $name
     * @param int|null $ignoreId
     * @return string
     */
    protected static function generateUniqueSlug(string $name, ?int $ignoreId)
    {
        $pinyin = new Pinyin();
        $baseSlug = $pinyin->permalink($name);
        $slug = $baseSlug;
        $count = 2;

        // 构建查询,检查 slug 是否已存在
        $query = self::where('slug', $slug);
        if ($ignoreId) {
            $query->where('id', '!=', $ignoreId);
        }

        // 如果存在,在后面添加数字直到唯一
        while ($query->exists()) {
            $slug = $baseSlug . '-' . $count++;
            $query = self::where('slug', $slug);
            if ($ignoreId) {
                $query->where('id', '!=', $ignoreId);
            }
        }

        return $slug;
    }
}

更新模型app/Models/Category.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasUniqueSlug;

class Category extends Model
{
    use HasFactory , HasUniqueSlug;

    protected $fillable = ['name','slug','description',];
    
    /**
     * 定义分类与文章之间的一对多关系(拥有多篇文章)
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function articles()
    {
        return $this->hasMany(Article::class);
    }
}

创建控制器命令

php artisan make:controller CategoryController

编辑app/Http/Controllers/CategoryController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Category;
use Illuminate\Validation\Rule;
use Illuminate\Database\QueryException;
class CategoryController extends Controller
{
    /**
     * 分类列表
     * @param \Illuminate\Http\Request $request
     * @return mixed|\Illuminate\Http\JsonResponse
     */
    public function index(Request $request)
    {   
        $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;

        // 构建查询
        $query = Category::orderBy('created_at', 'desc');
        
        // 搜索过滤
        if ($search) {
            $query->where(function($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                    ->orWhere('slug', 'like', "%{$search}%");
            });
        }

        // 执行分页查询
        $categories = $query->paginate($perPage, ['*'], 'page', $page);

        // 返回 JSON 响应
        return response()->json($categories, 200);
    }

    /**
     * 新增分类
     * @param \Illuminate\Http\Request $request
     * @return mixed|\Illuminate\Http\JsonResponse
     */
    public function store(Request $request)
    {
         // 验证请求数据
        $validated = $request->validate([
            'name' => 'required|string|max:50|unique:categories,name',
            'description' => 'nullable|string',
        ], [
            'name.required' => '分类名称不能为空',
            'name.max' => '分类名称不能超过50个字符',
            'name.unique' => '分类名称已存在',
            'description.string' => '描述必须为字符串',
        ]);

        $category = Category::create($validated);

        return response()->json([
            'data' => $category,
            'message' => '创建分类成功',
        ], 201);
    }

    /**
     * 更新分类
     * @param \Illuminate\Http\Request $request
     * @param \App\Models\Category $category // 注意这里的命名空间,确保是 App\Models\Category
     * @return \Illuminate\Http\JsonResponse
     */
    public function update(Request $request, Category $category)
    {
        // 验证请求数据
        $validated = $request->validate([
            'name' => [
                'required',
                'string',
                'max:50',
                // 确保更新时,名称在除了当前分类之外是唯一的
                Rule::unique('categories', 'name')->ignore($category->id),
            ],
            'description' => 'nullable|string',
        ], [
            'name.required' => '分类名称不能为空',
            'name.max' => '分类名称不能超过50个字符',
            'name.unique' => '分类名称已存在',
            'description.string' => '描述必须为字符串',
        ]);

        $category->update($validated);

        return response()->json([
            'data' => $category,
            'message' => '更新分类成功',
        ], 200);
    }

    /**
     * 删除分类
     * @param \app\Models\Category $category
     * @return mixed|\Illuminate\Http\JsonResponse
     */
    public function destroy(Category $category)
    {
        try {
            $category->delete();
            return response()->json(null,204);
        } catch (QueryException $e) {
             // 检查是否是外键约束异常
            if ($e->getCode() === '23000') {
                return response()->json([
                    'message' => '该分类下存在文章,无法删除。'
                ], 409); // 409 Conflict
            }

            return response()->json([
                'message' => '删除分类失败',
            ], 500);
        }
    }

    /**
     * 前台展示:获取最新10条分类数据
     * @return \Illuminate\Http\JsonResponse
     */
    public function latest()
    {
        $categories = Category::select('id', 'name', 'slug')
            ->orderBy('created_at', 'desc')
            ->take(10)
            ->get();

        return response()->json([
            'data' => $categories,
            'message' => '获取最新分类成功'
        ], 200);
    }
}

编辑路由routes/api.php

use Illuminate\Http\Request;
use Overtrue\Pinyin\Pinyin;

// 测试slug生成接口
Route::get('/test-slug', function(Request $request) {
    $text = $request->input('text');

    $pinyin = new Pinyin();

    $slug = $pinyin->permalink($text);

    return response()->json([
        'original_title' => $text,
        'generated_slug' => $slug,
    ]);
});

// 分类、标签、文章公开接口
Route::get('/latest-categories', [CategoryController::class, 'latest']);
/*
|--------------------------------------------------------------------------
| 受保护接口(需要认证)
|--------------------------------------------------------------------------
*/
Route::middleware('auth:sanctum')->group(function(){

    // 用户相关
    Route::prefix('auth')->group(function(){
        Route::get('/user', [AuthController::class, 'getUserInfo']);
        Route::delete('/logout', [AuthController::class, 'logout']);
        Route::patch('/nickname', [AuthController::class, 'updateNickname']);
    });
    
	  // 分类管理
    Route::prefix('categories')->group(function () {
        Route::get('/', [CategoryController::class, 'index']);
        Route::post('/', [CategoryController::class, 'store']);
        Route::put('/{category}', [CategoryController::class, 'update']);
        Route::delete('/{category}', [CategoryController::class, 'destroy']);
    });

    // 上传
    Route::prefix('upload')->group(function() {
        Route::post('avatar', [UploadController::class, 'uploadAvatar']);
    });

});

前端

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值