先交代一句这个标题看起来平平无奇但网上书店管理系统恰恰是学习完整业务系统最好的切入点之一。它没有电商大厂那种高并发、分布式、秒杀的复杂度但又覆盖了商品、购物车、订单、库存、用户、支付回调、后台管理这些一个交易系统该有的全部核心环节麻雀虽小五脏俱全。如果你能把这个系统的数据表设计、状态流转和事务边界彻底搞清楚往后不管是做二手交易平台、课程售卖系统还是预约小程序思路基本都是通的。这篇文章我不会只丢给你一个 CRUD 代码模板而是从一次真实的项目落地过程出发把整个网上书店管理系统的设计思路、数据库建模、购物车与订单的工程实现、库存扣减的并发处理、后台统计 SQL 的写法、以及部署上线的经验完整过一遍。适合正在做毕业设计、想找一份扎实练手项目、或者准备接私活打基础的朋友。1. 这个系统到底在管理什么业务域拆分与功能边界很多初学者一上来就建表、写接口结果写了一半发现我这个图书的字段好像不够用订单状态不知道该放哪里改本质原因是没在动手之前把业务边界划清楚。网上书店管理系统听起来简单但把它拆开看其实可以分成四个互不干扰的业务域商品中心、交易中心、用户中心、运营后台。1.1 商品中心管的是卖什么商品中心的核心对象是图书。但图书在真实系统里不是一张表就能装下的你需要考虑几个维度的信息图书本身的基本属性书名、作者、出版社、ISBN、封面图、销售属性价格、库存、上下架状态、类目属性分类比如文学、科技、童书、以及内容描述简介、目录、详情页富文本。在这个项目里我把图书的上下架状态做成了status字段而不是直接删除记录。为什么因为图书一旦删除历史订单里关联的图书信息就查不到了对账、售后、补打发票全都会出问题。所以正确做法是逻辑删除或状态切换——用户端永远只查 status1 的在售图书后台可以看全部。1.2 交易中心管的是怎么卖交易中心是最复杂的一块包含购物车、下单、订单状态流转、支付对接这四个子模块。购物车本质上是一个预订单容器用户可能选了好几本书但最后只结算其中两本也可能加购后三小时才来付款所以购物车数据必须持久化不能只存在前端 localStorage 里后面我会单独讲购物车为什么强烈建议用 Redis 或数据库表。订单是整个交易中心的地基。我见过很多半吊子系统把订单状态设计成未付款/已付款/已完成这样简单的字符串然后到处用 if 判断状态后来需求一加就直接崩。正确做法是订单状态做成一个独立的状态机明确每个状态可以由哪些操作触发由哪些操作禁止触发。支付环节在这个项目里我会用模拟支付来做——本地项目接真实微信/支付宝需要营业执照和审核但你在代码里必须预留payment_id和paid_at字段等将来接真实支付时不用改表结构。1.3 用户中心管的是卖给谁用户中心不只是用户表的增删改查。你要考虑用户注册登录这里涉及密码不能明文存储的问题必须用 BCrypt 加盐加密、用户收货地址管理地址是一对多关系需要单独建表、以及用户的历史订单查询。权限这块我建议用最简单有效的方案用户表加一个role字段0是普通用户1是管理员。不做 RBAC 权限模型对网上书店这种体量的项目来说没必要——管理员和用户的功能边界很清晰用中间表搞角色权限反而增加了学习成本和维护成本。1.4 运营后台管的是卖得怎么样运营后台对应的是管理员端的操作界面包括图书管理上架、下架、改价、库存调整、订单管理发货、取消异常订单、分类管理、以及最基础的销售数据统计。这部分是很多初学者最容易忽视的但它恰恰是管理系统区别于展示网站的核心价值——管理端不是用户端的附属品而是帮助业务方做决策的工具。我在做后台统计的模块时把销售额按天做了聚合还加了图书销量排行榜。这两个统计功能实现起来就是几条 SQL 的事但展示出来的效果非常直观放在答辩或作品集里也很加分。2. 数据库建模从 4 张核心表到一整套可落地的 DDL数据库设计是整个项目最关键的地基工程。表结构没设计好后面写再多代码都像在危房里装修。我直接给出这套系统的完整核心表设计以及每一步设计决策背后的原因。2.1 用户表 users别把密码当普通字段存CREATE TABLE users ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(32) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, nickname VARCHAR(32) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, email VARCHAR(64) DEFAULT NULL COMMENT 邮箱, role TINYINT NOT NULL DEFAULT 0 COMMENT 角色0-普通用户 1-管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-正常 0-禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;密码字段我用VARCHAR(100)而不是常见的VARCHAR(32)就是因为 BCrypt 加密后的字符串长度在 60 位左右一开始设计成 32 位的话后面只能改表结构很麻烦。用户名必须加唯一索引这是登录账号的天然唯一标识。2.2 分类表 categories用层级结构支撑无限级分类CREATE TABLE categories ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(32) NOT NULL COMMENT 分类名称, parent_id BIGINT NOT NULL DEFAULT 0 COMMENT 父分类ID0表示顶级, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序权重, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书分类表;parent_id指向自身的id0表示顶级分类。这样做的好处是可以支撑文学 小说 科幻小说这种多级分类前端递归渲染分类树时只需要一个接口就能拿到全部分类。如果你只打算用一级分类那这个表可以简化但我建议还是保留parent_id因为需求扩到二级分类时不用改表。2.3 图书表 books销售属性与描述属性分离CREATE TABLE books ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 图书ID, title VARCHAR(128) NOT NULL COMMENT 书名, author VARCHAR(64) NOT NULL COMMENT 作者, isbn VARCHAR(20) NOT NULL COMMENT ISBN号, publisher VARCHAR(64) DEFAULT NULL COMMENT 出版社, category_id BIGINT NOT NULL COMMENT 分类ID, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 定价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, cover_url VARCHAR(255) DEFAULT NULL COMMENT 封面图URL, description TEXT COMMENT 图书简介, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-上架 0-下架, sales_count INT NOT NULL DEFAULT 0 COMMENT 累计销量, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;价格字段我用DECIMAL(10,2)而不是FLOAT或DOUBLE这是个老生常谈但必须强调的细节。浮点数在二进制中无法精确表示算钱会出大问题比如 0.1 0.2 在浮点数里不等于 0.3。钱相关的字段一律用定点数。isbn我建议加唯一索引但要注意老书可能出现多个 ISBN 对应同一本书的情况所以实际项目里我一般只在逻辑上保证不重复物理上留个普通索引即可。2.4 购物车表 cart_items临时性与持久性的平衡CREATE TABLE cart_items ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, book_id BIGINT NOT NULL COMMENT 图书ID, quantity INT NOT NULL DEFAULT 1 COMMENT 数量, checked TINYINT NOT NULL DEFAULT 1 COMMENT 是否选中结算, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;购物车这里有个设计决策user_id book_id做联合唯一索引。这样同一本书在同一个用户的购物车里只能存在一条记录再次加购时走的是更新数量而不是插入新行。checked字段用来标记用户勾选了哪些商品参与结算这也是大多数真实商城的设计。如果你想把系统做得更当代一点可以引入 Redis 存购物车把 key 设计成cart:{userId}field 是 bookIdvalue 是数量。但用 Redis 的话要注意数据持久化和过期策略否则用户过几天回来购物车空了体验很差。我的建议是学习阶段用数据库表业务稳定后再考虑缓存加速。2.5 订单表 orders状态机是订单模块的灵魂CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL COMMENT 用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0-待付款 1-已付款 2-已发货 3-已完成 4-已取消, receiver_name VARCHAR(32) NOT NULL COMMENT 收货人, receiver_phone VARCHAR(20) NOT NULL COMMENT 收货电话, receiver_address VARCHAR(255) NOT NULL COMMENT 收货地址, payment_method TINYINT DEFAULT NULL COMMENT 支付方式1-模拟支付, payment_id VARCHAR(64) DEFAULT NULL COMMENT 支付流水号, paid_at DATETIME DEFAULT NULL COMMENT 支付时间, shipped_at DATETIME DEFAULT NULL COMMENT 发货时间, completed_at DATETIME DEFAULT NULL COMMENT 完成时间, canceled_at DATETIME DEFAULT NULL COMMENT 取消时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;订单号order_no我单独列出来说因为这是很多人忽略的细节。订单号不能用数据库自增 ID因为自增 ID 暴露了业务量也容易被遍历爬取数据。正确生成方式可以是时间戳 用户ID后四位 随机数或者用雪花算法。我在这个项目里用yyyyMMddHHmmss 用户ID后四位 4位随机数生成 20 位长度订单号足够日常使用实现也不复杂。订单状态流转必须严格遵守待付款 →支付→ 已付款 →发货→ 已发货 →确认收货→ 已完成。待付款状态下可以取消已付款状态下管理员可以取消比如用户申请退款。我把它画成一张状态机图存在项目文档里写代码时每个状态变更都先查状态机判断是否允许。2.6 订单明细表 order_items一本书一条记录CREATE TABLE order_items ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 订单ID, book_id BIGINT NOT NULL COMMENT 图书ID, book_title VARCHAR(128) NOT NULL COMMENT 商品快照-书名, book_cover VARCHAR(255) DEFAULT NULL COMMENT 商品快照-封面, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价, quantity INT NOT NULL DEFAULT 1 COMMENT 购买数量, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;订单明细表里的商品快照是一个很关键的设计。为什么订单明细要冗余存一份book_title和book_cover而不是下单时只存book_id展示时再去关联查询图书表因为图书的信息是可变的——今天卖 79 元的书明天可能改成 69 元书名也可能更新。你的历史订单应该保留用户下单那一刻看到的信息否则对账和售后时会发现金额对不上、商品名对不上。这也是电商系统里的通用做法。3. 图书搜索与商品列表最容易被低估的入口功能网上书店的用户进来干的第一件事就是找书。搜索和列表做得好不好直接决定这个系统给人的第一印象。这里的难点不在 CRUD而在条件的组合查询和排序策略。3.1 多条件组合查询的 SQL 设计图书列表页通常有几个筛选项关键词书名/作者/ISBN、分类、价格区间、上下架状态。后台管理端还要加一个只看下架商品的选项。这里我用 MyBatis-Plus 的 LambdaQueryWrapper 做条件拼接核心逻辑如下LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); // 关键词搜索书名、作者、ISBN 三个字段任意匹配 if (StringUtils.hasText(keyword)) { wrapper.and(w - w.like(Book::getTitle, keyword) .or().like(Book::getAuthor, keyword) .or().eq(Book::getIsbn, keyword)); } // 分类过滤 if (categoryId ! null) { wrapper.eq(Book::getCategoryId, categoryId); } // 价格区间 if (minPrice ! null) { wrapper.ge(Book::getPrice, minPrice); } if (maxPrice ! null) { wrapper.le(Book::getPrice, maxPrice); } // 用户端强制只看上架商品管理端可传 status 参数控制 if (onlyOnSale) { wrapper.eq(Book::getStatus, 1); } // 默认按上架时间倒序 wrapper.orderByDesc(Book::getCreatedAt);有个细节要提醒多字段 like 查询时记得用and(条件...)把书名 like 或 作者 like 或 ISBN like这三个条件包成一个整体否则和后面的分类、价格条件拼接时会出现逻辑错误——OR的优先级在 SQL 里比AND低一旦不加括号查询结果就会出现关键词匹配到的所有分类下的书都被捞出来。3.2 搜索结果排序与分页排序我做了三档切换默认综合排序按创建时间倒序也就是新品优先、按价格从低到高、按价格从高到低、按销量。销量排序要用到sales_count字段这个字段在每次订单支付成功时累加。这里注意累加销量不应该在用户下单时做而是支付成功时——否则用户下单不付款销量数据就虚高了。分页我用 MyBatis-Plus 自带的分页插件前端传page和size参数返回总记录数和当前页数据列表。这个方案对中小项目完全够用不需要引入 Elasticsearch。等哪天你的数据量大到 SQL 慢查询扛不住了再考虑上 ES这是后话。3.3 搜索结果页的书卡组件前端图书卡片我强烈建议包含这些元素封面图、书名、作者、价格、累计销量标签。封面图上传时要做尺寸统一处理建议使用 1:1.4 左右的书封比例我实测下来 300x420 像素比较合适文件大小控制在 200KB 以内用 JPEG 格式。图太大既影响列表加载速度又浪费服务器存储空间。4. 购物车模块重构记从数据库表方案到 Redis 方案的演进购物车这个模块我第一次做的时候直接写数据库表版本功能很正常代码也很清晰。后来在面试和实际项目中被人问过一次如果购物车并发访问压力大怎么优化我才认真去把 Redis 版本也做了。这两个方案各有优劣我都讲一下你按照自己的场景选。4.1 数据库表的移动端购物车简单可靠适合学习和中小项目数据库方案的核心操作就是三个接口加购、更新数量、删除。加购的代码逻辑要注意先查后插的并发问题但因为有联合唯一索引uk_user_book兜底直接执行INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1就能把并发问题消灭在数据库层不需要在应用层加锁。INSERT INTO cart_items (user_id, book_id, quantity) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity);这种写法简洁高效MySQL 原生支持实现加购的语义是如果之前加过这本书数量累加如果没加过新建一行。结算时只要查询checked 1的条目关联图书表查出最新的价格和库存然后进入下单流程。数据库方案的缺点是每次用户访问购物车都要做一次联表查询而且用户对购物车的操作频率很高加购、改数量、删减、勾选数据库压力会随着用户量增长线性增加。但对一个日活几千的学习项目来说这个方案维护成本最低数据不会丢逻辑最好排查。4.2 Redis 版的购物车快但要自己处理持久化问题Redis 方案我用的结构是 Hashkey 是cart:{userId}field 是 bookIdvalue 是数量。加购操作在最理想的情况下是一条命令redisTemplate.opsForHash().increment(cart: userId, String.valueOf(bookId), 1);不需要先查再写天然解决并发累加问题性能比数据库方案高一个数量级。但 Redis 方案有个坑Redis 默认存在内存里如果服务器重启且没做持久化购物车数据就全丢了。我的处理办法是开启 RDB 快照持久化并且在用户结算下单成功后把 Redis 中对应的购物车条目删除保证已结算的商品不会残留。另一个坑是 Redis 存的是 bookId 和 quantity但展示购物车列表时还是要回到 MySQL 查图书表的详情书名、价格、封面所以实际开发中 Redis 方案并不会省掉联表只是把高频写的操作从 MySQL 挪到了 RedisMySQL 专心做读和事务。4.3 我的建议如果你在做一个需要在答辩现场稳定 Demo 的项目我建议直接用 MySQL 版简单、直观、好讲。如果你是想在简历上体现自己对性能的思考可以主动把 Redis 版也做了然后在技术方案对比那里写一段为什么选 Redis 而不是 MySQL。两种都能自圆其说重点是你真的理解取舍的依据。5. 订单模块的工程实现事务边界、订单号生成与库存扣减订单模块是整个系统里最难写对的部分因为它涉及多表更新、事务一致性和数据并发。我从三个核心问题讲怎么保证数据一致性、怎么生成不重复的订单号、怎么做库存扣减不乱扣。5.1 下单流程的事务边界创建订单的完整流程是校验商品状态和库存 → 计算总金额 → 生成订单主表记录 → 生成订单明细表记录 → 扣减库存 → 清空购物车对应条目。这六个操作要么全部成功要么全部回滚所以必须放在同一个事务里。Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto) { // 1. 校验购物车选中的条目 ListCartItem cartItems cartItemMapper.selectCheckedItems(dto.getUserId()); // 2. 遍历校验库存并计算总价 ListOrderItem orderItems new ArrayList(); BigDecimal totalAmount BigDecimal.ZERO; for (CartItem item : cartItems) { Book book bookMapper.selectById(item.getBookId()); if (book null || book.getStatus() ! 1) { throw new BusinessException(部分商品已下架请重新选购); } if (book.getStock() item.getQuantity()) { throw new BusinessException(《 book.getTitle() 》库存不足); } // 3. 生成订单明细快照 OrderItem orderItem new OrderItem(); orderItem.setBookId(book.getId()); orderItem.setBookTitle(book.getTitle()); orderItem.setPrice(book.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setSubtotal(book.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); orderItems.add(orderItem); totalAmount totalAmount.add(orderItem.getSubtotal()); } // 4. 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo(dto.getUserId())); order.setUserId(dto.getUserId()); order.setTotalAmount(totalAmount); order.setStatus(0); // ... 设置收货信息 // 5. 保存订单和明细 orderMapper.insert(order); orderItems.forEach(item - { item.setOrderId(order.getId()); orderItemMapper.insert(item); }); // 6. 扣减库存 cartItems.forEach(item - { bookMapper.decreaseStock(item.getBookId(), item.getQuantity()); cartItemMapper.deleteById(item.getId()); }); return order; }Transactional(rollbackFor Exception.class)这个注解有个细节为什么必须写明rollbackFor因为 Spring 默认只对 RuntimeException 回滚如果业务代码抛的是受检异常Exception 的子类但不是 RuntimeException事务不会回滚。写清楚rollbackFor Exception.class是为了让所有异常都触发回滚避免数据不一致。5.2 库存扣减防止超卖的正确打开方式库存扣减是并发场景最典型的问题。假设库存只剩 1 本两个用户同时下单都通过了库存是否充足的校验然后都去扣减库存最终库存变成了 -1但两个订单都创建成功了这就是超卖。很多人第一反应是用 Java 的synchronized锁一段代码但单机锁在集群部署时根本锁不住而且锁的范围不好控制容易把整个下单接口锁死。更正确的做法是把库存扣减做成一条原子 SQLUPDATE books SET stock stock - #{quantity} WHERE id #{bookId} AND stock #{quantity}通过WHERE stock #{quantity}这个条件数据库层面的行锁会保证同一时刻只有一个事务能成功执行这条更新。如果影响行数是 0说明库存不足直接抛异常回滚整个订单事务。这种方式不依赖任何第三方组件正确性由数据库事务保证是目前中小项目里最可靠、最易理解的方案。5.3 库存与销量的一致性同一个事务里的一减一加与扣减库存搭配的是销量累加。扣减库存发生在下单时累加销量发生在支付成功时。这两个操作的时间点不同但语义上库存代表可售数量销量代表已售数量它们的关系是库存 销量 初始入库数量。所以在支付回调这里是模拟支付里除了更新订单状态为已付款还要执行图书表销量的累加UPDATE books SET sales_count sales_count #{quantity} WHERE id #{bookId}同样要放在事务里执行。支付回调是这个系统里最容易出问题的地方——如果支付状态更新成功了但销量累加失败了数据就对不上了。把它们放进同一个事务后要么都成功要么都回滚保证核心数据的一致性。5.4 模拟支付的实现思路真实项目对接微信支付需要商户号、证书、回调验签学习项目里用一套模拟支付逻辑就够了。我通常的做法是在支付页面展示订单信息点击模拟支付按钮后前端调用后端/api/pay/mock接口后端模拟支付成功回调更新订单状态、写支付时间和支付流水号、累加销量。这个接口设计得贴近真实回调的样子接收订单号参数校验订单状态必须是待付款然后执行上述事务内的更新操作。将来接真实支付时只需要把/api/pay/mock的调用点替换成微信支付统一下单接口回调接口换成微信的异步通知 URL业务逻辑完全不用动。6. 运营后台管理端功能设计与统计报表 SQL后台管理系统是这个项目里含金量被低估的部分。很多学生项目后台就做了个简单的图书增删改查但这远远不够。一个能拿得出手的运营后台至少要包括图书上下架管理、订单处理与发货、分类管理、核心销售数据看板。6.1 图书上下架与库存调整后台图书列表管理端可以看到所有状态的书支持上下架切换和库存调整。这里有个细节下架操作不需要检查这本书是否在用户的购物车中因为用户结算时会再次校验商品状态和库存。也就是说后台的任何操作都不用反向影响用户侧的数据只需要保证用户在结算那一刻拿到的数据是准确的。库存调整时建议加上操作日志——记录谁在什么时间把库存从多少改成了多少。有了日志将来出问题可以追溯到责任人这在真实业务里是标配在答辩里也是一个亮眼的加分点。6.2 订单管理发货与取消的状态审批流后台订单列表默认展示所有待发货订单管理员点击发货按钮订单状态从已付款变更为已发货记录发货时间。这个操作同样要校验当前状态防止已取消的订单被发货。我的做法是更新时带状态条件UPDATE orders SET status 2, shipped_at NOW() WHERE id #{orderId} AND status 1如果影响行数为 0说明订单状态已经不是已付款了要提示管理员刷新页面重新确认。这种乐观更新的思路在状态机流转场景里屡试不爽比先查再改更安全也更简单。6.3 销售统计GROUP BY 的经典实践销售看板我最常写三个统计 SQL今日销售额、最近七天的每日销售额趋势、图书销量 Top 10。这三个都基于订单表和订单明细表。-- 今日销售额统计状态为已付款及之后的订单金额合计 SELECT IFNULL(SUM(total_amount), 0) AS today_sales FROM orders WHERE status IN (1, 2, 3) AND DATE(created_at) CURDATE(); -- 最近7天每日销售额 SELECT DATE(created_at) AS day, SUM(total_amount) AS amount FROM orders WHERE status IN (1, 2, 3) AND created_at DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(created_at) ORDER BY day; -- 图书销量TOP10 SELECT b.title, SUM(oi.quantity) AS total_sold FROM order_items oi LEFT JOIN books b ON oi.book_id b.id LEFT JOIN orders o ON oi.order_id o.id WHERE o.status IN (1, 2, 3) GROUP BY oi.book_id, b.title ORDER BY total_sold DESC LIMIT 10;注意销售额统计里status IN (1, 2, 3)这个条件表示统计的是已付款、已发货、已完成的订单不包括待付款和已取消的。如果不加这个状态过滤把用户下了单但没付款的金额全算进去报表数据会严重失真。统计模块的图表展示推荐用 ECharts折线图展示每日销售趋势、柱状图展示销量 Top10效果非常直观。前端代码量不大但视觉冲击力很强在演示的时候是一个很好的加分项。7. 前端快速搭建与接口联调的关键经验后台管理系统我用 Vue 3 Element Plus用户端前端我用 Vue 3 Vant。坦白讲对于以管理系统打头的项目前端的加分点不在花哨的动效而在页面功能完整性和交互合理性。以下几个经验特别想分享。7.1 用户端的核心页面路由用户端至少需要这些路由首页分类 图书瀑布流 搜索入口、图书详情页、购物车页、结算页、订单列表页、订单详情页、个人中心页包含地址管理。我建议把搜索框做成吸顶的用户浏览列表时可以随时切换关键词这个交互细节对转化率影响挺大。7.2 管理端的布局管理端我强烈推荐经典侧边栏 顶栏布局侧边栏按模块分组商品管理、订单管理、用户管理、数据统计顶栏放管理员信息和退出登录。表格类页面用 Element Plus 的el-table配合el-pagination分页组件再给每行操作按钮加上二次确认弹窗——比如下架图书、发货、取消订单这些敏感操作都需要弹窗确认避免误触。7.3 跨域与接口联调前后端分离开发时最常见的坑是跨域。我在 Spring Boot 后端写了一个全局的 CORS 配置类允许本地开发地址访问。联调时后端启动在 8080 端口前端 Vite 代理/api到http://localhost:8080这样前端代码里所有请求都写相对路径/api/xxx上线后不用改前端代码就可以直接部署。一个联调时的小技巧前端还没做好时先用 Apifox 或 Postman 把后端所有接口测通重点测试异常分支——库存不足、商品已下架、订单状态异常、未登录访问等。这些边界情况在后端先测好前端对接时就会非常顺畅。7.4 登录态管理我用 JWT 做登录态管理。用户登录成功后后端返回 token前端存在 localStorage 里请求拦截器统一在请求头加Authorization: Bearer {token}。后端用一个拦截器校验 token 并解析出用户 ID 存入 ThreadLocal后续接口直接从 ThreadLocal 取当前登录用户。管理员的接口额外检查角色是否为管理员不是则返回 403。这里有一个安全细节不要把用户 ID 和角色明文放在 JWT 里后不校验签名一定要用密钥签名并校验有效期。我也不建议把密码等敏感信息放进 token前端拿到 token 后解析出来的只能是公开信息。8. 部署上线的具体操作最便宜的方案也能跑出完整闭环最后聊聊部署。网上书店管理系统是一个标准的前后端分离项目部署方案有好几种我按推荐程度排个序。8.1 方案一单台云服务器 Docker Compose推荐我在实践中最终用的是这个方案一台 2 核 4G 的云服务器装好 Docker 和 Docker Compose编排三个容器——MySQL 8、Redis如果你用了、后端应用Spring Boot 打成 jar 包。前端构建出的静态文件用 Nginx 容器托管并用 Nginx 反向代理/api路径到后端容器。这样配置下来全链路就通了。Docker Compose 的关键配置长这样version: 3.8 services: mysql: image: mysql:8.0 container_name: bookstore-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: bookstore ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql restart: always backend: build: ./backend container_name: bookstore-backend ports: - 8080:8080 depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai restart: always frontend: image: nginx:alpine container_name: bookstore-frontend ports: - 80:80 volumes: - ./dist:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - backend restart: always用 Docker 部署的好处是环境问题被彻底隔离。不管本地开发环境是 Windows 还是 Mac到服务器上都是一个docker-compose up -d直接拉起整套服务。将来迁移服务器复制整个项目目录再执行一次命令就完事。8.2 数据库文件的备份策略部署上线后最怕的就是数据丢失。我在服务器上写了一个 crontab 定时任务每天凌晨 2 点用mysqldump备份数据库到磁盘mysqldump -uroot -proot123456 bookstore /backup/bookstore_$(date \%Y\%m\%d).sql find /backup -name *.sql -mtime 7 -exec rm {} \;第二条命令清理 7 天前的旧备份避免备份文件无限堆积占满磁盘。如果只想做学习演示本地写个 bat 或 shell 脚本手动备份也行但只要项目上了云服务器自动备份一定要配好。8.3 HTTPS 与域名这个项目如果只是演示和学习用 IP 端口访问完全够用。但如果想作为作品集展示给面试官看建议注册一个域名并配 HTTPS。免费的 SSL 证书现在申请很方便配置过程也不复杂。有了 HTTPS既能让演示环境看起来更专业也能避免浏览器对非 HTTPS 页面的 API 请求给出安全警告徒增不必要的困扰。9. 扩展方向从基础版本到高完成度系统的四条升级路径基础版本做完后你大概率会觉得不过瘾想再加东西。我根据自己的经验梳理四条高性价比的升级路径按投入产出比排序。9.1 引入 Redis 缓存热销图书与分类图书首页和搜索页是访问量最大的入口适合做缓存。把热门分类下的图书列表缓存到 Redis设置 5 分钟过期能显著降低数据库压力。做的时候要注意缓存穿透问题——如果某个分类下本来就没有书缓存里不应该存空值而是也要缓存否则请求会一直打到数据库。9.2 增加基于时间轮的订单超时自动取消待付款订单超过 30 分钟自动取消是电商系统的标配功能。实现方案从简单到复杂有几种定时任务扫描、延迟队列、Redis 过期监听。学习阶段用定时任务每 1 分钟扫一次待付款订单超时的取消并将库存加回去。这个功能虽然不是核心链路但加上以后系统的完整感立刻上一个台阶。9.3 对接真实支付如果项目用于毕设或实际商用可以考虑对接支付宝沙箱环境。支付宝沙箱可以模拟真实的支付流程不需要营业执照个人开发者就能申请。对接后支付回调、验签、订单状态同步这整套逻辑都变成真实的项目的说服力会比模拟支付强很多。9.4 增加用户积分与优惠券积分和优惠券系统的本质是在订单金额计算上加一层抵扣规则。设计时要注意优惠券的核销状态未使用/已使用/已过期、使用门槛满多少可用、有效期这些字段。这个功能实现后订单价格计算的复杂度会明显提升很适合作为深入学习的训练场。我个人在实际项目里做完这四条升级后整个系统的代码量大概从 5000 行涨到了 8000 行左右但这多出来的 3000 行覆盖了缓存、异步任务、第三方对接、营销玩法四类真实业务场景。对于想在面试里展现项目深度的人来说这部分的含金量比你写十个 CRUD 模块都高。最后分享一个小经验不要急着把所有功能一次性堆上去先把基础版本完整跑通、部署上线、让手机能真实访问到再一步步迭代升级。这个完整跑通的感觉会给你后面所有开发动作提供非常大的信心支撑。