第二部分:后端开发(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);
});
}