简介:这份资源是面向计算机及相关专业本科生、研究生的课程设计文档,围绕基于Spring Boot的网上购物商城系统展开,帮助读者理解Java、MySQL与B/S结构在电商项目中的落地方式,适合正在做课程设计或希望完整走一遍需求分析到系统测试流程的学习者。压缩包内共1个docx文件,约3.8MB,为完整的课程设计报告,涵盖摘要、系统开发技术、系统分析、数据库E-R图与表结构设计、功能实现及测试等章节,结构清晰,可直接作为撰写与答辩的参考模板。目前已有76人学习。文档对用户、商家、管理员三类角色的功能模块划分明确,测试部分从功能、性能、安全三个维度展开,并附有测试结果分析与改进方向,读者可据此掌握Spring Boot项目从设计到验证的完整思路,也能为后续系统维护与扩展提供参照。
1. 从一份课程设计说起:这个 Spring Boot 商城到底能跑出什么
很多同学拿到"基于 Spring Boot 的网上购物商城系统"这类课程设计源码时,第一反应是打开压缩包、找pom.xml、点运行,然后被一堆建表语句和配置项劝退。我拆过不少这类项目,说实话,它不是一个玩具 Demo,而是一个把电商主链路走通的三端系统:用户端能注册登录、浏览商品、加购物车、下单支付;商家端能上架商品、管订单;管理员端能管用户、管商家、管分类、管订单。技术栈是 Java + Spring Boot + MySQL + B/S 结构,前端走浏览器,后端跑在 Tomcat 里,数据落在 MySQL。适合谁?正在做课程设计、想找一个结构完整能二次开发的电商骨架的本科生,以及想练手 Spring Boot 全链路 CRUD 的初级开发者。下面我按"能干什么 → 怎么跑 → 怎么改"的顺序,把它拆开讲清楚。
2. 技术选型与整体架构:三端角色怎么在一个 Spring Boot 工程里共存
2.1 为什么是 Spring Boot + MySQL + B/S 这套组合
这个项目的技术选型基本是课程设计场景下的"标准答案",但每一项都有它站得住的理由。Spring Boot 的核心价值在于自动配置:传统 Spring 项目要写一堆web.xml、applicationContext.xml、DispatcherServlet 映射,而 Spring Boot 内嵌 Tomcat,一个main方法就能把 Web 服务拉起来,spring-boot-starter-web把 MVC 那一套全带上了。对于课程设计这种"时间紧、要出完整功能"的场景,少写配置就是少踩坑。
MySQL 作为关系型数据库,在这个系统里承担的是订单、商品、用户这些强结构化数据的存储。电商数据的特点是实体之间关系明确——一个订单属于一个用户、包含多个商品、对应一个地址,这种多对多、一对多的关系用关系型数据库表达最自然,用 E-R 图设计表结构也顺理成章。项目正文里明确提到用 MySQL 存储数据,这决定了后面所有表设计都围绕关系模型展开。
B/S 结构则是把"客户端"交给浏览器。用户、商家、管理员三种角色不需要装任何客户端,打开浏览器访问不同入口即可。这对课程设计很关键:你不用为三端分别写三套客户端,一套 Web 页面按角色渲染菜单就行。三层结构(浏览器 → Web 服务器 → 数据库服务器)也让职责边界清晰,浏览器只管展示和请求,服务器处理业务逻辑,数据库只管存。
提示:这套组合的性能上限不高,但胜在结构清晰、资料多、出问题好查。课程设计和中小型项目够用,别指望它扛高并发。
2.2 三端角色的权限边界与模块划分
系统结构图里把功能拆成三块:管理员模块、商家模块、用户模块。这个划分不是随便切的,它对应三种登录身份和三条权限线。
管理员是超级角色,功能最全:个人中心、用户管理、商家管理、商品分类管理、商品信息管理、我的收藏管理、系统管理、订单管理。注意"商品信息管理"和"订单管理"管理员也有,意味着管理员对全平台数据有兜底权限,商家上架的商品、用户下的订单,管理员都能看能改。
商家是供给方:注册登录后管自己的商品信息、看自己的收藏、处理自己的订单。关键点是"自己的"——商家只能操作归属自己的数据,这要求后端每个查询都带上商家 ID 过滤,不能只靠前端藏菜单。
用户是消费方:前台看首页和商品信息,注册登录后有购物车、个人中心、我的地址、我的收藏、订单管理。购买流程是"浏览 → 加购物车 → 提交订单 → 支付",正文的购买流程图把这个链路画得很清楚,其中有个判断分支:未登录用户点购买会被拦到登录页,登录后才继续。
三端共用一个工程、一套数据库,靠登录后的角色字段区分入口。常见做法是登录成功后把角色写进 Session,拦截器按角色放行对应路径。这种"单工程多角色"的结构对课程设计友好,部署简单,但要注意权限校验必须落在后端,前端菜单隐藏只是体验优化,不是安全边界。
2.3 目录结构与分层约定
虽然正文没贴完整目录树,但按 Spring Boot 电商项目的通行组织方式,源码大致是这么分的:
src/main/java/com/xxx/mall/ ├── controller/ # 三端接口入口,按角色或模块拆 ├── service/ # 业务逻辑,订单、购物车、商品 ├── service/impl/ # 实现类 ├── mapper/ 或 dao/ # 数据访问层 ├── entity/ # 实体类,对应数据库表 ├── config/ # 拦截器、跨域、静态资源映射 └── MallApplication.java # 启动类 src/main/resources/ ├── application.yml # 数据源、端口、MyBatis 配置 ├── mapper/ # XML 映射文件(若用 MyBatis) └── static/ 或 templates/ # 前端页面这个分层是理解项目的钥匙:controller只做参数接收和结果返回,service承载业务规则(比如下单要扣库存、算总价),mapper负责 SQL,entity是表结构的 Java 映射。二次开发时改功能,先定位到对应层,别在 controller 里堆业务逻辑,否则后面越改越乱。正文提到用 Spring Data JPA 规范做映射,实际项目里 MyBatis 和 JPA 都常见,看pom.xml里引的是哪个 starter 就能确认,这决定了你改数据访问层时是写 Repository 接口还是写 XML。
3. 核心模块拆解:从登录鉴权到下单,一条请求怎么走完
3.1 登录与角色鉴权:三种身份如何分流
登录是整个系统的入口,正文的登录流程图写得很直白:输入用户名密码 → 判断用户名是否存在 → 判断密码是否正确 → 登录成功。但真正落地时,三端登录要解决"同一个登录接口怎么区分角色"的问题。常见做法是登录时多传一个角色类型,或者用不同的登录入口路径。
@PostMapping("/login") public Result login(@RequestBody LoginDTO dto, HttpSession session) { // dto 里带 username、password、roleType(user/merchant/admin) User user = userService.findByUsername(dto.getUsername()); if (user == null) { return Result.fail("用户名不存在"); // 对应流程图第一个判断 } // 密码比对,实际项目里应存 MD5/BCrypt 哈希,不要明文 if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.fail("密码错误"); // 对应流程图第二个判断 } // 角色校验:防止用户拿用户账号登管理员入口 if (!user.getRole().equals(dto.getRoleType())) { return Result.fail("角色不匹配"); } session.setAttribute("loginUser", user); // 登录态写 Session return Result.ok(user); }这段代码对应流程图的两个判断分支,参数说明:LoginDTO承载前端表单,roleType是区分三端的关键字段,session用来保存登录态。逻辑上先查用户再比密码,顺序不能反——先比密码再查用户,等于给攻击者一个"用户名是否存在"的探测口。密码存储这里我特意用了passwordEncoder.matches,因为课程设计里明文存密码是高频翻车点,后面避坑章节会细说。
登录成功后,后续每个请求靠拦截器校验 Session:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { HttpSession session = req.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { resp.sendRedirect("/login"); // 未登录拦回登录页 return false; } return true; // 已登录放行 } }拦截器在config里注册,指定拦截路径和放行路径(登录页、注册页、静态资源要放行)。这里有个容易忽略的点:拦截器只校验"有没有登录",不校验"这个角色能不能访问这个接口"。商家想调管理员接口,只要登录了就能过拦截器。所以严格的做法是在拦截器里再比对角色,或者用注解在方法级做权限判断。课程设计阶段很多人只做登录校验,二次开发上生产前必须补角色校验。
3.2 商品浏览与购物车:数据怎么从表流到页面
商品浏览是用户端最轻的链路:首页查商品列表 → 点进详情查单个商品。对应到代码,controller接收分页参数,service调mapper查表,返回 JSON 给前端渲染。
@GetMapping("/goods/list") public Result listGoods(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Long categoryId) { // 按分类过滤 + 分页,categoryId 为空则查全部 Page<Goods> result = goodsService.pageQuery(page, size, categoryId); return Result.ok(result); }参数里page、size控制分页,categoryId是可选分类过滤,对应管理员维护的商品分类表。这里的分页建议用 MyBatis-Plus 的Page或手写limit,别一次性select *全查出来再在内存分页,商品一多页面就卡。
购物车是用户和订单之间的缓冲层。表结构上通常是一张cart表,字段包括用户 ID、商品 ID、数量、加入时间。加购物车的逻辑要处理"同一商品重复加"的情况——不是插一条新记录,而是把已有记录的数量累加:
public void addToCart(Long userId, Long goodsId, Integer num) { Cart exist = cartMapper.findByUserAndGoods(userId, goodsId); if (exist != null) { // 已存在则累加数量,避免购物车出现重复行 exist.setNum(exist.getNum() + num); cartMapper.updateNum(exist); } else { Cart cart = new Cart(); cart.setUserId(userId); cart.setGoodsId(goodsId); cart.setNum(num); cartMapper.insert(cart); } }这段逻辑看着简单,但它是下单正确性的基础。如果这里不做去重,购物车里同一商品出现多行,结算时总价和数量都会算错。参数num是本次加购数量,前端一般默认 1,详情页可以让用户选。二次开发时如果想加"库存校验",就在这个方法里先查商品库存,num超过库存直接返回提示。
3.3 订单生成:一次下单要动几张表
下单是整个系统最重的写操作,正文的订单实体 E-R 图列了一堆字段:订单编号、用户编号、商品编号、商品名称、商品图片、购买数量、价格、折扣价格、总价格、订单地址、支付类型、状态、下单时间。这说明订单表做了冗余设计——把商品名称、图片、下单时的价格都存进订单,而不是只存商品 ID 去关联查。
为什么要冗余?因为商品价格会变、商品可能下架。如果订单只存商品 ID,用户三个月后看历史订单,显示的是当前价格而不是下单时的价格,这就出错了。订单是"交易快照",下单那一刻的商品信息必须固化。这是电商系统的通用做法,也是这个项目设计里比较靠谱的一点。
下单的核心逻辑大致是:
@Transactional // 下单涉及多表写,必须加事务 public Long createOrder(Long userId, Long addressId, List<Cart> cartList) { BigDecimal total = BigDecimal.ZERO; Order order = new Order(); order.setOrderNo(generateOrderNo()); // 生成唯一订单号 order.setUserId(userId); order.setAddressId(addressId); order.setStatus(0); // 0 待支付 order.setCreateTime(new Date()); for (Cart cart : cartList) { Goods goods = goodsMapper.selectById(cart.getGoodsId()); // 校验库存,不足直接抛异常回滚 if (goods.getStock() < cart.getNum()) { throw new BizException("商品库存不足:" + goods.getName()); } // 扣库存 goodsMapper.reduceStock(goods.getId(), cart.getNum()); // 订单明细固化商品快照 OrderItem item = new OrderItem(); item.setOrderNo(order.getOrderNo()); item.setGoodsId(goods.getId()); item.setGoodsName(goods.getName()); // 快照:名称 item.setPrice(goods.getPrice()); // 快照:下单时价格 item.setNum(cart.getNum()); orderItemMapper.insert(item); total = total.add(goods.getPrice().multiply(new BigDecimal(cart.getNum()))); } order.setTotalPrice(total); orderMapper.insert(order); cartMapper.clearByUser(userId); // 清空已下单的购物车 return order.getId(); }这段代码有几个关键点值得说。第一,@Transactional不能省:扣库存、写订单、写明细、清购物车是四步写操作,任何一步失败都必须整体回滚,否则会出现"库存扣了订单没生成"这种脏数据。第二,库存校验和扣减要放在同一事务里,严格来说高并发下还得用乐观锁或update ... where stock >= num的原子操作,课程设计阶段用事务加校验基本够用,但要知道这是并发场景的薄弱点。第三,generateOrderNo()要保证唯一,常见做法是时间戳加随机数或用户 ID 拼接,别用自增主键当订单号暴露给前端。第四,订单明细单独一张表存商品快照,订单主表存总价和状态,这是"主表 + 明细表"的标准结构,比把所有商品塞进订单表一个字段要规范得多。
支付环节在课程设计里通常是模拟的:点支付把订单状态从 0 改成 1(已支付),不接真实支付网关。二次开发如果要接真实支付,改动点就在这个状态流转上,加支付回调接口和幂等处理。
4. 数据库设计与避坑:E-R 图落到建表语句时最容易翻车的地方
4.1 从 E-R 图到表结构:五张核心表怎么建
正文给了五张 E-R 图:商品分类、商家、订单、管理员、商品。实际落地时还要补用户表、购物车表、订单明细表、地址表、收藏表。核心表结构大致如下:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, username, password, phone, role, create_time | 用户表,role 区分身份 |
| merchant | id, account, password, name, phone, balance, status | 商家表,含余额和审核状态 |
| goods | id, merchant_id, category_id, name, price, stock, image, status | 商品表,status 控制上下架/审核 |
| orders | id, order_no, user_id, address_id, total_price, status, pay_type, create_time | 订单主表 |
| order_item | id, order_no, goods_id, goods_name, price, num | 订单明细,存商品快照 |
| cart | id, user_id, goods_id, num | 购物车 |
| address | id, user_id, receiver, phone, detail | 收货地址 |
| category | id, name, create_time | 商品分类 |
建表时几个字段类型要留意:金额一律用decimal(10,2),绝不能用float或double,浮点数算钱会出现0.1 + 0.2 = 0.30000000000000004这种玄学问题;状态字段用tinyint加注释(0 待支付、1 已支付、2 已发货、3 已完成、4 已取消),别用字符串存状态,查询和索引都吃亏;时间字段用datetime,默认值设CURRENT_TIMESTAMP省得每次手动填。
外键约束在课程设计里可以建也可以不建。建了能保证数据一致性,但删除商品时会被订单明细的外键挡住;不建则靠应用层保证。我的建议是核心关联(订单明细 → 订单)建外键,商品 → 分类这种可以只建索引不建外键,给运营删数据留余地。
4.2 避坑清单:五个我踩过的真实问题
现象一:登录一直提示密码错误,数据库里明明是对的。原因多半是注册时密码明文入库,登录时却用了passwordEncoder.matches去比对哈希,两边对不上。解决:注册和登录必须用同一套加密策略,注册时passwordEncoder.encode(rawPassword)存哈希,登录时matches比对。如果历史数据已经是明文,要么写个脚本批量加密,要么登录逻辑临时兼容明文,但后者是技术债,别留。
现象二:下单后库存没减,或者减了但订单没生成。原因是下单方法没加@Transactional,或者异常被try-catch吞掉了没往外抛,事务感知不到失败就不会回滚。解决:事务方法里业务异常直接抛RuntimeException子类,别 catch 了只打日志;确认启动类或配置类开了@EnableTransactionManagement(Spring Boot 自动开,但自定义数据源时要检查)。
现象三:商家能看到别的商家的商品和订单。原因是查询只按商品 ID 查,没带商家 ID 过滤,前端藏了菜单但接口没拦。解决:所有商家端查询强制拼merchant_id = 当前登录商家ID,这个 ID 从 Session 取,绝不能从前端参数传——前端传的 ID 可以被篡改。
现象四:中文商品名存进数据库变成问号。原因是数据库或表的字符集不是utf8mb4,或者 JDBC 连接串没指定编码。解决:建库建表统一CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,连接串加?useUnicode=true&characterEncoding=utf8。utf8mb4而不是utf8,是因为后者存不了 Emoji,商品名里带表情就报错。
现象五:分页查询越翻越慢,最后一页卡死。原因是用了limit offset, size且 offset 很大,MySQL 要扫描前 offset 行再丢弃。解决:数据量小时无所谓,数据量大时改用"记住上一页最后一条 ID"的游标分页(where id > lastId limit size),或者至少给排序字段建索引。课程设计数据量小感知不到,但这是二次开发上量后的第一个性能瓶颈。
5. 二次开发与验证:把这个课设骨架改成能用的系统
5.1 三个低成本的改造方向
拿到这个项目,最值得先做的改造不是加花哨功能,而是补三块"能上线"的短板。
第一块是密码安全。前面说过明文存储是高频问题,改造方式是引入BCryptPasswordEncoder,注册加密、登录比对,再写个一次性脚本把存量明文密码批量转哈希。改动集中在注册和登录两个方法,影响面小、收益大。
第二块是权限校验下沉。现在拦截器只校验登录态,改造思路是在拦截器里读 Session 里的角色,按请求路径前缀判断:/admin/**只放管理员,/merchant/**只放商家,/user/**放用户。更规范的做法是自定义注解@RequireRole("admin")加 AOP 切面,方法级控制,代码更干净。
第三块是订单状态机。现在订单状态是散落的数字,改造时把它收敛成一个枚举加状态流转校验:
public enum OrderStatus { UNPAID(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), DONE(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; // 构造、getter 省略 // 合法流转:待支付→已支付/已取消,已支付→已发货,已发货→已完成 public static boolean canTransfer(int from, int to) { return switch (from) { case 0 -> to == 1 || to == 4; case 1 -> to == 2; case 2 -> to == 3; default -> false; }; } }改状态前先canTransfer校验,防止"已取消的订单又被发货"这种脏流转。这个改动不大,但能让订单模块从"能跑"变成"讲得清规则"。
5.2 怎么验证改完没坏:一份最小回归清单
改完代码别急着交付,按这条链路走一遍:注册新用户 → 登录 → 浏览商品 → 加购物车(同一商品加两次,看数量是否累加)→ 提交订单(看库存是否扣减、购物车是否清空)→ 模拟支付(看状态是否变 1)→ 商家登录看订单是否可见 → 管理员登录看用户和订单是否可见。每一步都对照数据库表确认数据落对了,尤其是金额字段有没有精度丢失、订单明细里的商品名和价格是不是下单时的快照。
再补两个边界测试:库存为 1 时下单数量填 2,应该提示库存不足且不生成订单;未登录直接访问/user/order/list,应该被拦回登录页。这两条能覆盖事务回滚和权限拦截两个最容易出问题的点。
我自己的习惯是,每次动完订单或库存相关代码,都强制把上面这条链路从头走一遍,哪怕只改了一行 SQL。因为这类系统的坑往往不在你改的地方,而在你没想到的关联表上——有次我只改了订单状态字段类型,结果支付回调的比对逻辑跟着挂了,就是没走全链路验证。希望这份拆解能帮你少走几段弯路,把这份课设真正跑成自己能讲明白、能继续改的项目。
本文还有配套的精品资源,点击获取