从 0 到 1 实战:基于 Laravel 开发一个二手书交易系统 本文分享一个基于 Laravel 开发的二手书交易系统实战案例,详细介绍了项目背景、需求分析、数据库设计、模型关系、路由规划、图书发布、购物车、订单事务处理、模拟支付、权限控制以及后台管理等内容。项目围绕真实的二手书交易场景展开,重点解决了库存唯一、订单一致性和角色权限等实际开发问题,非常适合作为 Laravel 学习和面试展示项目。
基于 Laravel 开发一个二手书交易系统:项目设计、功能拆解与落地实践 一、项目背景 随着高校和读书社区的发展,二手书交易一直是一个很有现实价值的场景。很多学生和读者手中都有大量闲置书籍,这些书往往只看过一次,保存完好,但长期放置又会造成资源浪费。
市面上虽然存在一些综合类二手交易平台,但对于“书籍”这个垂直品类来说,它有一些比较鲜明的特点:
用户更关注书籍版本、出版社、成色、笔记情况
交易金额通常不高,但交易频次较高
搜索和分类的重要性很强
买家经常会关注同一本书的不同卖家报价
卖家对发布流程的便捷性要求较高
因此,我决定使用 Laravel 开发一个专门面向“二手书交易”的 Web 系统,用来实现一个完整的交易闭环,包括:
用户注册登录
图书发布
图书浏览与搜索
购物车
下单支付(模拟)
订单管理
卖家管理书籍状态
收藏与评价
后台管理
这个项目既能锻炼 Laravel 的基础能力,也能覆盖实际开发中常见的模块设计,是一个非常适合练手和写进简历的中型项目。
二、项目目标 本项目希望实现一个基本可用的二手书交易平台,满足以下核心目标:
1. 面向用户端 普通用户可以:
注册账号、登录系统
浏览二手书列表
查看书籍详情
按分类、关键词、价格筛选图书
发布自己想出售的书籍
收藏感兴趣的书籍
将书籍加入购物车
提交订单并完成模拟支付
查看历史订单
对已完成交易进行评价
2. 面向卖家端 卖家可以:
发布二手书信息
编辑书籍资料
上传书籍图片
管理库存状态
查看自己收到的订单
标记发货
处理下架、售罄等状态
3. 面向管理员 管理员可以:
管理用户
管理图书分类
审核图书信息
处理违规发布内容
查看订单统计
管理评价和举报信息
三、技术选型 这套系统的技术栈如下:
1 2 3 4 5 6 7 8 9 后端框架:Laravel 10 前端模板:Blade 数据库:MySQL 8.x 缓存/队列:Redis(可选) 认证方案:Laravel Breeze / Laravel Sanctum 文件存储:本地存储或 OSS UI 框架:Bootstrap 5 开发环境:PHP 8.2 + Composer + Node.js + Vite 部署环境:Nginx + PHP-FPM + MySQL
为什么选择 Laravel? Laravel 非常适合构建这种中小型业务系统,主要有几个原因:
MVC 结构清晰 模型、控制器、视图分离明确,便于维护。
Eloquent ORM 开发效率高 数据表之间的关系建模非常自然,特别适合电商/交易类系统。
路由和中间件机制成熟 用户权限、认证拦截、后台管理都比较方便。
生态完善 包括认证、文件上传、邮件通知、队列任务、API 开发等都有成熟方案。
适合作为完整案例展示 从数据库设计到前后台交互都能覆盖,容易形成可讲述的项目经验。
四、需求分析 在正式开始编码之前,我先对系统做了一个需求拆分。
五、核心功能模块 1. 用户模块 用户模块主要负责账号体系与个人中心功能,包含:
注册
登录
退出登录
修改个人资料
修改头像
修改密码
查看我的发布
查看我的订单
查看我的收藏
2. 图书模块 图书模块是系统的核心,主要包括:
发布图书
编辑图书
删除图书
图书详情展示
图书列表分页
分类筛选
关键词搜索
书籍状态管理(在售、已售、下架)
图书发布时,需要录入以下字段:
书名
作者
出版社
ISBN(可选)
原价
售价
成色
分类
描述
图片
是否有笔记/划线
卖家信息
3. 购物车模块 买家可以将感兴趣的二手书加入购物车,并进行统一结算。
由于二手书一般每本只有一件库存,所以这里的购物车逻辑和普通电商有些区别:
书籍通常只能买一件
同一本书一旦被购买,其他用户应无法继续下单
加入购物车并不代表锁定库存
真正扣减库存应发生在订单创建/支付环节
4. 订单模块 订单模块包括:
创建订单
查看订单详情
模拟支付
取消订单
卖家发货
买家确认收货
订单评价
订单状态设计为:
1 2 3 4 5 6 pending 待支付 paid 已支付 shipped 已发货 completed 已完成 cancelled 已取消 refunded 已退款(预留)
5. 收藏与评价模块 为了增强平台互动,可以加入:
收藏书籍
取消收藏
订单完成后进行评分
发表评论内容
展示卖家信誉
6. 后台管理模块 后台可以实现以下功能:
用户管理
分类管理
书籍审核
订单查看
举报处理
数据统计
六、系统角色设计 本系统主要有三类角色:
角色
描述
普通用户
可以浏览、购买、收藏图书
卖家
实际上也是普通用户,但可以发布图书
管理员
负责后台管理与审核
从实现角度来说,可以在 users 表中增加一个 role 字段:
卖家不需要单独的角色字段,因为“能发布图书的普通用户”天然就是卖家。
七、数据库设计 数据库设计是整个项目的关键部分。下面是我对主要数据表的设计思路。
八、数据表设计 1. users 用户表 1 2 3 4 5 6 7 8 9 10 id name email password avatar phone address role created_at updated_at
说明:
role 用于区分普通用户和管理员
avatar 存储头像路径
address 可以作为默认收货地址
2. categories 图书分类表 1 2 3 4 5 id name sort created_at updated_at
示例分类:
3. books 图书表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 id user_id category_id title author publisher isbn origin_price price condition_level description image status has_note stock view_count created_at updated_at deleted_at
字段说明:
user_id:卖家 ID
category_id:分类 ID
condition_level:成色,例如“95新”“8成新”
status:在售、已售、下架、待审核
has_note:是否有笔记
stock:库存,虽然二手书通常是 1,但可以预留多个册数场景
view_count:浏览量
deleted_at:软删除
推荐状态值:
1 2 3 4 5 pending 待审核 active 在售 sold 已售 off 已下架 rejected 审核拒绝
4. cart_items 购物车表 1 2 3 4 5 id user_id book_id created_at updated_at
说明:
一个用户可以收藏多个购物车商品
同一本书对于同一用户只能存在一条记录
5. orders 订单表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 id order_no buyer_id seller_id total_amount status payment_time shipping_time completion_time receiver_name receiver_phone receiver_address remark created_at updated_at
说明:
order_no 为订单编号
一个订单可以只买一本,也可以支持多本
buyer_id 是买家
seller_id 可在单卖家场景中存在;多卖家场景建议拆分子订单
6. order_items 订单明细表 1 2 3 4 5 6 7 8 id order_id book_id book_title book_price quantity created_at updated_at
说明:
下单时把书名和价格冗余存进去,避免后续图书信息修改影响订单历史
二手书场景下一般 quantity = 1
7. favorites 收藏表 1 2 3 4 5 id user_id book_id created_at updated_at
8. reviews 评价表 1 2 3 4 5 6 7 8 9 id order_id book_id buyer_id seller_id rating content created_at updated_at
评分建议使用 1~5 分。
9. reports 举报表(可选) 1 2 3 4 5 6 7 id user_id book_id reason status created_at updated_at
九、Eloquent 模型关系设计 Laravel 的优势之一就是模型关系表达非常清晰。
User 模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class User extends Authenticatable { public function books ( ) { return $this ->hasMany(Book::class); } public function orders ( ) { return $this ->hasMany(Order::class, 'buyer_id' ); } public function favorites ( ) { return $this ->hasMany(Favorite::class); } }
Book 模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Book extends Model { use SoftDeletes ; protected $fillable = [ 'user_id' , 'category_id' , 'title' , 'author' , 'publisher' , 'isbn' , 'origin_price' , 'price' , 'condition_level' , 'description' , 'image' , 'status' , 'has_note' , 'stock' , ]; public function seller ( ) { return $this ->belongsTo(User::class, 'user_id' ); } public function category ( ) { return $this ->belongsTo(Category::class); } }
Order 模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Order extends Model { protected $fillable = [ 'order_no' , 'buyer_id' , 'seller_id' , 'total_amount' , 'status' , 'receiver_name' , 'receiver_phone' , 'receiver_address' , 'remark' , ]; public function buyer ( ) { return $this ->belongsTo(User::class, 'buyer_id' ); } public function items ( ) { return $this ->hasMany(OrderItem::class); } }
十、项目目录结构设计 在 Laravel 中,这个项目我会尽量按职责拆分目录:
1 2 3 4 5 6 7 8 9 10 11 12 app/ ├── Http/ │ ├── Controllers/ │ │ ├── Front/ │ │ ├── Seller/ │ │ └── Admin/ │ ├── Requests/ │ └── Middleware/ ├── Models/ ├── Services/ ├── Repositories/ └── Helpers/
推荐拆分方式
Front:前台用户功能
Seller:卖家中心
Admin:后台管理
Requests:表单校验
Services:业务逻辑,比如创建订单、支付处理
Repositories:可选,用于封装数据访问
对于中型项目,Controller 不宜写太重 ,可以把复杂逻辑放到 Service 层。
十一、路由设计 前台路由示例 1 2 3 4 5 6 7 8 9 10 Route::get('/' , [HomeController::class, 'index' ])->name('home' ); Route::get('/books' , [BookController::class, 'index' ])->name('books.index' ); Route::get('/books/{book}' , [BookController::class, 'show' ])->name('books.show' ); Route::middleware('auth' )->group(function ( ) { Route::get('/cart' , [CartController::class, 'index' ])->name('cart.index' ); Route::post('/cart/add/{book}' , [CartController::class, 'add' ])->name('cart.add' ); Route::post('/orders/checkout' , [OrderController::class, 'checkout' ])->name('orders.checkout' ); Route::get('/orders' , [OrderController::class, 'index' ])->name('orders.index' ); });
卖家路由示例 1 2 3 4 5 6 Route::middleware('auth' )->prefix('seller' )->name('seller.' )->group(function ( ) { Route::get('/books' , [SellerBookController::class, 'index' ])->name('books.index' ); Route::get('/books/create' , [SellerBookController::class, 'create' ])->name('books.create' ); Route::post('/books' , [SellerBookController::class, 'store' ])->name('books.store' ); Route::get('/orders' , [SellerOrderController::class, 'index' ])->name('orders.index' ); });
后台路由示例 1 2 3 4 5 6 Route::middleware(['auth' , 'admin' ])->prefix('admin' )->name('admin.' )->group(function ( ) { Route::resource('users' , AdminUserController::class); Route::resource('categories' , AdminCategoryController::class); Route::resource('books' , AdminBookController::class); Route::resource('orders' , AdminOrderController::class); });
十二、关键业务流程设计
十三、图书发布流程 卖家发布一本二手书,大致流程如下:
1 2 3 4 5 6 7 8 进入发布页面 -> 填写图书信息 -> 上传图片 -> 提交表单 -> 后端校验 -> 保存数据库 -> 状态设为 pending 或 active -> 返回发布成功页面
表单校验示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class StoreBookRequest extends FormRequest { public function rules ( ): array { return [ 'title' => 'required|string|max:255' , 'author' => 'required|string|max:100' , 'category_id' => 'required|exists:categories,id' , 'price' => 'required|numeric|min:0.01' , 'origin_price' => 'nullable|numeric|min:0' , 'condition_level' => 'required|string|max:50' , 'description' => 'required|string|min:10' , 'image' => 'required|image|max:2048' , ]; } }
控制器示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function store (StoreBookRequest $request ) { $data = $request ->validated(); if ($request ->hasFile('image' )) { $data ['image' ] = $request ->file('image' )->store('books' , 'public' ); } $data ['user_id' ] = auth()->id(); $data ['status' ] = 'pending' ; $data ['stock' ] = 1 ; Book::create($data ); return redirect()->route('seller.books.index' ) ->with('success' , '图书发布成功,等待审核' ); }
十四、图书搜索与筛选设计 图书搜索是用户体验的重点之一。
支持筛选条件
关键词(书名、作者、出版社)
分类
价格区间
成色
排序方式(最新、价格升序、价格降序、热度)
查询示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public function index (Request $request ) { $query = Book::query() ->where('status' , 'active' ); if ($keyword = $request ->keyword) { $query ->where(function ($q ) use ($keyword ) { $q ->where('title' , 'like' , "%{$keyword} %" ) ->orWhere('author' , 'like' , "%{$keyword} %" ) ->orWhere('publisher' , 'like' , "%{$keyword} %" ); }); } if ($categoryId = $request ->category_id) { $query ->where('category_id' , $categoryId ); } if ($min = $request ->min_price) { $query ->where('price' , '>=' , $min ); } if ($max = $request ->max_price) { $query ->where('price' , '<=' , $max ); } $books = $query ->latest()->paginate(12 ); return view('books.index' , compact('books' )); }
这个逻辑虽然简单,但已经能满足大部分场景。后续还可以进一步加入:
Elasticsearch 全文搜索
热门搜索关键词统计
搜索联想
历史搜索记录
十五、购物车设计思路 购物车逻辑看似简单,实际上很容易出现并发问题。
添加购物车时要注意
图书必须是“在售”
不能添加自己发布的书
购物车中不能重复添加
库存必须大于 0
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function add (Book $book ) { if ($book ->user_id == auth()->id()) { return back()->with('error' , '不能购买自己发布的图书' ); } if ($book ->status !== 'active' || $book ->stock < 1 ) { return back()->with('error' , '该图书已不可购买' ); } CartItem::firstOrCreate([ 'user_id' => auth()->id(), 'book_id' => $book ->id, ]); return back()->with('success' , '已加入购物车' ); }
十六、订单创建与事务处理 订单创建是整个系统中最核心、最敏感的模块,必须确保数据一致性。
关键问题
下单时图书是否已被别人买走?
同一本二手书是否会被重复售卖?
扣减库存和创建订单是否要放进事务?
订单失败时是否要回滚?
答案是:必须使用数据库事务 。
下单核心流程 1 2 3 4 5 6 7 8 9 10 用户点击结算 -> 获取购物车商品 -> 检查每本书是否仍在售 -> 开启事务 -> 创建订单 -> 创建订单明细 -> 扣减库存 -> 修改图书状态为 sold -> 删除购物车记录 -> 提交事务
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 DB::transaction(function ( ) use ($cartItems , $request ) { $totalAmount = $cartItems ->sum(fn ($item ) => $item ->book->price); $firstBook = $cartItems ->first()->book; $order = Order::create([ 'order_no' => 'ORD' . now()->format('YmdHis' ) . rand(1000 , 9999 ), 'buyer_id' => auth()->id(), 'seller_id' => $firstBook ->user_id, 'total_amount' => $totalAmount , 'status' => 'pending' , 'receiver_name' => $request ->receiver_name, 'receiver_phone' => $request ->receiver_phone, 'receiver_address' => $request ->receiver_address, ]); foreach ($cartItems as $item ) { $book = Book::lockForUpdate()->find($item ->book_id); if (!$book || $book ->status !== 'active' || $book ->stock < 1 ) { throw new \Exception ("图书 {$book->title} 已不可购买" ); } OrderItem::create([ 'order_id' => $order ->id, 'book_id' => $book ->id, 'book_title' => $book ->title, 'book_price' => $book ->price, 'quantity' => 1 , ]); $book ->update([ 'stock' => 0 , 'status' => 'sold' , ]); } CartItem::where('user_id' , auth()->id())->delete(); });
为什么要使用 lockForUpdate()? 因为二手书库存非常敏感,同一本书可能被两个人同时抢购。
lockForUpdate() 能在事务中锁住这条记录,避免多个请求同时修改库存,防止“超卖”。
十七、模拟支付设计 由于这是一个练手项目,不接入真实支付,可以设计一个模拟支付流程。
流程 1 2 3 4 5 订单创建成功 -> 进入支付页面 -> 点击“确认支付” -> 修改订单状态为 paid -> 记录支付时间
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public function pay (Order $order ) { if ($order ->buyer_id !== auth()->id()) { abort(403 ); } if ($order ->status !== 'pending' ) { return back()->with('error' , '订单状态异常' ); } $order ->update([ 'status' => 'paid' , 'payment_time' => now(), ]); return redirect()->route('orders.show' , $order ) ->with('success' , '支付成功' ); }
后续若要扩展,也可以对接:
十八、卖家发货与买家确认收货 发货逻辑 卖家在订单状态为 paid 时,才可以点击发货。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function ship (Order $order ) { if ($order ->seller_id !== auth()->id()) { abort(403 ); } if ($order ->status !== 'paid' ) { return back()->with('error' , '订单尚未支付' ); } $order ->update([ 'status' => 'shipped' , 'shipping_time' => now(), ]); return back()->with('success' , '已发货' ); }
确认收货逻辑 买家在订单状态为 shipped 时可以确认收货:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function confirm (Order $order ) { if ($order ->buyer_id !== auth()->id()) { abort(403 ); } if ($order ->status !== 'shipped' ) { return back()->with('error' , '订单状态不允许确认收货' ); } $order ->update([ 'status' => 'completed' , 'completion_time' => now(), ]); return back()->with('success' , '交易完成' ); }
十九、权限控制设计 Laravel 的中间件特别适合做权限控制。
登录权限 未登录用户不能:
管理员权限 管理员才可以访问后台路由。
admin 中间件示例 1 2 3 4 5 6 7 8 public function handle (Request $request , Closure $next ) { if (auth()->check() && auth()->user()->role === 'admin' ) { return $next ($request ); } abort(403 , '无权限访问' ); }
二十、前端页面设计思路 尽管这是后端项目,但前端交互体验也很重要。
页面结构建议 1. 首页 展示:
2. 图书列表页 展示:
3. 图书详情页 展示:
大图
书籍基础信息
卖家信息
图书描述
是否有笔记
加购物车按钮
立即购买按钮
4. 卖家中心 展示:
我的发布
待售图书
已售图书
发布新图书
收到的订单
5. 个人中心 展示:
6. 管理后台 展示:
二十一、Blade 模板拆分建议 可以将 Blade 模板按以下方式组织:
1 2 3 4 5 6 7 8 9 10 resources/views/ ├── layouts/ │ └── app.blade.php ├── home/ ├── books/ ├── cart/ ├── orders/ ├── seller/ ├── admin/ └── components/
公共布局模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>@yield('title', '二手书交易系统')</title> @vite(['resources/css/app.css', 'resources/js/app.js']) </head> <body> @include('components.navbar') <main class="container mt-4"> @include('components.flash') @yield('content') </main> </body> </html>
这样后续维护会轻松很多。
二十二、项目中的难点与解决方案 这个项目虽然体量不算特别大,但在开发过程中还是会遇到一些比较典型的难点。
难点 1:防止二手书重复售卖 问题 二手书通常只有一件库存,如果两个买家同时购买,就容易发生超卖。
解决方案
下单时使用事务
查询库存时使用 lockForUpdate()
支付前后确保状态校验
图书状态从 active 变为 sold
难点 2:订单与图书状态的一致性 问题 订单创建成功但图书状态未更新,或者图书售出但订单失败,会造成脏数据。
解决方案 将以下操作放进同一个事务中:
创建订单
创建订单明细
更新图书库存
修改图书状态
清空购物车
只要其中一步失败,就整体回滚。
难点 3:书籍信息快照 问题 如果订单中的书名、价格直接关联 books 表,后续卖家修改图书信息会影响历史订单显示。
解决方案 在 order_items 表中冗余保存:
这样历史订单永远不会因为主表变化而失真。
难点 4:权限边界 问题 买家、卖家、管理员的操作权限不同,很容易出现越权访问。
解决方案 多层控制:
路由中间件限制
控制器内再次校验资源归属
使用 Policy 统一处理授权逻辑
例如:
1 2 3 4 public function update (User $user , Book $book ) { return $user ->id === $book ->user_id; }
难点 5:图片上传与存储 问题 图书图片是系统的重要组成部分,需要考虑:
文件大小限制
图片格式校验
存储路径管理
默认图处理
解决方案
使用 Laravel Storage
统一存入 public/books
保存相对路径到数据库
在前端通过 Storage::url() 渲染图片
二十三、性能优化思路 项目初期数据量不大,但仍然可以从一开始养成性能优化意识。
1. 避免 N+1 查询 图书列表页通常会展示:
如果不做预加载,很容易出现 N+1 问题。
1 $books = Book::with(['category' , 'seller' ])->latest()->paginate(12 );
2. 使用索引 建议为以下字段加索引:
books.category_id
books.status
books.user_id
orders.buyer_id
orders.seller_id
favorites.user_id
cart_items.user_id
3. 热门数据缓存 首页热门图书、分类列表可以做缓存:
1 2 3 $categories = Cache::remember('book_categories' , 3600 , function ( ) { return Category::orderBy('sort' )->get(); });
4. 图片压缩 如果图书图片过大,会明显影响页面加载速度。可以在上传后使用图片处理库进行压缩,比如 Intervention Image。
二十四、可扩展功能 这个项目如果继续深入,还可以扩展很多高级功能。
1. 即时聊天 买家可以和卖家直接沟通,比如:
可以使用:
Laravel WebSockets
Pusher
Echo
2. 举报与审核机制 针对违规内容、盗版书、虚假信息进行举报与审核。
3. 支付接入 接入真实支付系统,实现完整交易闭环。
4. 物流信息 卖家发货后填写快递单号,买家查看物流状态。
5. 消息通知 包括:
下单通知
支付通知
发货通知
收货通知
审核结果通知
可以使用:
6. API 化 前后端分离版本可使用:
Laravel Sanctum
Laravel Passport
Vue / React / UniApp
7. 评分信誉体系 为卖家建立信誉分:
8. 推荐系统 根据用户收藏、浏览、购买行为推荐类似图书。
二十五、项目开发流程回顾 整个项目可以按以下顺序推进:
第一步:初始化项目 1 laravel new used-book-market
或:
1 composer create-project laravel/laravel used-book-market
第二步:配置数据库 修改 .env:
1 2 3 4 5 6 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=used_book_market DB_USERNAME=root DB_PASSWORD=123456
第三步:安装认证脚手架 1 2 3 4 composer require laravel/breeze --dev php artisan breeze:install blade npm install && npm run build php artisan migrate
第四步:创建模型和迁移 1 2 3 4 5 6 7 php artisan make:model Book -mcr php artisan make:model Category -mcr php artisan make:model Order -mcr php artisan make:model OrderItem -mcr php artisan make:model CartItem -mcr php artisan make:model Favorite -mcr php artisan make:model Review -mcr
第五步:补充迁移文件并执行
第六步:实现前台列表、详情、发布功能 优先完成最关键的业务链路:
第七步:实现购物车与订单 完成交易闭环:
第八步:加入后台管理 补充:
第九步:优化样式和交互
表单校验提示
空状态页面
Toast 提示
图片预览
分页美化
原文链接: https://alexhuihui.github.io/article/20260417.html
版权声明: 转载请注明出处.