
1. 课程设计选题为什么是SpringBoot电商后台做JavaWeb课程设计最头疼的事情不是写代码而是“选什么题”。图书馆管理系统被做烂了学生管理系统也遍地都是答辩时老师一眼就能看出你是不是从网上原封不动搬下来的。我当时选这个“基于SpringBoot的电商后台管理系统”的时候就是冲着几点去的功能边界清晰、技术栈覆盖全面、前后端都能展示、扩展空间大。电商后台管理系统这个概念往大了说可以做成淘宝京东那种巨型中台往小了说就是一套管理商品、订单、用户、分类的后台界面。课程设计需要的是后者但麻雀虽小五脏俱全。它天然适合用来体现SpringBoot的核心能力SpringMVC的请求处理、MyBatis的数据持久化、Thymeleaf的服务端渲染、Maven的依赖管理甚至如果你愿意还能塞进Redis缓存、JWT鉴权、拦截器、AOP日志这些进阶点。答辩时老师问“你这个项目用了什么技术”随便挑一个都能讲上五分钟。再说为什么必须是“含前端界面”。很多课程设计只交接口或者用Swagger展示API看起来很高端但老师更关心的是“你能不能把整个系统串起来”。一个能跑通登录、商品上架、订单出库的后台页面比一百个孤立的Controller方法更有说服力。前端界面不需要多华丽Bootstrap后台模板就够用关键是前后端数据能打通表单能提交表格能刷新按钮能触发操作——这才是一个“完整系统”该有的样子。适合谁来参考如果你是准备做JavaWeb课程设计的大三学生、需要交实训项目但时间紧的专科生、或者刚学完SpringBoot想找个完整项目练手的自学者这篇内容应该能帮你把整个项目的骨架搭起来并且理解每一步为什么这么走。我会把里面的核心设计、代码组织、前后端对接、以及我实际踩过的坑都拆开讲。2. 整体架构与技术选型的背后逻辑2.1 技术栈组合的取舍先说我最终采用的这套组合SpringBoot 2.x MyBatis MySQL Thymeleaf Bootstrap Maven。每一环都有它在这个场景下非选不可的理由。SpringBoot不用多说它解决了Spring家族配置地狱的问题内嵌Tomcat一个jar包就能跑起来。课程设计的交付物如果是“一个能直接运行的项目”SpringBoot是最优解。你不需要在答辩现场配置Web服务器双击启动类就完事。MyBatis和Spring Data JPA的选择值得纠结一下。JPA写起来确实爽实体类一标注解CRUD方法基本不用写但问题在于课程设计里你要展示“SQL能力”。MyBatis会让你手写Mapper XML复杂的多表联查、动态条件筛选都得自己搞这反而成了展示点。比如商品列表的模糊搜索加分类筛选一句动态SQL就能讲清楚原理。而且国内企业用MyBatis的存量项目远多于JPA实训项目往就业方向靠一靠不亏。前端我用的是Thymeleaf加Bootstrap。有人会说都2024年了还不用Vue我也想过前后端分离但关键问题是课程设计的评审重点是“功能完整”而不是“架构前沿”。前后端分离意味着你要处理跨域、Token、异步渲染平白多出一堆工作量。Thymeleaf的服务端渲染逻辑更直观Controller里塞数据到Model页面用th:each一循环数据就出来了对初学者极度友好。答辩时讲“后端怎么把数据传到页面”这条链路十分钟就能讲透。Bootstrap就更好解释了它不需要Node环境下载全套静态资源丢进static目录就能用。AdminLTE这类模板虽然好看但文件多、依赖杂很容易因为路径引入问题白屏。课程设计的时间不应该花在配前端依赖上。2.2 模块划分别一上来就想做大而全电商后台系统的标准模块是用户管理、商品管理、分类管理、订单管理、轮播图管理、公告管理。但课程设计切忌一上来就规划六七个模块时间不够不说还容易每个模块都做得半生不熟。我更建议只做四个核心模块外加一个登录功能。用户管理用户的增删改查、启用禁用。这里用户指的是“后台管理员”不是C端消费者。分类管理商品分类的层级展示至少支持二级分类。商品管理商品的增删改查、图片上传、按分类筛选、按名称模糊搜索。这是核心中的核心。订单管理订单列表展示、按状态筛选、发货操作。不需要做购物车和支付那是前台商城的活儿。登录与拦截管理员登录、会话维持、未登录跳转登录页。为什么这样切因为这是后台管理系统它服务的对象是“运营人员”不是“消费者”。后台和前台的区别在于后台偏向数据管理前台偏向业务流程。很多同学做后台系统做着做着就把前台的购物车、下单逻辑也塞进来导致项目臃肿到无法收尾。后台系统展示“库存改了多少、订单发没发货”就够了。2.3 数据库设计要先想清楚再写代码数据库是这种管理系统的地基表关系理不顺后面写SQL会痛不欲生。我用的表结构不复杂一共五张表管理员表、分类表、商品表、订单表、订单项表。订单项表可以看情况删掉但如果想展示外键关联留着它最合适。CREATE TABLE admin_user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(255) NOT NULL, nickname varchar(50) DEFAULT NULL, status tinyint DEFAULT 1, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ); CREATE TABLE product_category ( id int NOT NULL AUTO_INCREMENT, parent_id int DEFAULT 0, name varchar(50) NOT NULL, sort int DEFAULT 0, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ); CREATE TABLE product ( id int NOT NULL AUTO_INCREMENT, category_id int NOT NULL, name varchar(100) NOT NULL, subtitle varchar(200) DEFAULT NULL, main_image varchar(255) DEFAULT NULL, detail text, price decimal(10,2) NOT NULL, stock int NOT NULL, status tinyint DEFAULT 1, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ); CREATE TABLE order_main ( id int NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL, user_id int NOT NULL, total_amount decimal(10,2) NOT NULL, status int DEFAULT 0, receiver_name varchar(50) DEFAULT NULL, receiver_phone varchar(20) DEFAULT NULL, receiver_address varchar(200) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ); CREATE TABLE order_item ( id int NOT NULL AUTO_INCREMENT, order_id int NOT NULL, product_id int NOT NULL, product_name varchar(100) DEFAULT NULL, product_image varchar(255) DEFAULT NULL, current_unit_price decimal(10,2) DEFAULT NULL, quantity int DEFAULT NULL, total_price decimal(10,2) DEFAULT NULL, PRIMARY KEY (id) );这里有两个设计细节值得讲一下。第一订单表里冗余了receiver_name、receiver_phone、receiver_address还有product表的product_name、product_image快照。为什么要冗余订单一旦生成商品后面改了名、下了架历史订单还得保留当时的快照信息。如果你下单时只存product_id等到展示订单时联表查出来的可能就是空数据。这是电商系统里很经典的一个设计思想业务数据要留快照不能完全依赖引用。第二金额字段一律用decimal千万别用float和double。浮点数在计算机里是近似存储0.1加0.2都可能变成0.30000000000000004钱算错了是要出事的。decimal在MySQL里是定点数精度可控。第三status字段我喜欢用tinyint存数字状态含义在Java代码里用枚举或常量类去定义。比如订单状态0待发货、1已发货、2已完成、3已取消商品状态1上架、0下架。这样数据库里看到的是数字代码里看到的是语义各取所需。3. 项目骨架搭建与核心配置3.1 从一个空的SpringBoot项目开始创建SpringBoot项目最简单的方式是用Spring Initializr或者直接用IDEA新建。需要注意的坑是课程设计阶段SpringBoot版本别追新2.7.x就非常稳。3.x版本要求JDK17起步很多同学电脑上还是JDK8一配环境就崩。JDK8配SpringBoot 2.xMySQL驱动用8.0系列这套组合我跑了无数次从来没有版本冲突。项目结构上我用的是标准分层com.example.mall ├── MallApplication.java ├── config │ └── WebMvcConfig.java ├── controller │ ├── AdminUserController.java │ ├── ProductController.java │ ├── CategoryController.java │ └── OrderController.java ├── service │ ├── AdminUserService.java │ ├── ProductService.java │ ├── CategoryService.java │ └── OrderService.java ├── mapper │ ├── AdminUserMapper.java │ ├── ProductMapper.java │ ├── CategoryMapper.java │ └── OrderMapper.java ├── entity │ ├── AdminUser.java │ ├── Product.java │ ├── Category.java │ └── OrderMain.java ├── common │ ├── Result.java │ ├── Constants.java │ └── PageQuery.java └── interceptor └── LoginInterceptor.java这种结构叫“按层分包”Controller控制器、Service服务层、Mapper数据访问层一层一层往下调。它的优点是层次清晰答辩时老师问“请求怎么从浏览器走到数据库”的时候你能顺着代码路径讲页面发请求到ControllerController调ServiceService调MapperMapper执行SQL返回结果。这个链路讲明白了项目就讲明白了大半。3.2 application.yml里的关键配置配置文件那段我吃过亏写出来提醒一下。MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver不是老旧的com.mysql.jdbc.Driver。连接地址里后边那串参数一个都不能少。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 thymeleaf: cache: false servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.mall.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezoneAsia/Shanghai这个参数不加MySQL 8.0连接时大概率报“The server time zone value”的时区错误。useSSLfalse是因为本地开发没必要加密不然控制台全是SSL警告。characterEncodingutf-8是保证中文不乱码的底线。MyBatis的map-underscore-to-camel-case这个配置很好用它可以把数据库里的create_time自动映射到Java实体类的createTime属性。如果你不开启它就得在结果映射里手动写resultMap麻烦且容易出错。至于log-impl配置成StdOutImpl是为了在控制台打印SQL。课程设计阶段把这个开着出问题的时候你肉眼能看到MyBatis实际执行的SQL是什么排查效率翻倍。entity层的代码没什么技术含量就是一堆成员变量加getter/setter。逻辑删除字段要不要做我建议不做直接物理删除。课程设计不是企业项目物理删除更直观数据库里删一行你就看到少一行容易跟老师解释。逻辑删除还容易在联表查询时漏加过滤条件平白给自己埋雷。3.3 统一返回对象与跨页面数据传递在非前后端分离的项目里Controller返回的是“视图名称”数据通过ModelAndView或者Model塞进模板。但有些操作比如新增商品、修改分类前端用的是Ajax请求没法直接跳转页面这时候就需要Controller返回JSON。所以项目里要有一个统一的Result类。public class Result { private Integer code; private String msg; private Object data; public static Result success(Object data) { Result result new Result(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } public static Result error(String msg) { Result result new Result(); result.setCode(500); result.setMsg(msg); return result; } }有了这个类以后页面跳转和Ajax请求可以共用一套Controller。比如新增商品成功后跳转列表页你用return redirect:/product/listAjax上传图片成功后你用return Result.success(path)。调用方各取所需接口风格统一。还有一个小点图片上传的文件保存路径。千万不要存到项目的src目录里因为SpringBoot打包成jar后src目录里的文件根本无法写入。正确做法是存到服务器上的一个固定目录比如D:/upload或者Linux下的/usr/local/upload然后在配置类里做一个虚拟路径映射让/user/images/**访问映射到真实磁盘路径。代码这样写Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourcePatterns(file: System.getProperty(user.dir) /upload/); } }这样存库的时候图片路径只存/upload/xxx.jpg页面拿到这个路径就能直接访问图片不用关心文件到底在磁盘哪个位置。4. 前端界面设计与后端对接的实操细节4.1 页面组织与公共布局前端界面我用的是经典的后台管理模板结构左侧边栏菜单、顶部导航栏、右侧内容区。三个页面区域的公共部分提取出来用Thymeleaf的片段语法th:fragment抽成公共组件。!-- templates/common/sidebar.html -- aside th:fragmentsidebar ul lia th:href{/admin/index}首页/a/li lia th:href{/category/list}分类管理/a/li lia th:href{/product/list}商品管理/a/li lia th:href{/order/list}订单管理/a/li lia th:href{/user/list}用户管理/a/li /ul /aside页面引入公共片段的方式很直接:。这样改一次菜单所有页面同步更新不用每个页面都复制一遍侧边栏代码。这是Thymeleaf模板引擎最实在的功能。登录页做成了单独的极简风格没有套后台模板框架就一个居中的表单框。表单提交用POST请求到/admin/loginController接收username和password查询数据库校验。密码这里我说一下课程设计阶段很多人用MD5但MD5在如今已经不安全了彩虹表一查就能还原。稍微讲究一点的做法是加盐后再做SHA-256甚至直接用Spring Security的BCryptPasswordEncoder。实操上我建议在注册或者初始化管理员数据时用哈希过的密码存库项目里用同样的算法验证避免明文密码平铺直叙暴露在数据库里。4.2 商品列表页的表格渲染与搜索筛选商品列表是后台系统里使用频率最高的页面。它的功能点包括分页展示、名称模糊搜索、分类下拉筛选、上架下架切换、编辑跳转、删除操作。这个页面做好了整个项目的核心展示力就出来了。Thymeleaf渲染列表的核心语法就两句th:each遍历th:text输出。配合Bootstrap的表格样式代码大致长这样table classtable table-bordered table-hover thead tr thID/th th商品名称/th th分类/th th价格/th th库存/th th状态/th th操作/th /tr /thead tbody tr th:eachproduct : ${pageInfo.list} td th:text${product.id}/td td th:text${product.name}/td td th:text${product.categoryName}/td td th:text${product.price}/td td th:text${product.stock}/td td span th:if${product.status 1} classbadge badge-success上架/span span th:if${product.status 0} classbadge badge-secondary下架/span /td td a th:href{/product/edit/{id}(id${product.id})} classbtn btn-warning btn-sm编辑/a button th:attrdata-id${product.id} classbtn btn-danger btn-sm delete-btn删除/button /td /tr /tbody /table注意这里有个细节商品实体product表里并没有categoryName这个字段它只有category_id。页面要显示分类名称传统做法是后端查出商品列表后遍历每个商品再根据category_id去查分类表。这样会出现N1次查询问题。更高效的办法是在商品查询的SQL里直接用LEFT JOIN把分类名称带出来。select idselectProductList resultTypecom.example.mall.entity.Product SELECT p.*, c.name AS category_name FROM product p LEFT JOIN product_category c ON p.category_id c.id where if testkeyword ! null and keyword ! AND p.name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null and categoryId ! 0 AND p.category_id #{categoryId} /if /where ORDER BY p.id DESC /select这样一来一次SQL就把列表页需要的数据全部拿到商品实体的categoryName字段也不需要再额外添加了直接让MyBatis的map-underscore-to-camel-case把category_name映射到categoryName属性上。动态SQL里的 标签和 标签是MyBatis的灵魂。没有它们搜索和筛选就要写多个Mapper方法代码丑且冗余。有了动态SQL搜索条件为空时SQL自动不带WHERE条件不为空时自动拼上完美解决“是否查询所有商品”和“是否按条件筛选”的切换。关于动态SQL我建议一定要在答辩时重点讲因为它是你区别于“只会模板生成器”的加分点。4.3 订单状态流转从待发货到完成订单管理的核心操作是“发货”。前台用户下单后订单状态是待发货。后台管理员看到订单点发货状态变成已发货。整个状态流转用状态机思想看特别清晰。// 订单状态常量 public static final int ORDER_STATUS_UNSHIPPED 0; // 待发货 public static final int ORDER_STATUS_SHIPPED 1; // 已发货 public static final int ORDER_STATUS_FINISHED 2; // 已完成 public static final int ORDER_STATUS_CANCELLED 3; // 已取消Controller层写一个发货接口接收订单ID。Service层先判断当前状态是不是待发货是则更新为已发货不是则返回错误提示。判断这一步必须做否则重复点击发货按钮状态就会乱跳。前端发货按钮也要在状态非待发货时禁用或者在页面加载时根据stauts字段判断要不要渲染这个按钮。订单的查询天然适合多条件筛选按订单号精确搜索、按状态搜索、按时间区间搜索。和商品筛选一样用动态SQL就能解决。订单模块还有个功能点是订单详情查看点开可以看到订单项列表。订单项在数据库里是order_item表通过order_id关联。页面用th:each遍历订单项列表时后端Service要完成订单主表和订单项表的组装这一步需要在Service层组合两个Mapper查询的结果而不是用一条SQL硬join。因为订单主表和订单项是一对多关系join出来的结果每一行都重复订单主信息反而不利于前端展示。4.4 前端表单校验与后端双重验证新增和编辑页面都有表单商品名称、价格、库存都不能空。前端我用H5的required属性加jQuery的校验比如价格必须大于0、库存必须是整数。但千万别把校验只放在前端因为Ajax请求可以直接绕过页面Postman一发就能往库里塞垃圾数据。后端校验我采用的是最简单的手动校验Controller里写几个if判断不通过就返回Result.error。if (StringUtils.isBlank(product.getName())) { return Result.error(商品名称不能为空); } if (product.getPrice() null || product.getPrice().compareTo(BigDecimal.ZERO) 0) { return Result.error(商品价格必须大于0); } if (product.getStock() null || product.getStock() 0) { return Result.error(商品库存不能小于0); }用了Hibernate Validator那套NotNull注解需要在实体类加一堆注解写校验规则配好了很强大但对课程设计来说上手成本高、讲解成本也高。手动校验虽然代码多但逻辑肉眼可见哪里错了改哪里对初学者最友好答辩现场改需求也灵活。一句话企业级项目用注解校验或者Validator工具类课程设计用手动判断就足够了。5. 登录会话管理与拦截器别让页面裸奔后台管理系统的所有页面都不能让未登录的人直接访问。实现这个功能最直接的方式是SpringMVC的HandlerInterceptor拦截器。拦截器的作用可以类比成小区的门禁闸机每个人进门都必须刷脸确认身份才能放行没登记的访客直接拦在门外。实现方式分三步写一个实现HandlerInterceptor接口的类在preHandle方法里判断session里有没有登录标记注册拦截器并配置拦截路径和放行路径登录成功后把管理员信息塞进session。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object admin request.getSession().getAttribute(loginAdmin); if (admin null) { // 判断是否是Ajax请求 String requestType request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestType)) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录\,\data\:null}); return false; } response.sendRedirect(/admin/login); return false; } return true; } }这个拦截器里有个细节Ajax请求和页面跳转的处理方式必须分开。普通页面未登录时重定向到登录页浏览器地址栏会跳走体验没问题。但如果是页面里的Ajax请求被重定向后返回的是登录页HTML代码前端解析JSON就会失败。所以代码里通过X-Requested-With请求头判断是不是Ajax是Ajax就写一段JSON返回401状态码。前端拿到401后统一弹窗提示“登录已过期请重新登录”然后跳转登录页这就顺了。注册拦截器在WebMvcConfig里完成Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns( /admin/login, /admin/doLogin, /css/**, /js/**, /images/**, /upload/**, /**/*.png, /**/*.jpg ); } }这套代码写完后整个后台系统就有了一扇“门”。你可以在后端代码里再写一个测试接口不登录访问指定页面就会发现请求被拦截重定向到了登录页。能在答辩现场演示这个过程比嘴上说“我做了登录校验”有力得多。6. 分页查询与PageHelper的使用分页是后台系统绕不开的需求。如果不分页商品5000条数据一次性渲染到表格上页面卡顿不说表格也不可读。课程设计里我推荐MyBatis分页插件PageHelper几行配置就搞定使用方式极简。dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependencyService层代码里只要在执行查询之前调用PageHelper.startPage(pageNum, pageSize)紧接着执行的这条SQL就会被自动加上LIMIT语句查询结果会被包装成PageInfo对象。PageInfo里包含了总条数、总页数、当前页码等所有分页需要的信息。public PageInfoProduct getProductPage(int pageNum, int pageSize, String keyword, Integer categoryId) { PageHelper.startPage(pageNum, pageSize); ListProduct productList productMapper.selectProductList(keyword, categoryId); return new PageInfo(productList); }Controller把PageInfo塞进Model前端拿pageInfo.list遍历数据拿pageInfo.pageNum到总页数循环生成页码链接。需要说明的一点是PageHelper分页的线程隔离原理它利用ThreadLocal把分页参数绑定到当前线程下一次查询时自动消费这些参数。如果你在startPage之后执行了多条查询SQL第二条SQL也会被分页导致结果错乱。所以startPage一定要紧挨着你要分页的那条SQL中间不要插入任何其他Mapper查询。这个坑我踩过一次排查了半小时最后发现是分页参数被无关查询消费了。7. 集成测试与真实开发中的问题排查7.1 商品图片上传失败的背后逻辑上传图片功能写好后我测试时发现一个问题明明上传成功的图片页面刷新后却显示不了。排查后发现原因是浏览器缓存图片路径没变浏览器直接用了缓存的旧图。解决办法是给图片路径加一个随机参数比如/upload/abc.jpg?v23123让浏览器认为这是一张新图。另一个更隐蔽的问题是文件重名覆盖。如果没有对上传文件名做处理两个人先后上传同名图片后上传的会把先上传的覆盖掉。解决方式是用UUID或者时间戳拼上原文件名。String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName System.currentTimeMillis() _ UUID.randomUUID().toString().replace(-, ) ext;这样生成的每个文件名都是唯一的不可能重复。图片传到磁盘后把newFileName拼到返回路径里存库。7.2 本地运行正常部署到别人电脑就报错代码在你自己电脑上运行得好好的换个环境或者打包给别人演示就报错。这个问题在课程设计答辩前特别常见原因基本都是硬编码。我把常见的几类硬编码场景列个表你自查一下硬编码位置典型问题正确做法application.yml里的数据库连接IP、端口、密码写死用环境变量或配置项外置图片上传路径写了D:/upload用System.getProperty(user.dir)动态拼接静态资源路径引用/localhost:8080开头用th:href{/css/style.css}相对路径SQL里的日期写死具体日期用NOW()函数或Java端动态传参本地和部署环境不一致的问题很多都是配置文件没抽离导致的。课程设计阶段不需要做多环境profile但只要养成“路径都用动态拼接、配置都放yml”的习惯项目换电脑也能一键跑起来。7.3 数据库连接报错的排查思路MySQL连接失败是最高频的问题。我遇到过的情况集中在这几种第一端口被占用或者MySQL服务没启动。Windows下打开任务管理器查看服务有没有在跑命令行用netstat -ano | findstr 3306看端口监听状态。第二用户名密码不对。同学之间互相拷贝application.yml账号密码也跟着复制过来自己的MySQL密码不一样连不上还一脸懵。改回自己的密码就好。第三MySQL 8.0的认证插件问题。MySQL 8默认用caching_sha2_password认证有些老版本的驱动不支持报错信息通常是Public Key Retrieval is not allowed。解决方式是在连接串末尾加allowPublicKeyRetrievaltrue或者把密码加密规则改成mysql_native_password。我更倾向加参数改认证规则会影响MySQL安全配置没必要。第四数据库没创建。连接串里写的mall这个库根本不存在MyBatis执行SQL时会报No tables used或者其他表不存在的错误。所以创建项目前先执行建库语句CREATE DATABASE mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;7.4 常见问题速查表报错信息可能原因解决方向Failed to configure a DataSource数据库连接配置缺失或错误检查yml和JDBC依赖java.sql.SQLSyntaxErrorExceptionSQL语法错误打开MyBatis日志看打印的SQL中文乱码直连URL没配characterEncoding连接串加useUnicodetruecharacterEncodingutf-8Thymeleaf模板解析失败页面找不到或语法错误检查templates目录和th:标签拼写端口被占用8080被其他进程占用改server.port或杀掉占用进程Invalid bound statementMapper接口和XML没绑定检查dao层扫描路径和XML里的namespace请求被拦截器404静态资源没放行核对排除路径配置PageHelper分页结果错乱startPage后执行了多条SQL让startPage紧贴目标查询这里再补充一个“解决思路型”的排查方法遇到报错先把控制台完整报错信息复制出来从最后一行往上找定位到你项目代码的那一帧排掉框架底层的干扰项问题基本就锁定在半屏以内。不要看见一个Exception就慌很多报错本质上是同一个问题引起的连锁反应。8. 演示准备与答辩解说词的核心节选项目做完只是第一步课程设计能不能拿高分演示环节占的权重非常高。很多同学项目本身做得挺好但演示的时候东点一下西点一下老师看得一头雾水分数自然上不去。我建议演示流程设计成一条主线从登录进入后台先看数据总览再依次走分类、商品、订单三个模块最后演示权限拦截和数据库设计。演示登录的时候可以提一句“密码加了盐做哈希没有明文存库”老师就知道你考虑过安全问题。进入商品列表页时先演示搜索功能再演示按分类筛选这就把动态SQL带出来了。重点演示一下分页把每页条数设置成5连续翻几页顺带点开一个商品看看编辑功能保存后再看列表刷新效果。订单模块的演示要突出状态流转。先展示待发货订单列表点发货按钮状态变已发货再次点击时按钮消失。整个过程让老师可以看到数据变化而不是停留在口述。展示数据库设计的时候不要直接在Navicat里把五张表全列出来挑重点讲。选中订单表和订单项表说清楚为什么冗余了商品快照字段选中商品表和分类表说清楚为什么外键关系要建在商品表这张“多”的一张表上。答辩时老师最爱问的问题我提前整理了一下为什么用MyBatis不用JPA答MyBatis手写SQL灵活可控多表联查和动态SQL是它的强项和SpringBoot整合简单。登录状态怎么维持的答Session存储登录标记拦截器统一校验Ajax请求返回401状态码处理。商品表为什么存category_id不存category_name答避免重复存储通过联合查询查出名称这是数据库范式化的基本要求。分页是怎么实现的答用了PageHelper插件核心原理是在SQL执行前动态拼接分页参数。这几个问题回答得清楚流畅项目基本就稳了。9. 按这个架子做出来的东西还能往哪扩如果课程设计完成后你还想继续优化或者想拿它做毕业设计的基础有几个扩展方向成本低、效果好。加一个商品批量上架下架前端用复选框选中多条数据后端接收ID数组一次性更新状态。这个功能展示了对批量操作的理解实现难度也不大。加一个登录验证码用Java生成图片验证码存Session登录时比对能大幅提升项目在老师眼里的完整度。接口层加一个AOP日志切面记录每个操作的管理员IP、操作内容、操作时间到日志表这就是一个小小的审计系统了。前端如果愿意折腾再把Thymeleaf模板一把梭换成Vue3加Element-Plus也是可能的但这是把整个项目的架构风格都要换掉的工作量没有一个月的时间不建议动。我个人的建议是课程设计阶段先把基础功能做扎实代码分层清晰、业务逻辑严谨、演示流程流畅这个项目就已经是优秀级别了。那些花里胡哨的进阶技术确实能加分但前提是你的基础盘不崩。很多同学死在半路不是因为功能太少而是因为想做的太多最后代码一坨乱麻答辩时讲都讲不清楚。10. 一点实操后的真心话最后说几句掏心窝的话。网上各种“毕业设计全套源码”下载下来解压即跑的情况很少。我看到过太多同学的“源码运行不起来”是因为改了一个类名没改XML里的resultMap或者少复制了一个配置文件到resources目录。这些框架层面层层嵌套的东西确实比传统的JSP项目要麻烦但这也是它的价值所在——你越是把这些东西跑通了你越理解一个Web应用的全貌。我最初跑这个项目的时候在Thymeleaf语法上卡了整整半天。th:each循环出来的变量死活取不到值调试发现是实体类属性和表字段大小写不一致MySQL字段叫update_timeJava属性我写成了updateTime加了下划线转驼峰配置之后就通了。还有一次数据库连接串少写了一个参数中文商品名全部变成问号差点以为要重做项目结果就是一行配置的事。所以这门课设计真正教你的不是SpringBoot怎么用而是面对一个不熟悉的系统时怎么一点点拆问题、定位问题、解决问题。项目跑起来只是及格线真正值钱的是你调试过程中长出来的经验——哪个配置最容易被忽略、哪个页面最容易出BUG、哪段逻辑解释起来最费劲。照着这个架子把项目搭起来之后你会对整个JavaWeb开发有一个很踏实的体感。以后再看到任何带SpringBoot字样的项目扫一眼就知道它的请求链路是怎么走的数据库大概长什么样哪些地方容易出问题。这个能力就是这门课真正想留给你的东西。