ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Web的网上书城系统设计与实现:从架构到并发扣库存

基于Web的网上书城系统设计与实现:从架构到并发扣库存 简介这套基于SpringBootVue的网上书城系统源码是面向Java学习者与毕业设计学生的完整工程采用前后端分离的B/S架构整合MyBatisPlus、MySQL、Maven和Ajax等技术涵盖用户注册登录、图书分类检索、购物车与订单管理、后台数据维护等核心模块。压缩包共855个文件体积17.42MB主要包含143个Java后端代码、52个Vue前端组件、156个JS脚本、46个CSS与46个HTML页面另有SQL数据库脚本、XML文件、MyBatis映射文件以及论文PDF和DOCX文档SVG、GIF、JPG、PNG等图片素材覆盖网页图标与演示配图目录按工程结构和论文章节组织便于逐层对照研读。资源已有452人学习下载。配套1-install.bat、2-run.bat、3-build.bat一键脚本可快速完成依赖安装、项目启动与重新构建读者能直接运行完整项目并在此基础上扩展秒杀、优惠券、评论等更多功能从数据库设计到前后端联调再到论文撰写均能获得可复用的参考非常适合作为课程设计或毕业设计的选题蓝本。1. 网上书城系统值不值得自己从头写搜索“网上书城系统源码”的人一半在做毕业设计另一半想找一个能写进简历的Web项目。网上书城这个题目看着“老掉牙”但它把图书信息管理、购物车、下单减库存、订单状态流转这一整套业务骨架串得很完整恰好覆盖Java Web开发从分层到事务管理的主线。比起卖车、卖农产品书城的数据结构更简单适合在短期内跑通“设计—实现—部署”全流程。这篇按“基于Web的网上书城系统设计与实现”的常见落地路径把技术选型、数据库建模、后端核心链路、前端接口对接和部署调优一次讲清楚。新手能照着把项目跑起来经验在5年以上的工程师重点看后面并发扣库存的验证方式和连接池参数边界。2. 设计与实现的第一步技术选型和网上书城系统的数据建模2.1 网上书城系统是JSP/Servlet还是SpringBoot先说结论毕设需求里写“基于Java Web”“B/S架构”在现在这个时间点不等于必须用JSP。SpringBoot Thymeleaf或者SpringBoot Vue也都属于基于Web的Java实现。我一般默认推荐SpringBoot理由很实际内嵌Tomcat让你不用再装独立容器、打war包starter机制把依赖版本统一管理事务和参数校验的注解支持比Servlet原生API顺手得多。JSP不是不能做但页面里混Java代码会拉低你调试购物车逻辑的效率而且在“源码”的可读性上JSP项目明显不如前后端分离或模板引擎项目。维度Servlet/JSPSSMSpringBoot Thymeleaf/前后端分离页面渲染JSP内嵌JavaJSP/FreeMarkerThymeleaf模板或JSON接口部署方式war包丢Tomcatwar包丢Tomcat内嵌Tomcat一条命令启动事务管理手写JDBCSpring XML或注解Transactional注解前后端分工后端兼职前端后端兼职前端清晰接口契约驱动适合场景课堂作业老项目维护新项目、毕设、简历项目提示如果学校指定“必须用JSP”那就用JSPServletMyBatis的经典组合但业务逻辑一定要放Service层JSP里只做循环渲染不写JDBC、不写事务。2.2 数据表设计E-R图里不能省的六张核心表网上书城系统的E-R关系并不复杂用户与订单是一对多订单与订单项是一对多图书与分类是多对一。真正容易翻车的是两个地方一是订单项必须保存商品快照二是库存更新不能用先查再改。这里给出一个精简但完整的最小建表SQL涵盖用户、图书、订单、订单项四张核心表。购物车表我建议不入库用Redis的Hash结构保存因为购物车属于过程数据丢失了用户重加一次就行不影响订单正确性。CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT BCrypt密文, nickname VARCHAR(32) DEFAULT COMMENT 昵称, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT 分类ID, title VARCHAR(128) NOT NULL COMMENT 书名, author VARCHAR(64) DEFAULT , price DECIMAL(10,2) NOT NULL COMMENT 定价用于展示, sale_price DECIMAL(10,2) NOT NULL COMMENT 销售价, stock INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 可售库存, sales INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 销量, cover_url VARCHAR(256) DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_sn VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE t_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, book_id BIGINT NOT NULL, book_title VARCHAR(128) NOT NULL COMMENT 下单时书名快照, book_price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL COMMENT 购买数量, KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单项表;这张表的设计有几个关键点price和sale_price分开是为了展示折扣前价格订单表用order_sn做业务订单号而不是直接用自增id避免通过订单号推测出你的销量order_item里的book_title和book_price是快照字段订单生成后图书改价、改名都不影响历史订单。t_book的stock字段带UNSIGNED约束库存不能为负数这条约束是最后一道防线后面扣库存的逻辑才是真正的防线。2.3 拿到网上书城系统源码包先看哪几个文件从网上下载的源码工程别急着点启动。我一般会按下面的顺序快速摸清项目底细先看pom.xml或build.gradle确认SpringBoot版本、JDK版本、依赖里有没有MyBatis-Plus、Redis、JWT这些组件。版本跨度大的项目直接跑起来大概率报错先知道版本后面排错才有的放矢。再看application.yml或application.properties重点看端口号、数据库连接地址、连接池类型。很多源码把localhost数据库名写死不改配置跑不起来。然后打开doc目录或sql目录找schema.sql和data.sql。没有SQL文件的源码要谨慎表结构都在实体类里意味着你还得自己建库。最后看拦截器或配置类确认登录校验是Session还是JWT。这会直接决定前端怎么传凭证也决定你测试接口时需不需要先登录拿Cookie。提示判断一份网上书城系统源码质量最直接的方式是看订单模块里有没有悲观锁或乐观扣减。如果下单逻辑是select stock再update stock这份源码只能用来交差不能上线。3. 网上书城系统的后端核心链路登录、检索、购物车、下单3.1 登录注册与Session状态管理网上书城系统的登录模块在单体部署场景下用HttpSession是成本最低的方案。Session由Servlet容器维护客户端拿到的是HttpOnly的Cookie不需要像JWT那样额外处理密钥和过期刷新。密码存储用BCrypt不用MD5因为MD5加盐和撞库的成本对书架项目来说完全不划算。下面这段Controller代码是典型的“参数校验-查询比对-写会话”三步PostMapping(/user/login) public R login(RequestBody User user, HttpSession session) { // 1. 校验参数是否为空 if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) { return R.error(400, 用户名或密码不能为空); } // 2. 查询用户密码用 BCrypt 比对 User dbUser userService.findByUsername(user.getUsername()); if (dbUser ! null BCrypt.checkpw(user.getPassword(), dbUser.getPassword())) { // 3. 登录成功后把用户ID写进Session后续接口只认Session session.setAttribute(LOGIN_USER_ID, dbUser.getId()); return R.success(登录成功); } return R.error(401, 用户名或密码错误); }这段代码的逻辑说明第1步先拦截空参数避免数据库层拿null做条件第2步是核心比对BCrypt.checkpw会从密文里提取盐值重新计算哈希所以数据库里存的是$2a$10$开头的字符串不是原始密码第3步把userId写进Session注意Session中没有必要放整个User对象放个ID就能满足后续“查购物车”“下单”等接口的鉴权需要。参数说明RequestBody要求前端传JSON字段名必须和User类属性对得上返回的R是统一响应体下面经常用到code和data两个字段。code含义使用场景200成功正常返回数据400参数错误表单缺字段、非法输入401未登录Session中无用户ID500服务器异常业务未捕获异常3.2 图书分页检索关键词、分类、价格区间一次查全图书列表是网上书城系统的门面能不能搜得准、分页分得对直接影响使用体验。使用MyBatis-Plus时LambdaQueryWrapper是最省事的条件构造器它用Lambda表达式引用实体类字段编译期就能发现字段名拼写错误。下面这段代码把关键词模糊搜索、分类筛选、价格区间三个高频条件攒在一起public PageResultBook pageBooks(BookQuery query) { LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); // 动态拼接条件关键词模糊匹配书名或作者 if (StringUtils.hasText(query.getKeyword())) { wrapper.and(w - w.like(Book::getTitle, query.getKeyword()) .or().like(Book::getAuthor, query.getKeyword())); } wrapper.eq(query.getCategoryId() ! null, Book::getCategoryId, query.getCategoryId()); wrapper.between(query.getMinPrice() ! null query.getMaxPrice() ! null, Book::getSalePrice, query.getMinPrice(), query.getMaxPrice()); wrapper.eq(Book::getStatus, 1).orderByDesc(Book::getSales); PageBook page new Page(query.getPageNum(), query.getPageSize()); bookMapper.selectPage(page, wrapper); return new PageResult(page.getRecords(), page.getTotal()); }逻辑说明wrapper.and(w - ...)里的Lambda子查询把“书名匹配”和“作者匹配”包在括号里避免和后面的categoryId条件发生SQL逻辑错乱这个细节很关键。eq方法的第二个参数如果是nullMyBatis-Plus会自动忽略这个条件所以不需要手动if拼接SQL。orderByDesc(Book::getSales)按销量倒序让热门书籍排前面。参数说明pageNum从1开始pageSize建议固定12或20total来自page.getTotal()它查的是COUNT(*)的结果不是记录数前端分页器要拿这个值算总页数。3.3 购物车下单的事务一致性扣库存别用先查再改下单是网上书城系统的核心事务也是Java面试里“八股”浓度最高的考点。很多初学者写库存扣减是先select stock再判断是否大于quantity最后update。这在单用户单线程下没问题一旦两个请求同时读到stock1并同时通过判断就会出现超卖。正确做法是直接把判断条件写进UPDATE语句Override Transactional(rollbackFor Exception.class) public OrderVO submitOrder(OrderSubmitDTO dto, Long userId) { // 1. 生成业务订单号不用数据库自增主键暴露业务量 String orderSn BS System.currentTimeMillis() RandomUtil.randomNumbers(4); BigDecimal total BigDecimal.ZERO; ListOrderItem items new ArrayList(); for (CartItem cart : dto.getItems()) { // 2. 乐观扣减库存影响行数为0说明库存不足 int rows bookMapper.reduceStock(cart.getBookId(), cart.getQuantity()); if (rows 0) { throw new BizException(图书库存不足或已下架); } Book book bookMapper.selectById(cart.getBookId()); total total.add(book.getSalePrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); items.add(buildOrderItem(book, cart.getQuantity())); } // 3. 落订单主表与订单明细 Order order new Order(); order.setOrderSn(orderSn).setUserId(userId).setTotalAmount(total).setStatus(0); orderMapper.insert(order); orderItemMapper.batchInsert(items); return new OrderVO(orderSn, total); }Mapper里的扣减SQL是这么写的Update(UPDATE t_book SET stock stock - #{qty} WHERE id #{bookId} AND stock #{qty}) int reduceStock(Param(bookId) Long bookId, Param(qty) Integer qty);逻辑说明UPDATE语句里的stock #{qty}就是并发防线数据库行锁会让第二个事务等第一个提交后再执行此时stock已经被扣掉所以影响行数会变成0代码里rows 0直接抛异常回滚。事务上加rollbackFor Exception.class很重要否则只有RuntimeException会触发回滚自定义BizException如果不是继承RuntimeException库存扣了订单没生成就是你最不想看到的现场。设计模式里的模板方法模式也适合放在这里把“校验-扣库存-落单”三步抽象成模板不同订单类型只需要实现各自的校验逻辑。3.4 这套链路能迁移到其他管理系统吗如果把User换成Seller、Book换成Car、OrderItem换成车辆配置快照这套网上书城系统的骨架就直接变成了二手车交易平台的订单模块。同理自习室管理系统的预订模块、农产品在线销售系统的下单模块核心都是“资源-库存-订单-明细”四张表。这也是为什么Java方向的项目经验容易互相迁移业务变了但事务边界、分页检索、状态机流转这些底层能力没变。理解了网上书城系统等于拿到了一张Web业务系统的通用图纸。4. 基于Web的网上书城系统前端页面与接口对接4.1 服务端渲染还是前后端分离网上书城不用纠结网上书城系统的前端方案常见选择是Thymeleaf服务端渲染和Vue前后端分离。Thymeleaf的好处是后端一个进程搞定所有事情Session天然共享不需要处理跨域Vue的好处是页面交互更流畅组件化让商品卡片和购物车列表的代码可以复用。对于以“跑通功能”为首要目标的网上书城我更偏向Thymeleaf因为少一层Node构建和代理转发出问题的面更小。如果你已经有Vue基础选前后端分离也完全可行重点看下面几节的接口约定和跨域配置。4.2 接口契约与统一返回结构前后端联调最怕各写各的字段。先约定一套接口清单比事后对接口效率高得多。网上书城系统最小接口集可以这样定义接口方法参数返回/api/book/pageGETpageNum, pageSize, categoryId, keyword, minPrice, maxPrice{code, msg, data:{records, total}}/api/book/{id}GET路径参数图书详情对象/api/cartGET/POST无 / {bookId, quantity}购物车列表或操作结果/api/order/submitPOST{items:[{bookId, quantity}]}{orderSn, totalAmount}统一返回结构R的定义建议固定为code、msg、data三个字段。code为200时前端只处理datacode非200时直接把msg弹出提示。这样前端不用为每个接口单独写异常分支对一个商品列表页来说能少写三分之一的条件判断。分页接口里的total字段必须来自数据库COUNT不能用当前页records.length代替否则切到第二页时分页器总页数就错了。4.3 跨域配置与登录态丢失的常见坑前后端分离模式下网上书城系统第一个拦路虎就是跨域。前端跑在5173端口后端跑在8080端口浏览器默认会拦截不同源的Ajax请求。下面是一份SpringBoot侧的CorsFilter配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:5173); // 前端实际地址 config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); // 允许携带Cookie UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/api/**, config); return new CorsFilter(source); } }逻辑说明setAllowCredentials(true)表示允许跨域请求携带Cookie这是Session登录态能生效的前提。而一旦开启这个选项addAllowedOrigin就不能配置成“*”必须写明确的前端地址否则浏览器会以“The value of the Access-Control-Allow-Origin header must not be the wildcard”为由直接拦截响应。参数说明registerCorsConfiguration里限定了/api/**路径避免把静态资源的处理也拖进跨域逻辑。前端axios请求需要额外配置withCredentials为true否则后端Session的Cookie不会被保存。4.4 商品列表页的筛选与分页计算逻辑商品列表页拿到接口返回的total之后分页计算通常是前端最容易算错的地方。看下面这段JavaScript// 假设每页展示12本书 const pageSize 12; const total res.data.total; const pageCount Math.ceil(total / pageSize); // 点击分页器时把当前页码传给后端 requestBookList({ pageNum: currentPage, pageSize });逻辑说明Math.ceil向上取整保证total为13时pageCount是2而不是1。currentPage从1开始和后端Page对象的页码保持一致。这里有一个常被忽略的参数细节后端SQL里对应的LIMIT语句是LIMIT (pageNum - 1) * pageSize, pageSize所以pageNum传0会查出来和1一样的结果但总页数会显示异常建议前端约束currentPage最小为1。5. 部署验证与参数调优网上书城系统上线前必做的事5.1 本地跑通的最小命令与初始化数据拿到源码后在项目根目录先看README里的环境要求。常见的基础环境组合是JDK 8 Maven 3.6 MySQL 5.7或者JDK 17 Maven 3.9 MySQL 8.0。确认之后执行下面两条命令mvn spring-boot:run -Dspring-boot.run.arguments--spring.profiles.activedev mysql -uroot -p doc/schema.sql mysql -uroot -p doc/data.sql第一条命令启动SpringBoot应用-Dspring-boot.run.arguments指定激活dev环境配置第二条命令导入表结构和初始数据。注意data.sql里如果含中文图书名导入前确认MySQL连接参数里有characterEncodingutf8mb4否则页面会出现中文乱码。5.2 数据库连接池参数与Tomcat线程数怎么给网上书城系统的并发量通常不高但连接池参数依然是面试常问点。HikariCP是SpringBoot默认连接池配置时要关注下面几个参数参数默认值建议值说明initialSize05启动时预建的连接数maxActive820-50峰值并发连接数不是越大越好maxWait1000ms3000ms获取连接超时时间leakDetectionThreshold0关闭60000连接泄漏检测阈值单位毫秒maxActive给到500是新手常犯的错误。连接数过多MySQL端默认的max_connections只有151多余的连接全在排队反而拖垮数据库。配合Tomcat容器线程数max-threads200这个值看20-50个数据库连接足够支撑一个几百人同时在线的书城系统。5.3 并发下单验证用压测命令揪出超卖问题项目上线前别只自己点一遍“加入购物车-下单”就收工。用curl或ab工具做一次简单的并发验证能提前暴露超卖和死锁# 准备订单请求体 order.json然后模拟100个请求、20个并发 ab -n 100 -c 20 -p order.json -T application/json http://localhost:8080/api/order/submit压测结束后直接查数据库库存SELECT id, title, stock, sales FROM t_book WHERE id 1;如果stock出现负数说明reduceStock里的stock #{qty}条件没生效检查Mapper的SQL是写在注解里还是XML里注意有没有多余的空格或参数名拼错。如果日志里出现“Deadlock found when trying to get lock”说明两个并发事务对多本书的加锁顺序不一致解决方法是把购物车里的bookId按升序排序后再循环扣减保证所有事务按同一顺序拿行锁。配合5.2里提到的leakDetectionThreshold60000还能在测试阶段发现连接是否被事务异常吃掉。这个参数会打印连接获取时的堆栈信息定位到具体是哪个方法没有commit或close。本文还有配套的精品资源点击获取
返回列表