资讯动态

网上书店系统设计实战:从架构到并发库存扣减的完整实现

发布时间:2026/9/8 7:16:09 来源:尧图企业网站定制
简介一套基于B/S架构的网上书店系统设计代码包含图书展示、搜索、购物车、订单提交及后台管理等模块适合学习JSP与动态网站开发、需要课程设计或毕业设计参考的学生。压缩包为RAR格式共60个文件包含20个ASP动态页面、JPG/GIF图片素材、DOC说明文档及Access数据库MDB文件整体仅2.19MB结构紧凑便于下载使用。目前已有52人浏览学习适合作为快速了解网上书店系统实现的参考案例。通过分析项目中的页面逻辑、数据库脚本和会话处理流程可以掌握动态网页与数据库增删改查、购物车实现、用户登录验证等典型功能附带的说明文档对系统模块和操作流程做了梳理便于理清设计思路并在本地部署环境中二次修改调试。 做网上书店系统设计很多人一上来就跳进代码里把CRUD写完就以为大功告成。等真正上线才发现搜索慢、库存超卖、订单状态混乱、支付回调对不上账随便一个问题都够你加两天班。这篇文章我想从一个完整项目的角度把网上书店系统的设计思路和核心代码实现从头到尾捋一遍从需求拆解、数据库设计、后端模块落到前端交互最后再聊聊我在实际开发里踩过的坑。适合正在做课程设计、毕业设计或者刚入职想独立上手Web项目的朋友参考。1. 网上书店系统的核心定位与整体设计思路1.1 项目需求怎么拆才算合理网上书店系统本质上是一个典型的电商类Web应用但它和淘宝这类大而全的平台不一样核心链路更聚焦。我习惯先把需求拆成两条主线一条是用户看得见的业务功能线另一条是用户感知不到但必须稳的支撑线。业务功能线解决的是用户怎么买书包括用户注册登录、图书浏览与检索、购物车管理、订单提交与支付、订单查询这几个环节。支撑线解决的是系统怎么运营包括后台图书管理、库存扣减、订单状态流转、数据统计。两线是上下层关系支撑线的每一环都服务于业务线的处理链路我在设计代码时就按这个逻辑分模块而不是按页面分模块。1.2 技术栈选型背后的取舍逻辑这套系统我用的是Spring Boot MyBatis Plus MySQL Vue Element UI的组合。前后端分离的架构严格来说后端提供一套RESTful API前端独立部署。Spring Boot用来做接口层和业务层MyBatis Plus减轻单表CRUD的样板代码量MySQL存业务数据Redis缓存热门图书信息和用户会话。可能有朋友问为什么不用JSP那套服务端渲染我的理由是网上书店系统的核心体验在于随手翻、加购快、结算顺前后端分离能让页面交互更灵活也能让接口复用性更强——就算以后要出小程序端或者移动端这套接口不需要大改。而在单机部署、课程设计或中小流量场景下Redis用不用其实可选但我建议加上理由后面讲库存时会提到。2. 数据库设计与核心表结构落地2.1 五张核心表的字段规划与关系梳理实体关系我控制在五张主表和一个关联表用户表t_user、图书表t_book、订单表t_order、订单明细表t_order_item、分类表t_category以及用户-收藏关联表t_user_favorite。用户表字段不复杂id、username、password密文存储、phone、email、create_time。注意一点密码一定不要存明文哪怕只是课程设计也要用MD5加盐或BCrypt处理这属于基本的安全素养。我用的BCrypt。图书表是最核心的一张表字段包括id、book_name、author、publisher、publish_date、category_id、price、stock、sales_count、cover_url、description、status。category_id做逻辑外键关联分类表存储上不建物理外键约束原因后面我会专门说。price和stock这两个字段用DECIMAL(10,2)和INT分别解决浮点精度问题和库存扣减原子性。订单表字段id、order_no唯一业务编号、user_id、total_amount、status、pay_time、create_time、update_time。订单明细表id、order_id、book_id、book_name、price、quantity、amount。为什么明细表要冗余一份book_name和price你想想图书信息是可以改的但一单成交后用户看到的历史订单必须还原当时的价格和书名如果只存book_id图书一旦下架或改名订单历史就失真了。2.2 为什么我用逻辑外键而不用物理外键这是一个新手特别容易忽略的地方。物理外键在删除或更新时会有约束校验看似安全但在高并发写入场景下会引发额外的锁开销而且线上数据迁移时非常痛苦。我在项目里采用的是逻辑外键 应用层保证的方案表里存category_id、user_id这类的关联字段拼SQL JOIN查询时依赖索引保证效率删除分类时在Service层先判断该分类下是否有书有则不允许删。这个取舍用生活里的话讲就是物理外键像交通管制每一辆车都停下来检查通行证安全但堵车逻辑外键像ETC大部分车辆直接走特殊车辆再引导处理。流量一大你会感谢这个选择。3. 后端核心模块的代码实现精讲3.1 用户注册登录与Session问题注册登录这块我用JWT代替了传统的HttpSession。选择JWT的核心理由是前端部署在其他端口或域名时Session会有跨域Cookie的麻烦而JWT是无状态的后端不保存登录态只要前端在Header里带上Token后端就能解析出用户信息。对于前后端分离项目来说这是最顺手的方案。登录接口的核心逻辑如下Override public LoginResult login(String username, String password) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUsername, username); User user userMapper.selectOne(wrapper); if (user null || !BCrypt.checkpw(password, user.getPassword())) { return LoginResult.fail(用户名或密码错误); } if (user.getStatus() 1) { return LoginResult.fail(账号已被禁用); } String token JwtUtil.createToken(user.getId(), user.getUsername()); return LoginResult.success(token, user); }注意几个细节密码校验用的是BCrypt.checkpw而不是先解密登录失败统一返回用户名或密码错误不区分是用户不存在还是密码错误避免账号枚举攻击生成Token时只放userId和username绝不能放密码等敏感信息。3.2 图书搜索的分页与多条件组合查询图书列表页是用户进店后看到的第一个核心页面性能直接决定第一印象。我采用MyBatis Plus的Page分页插件配合LambdaQueryWrapper做动态条件拼装。Override public PageBookVO queryBookPage(Integer pageNum, Integer pageSize, Integer categoryId, String keyword, String orderBy) { PageBook page new Page(pageNum, pageSize); LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Book::getCategoryId, categoryId) .and(keyword ! null !keyword.trim().isEmpty(), w - w .like(Book::getBookName, keyword) .or().like(Book::getAuthor, keyword) .or().like(Book::getPublisher, keyword)) .eq(Book::getStatus, 1); if (price_asc.equals(orderBy)) { wrapper.orderByAsc(Book::getPrice); } else if (price_desc.equals(orderBy)) { wrapper.orderByDesc(Book::getPrice); } else { wrapper.orderByDesc(Book::getSalesCount); } return bookMapper.selectPage(page, wrapper); }这里最关键的技巧是wrapper.eq(condition, column, value)这种写法condition为false时该条件自动拼不上这样前端传不传参数后端都不需要写多个if分支。搜索权重上我默认按书名、作者、出版社三个字段模糊匹配相关性排序在这种体量的系统里不必做太复杂。有个细节要提醒模糊查询用的LIKE %keyword%MySQL在这类查询上不会走索引数据到十万级后会有明显的慢查询。我的处理方案是搜索时优先走Redis缓存的热门书单只有在用户明确操作翻页或筛选时才查数据库。3.3 购物车与库存扣减的并发安全实现购物车我采用的是后端存储方案用一张t_cart表或者复用Redis里的Hash结构。考虑到购物车本身不涉及长期归档我用Redis存储key设计为cart:userIdfield对应bookIdvalue存数量。好处是读写快且天然支持并发控制。库存扣减是网上书店系统的重头戏也是并发Bug的高发区。很多同学直接写先查库存再判断再扣减这在并发场景下必然超卖。我使用的方案是条件更新一次完成扣减Override Transactional(rollbackFor Exception.class) public boolean createOrder(OrderCreateDTO dto) { // 生成订单号 String orderNo OrderNoGenerator.generate(); BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem itemList new ArrayList(); for (OrderItemDTO item : dto.getItems()) { // 原子扣减库存只用一条update语句 int rows bookMapper.deductStock(item.getBookId(), item.getQuantity()); if (rows 0) { throw new BusinessException(图书库存不足扣减失败); } // 查询图书信息计算明细 Book book bookMapper.selectById(item.getBookId()); OrderItem orderItem new OrderItem(); // 组装明细字段... itemList.add(orderItem); totalAmount totalAmount.add(book.getPrice().multiply( BigDecimal.valueOf(item.getQuantity()))); } // 插入订单主表和明细表 orderMapper.insert(order); orderItemMapper.insertBatch(itemList); }对应的SQL语句长这样UPDATE t_book SET stock stock - #{quantity}, sales_count sales_count #{quantity} WHERE id #{bookId} AND stock #{quantity}这条update语句利用MySQL的行锁机制让并发请求在数据库层面排队stock #{quantity}这个条件确保不会扣成负数。rows等于0就说明库存不够直接抛异常回滚事务。用这个方案不需要给整张表加锁性能也好。3.4 订单状态机的设计与支付回调处理订单的状态我设计了四个待支付(0)、已支付(1)、已发货(2)、已完成(3)外加一个取消(-1)。为什么不用太多状态状态越多状态流转的判断逻辑越复杂对小型系统来说够用清晰比完备更重要。订单状态流转我用一个简单的状态机来处理private static final MapInteger, ListInteger ORDER_STATUS_TRANSFER new HashMap(); static { ORDER_STATUS_TRANSFER.put(0, Arrays.asList(1, -1)); // 待支付 - 已支付/取消 ORDER_STATUS_TRANSFER.put(1, Arrays.asList(2)); // 已支付 - 已发货 ORDER_STATUS_TRANSFER.put(2, Arrays.asList(3)); // 已发货 - 已完成 }每次状态变更前先校验当前状态是否允许转向目标状态不允许就直接拒绝防止用户通过伪造请求实现待支付→已完成这种非法跳转。支付回调这里我特别想强调一个细节支付平台的回调接口一定要做幂等处理。什么意思就是支付平台可能因为网络抖动把同一个支付成功的通知发好几次。如果后端每次收到回调都去给订单加一个已支付状态就可能把已支付的订单状态覆盖或者重复累加积分。我的做法是在回调处理逻辑里先查订单当前状态如果已经是已支付直接返回成功不再重复处理。if (order.getStatus() 1) { return success; } // 幂等确认 order.setStatus(1); order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order);4. 前端页面的关键交互与传参逻辑4.1 首页图书展示与搜索交互的配合方式前端我采用Vue 2 Element UI这套组合。首页图书展示区接到后端的/api/book/page接口在created钩子里加载第一页数据用户点击分类或输入关键词时重置页码为1并重新请求数据。这里有个交互上的细节容易被新手忽略搜索框的输入除了点击搜索按钮要触发查询按下回车键也应该触发同样的逻辑。我统一封装了一个handleSearch方法按钮和回车键都绑定到这个方法上避免两处逻辑写两遍导致行为不一致。分页组件用Element UI的el-pagination布局里设置好total、current-page、page-size的绑定切换页码时回调同一个loadData方法。数据回显上每次切换分类或搜索时页面要平滑滚动到顶部。我监听list数据的更新在watch里执行window.scrollTo({top: 0, behavior: smooth})这个小操作对体验提升非常明显。4.2 购物车数量变更的实时反馈逻辑购物车里用户点击或-如果直接刷新整个页面每次操作都要等网络往返体感很差。我的方案是点击按钮时先做本地数量更新然后向后端发一个变更接口后端返回最新小计金额前端用响应数据更新总价。这里有一个前后端一致性的坑如果用户连续快速点击三次前端可能发出三个请求但每个请求携带的数量是基于点击瞬间的UI值算出来的有可能出现请求A是2、请求B还是2因为UI还没从A的响应里更新、请求C是3的情况。如果后端做的是set操作最终数量可能被覆盖成3但正确结果应该是加了三次的增量。所以我后端的加购变更接口设计的是增量接口用incr去累加数量而不是set一个固定值。前端还是保留本地UI乐观更新后端只会累加增量两者最终会一致。5. 常见问题排查与性能避坑实录5.1 高并发下订单重复提交的拦截方案有一个真实场景用户在结算页点了提交订单按钮因为网络延迟按钮loading状态没立即生效用户又点了一次结果后台生成了两笔一模一样的订单。这个问题我用前端和后端双重手段解决。前端在点击提交按钮后立即设置disabled并开启loading等接口返回后再恢复后端在创建订单的方法入口加了一个分布式锁Redis SetNXkey用userId拼接最近时间窗口比如lock:order:{userId}:{yyyyMMddHHmm}一分钟内同一个用户只能提交一次订单。锁获取失败的请求直接提示订单提交过于频繁请稍后再试。双保险下来重复订单基本绝迹。我在测试时还遇到过一个更隐蔽的情况用户提交订单后接口报错了但报错原因是库存不足按常理这个订单不该存在。排查发现是代码里没有在抛异常时把Redis中购物车的数据Rollback用户看到报错后又去加购再提交购物车数据就重叠了。后来我把购物车清理逻辑放在事务提交成功后执行而不是在创建订单方法内执行问题就解决了。5.2 图书查询变慢与分页深翻页的优化当图书表数据量到几万条后默认的limit 0,10没问题但用户翻到第1000页再去点下一页MySQL需要扫描和跳过前10000条记录开销明显上升。我实测过一张4万条记录的图书表翻到末尾时单条查询耗时从30ms涨到400ms左右。优化的方案是使用延迟关联或书签分页。延迟关联的思路是先查主键再查详情SELECT b.* FROM t_book b INNER JOIN (SELECT id FROM t_book ORDER BY id LIMIT 10000, 10) t ON b.id t.id子查询里只查主键走的是索引覆盖扫描回表记录只有10条性能比直接扫全行好很多。MyBatis Plus的Page插件本身不支持这种写法所以我针对深翻页单独写了一条自定义SQL。如果产品需求里根本没有翻页超过100页的场景其实不必过度优化意识到这个瓶颈就行。还有一点不要忽略status字段的条件。我习惯在所有查询图书的SQL里都带上status1上架状态防止后台把某本书下架后用户仍然能通过旧缓存或链接访问到详情页。5.3 Redis缓存穿透和击穿的简单应对图书详情页是热点访问区域缓存策略我用的是Cache Aside Pattern先读缓存没有则读数据库再回填缓存。但这个策略有两个隐患。一是缓存穿透——如果一个不存在的图书ID被持续请求比如恶意遍历ID每次都会穿透到数据库DB压力很大。我的应对是把查不到的ID也缓存一个空值过期时间设短一点比如60秒这样后续同类请求会命中空缓存DB压力立刻下降。二是缓存击穿——某个热点图书的缓存正好过期瞬间大量请求同时进来全部打到DB上。这里我用了一个简单方案加互斥锁。只有拿到锁的线程才能去查询数据库并回填缓存其他线程先查缓存查不到就短暂等待再重试。对于小型网上书店系统来说这个方案够用了不需要上Sentinel那套重型组件。最后说点我自己的实际操作体会。这个系统我从数据库建模到前端页面整体做完前后花了一周多时间其中一半时间其实花在并发控制和状态流转这类看不见的代码上。网上书店系统的难度不在于技术栈有多新而在于它把电商的完整链路浓缩到了极小的范围每一块都要亲手写才知道里面的坑。如果你想拿这个项目练手或做课程设计我建议先把下单和库存这条链路跑通再回头优化页面和缓存毕竟一个能稳定下单的系统比一个光鲜但一并发就挂的系统有价值得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价