图书销售网管理系统:从需求拆解到下单事务的工程化落地
简介这份资源是江西财经大学软件工程课程期末大作业《图书销售网管理系统》的完整考核报告面向软件工程专业学生及需要撰写课程设计文档的读者帮助理解从需求分析到系统测试的规范化开发流程。压缩包内仅含1个docx文档大小约271KB内容涵盖引言、需求分析、软件设计、实现与测试等章节并配有分层DFD数据流图、数据字典、类图与动态模型等图表。报告围绕图书销售网的设计与实现展开涉及经济、技术、操作三方面可行性分析以及用户注册登录、图书展示、购物车、订单处理、支付接口等模块的面向对象实现思路。目前已有2597人学习下载适合作为软件工程导论课程报告的参考范例也可用于学习需求建模、系统设计与文档撰写方法。1. 图书销售网管理系统一份期末大作业背后的工程化落地路径很多同学拿到“图书销售网管理系统”这个题目时第一反应是打开 IDE 直接建表写 CRUD结果做到一半发现订单状态对不上、库存扣成负数、用户权限乱成一锅粥。这个标题看起来是软件工程导论期末大作业但它本质上是一个完整的电商后台最小闭环用户浏览图书、加购下单、后台管理库存与订单。它适合正在做课程设计的学生也适合想用一个真实场景把 MVC 分层、数据库事务、权限控制串起来的初级开发者。我见过太多人把这类系统做成“能跑就行”的玩具最后答辩被问三个问题就卡壳。这篇笔记按工程化思路拆一遍从需求边界到建表、从下单事务到后台权限每一步都给出可复现的做法和参数。2. 先定边界再动手图书销售网管理系统的需求拆解与选型2.1 三类角色与最小功能集图书销售网管理系统如果按电商全量功能做一个月也做不完。期末大作业的合理边界是三类角色游客、注册用户、管理员。游客只能浏览和搜索图书注册用户可以加购物车、下单、查看自己的订单管理员负责图书增删改查、订单状态流转、用户管理。这个边界定下来功能点大约 18 到 22 个刚好覆盖软件工程导论要求的用例图、类图、时序图。我一般会先画一张用例图把“下单”这个用例拆成“加入购物车”“确认订单”“支付模拟”“库存扣减”四个子流程。很多同学把支付做成真实接口这是给自己挖坑。期末大作业里支付用模拟状态即可订单表里放一个pay_status字段0 未支付、1 已支付、2 已取消。库存扣减才是真正要写对的地方后面会单独讲。功能清单确定后写一份一页纸的需求规格说明列出每个用例的前置条件、主流程、异常流。这份文档在答辩时比代码更能体现软件工程思维也是防止后期需求蔓延的后悔药。2.2 技术选型为什么用 Spring Boot MyBatis MySQL选型原则是生态成熟、资料多、能体现分层思想。后端用 Spring Boot 2.7.x它把 Tomcat 内嵌启动一个 main 方法就能跑省去配置 web.xml 的玄学。持久层用 MyBatis 而不是 JPA因为图书销售网管理系统里订单查询涉及多表关联和动态条件MyBatis 的 XML 映射更直观也方便在答辩时讲清楚 SQL 优化。数据库用 MySQL 8.0字符集选utf8mb4排序规则utf8mb4_general_ci避免中文书名乱码。前端不用上 Vue 或 React用 Thymeleaf 服务端渲染就够了。期末大作业的重点是后端业务逻辑和数据库设计前端套一个 Bootstrap 模板把表格和表单排整齐即可。如果时间充裕可以用 AJAX 做购物车局部刷新但不要为了炫技引入前后端分离那会把部署复杂度拉高一个量级。提示JDK 版本选 8 或 11不要选 17 以上部分旧版 MyBatis 和 Thymeleaf 在模块化系统下会有反射报错排查起来很费时间。2.3 数据库表设计六张核心表与字段说明图书销售网管理系统的数据库至少需要六张表用户表、图书表、分类表、购物车表、订单表、订单明细表。下面给出建表 SQL字段类型和索引都按实际查询场景设计。-- 用户表区分普通用户和管理员 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 存储BCrypt哈希, role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表price用DECIMAL避免浮点误差 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100), category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, cover_url VARCHAR(255), INDEX idx_category (category_id), INDEX idx_title (title) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表status表示订单生命周期 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细记录下单时的价格快照 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(10,2) NOT NULL COMMENT 下单时价格不随图书调价变化, INDEX idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个关键设计点。第一order_item里的unit_price是价格快照不能直接关联book.price查询否则管理员改价后历史订单金额会变这是很多同学翻车的地方。第二book.title和category_id建了索引因为图书列表页的搜索和分类筛选是最高频查询。购物车表和分类表结构类似按同样思路补全即可。3. 从建表到跑通图书销售网管理系统的核心功能实现3.1 下单事务库存扣减与订单写入的原子性下单是图书销售网管理系统里最容易出 bug 的环节。典型错误写法是先查库存、再判断、再扣减、再写订单四步分开执行。并发场景下两个请求同时查到库存为 1都判断通过最后库存扣成 -1。正确做法是把库存扣减和订单写入放在同一个事务里并且用条件更新防止超卖。Service public class OrderService { Autowired private BookMapper bookMapper; Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Transactional(rollbackFor Exception.class) public void createOrder(Integer userId, ListCartItem items) { BigDecimal total BigDecimal.ZERO; // 先扣库存条件更新stock quantity 才执行 for (CartItem item : items) { int affected bookMapper.reduceStock(item.getBookId(), item.getQuantity()); if (affected 0) { throw new RuntimeException(库存不足 item.getBookId()); } Book book bookMapper.selectById(item.getBookId()); total total.add(book.getPrice().multiply(new BigDecimal(item.getQuantity()))); } // 写订单主表 Orders order new Orders(); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 写订单明细记录价格快照 for (CartItem item : items) { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setBookId(item.getBookId()); oi.setQuantity(item.getQuantity()); oi.setUnitPrice(bookMapper.selectById(item.getBookId()).getPrice()); orderItemMapper.insert(oi); } } }reduceStock对应的 SQL 是UPDATE book SET stock stock - #{qty} WHERE id #{id} AND stock #{qty}。这条语句利用数据库行锁保证原子性返回影响行数为 0 就说明库存不够直接抛异常触发回滚。Transactional的rollbackFor要显式写Exception.class否则遇到非运行时异常不会回滚这是 Spring 事务的默认行为很多人不知道。参数方面quantity要在进入事务前校验大于 0防止负数把库存加上去。订单总价用BigDecimal计算不要用double否则 0.1 0.2 会出现 0.30000000000000004 这种结果金额对不上。3.2 购物车与订单的会话管理购物车数据存哪里是期末大作业里争论最多的问题。存 Session 实现简单但服务器重启就丢存数据库能持久化但每次加购都要写库。我的建议是登录用户的购物车存数据库游客的购物车存 Session登录时把 Session 购物车合并到数据库。这样既保证体验又不会让游客数据丢失。Controller public class CartController { PostMapping(/cart/add) public String addToCart(RequestParam Integer bookId, RequestParam(defaultValue 1) Integer qty, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { // 游客操作Session中的Map MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) cart new HashMap(); cart.merge(bookId, qty, Integer::sum); session.setAttribute(cart, cart); } else { // 登录用户写数据库存在则累加数量 cartMapper.insertOrUpdate(user.getId(), bookId, qty); } return redirect:/cart/list; } }insertOrUpdate用 MySQL 的INSERT ... ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity)实现前提是(user_id, book_id)建了唯一索引。这个写法比先查再判断少一次数据库往返在高并发下更稳。注意VALUES(quantity)在 MySQL 8.0.20 之后标记为废弃新版本可以用别名写法但期末大作业用旧语法完全没问题。3.3 后台图书管理分页查询与动态条件管理员后台的图书列表需要支持按书名模糊搜索、按分类筛选、分页展示。MyBatis 的动态 SQL 在这里很合适用where和if标签拼条件避免手写字符串拼接导致 SQL 注入。select idselectByPage resultTypeBook SELECT * FROM book where if testkeyword ! null and keyword ! AND title LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select分页参数offset (pageNum - 1) * pageSizepageSize默认 10最大不要超过 50否则一次查太多数据页面会卡。keyword在传入前做 trim 处理防止用户输入空格导致查询结果为空。分类下拉框的数据从分类表查出来放 Model 里Thymeleaf 用th:each渲染。注意LIKE CONCAT(%, #{keyword}, %)不会走索引数据量超过一万条时查询会变慢。期末大作业数据量小可以接受如果想让答辩加分可以提一句“生产环境应改用全文索引或搜索引擎”。4. 图书销售网管理系统避坑五条血泪经验4.1 库存扣成负数并发下的超卖问题现象压测时两个线程同时下单同一本书库存从 1 变成 -1。原因先查后改的写法在并发下失去原子性。解决用UPDATE book SET stock stock - ? WHERE id ? AND stock ?条件更新检查影响行数。如果返回 0 就抛异常回滚。这个坑我在模拟项目里踩过当时用 JMeter 开 50 个线程跑库存直接负了 30 多。4.2 订单金额与图书现价不一致现象用户下单时总价 59.8管理员把书价改成 39.9 后用户查看历史订单显示 39.9。原因订单明细表只存了book_id查询时关联book表取现价。解决下单时把unit_price写入order_item查询订单直接读快照价格不再关联图书表。这个设计在电商系统里是铁律答辩时能讲清楚这一点老师会高看一眼。4.3 中文书名乱码字符集没统一现象图书标题在数据库里显示正常页面上变成问号。原因数据库连接 URL 没加characterEncodingutf8或者表字符集是latin1。解决JDBC URL 写成jdbc:mysql://localhost:3306/bookshop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai建表统一用utf8mb4。serverTimezone不写会报时区错误MySQL 8.0 驱动必须加。4.4 登录状态丢失Session 超时与拦截器配置现象用户登录后操作几步就跳回登录页。原因Session 默认超时 30 分钟但拦截器把静态资源也拦了导致 CSS 和 JS 请求被重定向。解决拦截器addPathPatterns(/**)但excludePathPatterns(/css/**, /js/**, /login, /register)。Session 超时时间在application.properties里设server.servlet.session.timeout60m期末演示够用。4.5 订单状态乱跳缺少状态机约束现象已取消的订单还能被管理员改成已发货。原因更新订单状态时没有校验当前状态。解决在 Service 层加状态流转判断只允许0-1、1-2、2-3、0-4这几种转换其他组合直接拒绝。用一个MapInteger, ListInteger定义允许的下一状态代码不到十行但能挡住大部分逻辑错误。5. 进阶技巧用接口测试与 SQL 日志验证系统可靠性5.1 用 Postman 跑通下单全链路系统写完后不要只靠手点页面验证。用 Postman 建一个 Collection按顺序发四个请求登录获取 Session、加购、下单、查订单。登录请求返回的 Cookie 在 Postman 里会自动带到后续请求这样能模拟真实会话。下单请求的 Body 用 JSON 传bookId和quantity断言返回状态码 200 且订单列表里多了一条记录。这套流程跑通说明核心链路没有断。5.2 打开 MyBatis SQL 日志定位慢查询在application.properties里加两行配置把 MyBatis 执行的 SQL 和参数打印到控制台mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl logging.level.com.example.mapperdebug启动后下单控制台会输出 Preparing: UPDATE book SET stock stock - ? WHERE id ? AND stock ?和 Parameters: 1(Integer), 5(Integer), 1(Integer)。如果发现某条查询在循环里反复执行比如订单明细逐条查图书那就是 N1 问题改成批量查询SELECT * FROM book WHERE id IN (...)一次拿回来。5.3 用唯一索引兜底防重复下单用户快速点两次提交按钮可能生成两笔相同订单。除了前端按钮置灰数据库层加一个防重标记更可靠。在orders表加request_id VARCHAR(64) UNIQUE前端每次进入确认订单页生成一个 UUID 放在隐藏域提交时带上。Service 层插入订单前先查request_id是否存在存在就返回已有订单。这样即使网络重试也不会重复扣库存。验证项工具通过标准库存扣减原子性JMeter 50 线程库存不小于 0订单金额快照手动改价后查历史订单金额不变中文显示浏览器查看图书列表无乱码权限拦截未登录访问后台 URL跳转登录页重复下单连续点击提交两次只生成一笔订单这套验证做完图书销售网管理系统基本能扛住答辩老师的追问。我自己的习惯是每次改完 SQL 或事务逻辑先把上面表格里的前两项跑一遍确认没有回归问题再继续写新功能。期末大作业时间紧但这两个检查花不了十分钟能省掉后面通宵排查的麻烦。希望帮到你。本文还有配套的精品资源点击获取