1. 项目概述与技术选型思考
1.1 这个项目到底解决什么问题
先说说我接触这类项目的感受。闲置服装交易,听起来就是个普通的二手商城,但真正动手做的时候你会发现,它和一般电商系统有个明显的区别:服装类目存在尺码、新旧程度、穿着场景这些维度,这意味着商品模型不能简单套用一个通用的“商品表”完事。同时,二手交易还牵扯买卖双方之间的沟通、交易状态的流转、甚至闲置物品的再循环价值展示,这些业务细节都是毕业设计能否拿高分的关键。
这套基于Spring Boot的共享汇闲置服装交易网站,核心就是围绕“用户注册登录、服装商品发布与管理、浏览搜索、下单交易、订单状态管理”这条主线来做的。它的定位非常清晰:面向高校学生或初级开发者,提供一个可以直接运行、可以二次开发、能在答辩时讲清楚业务逻辑的完整项目。源码编号05490,在各类毕设源码站点里属于比较典型的全栈单体应用(后端Spring Boot + 前端模板引擎或前后端分离均可跑通)。
从我个人的经验看,这类项目的价值不仅仅在于“能跑起来”,更在于它覆盖了Java Web开发的主干知识:Spring Boot自动装配、MyBatis映射、关系型数据库表设计、RESTful接口风格、文件上传与静态资源映射、Spring Security或拦截器做登录校验等。你把这一套吃透,不只是完成一个毕设,而是把Java后端开发的骨架都摸了一遍。
1.2 技术栈选型背后的为什么
看这个项目的技术组合,核心是Spring Boot + MyBatis(或MyBatis Plus)+ MySQL + 前端模板/Vue。我拆开说。
Spring Boot,选它不是因为“大家都用”,而是因为它在解决“配置地狱”这件事上确实做到了极致。毕业设计周期短,如果你用原生Spring加一堆XML配置,光环境搭建就能耗掉一两周。Spring Boot的自动配置机制,让数据源、事务管理器、Web容器这些基础组件“开箱即用”,你只需要关注业务代码。我用一个类比:Spring Boot就像一套精装修的出租房,你拎包入住;传统Spring则是毛坯房,水电改造都得自己来。
持久层用MyBatis而不是JPA/Hibernate,原因也很实际。毕设项目中,SQL和Java代码的可读性、可控性太重要了。MyBatis让你写SQL时心里有底,特别是像“按条件动态查询服装商品”(比如价格区间、尺码、新旧程度)这种多条件组合的场景,用动态SQL非常直观。Hibernate虽然自动建表方便,但一旦涉及复杂联表查询,调试起来会让你怀疑人生。
前端这块,原始的毕设项目很多用的是Thymeleaf服务端渲染,也有的是Spring Boot + Vue前后端分离。就这个项目来说,我更愿意推荐前后端分离的版本,因为它能体现你懂“接口设计”和“跨域处理”,这两点在答辩时加分很多。但如果你是前端零基础,用Thymeleaf模板引擎也完全够用,不用背Vue那套生命周期和组件通信。
1.3 从标题看项目的隐藏技术点
细看标题“springboot共享汇闲置服装交易网站”,里面其实藏了好几个容易被忽略但面试或答辩时可能会被追问的点:
- “共享汇”三个字暗示了平台属性,通常意味着平台不直接卖货,而是提供一个撮合交易的场所,这在权限设计上要区分普通用户和管理员。
- **“闲置服装”**是垂直电商场景,商品属性需要包含服装特有的维度:尺码、新旧程度、适合季节、是否可议价等,这些会成为数据库设计细节的一部分。
- **“交易网站”**说明一定有交易链路,哪怕是最简化的“下单-发货-确认收货”,这涉及订单状态机的设计,是业务逻辑里的重点也是难点。
- **“毕业设计源码”**意味着这个项目需要有演示友好性:测试数据要齐全、默认账号要能登录、启动步骤不能太复杂,否则评审老师打开一看是空的,印象分会大打折扣。
把一个标题拆到这种程度,你会发现做项目就有了方向。接下来的内容,我按自己动手实现这类项目的真实顺序,把核心环节过一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块与数据库设计
2.1 功能模块地图:先画清楚再动手
我习惯做项目之前先梳理模块边界。这个“共享汇闲置服装交易网站”从功能上可以切成这么几块:
- 用户模块:注册、登录、个人资料维护、头像上传、密码加密存储。管理员账号则多一个后台入口。
- 商品模块:发布闲置服装(标题、描述、图片、尺码、新旧程度、原价、转让价、交易方式)、商品上下架、商品列表展示、商品详情页。
- 搜索与筛选模块:关键词搜索、按分类/价格区间/新旧程度筛选、热门推荐。
- 交易模块:加入购物车或直接下单、订单生成、订单状态管理(待付款、已付款、已发货、已完成、已取消)、买卖双方的订单列表。
- 留言/评论模块:买家对商品的咨询,卖家回复,这是二手平台建立信任的关键功能。
- 管理后台模块:用户管理(禁用/启用)、商品审核(下架违规商品)、数据统计(商品数量、订单量、用户量)。
这些模块看起来多,但落到代码上,本质就是“增删改查 + 状态流转 + 权限控制”。真正花时间的,是那几个状态流转和关联查询。
2.2 数据库表结构设计:服装字段的特殊处理
数据库表设计是整个项目的地基。我见过太多毕设项目,表建得随意,后面写SQL写到哭。这里直接给出一套可以参考的表结构方案,字段名称可以直接抄:
用户表(user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键自增 |
| username | varchar(50) | 用户名,唯一 |
| password | varchar(100) | BCrypt加密后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像URL |
| phone | varchar(20) | 手机号 |
| role | tinyint | 0-普通用户 1-管理员 |
| status | tinyint | 0-禁用 1-正常 |
| create_time | datetime | 注册时间 |
服装商品表(clothing)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 发布者ID |
| title | varchar(100) | 商品标题 |
| description | text | 商品描述 |
| category | varchar(50) | 分类:上衣/裤装/裙装/外套/鞋靴/配饰 |
| size | varchar(20) | 尺码:S/M/L/XL/XXL |
| condition_level | tinyint | 新旧程度:1-全新 2-九成新 3-七成新 4-五成新及以下 |
| original_price | decimal(10,2) | 原价 |
| price | decimal(10,2) | 转让价 |
| is_bargain | tinyint | 是否可议价 |
| images | varchar(1000) | 图片URL,多张用逗号分隔 |
| status | tinyint | 0-待审核 1-上架中 2-已下架 3-已售出 |
| view_count | int | 浏览量 |
| create_time | datetime | 发布时间 |
这里特别注意两个设计细节。第一,images字段用逗号分隔存储多张图片URL,虽然这不符合数据库第三范式,但在这种轻量级项目里能极大简化查询逻辑——你不需要单独建一张图片表,查询详情时直接按逗号split再拼接完整URL即可。第二,condition_level用数字不用文字,方便后续做条件筛选和排序,比如“只看九成新以上”就是一个condition_level <= 2的查询条件,效率高且语义清晰。
订单表(orders)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单编号,唯一 |
| clothing_id | bigint | 商品ID |
| seller_id | bigint | 卖家ID |
| buyer_id | bigint | 买家ID |
| amount | decimal(10,2) | 成交金额 |
| status | tinyint | 0-待付款 1-待发货 2-待收货 3-已完成 4-已取消 |
| remark | varchar(255) | 买家备注 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 付款时间 |
| finish_time | datetime | 完成时间 |
留言表(message) 和 收藏表(favorite) 结构就比较简单,核心就是“谁在什么时间对哪个商品说了什么/收藏了什么”,这里不展开。
2.3 订单状态机的设计:面试官最爱问的点
订单状态流转是交易类项目的灵魂。我在简历上写项目经验时,几乎每次都会被问到“订单状态是怎么管理的”。这一块的回答质量,直接体现你有没有真正思考过业务逻辑。
这个项目的状态流转可以设计成这样:
text复制待付款(0) -> 待发货(1) -> 待收货(2) -> 已完成(3)
| |
| +----> 已取消(4)
+-----------> 已取消(4)(超时未付款自动取消,或用户主动取消)
注意几个边界:
- 待付款状态下单时生成,此时库存(如果有库存概念)需要预占。这个项目里没有库存概念,但你可以加一个
clothing.status = 3(已售出)来防止一件衣服被两个人同时下单。 - 事务问题:生成订单时,要同步把商品状态改为“已售出”,这两个操作必须在一个事务里。否则会出现订单生成了但商品还能被别人买走的情况。
- 状态流转校验:每个状态变更都要校验“当前状态是否允许该操作”。比如买家不能在“待发货”状态直接点“确认收货”,必须卖家先发货。这些校验你写在Service层,用if判断即可。
2.4 数据库设计避坑经验
毕设项目里最常见的表设计问题有三个,我必须拿出来说一下。
第一个是表名和字段名用了MySQL保留字,比如order、user、desc,导致SQL语句写SELECT * FROM order直接报语法错误。解决办法很简单:表名前加前缀,比如t_user、t_order,或者在SQL语句里用反引号包裹,但我强烈建议用前缀方案,干净省心。
第二个是时间字段类型不统一。有的表用datetime,有的用timestamp,格式化时各种不舒服。建议统一用datetime,Java实体里对应LocalDateTime,配合@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")直接输出格式化后的时间。
第三个是没建索引。项目初期数据量小感受不到,但答辩时老师可能会问“订单查询为什么快”,你得能回答出buyer_id和seller_id上建了索引,order_no建了唯一索引。建索引的SQL很简单:
sql复制ALTER TABLE t_order ADD INDEX idx_buyer_id (buyer_id);
ALTER TABLE t_order ADD UNIQUE KEY uk_order_no (order_no);
3. 后端核心实现与业务逻辑拆解
3.1 项目初始化与依赖配置
拿到这套源码,第一步不是急着写代码,而是先把环境跑通。我建议用Spring Initializr重新生成一个基础工程,再把源码里的业务代码迁移过来,这样能避免老项目自带的依赖冲突问题。
核心依赖清单,直接在pom.xml里配置:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
这里我要多说一句MyBatis Plus的选择。很多原始的毕设源码用的是MyBatis原生版本,但我在实际改造中更倾向换成MyBatis Plus,因为它内置了BaseMapper,单表CRUD连SQL都不用写,对于毕设项目来说能省下大量时间。而且它的分页插件PaginationInnerInterceptor用起来非常顺手,一个配置类就搞定:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
配置文件的要点是:URL里加上useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8,不然高版本MySQL连接时会报SSL警告或者时区错误,新手在这里卡住的不在少数。
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/clothing_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
3.2 登录认证模块:从Session到JWT
登录认证是几乎所有系统的第一道关卡。原始的毕设项目里,很多用的是Session + 拦截器的方式,简单,但没有前后端分离的“接口感”。我建议用**JWT(JSON Web Token)**做改造,这也是现在企业里面试时会问到的东西。
JWT的思路特别适合用来给不懂的人解释“无状态认证”:服务器不再保存用户的登录状态,而是把用户信息签名后发给客户端,客户端每次请求时把令牌带回来,服务器验签通过就信任它。就像你去健身房办了张卡,卡上写着你名字并盖了章,你每次进门出示卡片,前台核对印章无误就放行,不用每次都查会员档案。
在Spring Boot里落地,主要做三件事:
第一,写一个JWT工具类,负责生成和解析令牌,密钥放配置文件里:
java复制@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private long expire;
public String generateToken(Long userId, String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expire * 1000);
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.get("userId", Long.class);
}
}
第二,写一个拦截器(HandlerInterceptor),在preHandle里校验请求头中的Token:
java复制@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtils jwtUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
Long userId = jwtUtils.getUserIdFromToken(token);
request.setAttribute("userId", userId);
return true;
} catch (Exception e) {
// token无效
}
}
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}");
return false;
}
}
第三,注册拦截器并配置放行路径(登录注册接口、商品列表、商品详情等不需要登录就能访问):
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login", "/api/auth/register",
"/api/clothing/list", "/api/clothing/detail/**");
}
}
关于密码存储,务必使用BCrypt加密,别用MD5。MD5是摘要算法,不是加密算法,撞库攻击分分钟破解。Spring Security里自带的BCryptPasswordEncoder可以直接用,哪怕你没有引入完整的Spring Security,单独用这个类也是可以的。
3.3 服装商品模块:发布、列表、搜索
服装商品管理是这个项目的核心,我把代码逻辑拆成四个部分。
发布商品。接口接收一个ClothingVO对象,里面包含标题、描述、分类、尺码、新旧程度、原价、转让价等字段。后端要做五件事:
- 参数校验(
@Validated+ 注解,比如@NotBlank、@DecimalMin) - 从Token中获取当前登录用户的ID(之前拦截器已经放到了request attribute里)
- 图片上传处理(后面单独讲)
- 组装实体,
status初始化为1(直接上架,不做审核流程,简化毕设逻辑) - 写入数据库
商品列表。这里用MyBatis Plus的分页查询非常舒服:
java复制@Override
public Page<Clothing> getClothingList(ClothingQueryDTO dto) {
LambdaQueryWrapper<Clothing> wrapper = new LambdaQueryWrapper<>();
// 关键词搜索
if (StrUtil.isNotBlank(dto.getKeyword())) {
wrapper.like(Clothing::getTitle, dto.getKeyword())
.or().like(Clothing::getDescription, dto.getKeyword());
}
// 分类筛选
if (StrUtil.isNotBlank(dto.getCategory())) {
wrapper.eq(Clothing::getCategory, dto.getCategory());
}
// 价格区间筛选
if (dto.getMinPrice() != null) {
wrapper.ge(Clothing::getPrice, dto.getMinPrice());
}
if (dto.getMaxPrice() != null) {
wrapper.le(Clothing::getPrice, dto.getMaxPrice());
}
// 新旧程度筛选
if (dto.getConditionLevel() != null) {
wrapper.le(Clothing::getConditionLevel, dto.getConditionLevel());
}
// 只查上架中的商品
wrapper.eq(Clothing::getStatus, 1);
// 按发布时间倒序
wrapper.orderByDesc(Clothing::getCreateTime);
// 浏览量加一(异步可以优化,但毕设直接同步处理也OK)
return clothingMapper.selectPage(new Page<>(dto.getPageNum(), dto.getPageSize()), wrapper);
}
这段代码把“多条件动态查询”这个考点完美覆盖了。LambdaQueryWrapper避免了手写字符串列名容易出错的问题,同时也比XML里写一堆<if>标签要简洁得多。答辩时你能把这条链路上的方法讲清楚,老师基本不会再为难你。
商品详情。除了基础信息,详情页还需要展示卖家的信用信息(比如注册时间、发布的商品数量)和该商品下的所有留言。这里用一次关联查询把商品信息和卖家信息带出来:
java复制public ClothingDetailVO getClothingDetail(Long id) {
Clothing clothing = clothingMapper.selectById(id);
// 浏览量+1
clothing.setViewCount(clothing.getViewCount() + 1);
clothingMapper.updateById(clothing);
// 查卖家信息
User seller = userMapper.selectById(clothing.getUserId());
// 查留言列表
List<Message> messages = messageMapper.selectList(
new LambdaQueryWrapper<Message>()
.eq(Message::getClothingId, id)
.orderByDesc(Message::getCreateTime)
);
// 组装VO返回
}
我踩过的坑:浏览量+1如果直接写在selectById后面,每次刷新页面都会重复累加,这在测试时问题不大,但答辩时老师可能会问“你怎么防止刷浏览量”。优化方案是有的,比如用Redis的INCR命令,或者同一个用户一小时只算一次,但这些对于毕设来说属于“加分项”,你有余力再实现。
3.4 图片上传与静态资源映射
服装交易平台,图片展示是否美观直接影响使用体验。图片上传这块涉及两个知识点:文件存储 和 静态资源映射。
最简单也适合毕设的方案,是上传到本机的指定目录,然后通过Spring Boot的静态资源映射对外提供访问。具体做法:
第一步,配置上传路径,在application.yml里定义:
yaml复制file:
upload-path: /data/clothing-images/
第二步,写文件上传接口。我建议图片做压缩处理,因为手机拍照动不动就是好几MB,直接展示会让页面加载很慢。用Thumbnails库可以一行代码搞定:
java复制@PostMapping("/api/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
// 校验文件类型
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
if (!Arrays.asList(".jpg", ".jpeg", ".png", ".gif").contains(suffix.toLowerCase())) {
return Result.error("仅支持图片文件");
}
// 生成唯一文件名,防止重名覆盖
String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix;
File targetFile = new File(uploadPath + newFileName);
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
file.transferTo(targetFile);
// 返回可访问的URL
return Result.success("/images/" + newFileName);
}
第三步,配置静态资源映射,让URL以/images/开头的请求去找本地目录:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceHandler("file:" + uploadPath);
}
这里有一个非常重要的排查点:很多人图片上传成功但页面访问404,多半是file:协议写错了,或者上传路径的末尾没有加/。路径拼接问题真的很折磨人,建议统一用Paths.get()来拼接,不要手工拼字符串。
更省事的方式是配置一个虚拟路径映射,让项目直接把上传目录映射成一个URL路径:
yaml复制spring:
mvc:
static-path-pattern: /images/**
web:
resources:
static-locations: file:D:/clothing-images/,classpath:/static/
这种方式把/images/xxx.jpg直接映射到本地磁盘目录,思路简单,问题也少。
3.5 购物车与订单模块实现
购物车我建议做简化处理,用数据库表存储用户的购物车记录,表结构极简:id、user_id、clothing_id、create_time。因为每件闲置服装只有一件,不存在“数量”的概念,所以购物车表连数量字段都不用加。这是闲置交易场景和普通电商在业务模型上一个很重要的差异。
订单流程的代码实现,是后端逻辑里最需要谨慎对待的部分。我给出核心的下单方法,注意看事务注解:
java复制@Override
@Transactional(rollbackFor = Exception.class)
public OrderVO createOrder(Long buyerId, Long clothingId, String remark) {
// 1. 查询商品
Clothing clothing = clothingMapper.selectById(clothingId);
if (clothing == null) {
throw new BusinessException("商品不存在");
}
if (clothing.getStatus() != 1) {
throw new BusinessException("商品已下架或已售出");
}
// 2. 不能买自己的商品
if (clothing.getUserId().equals(buyerId)) {
throw new BusinessException("不能购买自己发布的商品");
}
// 3. 生成订单号,时间戳 + 随机数
String orderNo = "CL" + System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000));
// 4. 创建订单
Order order = new Order();
order.setOrderNo(orderNo);
order.setClothingId(clothingId);
order.setSellerId(clothing.getUserId());
order.setBuyerId(buyerId);
order.setAmount(clothing.getPrice());
order.setStatus(0);
order.setRemark(remark);
orderMapper.insert(order);
// 5. 商品状态改为已售出,防止重复下单
clothing.setStatus(3);
clothingMapper.updateById(clothing);
// 6. 将商品从其他用户的购物车中移除
favoriteMapper.delete(new LambdaQueryWrapper<Favorite>()
.eq(Favorite::getClothingId, clothingId));
return orderVO;
}
@Transactional注解保证了第4步和第5步的原子性——如果订单插入成功但商品状态更新失败,整个事务会回滚,不会产生脏数据。这是分布式事务的入门思想,虽然这里没有用上消息队列,但理解“事务边界”这个概念对后续学习非常有帮助。
订单状态的流转,每个方法里都要加状态校验。比如订单确认收货:
java复制@Override
@Transactional(rollbackFor = Exception.class)
public void confirmOrder(Long userId, Long orderId) {
Order order = orderMapper.selectById(orderId);
if (order == null) throw new BusinessException("订单不存在");
if (!order.getBuyerId().equals(userId)) throw new BusinessException("无权操作该订单");
if (order.getStatus() != 2) throw new BusinessException("当前订单状态无法确认收货");
order.setStatus(3);
order.setFinishTime(LocalDateTime.now());
orderMapper.updateById(order);
}
严格校验每一步的状态,是交易类项目防止“越权操作”和“状态错乱”的关键。
4. 前端交互与接口联调
4.1 前端方案的两种路线
这个项目的前端有两种实现路线,我分别说下选择的理由。
路线一:Thymeleaf服务端渲染
这是很多毕设源码自带的方案。后端接口直接返回ModelAndView,模板里嵌th:each遍历商品列表,表单提交用th:action指定POST地址。优点是结构简单,不需要额外启动前端项目,部署就是一个Spring Boot jar包;缺点是前后端耦合度高,接口复用性差,页面和Java代码的界限模糊。
路线二:Vue + Axios前后端分离
我更推荐这种路线,因为当前主流的Java Web开发趋势就是前后端分离。前端用Vue 2或Vue 3(配合Element UI做后台),通过Axios调用后端接口。后端只需要提供Restful API,前端通过Vue Router管理页面路由,通过Vuex或Pinia管理登录状态。
前端项目的核心是接口封装,把后端那些接口统一管起来:
text复制POST /api/auth/login 登录
POST /api/auth/register 注册
GET /api/clothing/list 商品分页列表
GET /api/clothing/detail/{id} 商品详情
POST /api/clothing 发布商品
PUT /api/clothing/{id} 修改商品
DELETE /api/clothing/{id} 删除商品
POST /api/order 创建订单
GET /api/order/my 我的订单列表
POST /api/order/{id}/cancel 取消订单
POST /api/order/{id}/confirm 确认收货
前后端分离后,跨域问题是必须处理的。在Spring Boot后端写一个CORS配置类,一劳永逸:
java复制@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:5173"); // 前端地址
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
4.2 接口联调的实操经验
联调阶段最折磨人的是接口文档不一致。我建议哪怕只有一个人开发,也把接口文档用Apifox或Postman维护起来,哪怕只是简单记录URL、请求参数、返回结构,都能让你在改代码时少掉很多头发。
这里分享一个我在联调时常用的返回结构统一封装:
java复制@Data
public class Result<T> {
private Integer code; // 200成功,4xx客户端错误,5xx服务端错误
private String message; // 提示信息
private T data; // 业务数据
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
前端统一拦截Axios响应,如果code != 200就弹出错误提示。这种做法让前后端沟通时“字段名对得上”成为第一优先级,彻底避免后端改字段、前端不知情的情况。
4.3 页面交互逻辑中的细节
服装交易网站有几个页面的交互逻辑值得拿出来讲。
首页的商品卡片,图片懒加载是个体验优化点。Vue指令v-lazy即可实现,当用户滚动到图片位置时才加载,避免首次加载太多图片导致白屏。
商品详情页的“立即购买”按钮,点击后应弹窗确认订单信息,展示商品缩略图、价格、卖家昵称,让用户确认后提交。这里的核心是确认订单信息不可被前端篡改,后端在创建订单时必须重新查库取价格,而不是信任前端传过来的amount字段。我在代码审查时见过有同学把价格直接放在请求体里,这是严重的安全漏洞。
订单列表页要区分“我买到的”和“我卖出的”两个Tab,后端对应两个不同的查询接口(按buyer_id和按seller_id),前端各自展示,状态用标签组件展示不同颜色。
5. 部署上线与环境搭建
5.1 从源码到本地运行:5步走
拿到源码后,正确的启动顺序应该是:
- 创建数据库,导入
clothing_db.sql脚本(源码里通常附带) - 修改
application.yml里的数据库用户名和密码 - 启动Redis(如果项目用到Redis做缓存,没有用到就跳过)
- Maven 打包或直接用IDE启动
Application主类 - 浏览器访问
http://localhost:8080
我遇到过太多同学卡在第4步,报各种乱七八糟的错,绝大多数原因就三个:JDK版本不匹配、Maven依赖下载失败、端口占用。
JDK版本问题,现在源码普遍要求JDK 8或JDK 11,如果你装了JDK 17,报错会显示“Unsupported class file major version 61”之类的。先确认自己的JDK版本,用java -version查看。如果源码是基于JDK 8的但你在JDK 17上跑,最快的方式是装一个JDK 8并切换环境变量。
Maven依赖下载失败,先试试将镜像源换成阿里云:
xml复制<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven mirror</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
端口占用,用netstat -ano | findstr 8080(Windows)或lsof -i:8080(macOS/Linux)找到占用进程,杀掉即可,或者直接在配置文件改端口server.port=8081。
5.2 打包部署:从本地到服务器
想把这个毕设项目部署到云服务器上给评审老师演示,有两种方案。
方案一:传统Jar包部署
bash复制mvn clean package -DskipTests
java -jar target/clothing-exchange-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
服务器上需要提前装好JDK和MySQL,然后通过nohup让程序后台运行:
bash复制nohup java -jar clothing-exchange.jar > app.log 2>&1 &
方案二:Docker部署
服务器装了Docker之后,一键部署的体验会好很多。写一个简单的Dockerfile:
dockerfile复制FROM openjdk:8-jre-alpine
WORKDIR /app
COPY target/clothing-exchange-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
构建并运行:
bash复制docker build -t clothing-exchange .
docker run -d -p 8080:8080 --name clothing-exchange clothing-exchange
Docker部署有个好处,就是本地开发环境和服务器环境完全一致,不会出现“我本地能跑你服务器不行”的尴尬。
5.3 图片目录的部署注意事项
部署到服务器后,图片上传路径需要特别注意。如果你在配置里写的是本机绝对路径(比如D:/clothing-images/),部署到Linux服务器上就会报“目录不存在”。经验之谈:用相对路径或启动参数动态指定。
最稳妥的做法,是在启动命令里通过--file.upload-path覆盖配置:
bash复制java -jar clothing-exchange.jar --file.upload-path=/var/clothing-images/
同时确保目录存在并有写权限:
bash复制mkdir -p /var/clothing-images
chmod 755 /var/clothing-images
6. 常见问题与排查技巧实录
6.1 启动报错问题速查
我把这类毕设项目里最高频的启动报错整理成一张速查表,都是我在带项目时遇到过的真实问题。
| 报错信息 | 原因分析 | 解决办法 |
|---|---|---|
Access denied for user 'root'@'localhost' |
数据库密码错误或用户无权限 | 检查application.yml中的用户名密码,或用root账号登录MySQL执行授权 |
Unknown database 'clothing_db' |
数据库还没创建 | 先执行CREATE DATABASE clothing_db DEFAULT CHARACTER SET utf8mb4 |
Port 8080 was already in use |
端口被占用 | 找到占用进程并结束,或修改server.port |
Failed to configure a DataSource |
数据源配置缺失 | 确认引入了mysql驱动依赖,并且application.yml中配置正确 |
java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed |
MySQL 8连接策略问题 | JDBC URL加allowPublicKeyRetrieval=true&useSSL=false |
Field XXMapper required a bean of type XXMapper |
Mapper接口没被扫描到 | 启动类加@MapperScan("com.example.mapper") |
最后一个问题出现的频率非常高,尤其是一些源码把@MapperScan放在了主启动类外面。MyBatis的Mapper接口扫描要么在启动类上加注解,要么在接口上加@Mapper,二者选其一即可。
6.2 运行期间的逻辑Bug排查
- 登录后跳转回登录页:多半是Session或Token的传递问题。检查前端有没有在请求头携带
Authorization,后端拦截器有没有正确放行登录接口。 - 图片上传成功但访问404:先检查上传目录的路径末尾是否有
/,再检查静态资源映射是否正确注册。 - 列表页商品数据为空:先用Navicat或命令行查数据库,确认表里有数据。如果表里有数据,查一下MyBatis的Mapper XML文件里的SQL是否有问题,特别是表名大小写。
- 下单时报“商品已售出”:查一下
clothing.status的初始值和判断逻辑是否一致。我见过有的源码销售中用的状态码是0而不是1,前端传参和后端判断对不上,导致所有商品都无法购买。 - 中文乱码:页面上中文显示乱码,99%是连接字符串没加
characterEncoding=utf8,或者在创建数据库时没有指定utf8mb4字符集。注意MySQL 8默认字符集是utf8mb4,而老项目创建SQL脚本里可能指定了latin1,导库时容易乱码,导入前检查脚本开头有没有SET NAMES utf8mb4。
6.3 答辩前必须能回答的8个问题
毕业设计答辩,老师问的无非围绕“为什么这么设计”和“遇到了什么困难”展开。提前准备好这几个问题的答案,场上会从容很多:
- 为什么选Spring Boot? 自动配置减少XML配置、起步依赖简化构建、内置Tomcat一键启动、生态成熟社区完善。
- 登录认证是怎么做的? 说清楚JWT的构成(Header.Payload.Signature),以及无状态认证相比Session的优势。
- 多条件搜索怎么实现? 用MyBatis(或MyBatis Plus的动态SQL),根据查询条件动态拼接WHERE子句。
- 下单过程中如何保证数据一致性?
@Transactional事务控制,订单表和商品表状态更新在同一事务中,失败自动回滚。 - 订单状态是怎么管理的? 画出状态流转图(文字描述),说明每个操作都校验当前状态。
- 图片上传怎么防止攻击? 校验文件后缀白名单、限制文件大小(Spring Boot用
spring.servlet.multipart.max-file-size配置)、文件名使用UUID随机而不是用户原始文件名。 - 项目遇到过什么坑? 选一个自己真实踩过的坑,比如图片404、跨域报错、数据库时区异常,描述排查过程。
- 项目还可以怎么改进? 引入Redis缓存热点商品、用Elasticsearch做全文搜索、接入WebSocket做买卖双方即时聊天、增加后台数据报表。
我给很多同学模拟过答辩,大家最缺的不是技术深度,而是对业务逻辑的完整表达。上面这些问题你如果能用自己的话讲顺,拿个优秀毕业设计的概率会非常大。
6.4 我的几个额外经验
最后说点跑偏但实用的小建议。
第一,一定要有一份完整的测试数据。这个项目如果只有一个账号、两三件商品,演示效果会很单薄。建议造10个用户、30件不同分类和尺码的服装商品,包含不同状态、不同价格,这样老师随便点什么都能看到内容。
第二,接口联调时把异常情况也测一遍。比如重复下单、买自己的商品、未登录访问订单接口、A用户想删B用户的商品,这些边界情况你提前用Postman测过,答辩时会非常自信。
第三,代码注释写清楚业务含义。不在多,关键在于核心方法(下单、订单状态流转、商品发布)注释为什么这么做,而不是解释这行代码是什么。老师翻你源码时,看到有思想的注释,第一印象就会很好。
这个项目虽然定位是“毕业设计源码”,但如果你不只是把代码跑通,而是真正理解了里面的表结构设计、事务边界控制、接口交互流程,那它的价值就远超一个“毕设模板”。毕竟,很多初级开发岗位日常做的业务,跟这类项目本质上没有太大区别。
