全栈(PHP + Vue + MySQL)开发旅游管理系统教程(二)

简介: 教程来源 http://qcycj.cn 本项目基于Laravel 9构建旅游API后端,集成JWT认证、跨域支持与完整CRUD功能。涵盖用户注册登录、旅游线路管理、订单事务处理、收藏功能及管理员权限控制,结构清晰、安全可靠,适配Vue等前端框架。

第二部分:后端开发(Laravel)

2.1 安装Laravel

# 使用Composer创建Laravel项目
composer create-project laravel/laravel travel-api
cd travel-api

# 安装JWT认证扩展包
composer require tymon/jwt-auth

# 安装跨域扩展包
composer require fruitcake/laravel-cors

2.2 环境配置

# 复制环境配置文件
cp .env.example .env

# 生成应用密钥
php artisan key:generate

# 生成JWT密钥
php artisan jwt:secret

.env配置文件:

APP_NAME="旅游管理系统API"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000

# 数据库配置
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=travel_db
DB_USERNAME=root
DB_PASSWORD=123456

# JWT配置
JWT_SECRET=your-jwt-secret-key

# CORS配置
FRONTEND_URL=http://localhost:5173

2.3 创建模型与数据表

# 创建模型和迁移文件
php artisan make:model Models/User -m
php artisan make:model Models/Category -m
php artisan make:model Models/Tour -m
php artisan make:model Models/Order -m
php artisan make:model Models/Favorite -m

# 运行迁移
php artisan migrate

2.4 配置Laravel
config/auth.php - 配置JWT认证驱动:

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

config/jwt.php - JWT配置:

return [
    'secret' => env('JWT_SECRET'),
    'ttl' => env('JWT_TTL', 60),        // Token有效期(分钟)
    'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),  // 刷新有效期(分钟)
    'algo' => env('JWT_ALGO', 'HS256'),
];

2.5 用户模型

<?php
// app/Models/User.php
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Database\Eloquent\Factories\HasFactory;

/**
 * 用户模型
 * 实现JWTSubject接口,提供JWT认证所需的方法
 */
class User extends Authenticatable implements JWTSubject
{
    use HasFactory;

    protected $table = 'users';
    protected $primaryKey = 'id';
    public $timestamps = true;

    protected $fillable = [
        'username',
        'email',
        'password',
        'name',
        'phone',
        'avatar',
        'role',
        'status',
        'email_verified_at'
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * JWT标识符(用户主键)
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * JWT自定义声明
     */
    public function getJWTCustomClaims()
    {
        return [
            'user_id' => $this->id,
            'username' => $this->username,
            'role' => $this->role
        ];
    }

    /**
     * 关联:用户拥有的订单
     */
    public function orders()
    {
        return $this->hasMany(Order::class, 'user_id', 'id');
    }

    /**
     * 关联:用户收藏的线路
     */
    public function favorites()
    {
        return $this->belongsToMany(Tour::class, 'favorites', 'user_id', 'tour_id')
                    ->withTimestamps();
    }
}

2.6 旅游线路模型

<?php
// app/Models/Tour.php
namespace App\Models;

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

/**
 * 旅游线路模型
 */
class Tour extends Model
{
    use HasFactory;

    protected $table = 'tours';
    protected $primaryKey = 'id';
    public $timestamps = true;

    protected $fillable = [
        'category_id',
        'name',
        'slug',
        'subtitle',
        'days',
        'price',
        'child_price',
        'stock',
        'sold_count',
        'main_image',
        'images',
        'departure_city',
        'destination_city',
        'transport',
        'accommodation',
        'meal_plan',
        'itinerary',
        'description',
        'features',
        'status',
        'is_featured',
        'view_count'
    ];

    protected $casts = [
        'price' => 'decimal:2',
        'child_price' => 'decimal:2',
        'images' => 'array'
    ];

    /**
     * 关联:所属分类
     */
    public function category()
    {
        return $this->belongsTo(Category::class, 'category_id', 'id');
    }

    /**
     * 关联:订单
     */
    public function orders()
    {
        return $this->hasMany(Order::class, 'tour_id', 'id');
    }

    /**
     * 关联:收藏用户
     */
    public function favoritedBy()
    {
        return $this->belongsToMany(User::class, 'favorites', 'tour_id', 'user_id')
                    ->withTimestamps();
    }

    /**
     * 增加浏览次数
     */
    public function incrementViewCount()
    {
        $this->increment('view_count');
    }

    /**
     * 获取是否被当前用户收藏
     */
    public function getIsFavoritedAttribute()
    {
        if (!auth()->check()) return false;
        return $this->favoritedBy()->where('user_id', auth()->id())->exists();
    }
}

2.7 订单模型

<?php
// app/Models/Order.php
namespace App\Models;

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

/**
 * 订单模型
 */
class Order extends Model
{
    use HasFactory;

    protected $table = 'orders';
    protected $primaryKey = 'id';
    public $timestamps = true;

    protected $fillable = [
        'order_no',
        'user_id',
        'tour_id',
        'tour_name',
        'tour_price',
        'departure_date',
        'adult_count',
        'child_count',
        'total_amount',
        'contact_name',
        'contact_phone',
        'contact_email',
        'remark',
        'order_status',
        'payment_status',
        'payment_time',
        'confirm_time',
        'cancel_time',
        'complete_time'
    ];

    protected $casts = [
        'tour_price' => 'decimal:2',
        'total_amount' => 'decimal:2',
        'departure_date' => 'date',
        'payment_time' => 'datetime',
        'confirm_time' => 'datetime',
        'cancel_time' => 'datetime',
        'complete_time' => 'datetime'
    ];

    // 订单状态常量
    const STATUS_PENDING = 0;      // 待确认
    const STATUS_CONFIRMED = 1;    // 已确认
    const STATUS_CANCELLED = 2;    // 已取消
    const STATUS_COMPLETED = 3;    // 已完成

    // 支付状态常量
    const PAYMENT_UNPAID = 0;      // 未支付
    const PAYMENT_PAID = 1;        // 已支付

    /**
     * 关联:所属用户
     */
    public function user()
    {
        return $this->belongsTo(User::class, 'user_id', 'id');
    }

    /**
     * 关联:旅游线路
     */
    public function tour()
    {
        return $this->belongsTo(Tour::class, 'tour_id', 'id');
    }

    /**
     * 获取订单状态文本
     */
    public function getStatusTextAttribute()
    {
        $statusMap = [
            self::STATUS_PENDING => '待确认',
            self::STATUS_CONFIRMED => '已确认',
            self::STATUS_CANCELLED => '已取消',
            self::STATUS_COMPLETED => '已完成'
        ];
        return $statusMap[$this->order_status] ?? '未知';
    }

    /**
     * 获取支付状态文本
     */
    public function getPaymentStatusTextAttribute()
    {
        return $this->payment_status == self::PAYMENT_PAID ? '已支付' : '未支付';
    }
}

2.8 JWT控制器

<?php
// app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuthExceptions\JWTException;

/**
 * 用户认证控制器
 * 处理用户注册、登录、获取个人信息等
 */
class AuthController extends Controller
{
    /**
     * 用户注册
     * 
     * 业务流程:
     * 1. 验证请求参数
     * 2. 检查用户名和邮箱是否已存在
     * 3. 创建新用户
     * 4. 生成JWT令牌
     * 5. 返回用户信息和令牌
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function register(Request $request)
    {
        // 1. 验证请求数据
        $validator = Validator::make($request->all(), [
            'username' => 'required|string|min:3|max:50|unique:users',
            'email' => 'required|string|email|max:100|unique:users',
            'password' => 'required|string|min:6|max:20',
            'name' => 'nullable|string|max:50'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'code' => 400,
                'message' => '参数验证失败',
                'errors' => $validator->errors()
            ], 400);
        }

        // 2. 创建新用户
        $user = User::create([
            'username' => $request->username,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'name' => $request->name ?? $request->username,
            'role' => 'user',
            'status' => 1
        ]);

        // 3. 生成JWT令牌
        $token = JWTAuth::fromUser($user);

        // 4. 返回响应
        return response()->json([
            'code' => 201,
            'message' => '注册成功',
            'data' => [
                'user_id' => $user->id,
                'username' => $user->username,
                'email' => $user->email,
                'name' => $user->name,
                'role' => $user->role,
                'token' => $token,
                'token_type' => 'bearer',
                'expires_in' => config('jwt.ttl') * 60
            ]
        ], 201);
    }

    /**
     * 用户登录
     * 
     * 业务流程:
     * 1. 验证请求参数
     * 2. 检查用户状态是否正常
     * 3. 验证用户名密码
     * 4. 生成JWT令牌
     * 5. 记录最后登录时间
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        // 1. 验证请求数据
        $validator = Validator::make($request->all(), [
            'username' => 'required|string',
            'password' => 'required|string'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'code' => 400,
                'message' => '用户名和密码不能为空'
            ], 400);
        }

        // 2. 查找用户
        $user = User::where('username', $request->username)->first();

        if (!$user) {
            return response()->json([
                'code' => 401,
                'message' => '用户名或密码错误'
            ], 401);
        }

        // 3. 检查用户状态
        if ($user->status != 1) {
            return response()->json([
                'code' => 403,
                'message' => '账号已被禁用,请联系管理员'
            ], 403);
        }

        // 4. 验证密码
        if (!Hash::check($request->password, $user->password)) {
            return response()->json([
                'code' => 401,
                'message' => '用户名或密码错误'
            ], 401);
        }

        // 5. 生成JWT令牌
        $token = JWTAuth::fromUser($user);

        // 6. 更新最后登录时间
        $user->updated_at = now();
        $user->save();

        // 7. 返回响应
        return response()->json([
            'code' => 200,
            'message' => '登录成功',
            'data' => [
                'user_id' => $user->id,
                'username' => $user->username,
                'email' => $user->email,
                'name' => $user->name,
                'avatar' => $user->avatar,
                'role' => $user->role,
                'token' => $token,
                'token_type' => 'bearer',
                'expires_in' => config('jwt.ttl') * 60
            ]
        ]);
    }

    /**
     * 获取当前用户信息
     * 需要在请求头中携带JWT令牌:Authorization: Bearer <token>
     * 
     * @return \Illuminate\Http\JsonResponse
     */
    public function profile()
    {
        try {
            $user = auth()->userOrFail();

            return response()->json([
                'code' => 200,
                'data' => [
                    'id' => $user->id,
                    'username' => $user->username,
                    'email' => $user->email,
                    'name' => $user->name,
                    'phone' => $user->phone,
                    'avatar' => $user->avatar,
                    'role' => $user->role,
                    'created_at' => $user->created_at
                ]
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'code' => 401,
                'message' => '请先登录'
            ], 401);
        }
    }

    /**
     * 登出
     * 使当前JWT令牌失效
     * 
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()->logout();

        return response()->json([
            'code' => 200,
            'message' => '退出登录成功'
        ]);
    }

    /**
     * 刷新JWT令牌
     * 
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        try {
            $newToken = auth()->refresh();

            return response()->json([
                'code' => 200,
                'data' => [
                    'token' => $newToken,
                    'token_type' => 'bearer',
                    'expires_in' => config('jwt.ttl') * 60
                ]
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'code' => 401,
                'message' => '刷新令牌失败'
            ], 401);
        }
    }

    /**
     * 修改密码
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function changePassword(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'old_password' => 'required|string',
            'new_password' => 'required|string|min:6|max:20',
            'confirm_password' => 'required|string|same:new_password'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'code' => 400,
                'message' => '参数验证失败',
                'errors' => $validator->errors()
            ], 400);
        }

        $user = auth()->user();

        if (!Hash::check($request->old_password, $user->password)) {
            return response()->json([
                'code' => 400,
                'message' => '原密码错误'
            ], 400);
        }

        $user->password = Hash::make($request->new_password);
        $user->save();

        return response()->json([
            'code' => 200,
            'message' => '密码修改成功'
        ]);
    }
}

2.9 旅游线路控制器

<?php
// app/Http/Controllers/TourController.php
namespace App\Http\Controllers;

use App\Models\Tour;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;

/**
 * 旅游线路控制器
 * 处理旅游线路的增删改查、分类筛选等功能
 */
class TourController extends Controller
{
    /**
     * 旅游线路列表(分页+筛选)
     * 
     * 请求参数说明:
     * - page: 页码,默认1
     * - limit: 每页数量,默认10
     * - keyword: 搜索关键词(在名称、副标题中匹配)
     * - category_id: 分类ID筛选
     * - min_price: 最低价格筛选
     * - max_price: 最高价格筛选
     * - sort_by: 排序字段(price/view_count/sold_count/created_at)
     * - sort_order: 排序方向(asc/desc)
     * - is_featured: 是否推荐(0/1)
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function index(Request $request)
    {
        $query = Tour::with('category')->where('status', 1);

        // 关键词搜索
        if ($request->has('keyword') && !empty($request->keyword)) {
            $keyword = '%' . $request->keyword . '%';
            $query->where(function($q) use ($keyword) {
                $q->where('name', 'like', $keyword)
                  ->orWhere('subtitle', 'like', $keyword)
                  ->orWhere('description', 'like', $keyword);
            });
        }

        // 分类筛选
        if ($request->has('category_id') && $request->category_id) {
            $query->where('category_id', $request->category_id);
        }

        // 价格范围筛选
        if ($request->has('min_price')) {
            $query->where('price', '>=', $request->min_price);
        }
        if ($request->has('max_price')) {
            $query->where('price', '<=', $request->max_price);
        }

        // 推荐筛选
        if ($request->has('is_featured')) {
            $query->where('is_featured', $request->is_featured);
        }

        // 排序
        $sortBy = $request->get('sort_by', 'created_at');
        $sortOrder = $request->get('sort_order', 'desc');

        if (in_array($sortBy, ['price', 'view_count', 'sold_count', 'created_at'])) {
            $query->orderBy($sortBy, $sortOrder);
        } else {
            $query->orderBy('created_at', 'desc');
        }

        // 分页
        $page = $request->get('page', 1);
        $limit = $request->get('limit', 10);
        $offset = ($page - 1) * $limit;

        $total = $query->count();
        $tours = $query->skip($offset)->take($limit)->get();

        // 转换为API需要的格式
        $tours->transform(function($tour) {
            return [
                'id' => $tour->id,
                'name' => $tour->name,
                'slug' => $tour->slug,
                'subtitle' => $tour->subtitle,
                'days' => $tour->days,
                'price' => $tour->price,
                'child_price' => $tour->child_price,
                'stock' => $tour->stock,
                'sold_count' => $tour->sold_count,
                'main_image' => $tour->main_image,
                'departure_city' => $tour->departure_city,
                'transport' => $tour->transport,
                'accommodation' => $tour->accommodation,
                'is_featured' => $tour->is_featured,
                'view_count' => $tour->view_count,
                'category' => $tour->category ? [
                    'id' => $tour->category->id,
                    'name' => $tour->category->name
                ] : null,
                'created_at' => $tour->created_at
            ];
        });

        return response()->json([
            'code' => 200,
            'data' => [
                'tours' => $tours,
                'pagination' => [
                    'current_page' => (int)$page,
                    'per_page' => (int)$limit,
                    'total' => $total,
                    'last_page' => ceil($total / $limit)
                ]
            ]
        ]);
    }

    /**
     * 旅游线路详情
     * 
     * 业务流程:
     * 1. 根据slug或ID查询线路
     * 2. 增加浏览次数
     * 3. 获取分类和收藏状态信息
     * 
     * @param string $slug
     * @return \Illuminate\Http\JsonResponse
     */
    public function show($slug)
    {
        // 支持ID或Slug查询
        if (is_numeric($slug)) {
            $tour = Tour::with('category')->find($slug);
        } else {
            $tour = Tour::with('category')->where('slug', $slug)->first();
        }

        if (!$tour) {
            return response()->json([
                'code' => 404,
                'message' => '旅游线路不存在'
            ], 404);
        }

        // 增加浏览次数
        $tour->incrementViewCount();

        return response()->json([
            'code' => 200,
            'data' => [
                'id' => $tour->id,
                'category_id' => $tour->category_id,
                'category_name' => $tour->category ? $tour->category->name : null,
                'name' => $tour->name,
                'slug' => $tour->slug,
                'subtitle' => $tour->subtitle,
                'days' => $tour->days,
                'price' => $tour->price,
                'child_price' => $tour->child_price,
                'stock' => $tour->stock,
                'sold_count' => $tour->sold_count,
                'main_image' => $tour->main_image,
                'images' => $tour->images,
                'departure_city' => $tour->departure_city,
                'destination_city' => $tour->destination_city,
                'transport' => $tour->transport,
                'accommodation' => $tour->accommodation,
                'meal_plan' => $tour->meal_plan,
                'itinerary' => $tour->itinerary,
                'description
                                 'description' => $tour->description,
                'features' => $tour->features,
                'status' => $tour->status,
                'is_featured' => $tour->is_featured,
                'view_count' => $tour->view_count,
                'is_favorited' => $tour->is_favorited,
                'created_at' => $tour->created_at,
                'updated_at' => $tour->updated_at
            ]
        ]);
    }

    /**
     * 获取所有线路分类(树形结构)
     * 
     * @return \Illuminate\Http\JsonResponse
     */
    public function getCategories()
    {
        $categories = Category::where('status', 1)
            ->orderBy('sort_order', 'asc')
            ->get();

        // 构建树形结构
        $tree = $this->buildCategoryTree($categories);

        return response()->json([
            'code' => 200,
            'data' => $tree
        ]);
    }

    /**
     * 构建分类树(递归方法)
     * 
     * @param \Illuminate\Support\Collection $categories
     * @param int $parentId
     * @return array
     */
    private function buildCategoryTree($categories, $parentId = 0)
    {
        $tree = [];
        foreach ($categories as $category) {
            if ($category->parent_id == $parentId) {
                $children = $this->buildCategoryTree($categories, $category->id);
                $node = [
                    'id' => $category->id,
                    'name' => $category->name,
                    'slug' => $category->slug,
                    'description' => $category->description,
                    'sort_order' => $category->sort_order,
                    'children' => $children
                ];
                $tree[] = $node;
            }
        }
        return $tree;
    }

    /**
     * 添加旅游线路(管理员)
     * 需要admin权限
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'category_id' => 'required|exists:categories,id',
            'name' => 'required|string|max:200',
            'days' => 'required|integer|min:1',
            'price' => 'required|numeric|min:0',
            'stock' => 'required|integer|min:0',
            'departure_city' => 'required|string|max:50',
            'transport' => 'nullable|string|max:50',
            'accommodation' => 'nullable|string|max:50'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'code' => 400,
                'message' => '参数验证失败',
                'errors' => $validator->errors()
            ], 400);
        }

        // 生成slug(URL友好名称)
        $slug = Str::slug($request->name);
        $originalSlug = $slug;
        $counter = 1;

        // 确保slug唯一
        while (Tour::where('slug', $slug)->exists()) {
            $slug = $originalSlug . '-' . $counter++;
        }

        $tour = Tour::create([
            'category_id' => $request->category_id,
            'name' => $request->name,
            'slug' => $slug,
            'subtitle' => $request->subtitle,
            'days' => $request->days,
            'price' => $request->price,
            'child_price' => $request->child_price ?? 0,
            'stock' => $request->stock,
            'main_image' => $request->main_image,
            'images' => $request->images,
            'departure_city' => $request->departure_city,
            'destination_city' => $request->destination_city,
            'transport' => $request->transport,
            'accommodation' => $request->accommodation,
            'meal_plan' => $request->meal_plan,
            'itinerary' => $request->itinerary,
            'description' => $request->description,
            'features' => $request->features,
            'is_featured' => $request->is_featured ?? 0,
            'status' => 1
        ]);

        return response()->json([
            'code' => 201,
            'message' => '添加成功',
            'data' => $tour
        ], 201);
    }

    /**
     * 更新旅游线路(管理员)
     * 
     * @param Request $request
     * @param int $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function update(Request $request, $id)
    {
        $tour = Tour::find($id);

        if (!$tour) {
            return response()->json([
                'code' => 404,
                'message' => '旅游线路不存在'
            ], 404);
        }

        $validator = Validator::make($request->all(), [
            'category_id' => 'exists:categories,id',
            'name' => 'string|max:200',
            'days' => 'integer|min:1',
            'price' => 'numeric|min:0',
            'stock' => 'integer|min:0'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'code' => 400,
                'message' => '参数验证失败',
                'errors' => $validator->errors()
            ], 400);
        }

        // 如果名称变了,更新slug
        if ($request->has('name') && $request->name != $tour->name) {
            $slug = Str::slug($request->name);
            $originalSlug = $slug;
            $counter = 1;
            while (Tour::where('slug', $slug)->where('id', '!=', $id)->exists()) {
                $slug = $originalSlug . '-' . $counter++;
            }
            $tour->slug = $slug;
            $tour->name = $request->name;
        }

        // 更新其他字段
        $fillable = [
            'category_id', 'subtitle', 'days', 'price', 'child_price', 'stock',
            'main_image', 'images', 'departure_city', 'destination_city',
            'transport', 'accommodation', 'meal_plan', 'itinerary',
            'description', 'features', 'is_featured'
        ];

        foreach ($fillable as $field) {
            if ($request->has($field)) {
                $tour->$field = $request->$field;
            }
        }

        $tour->save();

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

    /**
     * 上下架旅游线路(管理员)
     * 
     * @param int $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function toggleStatus($id)
    {
        $tour = Tour::find($id);

        if (!$tour) {
            return response()->json([
                'code' => 404,
                'message' => '旅游线路不存在'
            ], 404);
        }

        $tour->status = $tour->status == 1 ? 0 : 1;
        $tour->save();

        $statusText = $tour->status == 1 ? '上架' : '下架';

        return response()->json([
            'code' => 200,
            'message' => $statusText . '成功'
        ]);
    }

    /**
     * 删除旅游线路(管理员)
     * 
     * @param int $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function destroy($id)
    {
        $tour = Tour::find($id);

        if (!$tour) {
            return response()->json([
                'code' => 404,
                'message' => '旅游线路不存在'
            ], 404);
        }

        $tour->delete();

        return response()->json([
            'code' => 200,
            'message' => '删除成功'
        ]);
    }
}

2.10 订单控制器

<?php
// app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;

use App\Models\Order;
use App\Models\Tour;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;

/**
 * 订单控制器
 * 处理订单创建、查询、取消等业务逻辑
 */
class OrderController extends Controller
{
    /**
     * 创建订单
     * 
     * 业务流程(事务性操作):
     * 1. 验证请求参数(旅游线路ID、出发日期、人数等)
     * 2. 查询旅游线路信息,验证库存
     * 3. 计算订单总金额
     * 4. 生成唯一订单号
     * 5. 扣减线路库存(关键操作)
     * 6. 创建订单记录
     * 7. 返回订单信息
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function create(Request $request)
    {
        // 1. 验证请求参数
        $validator = Validator::make($request->all(), [
            'tour_id' => 'required|exists:tours,id',
            'departure_date' => 'required|date|after:today',
            'adult_count' => 'required|integer|min:1',
            'child_count' => 'required|integer|min:0',
            'contact_name' => 'required|string|max:50',
            'contact_phone' => 'required|regex:/^1[3-9]\d{9}$/',
            'contact_email' => 'nullable|email'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'code' => 400,
                'message' => '参数验证失败',
                'errors' => $validator->errors()
            ], 400);
        }

        // 2. 查询旅游线路
        $tour = Tour::find($request->tour_id);

        if (!$tour || $tour->status != 1) {
            return response()->json([
                'code' => 400,
                'message' => '线路已下架,无法预订'
            ], 400);
        }

        // 3. 计算总人数和总金额
        $totalPeople = $request->adult_count + $request->child_count;
        $totalAmount = $tour->price * $request->adult_count + $tour->child_price * $request->child_count;

        // 4. 检查库存
        if ($tour->stock < $totalPeople) {
            return response()->json([
                'code' => 400,
                'message' => '库存不足,当前剩余:' . $tour->stock . '人'
            ], 400);
        }

        // 5. 使用数据库事务执行操作
        DB::beginTransaction();

        try {
            // 6. 生成唯一订单号
            $orderNo = $this->generateOrderNo();

            // 7. 扣减库存
            $tour->decrement('stock', $totalPeople);
            $tour->increment('sold_count', $totalPeople);

            // 8. 创建订单
            $order = Order::create([
                'order_no' => $orderNo,
                'user_id' => auth()->id(),
                'tour_id' => $tour->id,
                'tour_name' => $tour->name,
                'tour_price' => $tour->price,
                'departure_date' => $request->departure_date,
                'adult_count' => $request->adult_count,
                'child_count' => $request->child_count,
                'total_amount' => $totalAmount,
                'contact_name' => $request->contact_name,
                'contact_phone' => $request->contact_phone,
                'contact_email' => $request->contact_email,
                'remark' => $request->remark,
                'order_status' => Order::STATUS_PENDING,
                'payment_status' => Order::PAYMENT_UNPAID
            ]);

            DB::commit();

            return response()->json([
                'code' => 201,
                'message' => '订单创建成功',
                'data' => [
                    'order_id' => $order->id,
                    'order_no' => $order->order_no,
                    'tour_name' => $order->tour_name,
                    'departure_date' => $order->departure_date,
                    'adult_count' => $order->adult_count,
                    'child_count' => $order->child_count,
                    'total_amount' => $order->total_amount,
                    'order_status' => $order->order_status,
                    'order_status_text' => $order->status_text,
                    'created_at' => $order->created_at
                ]
            ], 201);

        } catch (\Exception $e) {
            DB::rollBack();

            return response()->json([
                'code' => 500,
                'message' => '订单创建失败,请稍后重试'
            ], 500);
        }
    }

    /**
     * 获取当前用户的订单列表
     * 
     * 支持筛选:status参数可筛选订单状态
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function myOrders(Request $request)
    {
        $query = Order::where('user_id', auth()->id())
            ->with('tour')
            ->orderBy('created_at', 'desc');

        // 按状态筛选
        if ($request->has('status') && $request->status !== '') {
            $query->where('order_status', $request->status);
        }

        // 分页
        $page = $request->get('page', 1);
        $limit = $request->get('limit', 10);
        $offset = ($page - 1) * $limit;

        $total = $query->count();
        $orders = $query->skip($offset)->take($limit)->get();

        // 格式化订单数据
        $orders->transform(function($order) {
            return [
                'id' => $order->id,
                'order_no' => $order->order_no,
                'tour_id' => $order->tour_id,
                'tour_name' => $order->tour_name,
                'tour_image' => $order->tour ? $order->tour->main_image : null,
                'departure_date' => $order->departure_date->format('Y-m-d'),
                'adult_count' => $order->adult_count,
                'child_count' => $order->child_count,
                'total_amount' => $order->total_amount,
                'order_status' => $order->order_status,
                'order_status_text' => $order->status_text,
                'payment_status' => $order->payment_status,
                'payment_status_text' => $order->payment_status_text,
                'created_at' => $order->created_at->format('Y-m-d H:i:s')
            ];
        });

        return response()->json([
            'code' => 200,
            'data' => [
                'orders' => $orders,
                'pagination' => [
                    'current_page' => (int)$page,
                    'per_page' => (int)$limit,
                    'total' => $total,
                    'last_page' => ceil($total / $limit)
                ]
            ]
        ]);
    }

    /**
     * 获取订单详情
     * 
     * @param int $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function detail($id)
    {
        $order = Order::where('user_id', auth()->id())
            ->with('tour')
            ->find($id);

        if (!$order) {
            return response()->json([
                'code' => 404,
                'message' => '订单不存在'
            ], 404);
        }

        return response()->json([
            'code' => 200,
            'data' => [
                'id' => $order->id,
                'order_no' => $order->order_no,
                'tour_id' => $order->tour_id,
                'tour_name' => $order->tour_name,
                'tour_image' => $order->tour ? $order->tour->main_image : null,
                'tour_price' => $order->tour_price,
                'departure_date' => $order->departure_date->format('Y-m-d'),
                'adult_count' => $order->adult_count,
                'child_count' => $order->child_count,
                'total_amount' => $order->total_amount,
                'contact_name' => $order->contact_name,
                'contact_phone' => $order->contact_phone,
                'contact_email' => $order->contact_email,
                'remark' => $order->remark,
                'order_status' => $order->order_status,
                'order_status_text' => $order->status_text,
                'payment_status' => $order->payment_status,
                'payment_status_text' => $order->payment_status_text,
                'payment_time' => $order->payment_time ? $order->payment_time->format('Y-m-d H:i:s') : null,
                'confirm_time' => $order->confirm_time ? $order->confirm_time->format('Y-m-d H:i:s') : null,
                'created_at' => $order->created_at->format('Y-m-d H:i:s')
            ]
        ]);
    }

    /**
     * 取消订单
     * 
     * 业务流程:
     * 1. 验证订单是否可取消(待确认状态)
     * 2. 恢复线路库存
     * 3. 更新订单状态为已取消
     * 
     * @param int $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function cancel($id)
    {
        $order = Order::where('user_id', auth()->id())->find($id);

        if (!$order) {
            return response()->json([
                'code' => 404,
                'message' => '订单不存在'
            ], 404);
        }

        // 只有待确认的订单可以取消
        if ($order->order_status != Order::STATUS_PENDING) {
            return response()->json([
                'code' => 400,
                'message' => '当前订单状态无法取消'
            ], 400);
        }

        DB::beginTransaction();

        try {
            // 恢复库存
            $totalPeople = $order->adult_count + $order->child_count;
            $tour = Tour::find($order->tour_id);
            if ($tour) {
                $tour->increment('stock', $totalPeople);
                $tour->decrement('sold_count', $totalPeople);
            }

            // 更新订单状态
            $order->order_status = Order::STATUS_CANCELLED;
            $order->cancel_time = now();
            $order->save();

            DB::commit();

            return response()->json([
                'code' => 200,
                'message' => '订单已取消'
            ]);

        } catch (\Exception $e) {
            DB::rollBack();

            return response()->json([
                'code' => 500,
                'message' => '取消失败,请稍后重试'
            ], 500);
        }
    }

    /**
     * 确认订单(管理员)
     * 
     * @param int $id
     * @return \Illuminate\Http\JsonResponse
     */
    public function confirm($id)
    {
        $order = Order::find($id);

        if (!$order) {
            return response()->json([
                'code' => 404,
                'message' => '订单不存在'
            ], 404);
        }

        if ($order->order_status != Order::STATUS_PENDING) {
            return response()->json([
                'code' => 400,
                'message' => '当前订单状态无法确认'
            ], 400);
        }

        $order->order_status = Order::STATUS_CONFIRMED;
        $order->confirm_time = now();
        $order->save();

        return response()->json([
            'code' => 200,
            'message' => '订单已确认'
        ]);
    }

    /**
     * 订单统计(管理员)
     * 
     * @return \Illuminate\Http\JsonResponse
     */
    public function statistics()
    {
        $totalOrders = Order::count();
        $pendingOrders = Order::where('order_status', Order::STATUS_PENDING)->count();
        $confirmedOrders = Order::where('order_status', Order::STATUS_CONFIRMED)->count();
        $completedOrders = Order::where('order_status', Order::STATUS_COMPLETED)->count();
        $cancelledOrders = Order::where('order_status', Order::STATUS_CANCELLED)->count();

        $totalRevenue = Order::where('order_status', '!=', Order::STATUS_CANCELLED)
            ->sum('total_amount');

        // 今日订单数
        $todayOrders = Order::whereDate('created_at', today())->count();

        // 今日收入
        $todayRevenue = Order::where('order_status', '!=', Order::STATUS_CANCELLED)
            ->whereDate('created_at', today())
            ->sum('total_amount');

        return response()->json([
            'code' => 200,
            'data' => [
                'overview' => [
                    'total_orders' => $totalOrders,
                    'pending_orders' => $pendingOrders,
                    'confirmed_orders' => $confirmedOrders,
                    'completed_orders' => $completedOrders,
                    'cancelled_orders' => $cancelledOrders,
                    'total_revenue' => $totalRevenue
                ],
                'today' => [
                    'orders' => $todayOrders,
                    'revenue' => $todayRevenue
                ]
            ]
        ]);
    }

    /**
     * 生成唯一订单号
     * 格式:TO + 年月日时分秒 + 6位随机数
     * 示例:TO20240501123456012345
     * 
     * @return string
     */
    private function generateOrderNo()
    {
        $prefix = 'TO';
        $datetime = date('YmdHis');
        $random = rand(100000, 999999);

        $orderNo = $prefix . $datetime . $random;

        // 确保唯一性
        while (Order::where('order_no', $orderNo)->exists()) {
            $random = rand(100000, 999999);
            $orderNo = $prefix . $datetime . $random;
        }

        return $orderNo;
    }
}

2.11 收藏控制器

<?php
// app/Http/Controllers/FavoriteController.php
namespace App\Http\Controllers;

use App\Models\Tour;
use App\Models\Favorite;
use Illuminate\Http\Request;

/**
 * 收藏控制器
 * 处理用户收藏旅游线路的功能
 */
class FavoriteController extends Controller
{
    /**
     * 获取当前用户的收藏列表
     * 
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function index(Request $request)
    {
        $user = auth()->user();

        $page = $request->get('page', 1);
        $limit = $request->get('limit', 10);
        $offset = ($page - 1) * $limit;

        $favorites = $user->favorites()
            ->with('category')
            ->orderBy('favorites.created_at', 'desc')
            ->skip($offset)
            ->take($limit)
            ->get();

        $total = $user->favorites()->count();

        $favorites->transform(function($tour) {
            return [
                'id' => $tour->id,
                'name' => $tour->name,
                'price' => $tour->price,
                'days' => $tour->days,
                'main_image' => $tour->main_image,
                'departure_city' => $tour->departure_city,
                'category_name' => $tour->category ? $tour->category->name : null,
                'created_at' => $tour->pivot->created_at->format('Y-m-d H:i:s')
            ];
        });

        return response()->json([
            'code' => 200,
            'data' => [
                'favorites' => $favorites,
                'pagination' => [
                    'current_page' => (int)$page,
                    'per_page' => (int)$limit,
                    'total' => $total,
                    'last_page' => ceil($total / $limit)
                ]
            ]
        ]);
    }

    /**
     * 添加收藏
     * 
     * @param int $tourId
     * @return \Illuminate\Http\JsonResponse
     */
    public function add($tourId)
    {
        $tour = Tour::find($tourId);

        if (!$tour) {
            return response()->json([
                'code' => 404,
                'message' => '旅游线路不存在'
            ], 404);
        }

        $user = auth()->user();

        // 检查是否已收藏
        if ($user->favorites()->where('tour_id', $tourId)->exists()) {
            return response()->json([
                'code' => 400,
                'message' => '已经收藏过该线路'
            ], 400);
        }

        $user->favorites()->attach($tourId);

        return response()->json([
            'code' => 200,
            'message' => '收藏成功'
        ]);
    }

    /**
     * 取消收藏
     * 
     * @param int $tourId
     * @return \Illuminate\Http\JsonResponse
     */
    public function remove($tourId)
    {
        $user = auth()->user();

        if (!$user->favorites()->where('tour_id', $tourId)->exists()) {
            return response()->json([
                'code' => 404,
                'message' => '未收藏该线路'
            ], 404);
        }

        $user->favorites()->detach($tourId);

        return response()->json([
            'code' => 200,
            'message' => '取消收藏成功'
        ]);
    }

    /**
     * 检查是否已收藏
     * 
     * @param int $tourId
     * @return \Illuminate\Http\JsonResponse
     */
    public function check($tourId)
    {
        $user = auth()->user();
        $isFavorited = $user->favorites()->where('tour_id', $tourId)->exists();

        return response()->json([
            'code' => 200,
            'data' => [
                'is_favorited' => $isFavorited
            ]
        ]);
    }
}

2.12 路由配置

<?php
// routes/api.php

use App\Http\Controllers\AuthController;
use App\Http\Controllers\TourController;
use App\Http\Controllers\OrderController;
use App\Http\Controllers\FavoriteController;

/*
|--------------------------------------------------------------------------
| API路由
|--------------------------------------------------------------------------
*/

// 公开路由(无需认证)
Route::post('/user/register', [AuthController::class, 'register']);
Route::post('/user/login', [AuthController::class, 'login']);

// 旅游线路公开接口
Route::get('/tours', [TourController::class, 'index']);
Route::get('/tours/{slug}', [TourController::class, 'show']);
Route::get('/tours/categories/list', [TourController::class, 'getCategories']);

// 需要认证的路由(需要登录)
Route::middleware(['auth:api'])->group(function () {
    // 用户相关
    Route::get('/user/profile', [AuthController::class, 'profile']);
    Route::put('/user/password', [AuthController::class, 'changePassword']);
    Route::post('/user/logout', [AuthController::class, 'logout']);
    Route::post('/user/refresh', [AuthController::class, 'refresh']);

    // 收藏功能
    Route::get('/favorites', [FavoriteController::class, 'index']);
    Route::post('/favorites/{tourId}', [FavoriteController::class, 'add']);
    Route::delete('/favorites/{tourId}', [FavoriteController::class, 'remove']);
    Route::get('/favorites/check/{tourId}', [FavoriteController::class, 'check']);

    // 订单功能
    Route::post('/orders', [OrderController::class, 'create']);
    Route::get('/orders', [OrderController::class, 'myOrders']);
    Route::get('/orders/{id}', [OrderController::class, 'detail']);
    Route::put('/orders/{id}/cancel', [OrderController::class, 'cancel']);
});

// 管理员路由
Route::middleware(['auth:api', 'admin'])->prefix('admin')->group(function () {
    // 线路管理
    Route::post('/tours', [TourController::class, 'store']);
    Route::put('/tours/{id}', [TourController::class, 'update']);
    Route::delete('/tours/{id}', [TourController::class, 'destroy']);
    Route::put('/tours/{id}/status', [TourController::class, 'toggleStatus']);

    // 订单管理
    Route::put('/orders/{id}/confirm', [OrderController::class, 'confirm']);
    Route::get('/orders/statistics', [OrderController::class, 'statistics']);
});

2.13 添加管理员中间件

<?php
// app/Http/Middleware/AdminMiddleware.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

/**
 * 管理员权限中间件
 * 验证当前登录用户是否为管理员
 */
class AdminMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        if (!auth()->check()) {
            return response()->json([
                'code' => 401,
                'message' => '请先登录'
            ], 401);
        }

        if (auth()->user()->role !== 'admin') {
            return response()->json([
                'code' => 403,
                'message' => '权限不足,需要管理员权限'
            ], 403);
        }

        return $next($request);
    }
}

2.14 注册中间件

// app/Http/Kernel.php
protected $routeMiddleware = [
    // ...
    'admin' => \App\Http\Middleware\AdminMiddleware::class,
];

2.15 配置跨域

// app/Http/Middleware/Cors.php
namespace App\Http\Middleware;

use Closure;

class Cors
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        $response->headers->set('Access-Control-Allow-Origin', env('FRONTEND_URL', 'http://localhost:5173'));
        $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
        $response->headers->set('Access-Control-Allow-Credentials', 'true');

        if ($request->getMethod() == 'OPTIONS') {
            $response->setStatusCode(200);
        }

        return $response;
    }
}

2.16 全局异常处理

// app/Exceptions/Handler.php
public function register()
{
    $this->renderable(function (\Illuminate\Auth\AuthenticationException $e, $request) {
        return response()->json([
            'code' => 401,
            'message' => '未登录或登录已过期'
        ], 401);
    });

    $this->renderable(function (\Illuminate\Validation\ValidationException $e, $request) {
        return response()->json([
            'code' => 400,
            'message' => '参数验证失败',
            'errors' => $e->errors()
        ], 400);
    });

    $this->renderable(function (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e, $request) {
        return response()->json([
            'code' => 404,
            'message' => '接口不存在'
        ], 404);
    });

    $this->renderable(function (\Exception $e, $request) {
        return response()->json([
            'code' => 500,
            'message' => '服务器内部错误'
        ], 500);
    });
}

来源:
http://oieaw.cn

相关文章
|
8天前
|
缓存 人工智能 自然语言处理
我对比了8个Claude API中转站,踩了不少坑,总结给你
本文是个人开发者耗时1周实测的8大Claude中转平台横向评测,聚焦Claude Code真实体验:以加权均价(¥/M token)、内部汇率、缓存支持、模型真实性及稳定性为核心指标。
3370 20
|
20天前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
17857 60
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
|
1天前
|
SQL 人工智能 弹性计算
阿里云发布 Agentic NDR,威胁检测与响应进入智能体时代
欢迎前往阿里云云防火墙控制台体验!
1154 2
|
4天前
|
人工智能 JSON BI
DeepSeek V4 来了!超越 Claude Sonnet 4.5,赶紧对接 Claude Code 体验一把
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro 的真实体验与避坑记录 本文记录我将 Claude Code 对接 DeepSeek 最新模型(V4Pro)后的真实体验,测试了 Skills 自动化查询和积木报表 AI 建表两个场景——有惊喜,也踩
1785 8
|
15天前
|
人工智能 JavaScript Ubuntu
低成本搭建AIP自动化写作系统:Hermes保姆级使用教程,长文和逐步实操贴图
我带着怀疑的态度,深度使用了几天,聚焦微信公众号AIP自动化写作场景,写出来的几篇文章,几乎没有什么修改,至少合乎我本人的意愿,而且排版风格,也越来越完善,同样是起码过得了我自己这一关。 这个其实OpenClaw早可以实现了,但是目前我觉得最大的区别是,Hermes会自主总结提炼,并更新你的写作技能。 相信就冲这一点,就值得一试。 这篇帖子主要就Hermes部署使用,作一个非常详细的介绍,几乎一步一贴图。 关于Hermes,无论你赞成哪种声音,我希望都是你自己动手行动过,发自内心的选择!
3159 29
|
3天前
|
人工智能 缓存 BI
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro,跑完 Skills —— OA 审批、大屏、报表、部署 5 大实战场景后的真实体验 ![](https://oscimg.oschina.net/oscnet/up608d34aeb6bafc47f
1416 3
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
|
4天前
|
机器学习/深度学习 缓存 测试技术
DeepSeek-V4开源:百万上下文,Agent能力比肩顶级闭源模型
DeepSeek-V4正式开源!含V4-Pro(1.6T参数)与V4-Flash(284B参数)双版本,均支持百万token上下文。首创混合注意力架构,Agent能力、世界知识与推理性能全面领先开源模型,数学/代码评测比肩顶级闭源模型。
1712 6
|
5天前
|
人工智能 测试技术 API
阿里Qwen3.6-27B正式开源:网友直呼“太牛了”!
阿里云千问3.6系列重磅开源Qwen3.6-27B稠密大模型!官网:https://t.aliyun.com/U/JbblVp 仅270亿参数,编程能力媲美千亿模型,在SWE-bench等权威基准中表现卓越。支持多模态理解、本地部署及OpenClaw等智能体集成,已开放Hugging Face与ModelScope下载。