简介:这是一套面向计算机专业本科生的Java毕业设计实战资源,基于Spring Boot快速开发校园二手电动车交易平台,解决高校学生间闲置电动车高效流转与安全交易的实际需求。资源包含完整前后端源码、MySQL建库脚本、详细说明文档及毕业论文(LW),覆盖用户认证、商品发布与检索、订单全流程管理、后台统计与内容运维等核心模块,适合作为课程设计、毕设选题或Spring Boot全栈开发入门实践。压缩包共1177个文件,含45个Java后端控制器与实体类(如OrderController、UserControler)、516个前端交互JS脚本、98个HTML页面、92个PNG/JPG图片及72个CSS样式文件,辅以SQL建表语句与配置文件,总大小18.16MB。已有62人学习下载,开箱即用,部署后可直接运行,具备清晰的MVC分层结构与典型电商功能闭环,便于理解Spring Boot整合MyBatis、RESTful接口设计及前后端联调逻辑。
1. 项目概述与核心价值
最近在整理硬盘,翻出来一个几年前带学生做的毕业设计项目,一个基于SpringBoot的校园二手电动车交易系统。当时这个项目反响不错,好几个学生靠着它顺利拿到了offer。今天索性把它拿出来,结合这几年技术栈的演进和实际开发中的一些新坑,重新梳理一遍,把源码、设计思路和那些“教科书上不会写”的实操细节都分享出来。无论你是正在为Java毕业设计发愁的在校生,还是想快速上手一个完整SpringBoot项目的初级开发者,这篇文章都能给你提供一个清晰、可落地的“抄作业”模板。
这个系统的核心场景非常明确:解决大学校园内二手电动车交易信息不对称、交易流程不规范、缺乏信任保障的问题。想象一下,毕业生离校时,一辆八成新的电动车可能几百块就急着出手;而新生入学,又想买辆实惠的代步工具。传统的贴吧、QQ群交易,信息杂乱,真假难辨,还容易扯皮。我们这个系统要做的,就是搭建一个校内实名认证的垂直交易平台,让卖家能方便地发布车源,买家能高效地筛选、联系,并通过平台提供的沟通和交易见证(虽然不涉及直接支付,但可记录意向)来提升交易的安全感和效率。
技术选型上,我们采用了经典的SpringBoot + MySQL组合。SpringBoot的好处不用多说,约定大于配置,能让你快速跳过繁琐的XML和环境搭建,直击业务逻辑。MySQL作为成熟稳定的关系型数据库,应对毕业设计级别的数据量和并发绰绰有余。整个项目结构清晰,包含了用户管理、商品(电动车)发布与浏览、收藏、留言咨询、后台管理等核心模块,完全满足软件工程课程对系统完整性、规范性的要求。接下来,我会带你深入这个项目的“五脏六腑”,不仅告诉你怎么跑起来,更会解释为什么这么设计,以及在实际编码和部署中可能遇到哪些“坑”。
2. 系统整体架构与设计思路拆解
2.1 为什么是SpringBoot + MyBatis + MySQL?
很多新手会问,Java框架那么多,SSH、SSM、SpringBoot,到底选哪个?对于毕业设计或者快速原型开发,我的答案很明确:SpringBoot是首选。它本质上不是新技术,而是整合了Spring系列框架(Spring MVC, Spring Data JPA等)的最佳实践,并提供了自动化配置和起步依赖。这意味着你不需要再写一堆web.xml、spring-mvc.xml配置文件,也不用纠结各种jar包版本冲突。一个pom.xml文件,引入spring-boot-starter-web、spring-boot-starter-data-jpa(或mybatis-spring-boot-starter)等依赖,项目骨架就搭好了大半。
在这个项目中,我们选择了MyBatis作为持久层框架,而不是JPA。这里有个考量:对于业务逻辑相对复杂,需要高度优化SQL的场景,MyBatis的灵活性更大。比如,我们需要联表查询电动车信息、车主信息、以及交易状态,甚至后期可能要做复杂的数据统计。MyBatis允许你直接编写和优化SQL语句,对SQL能力还在成长期的学生来说,既能锻炼基本功,也更容易调试。当然,JPA的快速开发能力也很诱人,这取决于你的侧重点。我们的选择是基于“毕业设计应适当展示底层操作能力”的判断。
数据库选择MySQL 5.7或8.0。8.0版本性能更好,功能更全(如窗口函数),但部分学校机房或云服务器可能仍预装5.7。为了最大兼容性,源码中的SQL语法会尽量兼容5.7。设计数据库时,我们遵循了几个原则:
- 表名、字段名一律小写,用下划线分隔:例如
user_info,electric_vehicle。这是为了兼容不同操作系统(Linux大小写敏感)和养成良好的规范。 - 为每个表设置自增主键
id,并作为业务逻辑的唯一标识,避免使用学号、手机号等业务字段做主键。 - 建立合理的索引:在
user_id(外键)、vehicle_status(状态)、create_time(创建时间)等高频查询字段上建立普通索引,在title或description上考虑使用全文索引(如果搜索需求强)。这是毕业设计中容易忽略但能体现设计深度的点。 - 字段设计考虑扩展性:比如电动车表
electric_vehicle,除了品牌、型号、价格、图片等基础字段,我们还加了battery_health(电池健康度,百分比)、mileage(行驶里程)等二手车交易关键属性。
2.2 核心功能模块设计
系统主要分为前台用户系统和后台管理系统两大块。
前台用户系统面向学生用户,核心功能链是:
- 用户中心:注册、登录(含密码加密)、个人信息维护(实名认证关联学号,这里需注意隐私,通常模拟或脱敏处理)、头像上传。
- 商品(电动车)模块:这是核心。
- 发布:表单填写(品牌、型号、价格、图片上传、描述),发布后进入审核状态(后台管理员审核)。
- 浏览与搜索:列表分页展示,可按价格、发布时间、品牌等筛选。搜索框支持对标题和描述的关键词模糊查询。
- 详情查看:展示车辆所有信息、卖家信息(脱敏后)、以及留言咨询区。
- 互动模块:
- 收藏:用户可收藏心仪的电动车,在个人中心查看。
- 留言咨询:在商品详情页,买家可以给卖家留言提问,卖家可回复。这里设计成异步通信,不要求实时在线。
- 消息通知:当商品被收藏、收到新留言时,通过站内信(数据库存储)或模拟邮件(控制台打印日志)通知用户。这是一个加分项,展示了事件驱动的思想。
后台管理系统面向平台管理员(可能是勤工助学的学生),核心功能是:
- 内容审核:审核用户发布的电动车信息,防止虚假、违规信息。审核通过后才在前台显示。
- 用户管理:查看用户列表,禁用违规账号。
- 商品管理:可下架违规商品,查看所有交易信息。
- 数据统计:简单的仪表盘,展示每日新增用户数、商品数、热门品牌等。用ECharts实现一个简单的图表,能极大提升项目观感。
注意:关于“实名认证”与“支付”的毕业设计边界这是一个毕业设计,不是真正的商业系统。因此,“实名认证”通常模拟实现,比如在用户表增加一个
student_id字段,后台逻辑校验其格式(如学号规则),而非对接公安系统。支付功能更是强烈建议不要实现。涉及资金流、第三方SDK集成(微信支付、支付宝)、安全协议(HTTPS、签名),复杂度高且容易踩坑。我们的系统仅记录“交易意向”,买卖双方线下交易。这完全符合毕业设计的要求,且更安全可控。
2.3 技术栈深度解析与版本选择
- SpringBoot 2.x:我们选择2.7.x版本,这是一个长期支持(LTS)版本,稳定且资料丰富。避免使用最新的3.x大版本,因为其要求Java 17+,可能与环境不兼容。
- MyBatis 3:配合
mybatis-spring-boot-starter,简化整合。 - MySQL Driver:使用
mysql-connector-java8.x版本,连接URL中需要显式指定时区serverTimezone=Asia/Shanghai,这是新手常踩的坑。 - 前端模板引擎:选用Thymeleaf。相比于JSP,Thymeleaf是SpringBoot官方推荐的自然模板引擎,语法更现代,与Spring MVC集成无缝,支持HTML原型。对于毕业设计,它足够简单,能让你专注于后端逻辑,同时产出带动态数据的页面。
- 项目管理与构建工具:Maven。在
pom.xml中明确定义所有依赖版本,避免版本冲突。我会在源码中提供一份整理好的pom.xml。 - 版本控制:Git。源码包中虽然不包含.git目录,但强烈建议你从初始化项目开始就使用Git,这是程序员的基本素养。提交信息写清楚,这也是你项目文档的一部分。
3. 核心模块详细实现与代码解析
3.1 数据库设计与建表实战
我们先从根本的数据库开始。这里给出核心表结构,并解释设计理由。
1. 用户表 (user_info)
CREATE TABLE `user_info` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL UNIQUE COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `student_id` varchar(20) DEFAULT NULL COMMENT '学号(模拟实名)', `phone` varchar(15) DEFAULT NULL COMMENT '手机号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `role` tinyint(1) DEFAULT '0' COMMENT '角色:0-普通用户,1-管理员', `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_username` (`username`), KEY `idx_student_id` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';设计要点:
password字段长度设为255,为使用BCrypt等强哈希算法留足空间。role和status使用tinyint,比varchar存储效率更高。create_time和update_time是审计字段,便于追踪。ON UPDATE CURRENT_TIMESTAMP会自动更新修改时间。- 为
username和student_id建立索引,加速登录和查询。
2. 电动车信息表 (electric_vehicle)
CREATE TABLE `electric_vehicle` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '车辆ID', `user_id` int(11) NOT NULL COMMENT '发布者ID', `title` varchar(100) NOT NULL COMMENT '标题', `brand` varchar(50) DEFAULT NULL COMMENT '品牌', `model` varchar(50) DEFAULT NULL COMMENT '型号', `price` decimal(10,2) NOT NULL COMMENT '价格', `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价', `mileage` int(11) DEFAULT NULL COMMENT '行驶里程(公里)', `battery_health` int(3) DEFAULT NULL COMMENT '电池健康度(%)', `description` text COMMENT '详细描述', `main_image` varchar(255) NOT NULL COMMENT '主图URL', `image_urls` json DEFAULT NULL COMMENT '其他图片URL数组(JSON格式)', `status` tinyint(1) DEFAULT '0' COMMENT '状态:0-待审核,1-已发布,2-已售出,3-已下架', `view_count` int(11) DEFAULT '0' COMMENT '浏览数', `collect_count` int(11) DEFAULT '0' COMMENT '收藏数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`), KEY `idx_brand` (`brand`), KEY `idx_price` (`price`), FULLTEXT KEY `ft_idx_title_desc` (`title`,`description`) -- 全文索引,用于搜索 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电动车信息表';设计要点:
price使用decimal(10,2),精确存储金额,避免浮点数精度问题。image_urls字段使用JSON类型(MySQL 5.7.8+支持),方便存储图片数组。如果版本低,可用TEXT类型存储JSON字符串。status状态机设计清晰,定义了车辆的生命周期。- 建立了多种索引:外键索引(
user_id)、状态索引(status)、品牌价格等筛选索引(brand,price),以及全文索引(ft_idx_title_desc)用于关键词搜索。这是性能优化的关键。 view_count和collect_count是冗余字段,用于避免每次显示都要COUNT关联表,用空间换时间。
3. 收藏表 (favorite) 和 留言表 (message)这两个是典型的关联表。
-- 收藏表 CREATE TABLE `favorite` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `vehicle_id` int(11) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_vehicle` (`user_id`,`vehicle_id`), -- 防止重复收藏 KEY `idx_user_id` (`user_id`), KEY `idx_vehicle_id` (`vehicle_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表'; -- 留言咨询表 CREATE TABLE `message` ( `id` int(11) NOT NULL AUTO_INCREMENT, `vehicle_id` int(11) NOT NULL COMMENT '关联车辆', `from_user_id` int(11) NOT NULL COMMENT '留言者', `to_user_id` int(11) NOT NULL COMMENT '接收者(车主)', `content` text NOT NULL COMMENT '留言内容', `parent_id` int(11) DEFAULT '0' COMMENT '父留言ID,0表示顶层留言', `is_read` tinyint(1) DEFAULT '0' COMMENT '是否已读', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_vehicle_id` (`vehicle_id`), KEY `idx_to_user_id` (`to_user_id`), KEY `idx_parent_id` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='留言表';设计要点:
favorite表的唯一索引uk_user_vehicle确保了数据唯一性,是业务逻辑的保证。message表的parent_id实现了简单的留言回复功能,is_read用于消息状态跟踪。
3.2 后端核心逻辑实现:以商品发布与浏览为例
1. 实体类(Entity)映射使用MyBatis,首先创建与表对应的实体类。这里以ElectricVehicle为例,使用Lombok简化代码。
import lombok.Data; import java.math.BigDecimal; import java.util.Date; import java.util.List; @Data public class ElectricVehicle { private Integer id; private Integer userId; private String title; private String brand; private String model; private BigDecimal price; private BigDecimal originalPrice; private Integer mileage; private Integer batteryHealth; private String description; private String mainImage; private List<String> imageUrls; // 对应JSON字段,需自定义类型处理器 private Integer status; private Integer viewCount; private Integer collectCount; private Date createTime; private Date updateTime; // 非数据库字段,用于前端展示 private String userNickname; // 发布者昵称 private String userAvatar; // 发布者头像 }实操心得:关于
imageUrls的JSON处理MySQL中的JSON字段,在MyBatis中需要自定义类型处理器(TypeHandler)来与Java的List<String>互相转换。这是一个很好的深入理解MyBatis的机会。我们在源码中提供了一个JsonTypeHandler,你只需要在mybatis-config.xml中注册,或者在Mapper的@Result注解中指定即可。如果觉得麻烦,也可以将imageUrls定义为String类型,在Service层手动进行JSON的序列化和反序列化。
2. Mapper接口与XML映射MyBatis的Mapper接口定义数据操作方法,XML文件编写SQL。
// ElectricVehicleMapper.java @Mapper public interface ElectricVehicleMapper { int insert(ElectricVehicle vehicle); int updateById(ElectricVehicle vehicle); ElectricVehicle selectById(Integer id); // 条件分页查询:这是核心复杂查询 List<ElectricVehicle> selectByCondition(@Param("brand") String brand, @Param("minPrice") BigDecimal minPrice, @Param("maxPrice") BigDecimal maxPrice, @Param("status") Integer status, @Param("keyword") String keyword, @Param("orderBy") String orderBy); // 增加浏览数 int incrementViewCount(Integer id); }对应的ElectricVehicleMapper.xml中,selectByCondition的SQL片段展示了动态SQL和条件查询的强大:
<select id="selectByCondition" resultType="com.campus.ev.entity.ElectricVehicle"> SELECT v.*, u.nickname as userNickname, u.avatar as userAvatar FROM electric_vehicle v LEFT JOIN user_info u ON v.user_id = u.id WHERE v.status = 1 <!-- 默认只查已发布的 --> <if test="brand != null and brand != ''"> AND v.brand = #{brand} </if> <if test="minPrice != null"> AND v.price >= #{minPrice} </if> <if test="maxPrice != null"> AND v.price <= #{maxPrice} </if> <if test="keyword != null and keyword != ''"> AND (MATCH(v.title, v.description) AGAINST(#{keyword}) OR v.title LIKE CONCAT('%', #{keyword}, '%')) </if> ORDER BY <choose> <when test="orderBy == 'price_asc'">v.price ASC</when> <when test="orderBy == 'price_desc'">v.price DESC</when> <when test="orderBy == 'time_desc'">v.create_time DESC</when> <otherwise>v.create_time DESC</otherwise> <!-- 默认按时间倒序 --> </choose> LIMIT #{offset}, #{pageSize} <!-- 分页参数通过PageHelper拦截器自动计算 --> </select>设计要点:
- 使用
<if>标签实现动态SQL,避免拼接字符串带来的SQL注入风险。 - 联表查询(
LEFT JOIN)一次性获取发布者信息,减少数据库查询次数(N+1问题)。 - 支持全文索引(
MATCH...AGAINST)和模糊查询(LIKE)两种搜索方式,提升搜索体验。 - 使用
<choose>实现灵活排序。 - 分页使用PageHelper插件,在Service层只需调用
PageHelper.startPage(pageNum, pageSize),后续的查询就会自动分页。这是国内MyBatis生态中最流行的分页方案。
3. Service层业务逻辑Service层处理核心业务规则,如发布前的校验、状态变更等。
@Service public class ElectricVehicleService { @Autowired private ElectricVehicleMapper vehicleMapper; @Autowired private FileStorageService fileStorageService; // 文件上传服务 public Result publishVehicle(ElectricVehicle vehicle, MultipartFile mainImage, MultipartFile[] otherImages, Integer userId) { // 1. 基础校验 if (vehicle.getPrice() == null || vehicle.getPrice().compareTo(BigDecimal.ZERO) <= 0) { return Result.error("价格必须大于0"); } // ... 其他字段校验 // 2. 处理图片上传 try { String mainImageUrl = fileStorageService.store(mainImage); vehicle.setMainImage(mainImageUrl); List<String> otherImageUrls = new ArrayList<>(); for (MultipartFile file : otherImages) { if (!file.isEmpty()) { otherImageUrls.add(fileStorageService.store(file)); } } vehicle.setImageUrls(otherImageUrls); } catch (IOException e) { return Result.error("图片上传失败"); } // 3. 设置属性 vehicle.setUserId(userId); vehicle.setStatus(0); // 待审核状态 vehicle.setViewCount(0); vehicle.setCollectCount(0); // 4. 持久化 int rows = vehicleMapper.insert(vehicle); return rows > 0 ? Result.success("发布成功,等待审核") : Result.error("发布失败"); } public PageInfo<ElectricVehicle> getPublishedVehicles(int pageNum, int pageSize, String brand, BigDecimal minPrice, BigDecimal maxPrice, String keyword, String orderBy) { // 使用PageHelper分页 PageHelper.startPage(pageNum, pageSize); List<ElectricVehicle> list = vehicleMapper.selectByCondition(brand, minPrice, maxPrice, 1, keyword, orderBy); // status=1 已发布 return new PageInfo<>(list); } }4. Controller层API设计Controller接收HTTP请求,调用Service,返回JSON或视图。
@RestController @RequestMapping("/api/vehicle") public class ElectricVehicleController { @Autowired private ElectricVehicleService vehicleService; @PostMapping("/publish") public Result publish(@Valid @RequestBody VehiclePublishRequest request, @RequestParam("mainImage") MultipartFile mainImage, @RequestParam(value = "otherImages", required = false) MultipartFile[] otherImages, HttpSession session) { // 从session中获取当前用户ID (需登录拦截器) Integer userId = (Integer) session.getAttribute("userId"); if (userId == null) { return Result.error(401, "未登录"); } // 将request转换为Entity,调用Service return vehicleService.publishVehicle(convertToEntity(request), mainImage, otherImages, userId); } @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String brand, @RequestParam(required = false) BigDecimal minPrice, @RequestParam(required = false) BigDecimal maxPrice, @RequestParam(required = false) String keyword, @RequestParam(required = false) String orderBy) { PageInfo<ElectricVehicle> pageInfo = vehicleService.getPublishedVehicles(pageNum, pageSize, brand, minPrice, maxPrice, keyword, orderBy); return Result.success(pageInfo); } }注意事项:文件上传与存储毕业设计中,文件(图片)上传通常有两种方案:
- 本地存储:将图片保存在服务器本地目录(如
/upload),并在application.properties中配置静态资源映射(spring.mvc.static-path-pattern和spring.web.resources.static-locations)。简单,但部署时需注意目录权限和备份。- 对象存储(推荐):使用阿里云OSS、腾讯云COS等云服务。虽然需要注册和少量费用(通常有免费额度),但能让你学习如何集成第三方SDK,且解决了本地存储的移植性和扩展性问题。源码中会提供一个本地存储的示例,并注释说明如何替换为OSS。
3.3 前端页面与Thymeleaf集成
我们使用Thymeleaf渲染服务器端页面。关键在于将后端数据模型安全地传递到HTML中。
1. 商品列表页 (list.html) 关键片段
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>校园二手电动车市场</title> <!-- 引入Bootstrap等CSS/JS --> </head> <body> <!-- 搜索筛选区 --> <form th:action="@{/vehicle/list}" method="get" class="form-inline"> <input type="text" name="keyword" th:value="${param.keyword}" placeholder="搜索电动车..."> <select name="brand"> <option value="">所有品牌</option> <option th:each="brand : ${brandList}" th:value="${brand}" th:text="${brand}" th:selected="${param.brand == brand}"></option> </select> <!-- 价格区间、排序等 --> <button type="submit">搜索</button> </form> <!-- 商品列表 --> <div th:if="${pageInfo.list != null and !pageInfo.list.empty}"> <div class="row"> <div th:each="vehicle : ${pageInfo.list}" class="col-md-4"> <div class="card"> <img th:src="@{${vehicle.mainImage}}" class="card-img-top" alt="..."> <div class="card-body"> <h5 class="card-title" th:text="${vehicle.title}">标题</h5> <p class="card-text">价格:<span th:text="${#numbers.formatDecimal(vehicle.price, 1, 2)}">0</span>元</p> <p class="card-text">里程:<span th:text="${vehicle.mileage}">0</span>公里</p> <a th:href="@{/vehicle/detail/{id}(id=${vehicle.id})}" class="btn btn-primary">查看详情</a> <button th:onclick="collectVehicle([[${vehicle.id}]])" class="btn btn-outline-secondary">收藏</button> </div> </div> </div> </div> <!-- 分页 --> <nav> <ul class="pagination"> <li class="page-item" th:classappend="${pageInfo.isFirstPage} ? 'disabled' : ''"> <a class="page-link" th:href="@{/vehicle/list(pageNum=${pageInfo.prePage}, pageSize=${pageInfo.pageSize}, keyword=${param.keyword}, brand=${param.brand})}">上一页</a> </li> <li th:each="pageNum : ${pageInfo.navigatepageNums}" class="page-item" th:classappend="${pageNum == pageInfo.pageNum} ? 'active' : ''"> <a class="page-link" th:href="@{/vehicle/list(pageNum=${pageNum}, pageSize=${pageInfo.pageSize}, keyword=${param.keyword}, brand=${param.brand})}" th:text="${pageNum}">1</a> </li> <li class="page-item" th:classappend="${pageInfo.isLastPage} ? 'disabled' : ''"> <a class="page-link" th:href="@{/vehicle/list(pageNum=${pageInfo.nextPage}, pageSize=${pageInfo.pageSize}, keyword=${param.keyword}, brand=${param.brand})}">下一页</a> </li> </ul> </nav> </div> <div th:if="${pageInfo.list == null or pageInfo.list.empty}"> <p>暂无商品</p> </div> <script> function collectVehicle(vehicleId) { fetch('/api/favorite/add?vehicleId=' + vehicleId, {method: 'POST'}) .then(response => response.json()) .then(data => { alert(data.message); }); } </script> </body> </html>Thymeleaf要点:
th:each用于循环列表。th:text和th:value用于动态设置文本和值。th:href构建动态URL,支持路径参数(@{/detail/{id}(id=${id})})和查询参数。th:if用于条件渲染。#numbers.formatDecimal是Thymeleaf的工具方法,用于格式化数字。- 分页信息
pageInfo来自PageHelper返回的PageInfo对象,它包含了当前页、总页数、导航页码等所有分页数据。
2. 商品发布页与文件上传发布页表单需要设置enctype="multipart/form-data",Controller使用MultipartFile接收。
<form th:action="@{/vehicle/publish}" method="post" enctype="multipart/form-data"> <input type="text" name="title" required> <input type="number" step="0.01" name="price" required> <!-- 其他字段 --> <input type="file" name="mainImage" accept="image/*" required> <input type="file" name="otherImages" accept="image/*" multiple> <!-- multiple支持多选 --> <button type="submit">发布</button> </form>4. 项目配置、运行与部署详解
4.1 关键配置文件解析
application.properties/application.yml这是SpringBoot的核心配置文件。我们使用.yml格式,更简洁。
server: port: 8080 servlet: context-path: /ev # 项目上下文路径 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_ev?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password servlet: multipart: max-file-size: 10MB # 单个文件最大 max-request-size: 50MB # 总请求最大 thymeleaf: cache: false # 开发时关闭缓存,修改html即时生效 prefix: classpath:/templates/ suffix: .html mvc: static-path-pattern: /static/** # 静态资源访问路径 web: resources: static-locations: classpath:/static/, file:${upload.path} # 静态资源位置,包含上传目录 # MyBatis配置 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.campus.ev.entity configuration: map-underscore-to-camel-case: true # 自动将下划线字段映射为驼峰属性 # 日志配置 logging: level: com.campus.ev.mapper: debug # 打印SQL语句,调试用 # 自定义配置 upload: path: /tmp/ev-upload/ # 文件上传保存路径,Linux/Mac # Windows示例: D:/upload/ev/避坑指南:数据库时区与SSL
serverTimezone=Asia/Shanghai必须设置,否则插入时间字段可能出错。useSSL=false在本地开发或内网环境通常关闭,如果MySQL 8.0强制要求SSL,你需要配置证书或调整为useSSL=true&requireSSL=false。
4.2 项目启动与初始化
- 环境准备:确保本地安装JDK 8+、Maven 3.6+、MySQL 5.7+。使用IDE(IntelliJ IDEA或Eclipse STS)导入Maven项目。
- 数据库初始化:在MySQL中创建数据库
campus_ev,然后执行项目sql/目录下的建表脚本。 - 修改配置:将
application.yml中的数据库密码、上传路径等改为你自己的。 - 启动项目:找到主类
CampusEvApplication(通常有@SpringBootApplication注解),直接运行它的main方法。看到控制台输出Started ... in X seconds即成功。 - 访问系统:打开浏览器,访问
http://localhost:8080/ev(根据你设置的context-path)。
4.3 部署到云服务器(以Linux为例)
毕业设计答辩时,有一个在线的可访问系统会大大加分。部署流程如下:
- 打包:在项目根目录执行
mvn clean package -DskipTests,会在target目录生成一个campus-ev-0.0.1-SNAPSHOT.jar文件。 - 服务器准备:购买一台最低配置的云服务器(如1核1G),安装CentOS 7或Ubuntu 20.04。确保开放了8080端口(或你设置的端口)的安全组规则。
- 上传与运行:
# 1. 使用scp或FTP工具将jar包上传到服务器,例如/home目录 # 2. 登录服务器,安装Java环境(如果未安装) # CentOS: sudo yum install java-1.8.0-openjdk-devel # Ubuntu: sudo apt install openjdk-8-jdk # 3. 运行项目(后台运行) cd /home nohup java -jar campus-ev-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 & # --spring.profiles.active=prod 指定使用生产环境配置(需在application-prod.yml中配置生产数据库等) - 配置生产环境:创建
application-prod.yml,配置生产环境的数据库连接(通常是云数据库RDS的内网地址)、文件上传路径(如/data/upload)等。 - 使用Nginx反向代理(可选但推荐):让服务运行在80端口,并隐藏端口号。
# 在Nginx配置文件中添加 server { listen 80; server_name your-domain.com; # 你的域名或服务器IP location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 静态资源直接由Nginx处理,效率更高 location /static/ { alias /home/your-project/static/; expires 30d; } location /upload/ { alias /data/upload/; expires 30d; } }
5. 常见问题排查与进阶优化建议
5.1 开发与运行中的典型问题
数据库连接失败
- 现象:
Communications link failure或Access denied for user。 - 排查:
- 检查
application.yml中的url、username、password是否正确。 - 检查MySQL服务是否启动:
systemctl status mysqld。 - 检查用户是否有远程连接权限(如果数据库不在本地):
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'password' WITH GRANT OPTION; FLUSH PRIVILEGES;(生产环境慎用)。 - 检查服务器防火墙或云服务商安全组是否开放了3306端口。
- 检查
- 现象:
MyBatis映射错误或查询不到数据
- 现象:返回
null或报BindingException。 - 排查:
- 开启MyBatis SQL日志:在
application.yml设置logging.level.com.yourpackage.mapper=DEBUG,查看控制台打印的SQL语句是否正确。 - 检查实体类字段名与数据库列名是否对应(注意驼峰和下划线转换)。
- 检查Mapper XML文件中的
resultMap或resultType是否正确。 - 检查传入查询的参数是否为
null,导致动态SQL拼接异常。
- 开启MyBatis SQL日志:在
- 现象:返回
文件上传失败或图片不显示
- 现象:上传后找不到文件,或前端图片
404。 - 排查:
- 检查上传目录是否存在且有读写权限:
mkdir -p /tmp/ev-upload && chmod 755 /tmp/ev-upload。 - 检查
application.yml中spring.servlet.multipart.max-file-size是否设置过小。 - 检查静态资源映射:确保
spring.web.resources.static-locations包含了上传目录的file:路径。访问http://localhost:8080/ev/upload/xxx.jpg看是否能直接访问。 - 前端图片
src路径是否正确,是否拼接了完整的URL或正确的上下文路径。
- 检查上传目录是否存在且有读写权限:
- 现象:上传后找不到文件,或前端图片
PageHelper分页不生效
- 现象:查询返回所有数据,没有分页。
- 排查:
- 确保在调用Mapper查询方法之前执行
PageHelper.startPage(pageNum, pageSize)。 - 确保PageHelper的依赖已正确引入(
com.github.pagehelper:pagehelper-spring-boot-starter)。 - 确保查询语句只有一条,且后面没有跟其他查询(PageHelper对紧随其后的第一条查询生效)。
- 确保在调用Mapper查询方法之前执行
5.2 项目进阶优化与扩展方向
如果你想让项目在答辩时更出彩,可以考虑以下扩展点:
- 引入Redis缓存:缓存首页热门商品列表、品牌列表等不常变化的数据,大幅减轻数据库压力。使用Spring Boot的
spring-boot-starter-data-redis可以轻松集成。 - 实现简单的站内信/通知系统:当商品被收藏、收到留言时,向卖家发送通知。可以用数据库表存储消息,用户登录后轮询或使用WebSocket实现实时推送(更高阶)。
- 增加后台数据统计图表:使用ECharts或AntV G2,在后台展示用户增长趋势、商品分类统计等。这需要你写一些统计SQL,如
GROUP BY DATE(create_time)。 - 实现简单的权限控制:使用Spring Security或Shiro,更精细地控制后台管理页面的访问权限(例如,只有
role=1的管理员才能访问/admin/**路径)。 - 接口文档化:使用Swagger或Knife4j自动生成API文档。这对于展示项目的规范性很有帮助。只需引入依赖并添加一个配置类即可。
- 容器化部署:编写
Dockerfile,将项目打包成Docker镜像。这能让你在简历上写上“有Docker实践经验”,是一个很大的亮点。 - 前端与后端分离(可选):如果你对Vue或React有兴趣,可以将Thymeleaf模板替换为纯后端API,前端单独做一个项目。这更符合现代Web开发趋势,但工作量也会增加。
这个校园二手电动车交易系统项目,麻雀虽小,五脏俱全。它覆盖了SpringBoot Web开发的绝大部分核心知识点:MVC架构、数据库操作、事务管理、文件上传、分页、搜索、简单的权限控制等。通过从头到尾实现它,你不仅能完成一份高质量的毕业设计,更能系统地掌握一个Java Web服务从设计、开发到部署上线的完整流程。源码包里的说明文档会提供更详细的模块说明和数据库字典,希望能帮你少走弯路。如果在实现过程中遇到问题,多看看日志,善用搜索引擎,大部分坑前人都踩过。祝你编码顺利,答辩成功!
本文还有配套的精品资源,点击获取