ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Web实战:基于Spring Boot的网上家具销售系统设计与实现

Java Web实战:基于Spring Boot的网上家具销售系统设计与实现 简介这是一份基于Web的网上家具用品销售系统毕业设计论文面向计算机专业学生及Java技术爱好者尤其适合准备毕设或课程项目的开发者参考。文档完整呈现了从需求分析、系统设计、技术选型到功能模块实现的全部过程系统采用JSPJavaMySQL技术栈详细阐述了个人中心、首页轮播、家具资讯、客户审核、家具分类、家具审核、卖家审核、售后维权及统计中心等核心功能的设计逻辑。资源包含1个doc文件压缩包大小1.66MB为完整毕业论文文稿涵盖中英文摘要、目录、系统架构说明以及关键实现要点并涉及JSP、SSH、MySQL相关术语解析便于读者直接借鉴论文框架和开发思路。该资源已有51人浏览学习内容紧凑实用能帮助学习者快速理解电商类管理系统的构建方法同时获得项目管理与开发技巧方面的一手参考。1. 从选题到答辩这个标题到底在做一个什么系统如果你正在为毕业设计或课程项目找方向“基于web的网上家具用品销售系统的设计与实现”这类标题几乎是每年都会出现的经典款。它本质上是做一个 B2C 模式的电商网站业务范围锁定在家具用品用户浏览商品、加入购物车、下单支付管理员在后台维护商品分类、库存和订单状态。技术栈通常是 Java Web 路线使用 Spring Boot 或 SSM 框架作为后端配合 MySQL 数据库前端用 JSP 或 Thymeleaf 模板渲染页面。这个选题的实用之处在于它把电商系统里最核心的闭环都覆盖了用户端、管理端、订单流、库存扣减。家具品类的特殊性在于商品属性较多——材质、尺寸、颜色、风格这让商品模型的字段设计比普通百货更复杂也更容易在答辩时讲出深度。适合三类人一是基础扎实、想用一个完整项目证明自己工程能力的 Java Web 学习者二是需要稳妥通过答辩、不想在选题上冒险的毕业生三是想快速搭一个可演示的商城 Demo、后续准备扩展成真实业务的开发者。需要说明的是网上大量流传的“某某管理系统源码”质量参差不齐有些甚至不带数据库脚本照着重现很容易翻车。基于 web 的家具销售系统不是一个“大而全”的企业级电商平台它的价值在于用有限的时间把业务闭环跑通把 CRUD、分页、购物车、订单状态机这些 Web 基本功练扎实。接下来的章节我以 Spring Boot Thymeleaf MySQL 为技术底座把整个系统的设计与实现过程拆开讲清楚。2. 系统技术选型与数据库设计为什么用这几样组合2.1 技术选型的取舍逻辑和理由在动手写代码之前要先明确技术栈的选择依据。市面上常见的 Java Web 项目有几种组合纯 JSP Servlet JDBCSSMSpring Spring MVC MyBatis以及 Spring Boot 整合型项目。我一般会用 Spring Boot Thymeleaf MyBatis-Plus MySQL原因是 Spring Boot 的自动配置能力能省去大量 XML 配置让项目结构更清晰答辩时也更容易说清楚分层思路。前端方面有人喜欢 JSP有人用 Thymeleaf。我的建议是 Thymeleaf因为它是 Spring Boot 官方推荐的模板引擎语法更接近 HTML 原生标签对前端不熟的人更友好。JSP 在 Spring Boot 里需要额外引入 JSP 解析依赖打包成 jar 时还有路径问题属于自己给自己挖坑。这个组合的优势在于一是生态成熟遇到问题搜索量大、解决方案多二是层次划分清晰Controller 层、Service 层、Mapper 层的职责边界分明答辩时能按层次讲清楚三是后期如果想把单体拆分Spring Boot 项目的迁移成本低。很多同学在选型时容易被“新技术”吸引比如用 Vue 前后端分离但毕业设计场景没有必要刻意上复杂度单体能跑通、能讲明白原理才是最重要的。2.2 数据库表结构设计家具商品的关键字段和表关系数据库设计是评审老师通常会重点提问的部分。对于家具销售系统我建议设计六张核心表用户表、分类表、商品表、购物车表、订单表、订单明细表。这六张表的关系是用户与购物车一对多用户与订单一对多订单与订单明细一对多商品与订单明细多对多通过明细表关联分类与商品一对多。家具商品和普通商品的最大区别在于描述字段密集材质、尺寸、颜色、风格、适用空间。这些字段如果全都塞进商品表里会导致字段爆炸查询时也难以维护。我建议用“商品主表 商品属性扩展表”的方式主表存公共字段如商品名称、价格、库存、销量、上下架状态扩展表存家具特有的属性信息如材质、尺寸、颜色、风格。下面这条 SQL 是商品主表的建表语句CREATE TABLE product ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 商品ID, category_id int(11) NOT NULL COMMENT 所属分类ID, product_name varchar(128) NOT NULL COMMENT 商品名称, subtitle varchar(256) DEFAULT NULL COMMENT 副标题, main_image varchar(255) DEFAULT NULL COMMENT 主图路径, detail text COMMENT 商品详情, price decimal(10,2) NOT NULL COMMENT 当前价格, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, sales int(11) NOT NULL DEFAULT 0 COMMENT 销量, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1上架 2下架, create_time datetime NOT NULL COMMENT 创建时间, update_time datetime NOT NULL COMMENT 更新时间, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT家具商品主表;这是一个非常稳的五张表设计category_id指向分类表main_image存的是图片相对路径而非二进制数据避免数据库体积膨胀sales字段用于商品列表按销量排序status用于下架逻辑而非物理删除。商品属性扩展表的建表语句CREATE TABLE product_attribute ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 属性ID, product_id int(11) NOT NULL COMMENT 商品ID, attr_name varchar(64) NOT NULL COMMENT 属性名如材质、尺寸、风格, attr_value varchar(255) NOT NULL COMMENT 属性值如实木、1800*800mm, PRIMARY KEY (id), KEY idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT家具商品属性扩展表;这样设计的理由是家具的属性和价格、库存不同价格库存是固定字段属性却是随品类变化的。沙发有面料、填充物书桌有桌面材质、桌腿材质。用扩展表存键值对新品类加属性不用改表结构。订单表和订单明细表是电商系统的核心建表语句如下CREATE TABLE order_info ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id int(11) NOT NULL COMMENT 下单用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, receiver_name varchar(32) NOT NULL COMMENT 收货人, receiver_phone varchar(16) NOT NULL COMMENT 联系电话, receiver_address varchar(256) NOT NULL COMMENT 收货地址, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 订单状态1待付款 2待发货 3待收货 4已完成 5已取消, create_time datetime NOT NULL COMMENT 下单时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE order_item ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL COMMENT 订单ID, product_id int(11) NOT NULL COMMENT 商品ID, product_name varchar(128) NOT NULL COMMENT 商品名称快照, product_image varchar(255) DEFAULT NULL COMMENT 商品图片快照, price decimal(10,2) NOT NULL COMMENT 下单时单价, quantity int(11) NOT NULL COMMENT 购买数量, total_price decimal(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;订单主表和明细表的分工是订单表只存一次交易的整体信息如收货地址、总金额、状态明细表存每一件商品的快照信息。快照的含义是在下单那一刻把商品名称、价格、图片复制一份存进明细表这样后续商品改名、图片变更甚至被删除订单的历史记录仍然是准确的。这是电商系统一个很重要的设计细节答辩时能够主动说出来会显得你对业务有过思考。2.3 购物车表的两种实现方式对比购物车是电商系统的必经功能但实现方式有讲究。一种是后端购物车用数据库表存另一种是本地购物车用 Cookie 或 LocalStorage 存。两者各有适用场景我的建议是毕业设计直接用后端购物车因为逻辑完整且在答辩时好讲清楚。后端购物车的建表语句CREATE TABLE cart_item ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 用户ID, product_id int(11) NOT NULL COMMENT 商品ID, quantity int(11) NOT NULL DEFAULT 1 COMMENT 数量, checked tinyint(1) NOT NULL DEFAULT 1 COMMENT 是否勾选1勾选 0未勾选, create_time datetime NOT NULL COMMENT 加入时间, update_time datetime NOT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车表;这里有几个设计要点user_id和product_id的组合加唯一索引可以避免同一用户反复添加同一商品时产生多条脏数据checked字段用于“选中结算”功能用户可以在购物车里勾选部分商品去结算。如果你想把库存扣减放在加入购物车时那就需要额外的冻结库存字段复杂度会提升不少。常见做法是库存校验放在提交订单时做而不是放入购物车时做。后端购物车的优势是用户换设备后购物车数据不丢但需要登录才能添加商品。本地购物车免登录体验更好但后续结算需要把本地数据同步到后端还要考虑数据合并问题。做毕业设计的话逻辑越清晰越可控后端购物车更稳妥。2.4 商品分页查询和图片上传的常见落地姿势商品列表页的分页查询通常是必考的考点。如果你用的是 MyBatis-Plus分页可以依靠其内置的分页插件实现需要先注册一个分页拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setOverflow(false); // 超出页数时是否回第一页 pagination.setMaxLimit(100L); // 单页最大条数防止恶意大页请求 interceptor.addInnerInterceptor(pagination); return interceptor; } }Controller 层的查询接口RestController RequestMapping(/api/product) public class ProductController { Autowired private ProductService productService; GetMapping(/page) public ResultIPageProductVO page( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Integer categoryId, RequestParam(required false) String keyword) { PageProduct page new Page(pageNum, pageSize); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(categoryId ! null, Product::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Product::getProductName, keyword) .eq(Product::getStatus, 1) // 只查上架商品 .orderByDesc(Product::getSales); IPageProduct result productService.page(page, wrapper); return Result.success(ProductVO.convertFrom(result)); } }这段代码的核心逻辑是条件构造器里eq(categoryId ! null, ...)的意思是当分类 ID 不为空时才拼接等值条件这是条件查询的通用写法可以避免写多个 if 分支。eq(Product::getStatus, 1)保证用户端永远看不到下架商品这是数据安全的底线。图片上传建议用本地文件存储而非数据库存 Blob。在配置文件中指定上传路径和访问映射spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB app: upload: path: /data/upload/ access-prefix: /images/**然后在配置类中把本地路径映射为 URL 访问Configuration public class WebConfig implements WebMvcConfigurer { Value(${app.upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceHandler(file: uploadPath); } }图片上传的坑主要在于路径不能写绝对路径写死否则换环境就崩文件名要重命名用 UUID 或时间戳加随机数防止用户上传重复名文件互相覆盖文件类型要做白名单校验不能只靠前端限制。关于数据库和表结构设计这里有个容易被忽略的细节字符集一定要用utf8mb4而不是utf8。家具商品名称里如果包含生僻字或特殊符号MySQL 的utf8字符集最多 3 字节可能存不进去而utf8mb4才是完整支持 Unicode 的字符集。这个坑在答辩演示时如果被发现会很尴尬。3. 核心功能模块实现从注册登录到订单流转的完整代码走读3.1 用户注册与登录Session 方案和 MD5 加密要注意什么用户模块是系统的入口。注册逻辑主要做三件事校验用户名是否重复、密码加密存储、写入用户记录。登录逻辑则是校验用户名密码、写入 Session、记录登录状态。密码加密是最容易被忽视的点。很多初级项目直接把明文密码存进数据库这是安全事故的源头。在 Java Web 项目中无论使用 MD5 还是 BCrypt都要加盐处理。以下用 Spring Boot 项目里常见的方案演示Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public void register(UserRegisterReq req) { // 1. 校验用户名是否已存在 Long count userMapper.selectCount( new LambdaQueryWrapperUser() .eq(User::getUsername, req.getUsername())); if (count 0) { throw new BizException(用户名已被注册); } // 2. 密码加盐后 MD5 加密 String salt UUID.randomUUID().toString().replace(-, ); String encrypted DigestUtils.md5DigestAsHex( (req.getPassword() salt).getBytes(StandardCharsets.UTF_8)); // 3. 组装实体并写入 User user new User(); user.setUsername(req.getUsername()); user.setSalt(salt); user.setPassword(encrypted); user.setCreateTime(LocalDateTime.now()); userMapper.insert(user); } Override public User login(String username, String password) { User user userMapper.selectOne( new LambdaQueryWrapperUser() .eq(User::getUsername, username)); if (user null) { throw new BizException(用户不存在); } String encrypted DigestUtils.md5DigestAsHex( (password user.getSalt()).getBytes(StandardCharsets.UTF_8)); if (!encrypted.equals(user.getPassword())) { throw new BizException(密码错误); } return user; } }这段代码的逻辑说明MD5 本身在算法层面已经不够安全但配合随机盐、加盐后存储的方式在毕业设计场景下虽非最佳实践但可用足够应付大多数教学环境。如果要更安全建议替换为 BCrypt。登录成功后的 Session 写入放在 Controller 层处理更清晰PostMapping(/login) public ResultUserVO login(RequestBody LoginReq req, HttpSession session) { User user userService.login(req.getUsername(), req.getPassword()); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(7200); // 2小时有效 return Result.success(UserVO.from(user)); }登录态保持用 HttpSession这是 Java Web 最经典的做法。Session 的本质是服务端存储用户状态并通过 Cookie 中的 JSESSIONID 关联识别。在单体应用里这个方案足够稳定不需要引入 Redis 共享 Session。但你要在答辩时能说出 Session 的失效机制和超时时间设置。用户模块的一个常见缺陷是不做“找回密码”。如果时间紧张可以不做但至少要在用户表中预留手机号或邮箱字段。3.2 商品浏览与商品详情列表、筛选、搜索的用户端实现商品展示是用户最早接触的界面。用户端首页通常展示分类导航、热销榜、新品推荐位。家具品类下用户最关心的是风格和尺寸所以商品列表页的筛选条件建议包含分类筛选、价格区间、风格筛选如北欧、中式、美式、排序默认综合、销量、价格升序/降序。后端查询接口的参数比较多可以封装成一个查询对象Data public class ProductQueryReq { private Integer pageNum 1; private Integer pageSize 12; private Integer categoryId; // 分类ID private String keyword; // 关键词 private String style; // 风格 private BigDecimal minPrice; // 最低价 private BigDecimal maxPrice; // 最高价 private String sortField; // sales / price private String sortOrder; // asc / desc }对应的查询逻辑是动态拼装条件。这里要注意一点价格区间的前端要校验minPrice不大于maxPrice后端也要二次校验否则会产生空结果或错误结果。排序字段要用白名单机制不能直接把用户传的字段名拼进 SQL这是防 SQL 注入的底线。MyBatis-Plus 的QueryWrapper本身是参数化的不易被注入但orderBy的列名如果直接拼接仍存在风险。常见做法是先校验字段名不在白名单内就使用默认排序String field sortField; SetString allowFields Set.of(sales, price, create_time); if (sortField null || !allowFields.contains(sortField)) { field sales; } wrapper.last(ORDER BY field (desc.equals(sortOrder) ? DESC : ASC));商品详情页要展示的信息较多包括主图、详情图文、属性参数、销量、库存、用户评论等。详情页的接口按商品 ID 组合查询商品主表、属性扩展表再查销量和评论。这里不需要做缓存但如果后续演示时发现商品详情页加载慢可以考虑用 Redis 缓存商品详情并把库存变更时同步更新缓存。3.3 购物车操作加入、修改数量、删除、结算勾选购物车模块的用户操作路径并不算复杂核心接口就 4 个RestController RequestMapping(/api/cart) public class CartController { Autowired private CartService cartService; PostMapping(/add) public ResultVoid add(RequestBody CartAddReq req, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { return Result.error(请先登录); } cartService.addItem(user.getId(), req.getProductId(), req.getQuantity()); return Result.success(); } PutMapping(/update) public ResultVoid update(RequestBody CartUpdateReq req, HttpSession session) { User user (User) session.getAttribute(loginUser); cartService.updateQuantity(user.getId(), req.getProductId(), req.getQuantity()); return Result.success(); } DeleteMapping(/delete) public ResultVoid delete(RequestParam Integer productId, HttpSession session) { User user (User) session.getAttribute(loginUser); cartService.deleteItem(user.getId(), productId); return Result.success(); } GetMapping(/list) public ResultListCartVO list(HttpSession session) { User user (User) session.getAttribute(loginUser); return Result.success(cartService.getCartList(user.getId())); } }Service 层的实现要点是“幂等”同一商品再次加入购物车时数量累加而不是新增一行记录。利用数据库层的唯一约束uk_user_product在插入时使用冲突更新public void addItem(Long userId, Long productId, Integer quantity) { CartItem cartItem new CartItem(); cartItem.setUserId(userId); cartItem.setProductId(productId); cartItem.setQuantity(quantity); cartItem.setChecked(true); cartItem.setCreateTime(LocalDateTime.now()); cartItem.setUpdateTime(LocalDateTime.now()); cartMapper.insertOrUpdate(cartItem); // 有唯一索引冲突时走更新 }这里insertOrUpdate在 MyBatis-Plus 里没有直接封装可以用saveOrUpdate或者先查后插、存在则更新。购物车列表返回给前端时需要联查商品名称、图片、单价和库存状态商品下架或库存不足时给予标识提示public ListCartVO getCartList(Long userId) { ListCartItem items cartMapper.selectList( new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, userId) .orderByDesc(CartItem::getCreateTime)); if (items.isEmpty()) { return Collections.emptyList(); } ListLong productIds items.stream() .map(CartItem::getProductId).collect(Collectors.toList()); ListProduct products productMapper.selectBatchIds(productIds); MapLong, Product map products.stream() .collect(Collectors.toMap(Product::getId, p - p)); return items.stream().map(item - { Product product map.get(item.getProductId()); if (product null || product.getStatus() ! 1) { return CartVO.unavailable(item.getProductId()); // 失效商品 } CartVO vo new CartVO(); vo.setProductId(product.getId()); vo.setProductName(product.getProductName()); vo.setProductImage(product.getMainImage()); vo.setPrice(product.getPrice()); vo.setStock(product.getStock()); vo.setQuantity(item.getQuantity()); vo.setChecked(item.getChecked()); return vo; }).collect(Collectors.toList()); }购物车模块的常见遗漏在“库存校验”。加入购物车时就应该校验库存并给用户提示而不是等到结算下单时才提示。结算时的校验也不能省略两者都要做加入时校验是体验优化结算时校验是业务正确性保障。3.4 订单生成购物车到订单的数据一致性处理提交订单是整个系统最核心、最容易出错的地方。它的操作不是单表写入而是“读购物车、校验库存、扣减库存、生成订单主表、批量生成订单明细、清空购物车”这五步的联动操作。这五步必须在一个事务里完成否则会出现“订单生成了但库存没有扣减”或者“库存扣减了但订单没生成”的数据不一致问题。实现方案如下Service public class OrderServiceImpl implements OrderService { Override Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, Address address, ListLong checkedProductIds) { // 1. 查询用户购物车中被勾选的商品 ListCartItem cartItems cartMapper.selectList( new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, userId) .eq(CartItem::getChecked, true) .in(CartItem::getProductId, checkedProductIds)); if (cartItems.isEmpty()) { throw new BizException(未选择任何商品); } // 2. 联查商品信息逐项校验库存 ListLong productIds cartItems.stream() .map(CartItem::getProductId).collect(Collectors.toList()); ListProduct products productMapper.selectList( new LambdaQueryWrapperProduct() .in(Product::getId, productIds)); MapLong, Product prodMap products.stream() .collect(Collectors.toMap(Product::getId, p - p)); for (CartItem item : cartItems) { Product product prodMap.get(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new BizException(商品已下架 item.getProductId()); } if (product.getStock() item.getQuantity()) { throw new BizException(库存不足 product.getProductName()); } } // 3. 计算总金额并生成订单主表 BigDecimal totalAmount BigDecimal.ZERO; for (CartItem item : cartItems) { totalAmount totalAmount.add( prodMap.get(item.getProductId()).getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); } OrderInfo order new OrderInfo(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setReceiverName(address.getReceiverName()); order.setReceiverPhone(address.getReceiverPhone()); order.setReceiverAddress(address.getReceiverAddress()); order.setStatus(1); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); // 4. 生成订单明细表并扣减库存 for (CartItem item : cartItems) { Product product prodMap.get(item.getProductId()); OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getProductName()); orderItem.setProductImage(product.getMainImage()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setTotalPrice(product.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemMapper.insert(orderItem); // 扣减库存UPDATE WHERE 条件防止超卖 int updated productMapper.deductStock(product.getId(), item.getQuantity()); if (updated 0) { throw new BizException(库存不足扣减失败); } } // 5. 清空购物车中已结算的商品 cartMapper.delete(new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, userId) .in(CartItem::getProductId, productIds)); return OrderVO.from(order); } }这段代码有几个值得注意的点。事务注解Transactional(rollbackFor Exception.class)加在 Service 方法上默认只在运行时异常时回滚主动rollbackFor指定 Exception 是为了让受检异常也能触发回滚。第 4 步扣减库存时deductStock的 SQL 写法是关键必须是UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}。这样即使两个用户同时购买同一个商品数据库层面也会通过行锁保证只有一个操作能成功扣减不会出现超卖。第 5 步清空购物车时我用的是productIds而不是订单里的明细因为此时订单明细里本来就是这些商品ID。订单号不能简单用自增 ID不然用户可以通过订单号推断平台的订单量和走势。常见做法是用“时间戳 随机数”或“日期 用户ID 随机数”。还有一些系统中会加入机器标识和序列号不过毕业设计场景下做到前一种就足够了private String generateOrderNo() { return FT LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) String.format(%04d, ThreadLocalRandom.current().nextInt(10000)); }事务里的异常处理要特别注意如果第 4 步扣减库存失败抛了异常第 3 步的订单主表和第 5 步的清空购物车会自动回滚这是事务的整体保障。但如果出现数据库死锁比如两个线程同时持有不同商品行锁后再请求对方的行锁事务会自动重试吗不会MySQL InnoDB 检测到死锁会回滚其中一个事务业务代码需要捕获死锁异常并提示用户稍后重试。3.5 后台管理端商品上下架、订单发货与权限控制后台管理端是另一个重要的模块也是答辩时容易忽略的部分。很多同学只顾着做前台展示后台管理做得简陋这等于没有完成“系统的设计与实现”中的“系统”二字。后台至少需要四个子模块商品管理、分类管理、订单管理、用户管理。商品管理的最核心操作是上下架和库存调整。上下架本质上就是修改status字段不需要物理删除商品。库存调整在管理端的操作是设置“实际库存”而不是在现有库存上增减从操作语义上说更符合仓库盘点习惯public void updateStock(Long productId, Integer stock) { if (stock null || stock 0) { throw new BizException(库存不能为负数); } Product product new Product(); product.setId(productId); product.setStock(stock); product.setUpdateTime(LocalDateTime.now()); productMapper.updateById(product); }订单管理端最核心的操作是订单流转将待付款订单标记为已取消待发货订单标记为已发货待收货订单标记为已完成。这四类状态流转要放在一个状态机里控制不允许跨状态跳转比如“待付款”不能直接跳到“已完成”。实现上可以基于枚举或预定义的状态流转表public void changeOrderStatus(Long orderId, Integer targetStatus) { OrderInfo order orderMapper.selectById(orderId); if (order null) { throw new BizException(订单不存在); } // 状态流转合法性校验只允许相邻状态流转 MapInteger, SetInteger allowedTransitions Map.of( 1, Set.of(2, 5), // 待付款 - 待发货 / 已取消 2, Set.of(3), // 待发货 - 待收货 3, Set.of(4) // 待收货 - 已完成 ); SetInteger allowed allowedTransitions.get(order.getStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new BizException(非法的订单状态流转); } order.setStatus(targetStatus); orderMapper.updateById(order); }这里的状态流转表是一个简化版。真实业务中“待付款 - 已取消”通常由用户主动取消或超时自动取消触发而“待付款 - 待发货”在货到付款模式中存在在线支付模式中应由支付回调触发。毕业设计的订单状态流转做到这个程度已经足够。管理端权限控制是另一个需要认真对待的点。我不建议直接在后台管理 Controller 的每个接口里手写 if 判断管理员身份而建议借助拦截器或 Spring AOP 统一处理。常见做法是定义一个RequireAdmin注解放在管理端接口的方法上然后在拦截器中检查当前 Session 中的用户角色Component public class AdminAuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; RequireAdmin requireAdmin handlerMethod.getMethodAnnotation(RequireAdmin.class); if (requireAdmin null) { return true; } User user (User) request.getSession().getAttribute(loginUser); if (user null || !admin.equals(user.getRole())) { response.setStatus(401); response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\msg\:\无权限访问\}); return false; } return true; } }需要注意的是拦截器只拦截 Controller 层的 URL对静态资源要设置放行规则。管理端的静态资源如 CSS、JS、图片不需要拦截。前端页面上控制“是否显示管理入口”只是视觉上的隐藏后端接口上必须有实际的权限校验否则别人猜出接口路径就能直接操作后台这是严重的安全漏洞。4. 系统联调与避坑排查从环境配置到数据混乱的 5 个血泪经验4.1 环境配置类Tomcat/端口/数据库连接导致的启动失败系统开发完成进入联调阶段会遇到一波环境问题。这部分的坑数量最多复现率也最高。现象一项目启动时报端口 8080 被占用或者 Spring Boot 启动成功但浏览器无法访问。原因是同时有多个 Java 进程占用了同一个端口或者防火墙拦截了端口。解决方法是先查端口占用在命令行执行netstat -ano | findstr 8080找到占用进程 PID再通过任务管理器结束对应进程。Spring Boot 项目的端口是可以通过server.port直接修改的如果 8080 频繁被占改到 8081 或 9090 都是可行的。现象二数据库连接失败报Access denied for user rootlocalhost。原因是账号密码错、或授权表配置问题。常规排查思路是先用命令行确认账号密码能连上数据库再检查application.yml中url/username/password是否和本地 MySQL 一致。需要注意 MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver不是旧版的com.mysql.jdbc.Driver如果驱动版本不匹配会报很奇怪的连接错误。现象三页面中文全部显示为乱码。原因常见于字符集不统一。解决方法是确认三处保持一致数据库连接 URL 加characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai页面文件编码为 UTF-8MySQL 表字符集为 utf8mb4。这三处有一个不一致就会出乱码。现象四前端页面能打开但请求后端接口 404。原因通常是RequestMapping的路径写错或后端没有启动。排查方式是直接看浏览器 F12 的 Network 面板确认请求 URL 和后端映射路径是否完全一致。如果项目配置了server.servlet.context-path/mall那么所有接口前缀都要加上这个路径。现象五图片上传后无法访问页面404或显示空白。原因是本地文件上传路径和访问映射路径不匹配。常见做法是配置类中添加了资源映射但映射的目录不存在或上传时文件名没有重命名导致路径错乱。解决方法是检查配置文件里的上传路径是否为绝对路径且目录已创建同时看控制台日志里实际保存的路径和数据库里存的路径是否一致。4.2 数据库常见问题外键、数据死锁与 SQL 注入防护关于外键我的经验是表设计时不推荐强物理外键。逻辑外键已足够原因有两点一是物理外键在插入、更新时会触发额外检查影响写入性能二是毕业设计的数据量不大物理外键的性能影响不明显但一旦需要做分库分表物理外键会成为迁移阻碍。所以只需在逻辑上维护关联关系比如订单表的user_id对应到用户表的id。数据死锁问题在订单提交时容易出现。比如两个用户同时购回一个买了商品 A 和 B另一个买了 B 和 A如果数据库加锁顺序不一致就可能在 MySQL 的 InnoDB 引擎中产生死锁。解决方式是按固定顺序锁定资源。在上述订单生成代码中我按product_ids排序后再逐个扣减库存可以避免大部分死锁。如果真出现死锁事务会自动回滚但用户会看到失败提示。SQL 注入风险主要来自前端框架生成的查询条件。使用 MyBatis-Plus 的LambdaQueryWrapper时条件是参数化预编译的注入风险较低。但如果你为了灵活直接写原生 SQL就必须使用#{}占位符千万不能把用户输入直接拼接进ORDER BY或LIKE子句。在LIKE场景中如果有用户输入了%或_预编译也会被当成通配符使用解决办法是对用户输入做转义。4.3 功能逻辑类库存超卖、订单状态异常与数据不一致超卖问题在并发量高的场景下极易出现。如果在扣减库存逻辑中不能保证原子性比如先查询库存再在 Java 代码中判断stock quantity然后更新并发情况下两个请求同时读到库存为 1各自判断库存充足最后都执行setStock(stock - 1)就可能会把库存扣为负值。这就是典型的超卖。解决办法是扣减 SQL 中带上WHERE stock quantity条件让数据库来保证原子性。本文前面的deductStock方法就是这么实现的。订单状态异常通常发生在用户重复点击提交订单时。用户在某些网络慢的场景下看不到反馈连点多次提交按钮就会产生多个重复订单。解决方式是前端在提交后禁用按钮加上防止重复提交的 token 机制或者后端做幂等性判断。幂等性判断可以依据用户的“操作标记”比如用户在当前会话中已经提交过相同购物车的结算第二次提交时拿不到购物车记录。更通用的方案是后端在创建订单前检查用户是否有“未付款且内容一致”的订单。数据不一致的问题常因编码时疏忽了事务边界。比如支付回调更新订单状态和扣减库存分布在两个 Service 方法中但因为不在同一个事务里可能订单状态更新了而库存没有扣减。解决方式是把业务操作尽量收敛到一个事务方法中或者使用分布式事务方案。单体应用里一个Transactional方法完成全流程即可。4.4 前端联调类接口跨域与页面加载慢如果你坚持做了前后端分离跨域问题几乎无法避免。前端工程跑在 8080后端跑在 8090浏览器会拦截跨域请求。最简单的解决方式是后端加一个全局 CORS 配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }需要提醒的是allowedOriginPatterns(*)和allowCredentials(true)同时使用在较新的 Spring Boot 版本中会出问题。如果凭证模式下不允许所有来源需要指定具体的来源域名。毕业设计如果是前后端分离的项目这是我踩过比较深的坑之一。页面加载慢的问题常见于商品列表页一次性查询大量数据图片没有做压缩处理。家具商品图片往往体积不小一张原图动不动 300KB 以上。常见缓解方式是列表页使用缩略图详情页使用大图。可以把主图路径拆出多个尺寸版本前端按需加载。如果时间紧张至少也要在图片上传时做压缩控制在单张 200KB 以内。4.5 环境部署类打包成 jar 还是 war 时的路径与资源问题Spring Boot 项目的部署形态通常有 jar 和 war 两种。毕业设计演示阶段推荐用 jar 包方式运行因为一条命令就能启动java -jar demo.jar。如果使用 war 包部署到外部 Tomcat则要对 Spring Boot 的启动类做出调整需要继承SpringBootServletInitializer。这个细节容易遗忘一打包发现无法启动。如果使用 jar 包方式务必注意静态资源和上传文件的路径问题。jar 包内的资源是只读的如果你把用户上传的图片保存到项目内的/upload目录重启项目后文件可能就消失了。正确做法是把上传路径配置成外部磁盘路径比如/data/upload并在配置文件里做映射。这个点他们在联调阶段非常容易踩到尤其是重启项目后发现图片全部 404 时。打包命令本身倒是很简单mvn clean package -DskipTests打包后如果运行报no main manifest attribute多数是 build 插件未配置正确。Spring Boot 必须使用spring-boot-maven-plugin普通 maven-compiler 插件打出来的 jar 是不含启动类信息的。这个细节在答辩前夜修复过的大有人在。5. 交付与上线前的验证清单用一套测试用例证明系统可靠到系统做完时真正拉开完成度差距的是你是否有一份结构清晰的测试与验证记录。评审老师在答辩时喜欢问“你的系统测试过吗怎么测的”。如果你能拿出功能测试清单和测试结果说服力远高于口头描述。验证建议按业务链路来组织从用户注册到订单完成整理成一张覆盖全流程的测试表测试模块测试步骤预期结果是否通过注册输入用户名、密码、确认密码提示注册成功跳转登录页通过登录正确密码登录跳转首页并显示用户名通过商品浏览点击分类菜单展示对应分类商品列表通过商品搜索输入关键词“沙发”展示名称包含沙发的商品通过购物车将商品加入购物车购物车数量增加库存不变通过购物车修改数量为 999提示库存不足数量不变通过提交订单勾选商品并提交订单生成库存对应减少通过订单状态管理端标记发货订单状态变为待收货通过管理员权限普通用户访问后台地址返回 401 提示无权限通过模块间的关联测试比单点功能验证更重要。比如下单后库存要扣减、购物车要清空、订单明细数量和商品明细一一对应三项必须同时成立。这部分我是用一个测试脚本来验证的准备一把椅子商品初始库存 50注册两个测试用户分别下单 1 件和 30 件确认生成两条订单、库存变为 19、购物车均变为空。三个断言同时通过说明核心链路没有问题。性能层面的验证也可以用简易手段做。在商品详情接口上使用 JMeter 或 Apifox 发起 100 个并发请求观察平均响应时间和错误率。单体项目在 100 并发下响应时间应当控制在 500ms 以内如果超过这个值最常见的因素是数据库连接池太小或 SQL 缺少索引。在商品表的category_id、status字段上建好索引分页查询性能就会有明显提升。展示与技术阐述层面建议在写系统说明时把设计要点前置状态机流转、库存原子扣减、订单快照明细、权限注解拦截把实现思路讲出来。这些设计细节足以证明代码不是“增删改查汇编”才是真正的设计与实现。一些常见做法可以在后续迭代中沉淀成自己的工程习惯把系统初始化的 SQL 脚本放在项目根目录的sql/文件夹里方便评审老师直接导入环境配置用application-dev.yml和application-prod.yml分开管理代码里关键业务处写好中文注释逻辑能看懂印象分就上去了。最后留一句我的习惯叮嘱上线前把测试数据删干净别让评审老师看到你本地建了一堆“测试椅子的订单”。希望这篇拆解能帮你的项目少走几步弯路顺利落地。本文还有配套的精品资源点击获取
返回列表