1. 项目概述
这个基于SSM框架的二手物品交易网站系统是我最近完成的一个课程设计/毕业设计项目。作为一个有10年开发经验的程序员,我想分享一下这个项目的完整实现过程和技术细节。这个系统实现了商品发布、订单处理和分类检索等核心功能,采用了Spring Boot + MyBatis + Vue的前后端分离架构,数据库使用MySQL。
在实际开发过程中,我发现很多同学在做类似项目时都会遇到一些共性问题:比如SSM框架整合不顺畅、前后端对接困难、数据库设计不合理等。这个项目我特别注重了这些问题点的解决,现在把完整的开发思路和实现过程分享出来,希望能帮助到正在做课程设计或毕业设计的同学。
2. 系统架构设计
2.1 技术选型分析
选择合适的技术栈是项目成功的关键。经过仔细评估,我最终确定了以下技术组合:
后端技术栈:
- Spring Boot 2.7.x:简化配置,快速构建项目
- MyBatis-Plus 3.5.x:增强的ORM框架,减少SQL编写
- Shiro 1.10.x:负责认证和授权
- Lombok:简化实体类开发
- Hutool:Java工具包
前端技术栈:
- Vue 3.x:前端框架
- Element Plus:UI组件库
- Axios:HTTP客户端
- Vue Router:路由管理
- Pinia:状态管理
数据库:
- MySQL 8.0:关系型数据库
- Redis 6.x:缓存和会话管理
选择这些技术主要基于以下考虑:
- Spring Boot的自动配置和起步依赖能大幅减少配置工作
- MyBatis-Plus提供了强大的CRUD操作和代码生成器
- Vue 3的组合式API更适合复杂前端逻辑
- MySQL 8.0在性能和功能上都有显著提升
2.2 系统架构设计
系统采用典型的三层架构:
表示层(Web层) │ ├── 前端:Vue + Element Plus │ 业务逻辑层(Service层) │ ├── Spring Boot + MyBatis-Plus │ 数据访问层(DAO层) │ ├── MySQL + Redis这种分层架构的优势在于:
- 职责分离,各层专注自己的功能
- 便于团队协作开发
- 易于维护和扩展
- 可以针对不同层进行独立测试
2.3 数据库设计
数据库设计遵循第三范式,主要表结构如下:
用户表(user):
- id: 主键
- username: 用户名
- password: 加密密码
- phone: 手机号
- email: 邮箱
- avatar: 头像
- status: 状态
商品表(goods):
- id: 主键
- user_id: 卖家ID
- category_id: 分类ID
- title: 商品标题
- price: 价格
- original_price: 原价
- description: 描述
- images: 图片JSON
- status: 状态
订单表(order):
- id: 主键
- order_no: 订单编号
- buyer_id: 买家ID
- seller_id: 卖家ID
- goods_id: 商品ID
- amount: 金额
- status: 状态
- address: 收货地址
分类表(category):
- id: 主键
- name: 分类名称
- parent_id: 父分类ID
- level: 层级
数据库关系图如下:
用户 1:n 商品 用户 1:n 订单(作为买家) 用户 1:n 订单(作为卖家) 商品 1:n 订单 分类 1:n 商品设计心得:在实际开发中,商品图片存储采用了JSON格式存储多个图片URL,而不是创建单独的图片表,这样简化了查询逻辑。但要注意JSON字段在MySQL中的索引问题。
3. 核心功能实现
3.1 用户认证模块
用户认证采用了Shiro框架,实现了以下功能:
- 基于用户名密码的登录
- 基于JWT的无状态认证
- 角色和权限控制
关键代码示例:
// Shiro配置类 @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/api/user/login", "anon"); filterMap.put("/api/user/register", "anon"); filterMap.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterMap); return factoryBean; } } // JWT工具类 public class JwtUtil { private static final String SECRET = "your-secret-key"; private static final long EXPIRE = 7 * 24 * 60 * 60 * 1000L; // 7天 public static String generateToken(String username) { Date now = new Date(); Date expire = new Date(now.getTime() + EXPIRE); return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(username) .setIssuedAt(now) .setExpiration(expire) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); } public static boolean verifyToken(String token) { try { Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token); return true; } catch (Exception e) { return false; } } }注意事项:在实际部署时,SECRET密钥应该从配置文件中读取,而不是硬编码在代码中。同时要考虑定期更换密钥的安全策略。
3.2 商品管理模块
商品管理实现了以下功能:
- 商品发布
- 商品编辑
- 商品上下架
- 商品搜索和筛选
核心接口设计:
@RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; @PostMapping public Result publishGoods(@RequestBody GoodsDTO goodsDTO) { return goodsService.publishGoods(goodsDTO); } @PutMapping("/{id}") public Result updateGoods(@PathVariable Long id, @RequestBody GoodsDTO goodsDTO) { return goodsService.updateGoods(id, goodsDTO); } @GetMapping("/{id}") public Result getGoodsDetail(@PathVariable Long id) { return goodsService.getGoodsDetail(id); } @GetMapping public Result searchGoods(@RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size) { return goodsService.searchGoods(keyword, categoryId, page, size); } }前端商品表单关键代码:
<template> <el-form :model="goodsForm" :rules="rules" ref="formRef"> <el-form-item label="商品标题" prop="title"> <el-input v-model="goodsForm.title"></el-input> </el-form-item> <el-form-item label="商品分类" prop="categoryId"> <el-cascader v-model="goodsForm.categoryId" :options="categoryOptions" :props="{value: 'id', label: 'name'}" ></el-cascader> </el-form-item> <el-form-item label="商品图片" prop="images"> <el-upload action="/api/upload" list-type="picture-card" :file-list="fileList" :on-success="handleUploadSuccess" > <el-icon><Plus /></el-icon> </el-upload> </el-form-item> <el-form-item label="商品描述" prop="description"> <el-input type="textarea" v-model="goodsForm.description"></el-input> </el-form-item> </el-form> </template>开发技巧:商品图片上传时,前端可以先压缩图片再上传,减少带宽占用。可以使用compressorjs等库实现图片压缩。
3.3 订单处理模块
订单处理流程包括:
- 创建订单
- 支付订单
- 发货
- 确认收货
- 评价
状态机设计:
待支付 → 已支付 → 已发货 → 已完成 ↘ 已取消订单服务核心逻辑:
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private GoodsMapper goodsMapper; @Transactional public Result createOrder(OrderDTO orderDTO) { // 1. 检查商品状态 Goods goods = goodsMapper.selectById(orderDTO.getGoodsId()); if (goods == null || goods.getStatus() != GoodsStatus.ON_SALE) { return Result.fail("商品不存在或已下架"); } // 2. 创建订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setBuyerId(orderDTO.getBuyerId()); order.setSellerId(goods.getUserId()); order.setGoodsId(goods.getId()); order.setAmount(goods.getPrice()); order.setStatus(OrderStatus.WAIT_PAYMENT); orderMapper.insert(order); // 3. 锁定商品 goods.setStatus(GoodsStatus.LOCKED); goodsMapper.updateById(goods); return Result.success(order); } private String generateOrderNo() { // 时间戳+随机数 return System.currentTimeMillis() + "" + (int)(Math.random() * 1000); } }注意事项:订单创建时要加事务注解@Transactional,确保订单创建和商品状态更新要么都成功,要么都失败。同时要考虑并发情况下的商品超卖问题。
4. 系统优化与部署
4.1 性能优化
缓存策略:
- 使用Redis缓存热门商品
- 实现二级缓存(MyBatis二级缓存+Redis)
- 缓存分类数据
数据库优化:
- 添加合适的索引
- 优化慢查询
- 使用读写分离
前端优化:
- 图片懒加载
- 路由懒加载
- 组件按需引入
缓存配置示例:
@Configuration @EnableCaching public class CacheConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) .disableCachingNullValues() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .transactionAware() .build(); } }4.2 安全防护
XSS防护:
- 前端使用vue-sanitize过滤输入
- 后端对输出进行转义
CSRF防护:
- 使用SameSite Cookie
- 添加CSRF Token
SQL注入防护:
- 使用MyBatis预编译
- 避免拼接SQL
敏感数据保护:
- 密码加盐哈希
- 敏感字段加密存储
安全配置示例:
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .headers() .xssProtection() .and() .contentSecurityPolicy("script-src 'self'") .and() .authorizeRequests() .antMatchers("/api/public/**").permitAll() .anyRequest().authenticated(); } }4.3 部署方案
推荐使用Docker Compose进行容器化部署:
version: '3' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: secondhand ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql redis: image: redis:6 ports: - "6379:6379" volumes: - redis_data:/data backend: build: ./backend ports: - "8080:8080" depends_on: - mysql - redis frontend: build: ./frontend ports: - "80:80" volumes: mysql_data: redis_data:部署步骤:
- 安装Docker和Docker Compose
- 编写Dockerfile和docker-compose.yml
- 构建镜像:
docker-compose build - 启动服务:
docker-compose up -d
部署建议:生产环境应该使用Nginx做反向代理和负载均衡,并配置HTTPS证书。同时要设置日志轮转和监控告警。
5. 常见问题与解决方案
5.1 开发环境问题
问题1:MySQL连接失败,时区错误
解决方案:在连接字符串中添加时区参数jdbc:mysql://localhost:3306/secondhand?useSSL=false&serverTimezone=Asia/Shanghai
问题2:前端跨域问题
解决方案:后端添加CORS配置
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*"); } }5.2 业务逻辑问题
问题1:商品超卖
解决方案:使用乐观锁或分布式锁
// 乐观锁实现 @Transactional public Result createOrderWithOptimisticLock(OrderDTO orderDTO) { Goods goods = goodsMapper.selectById(orderDTO.getGoodsId()); if (goods.getStock() <= 0) { return Result.fail("库存不足"); } int updated = goodsMapper.reduceStockWithVersion(goods.getId(), goods.getVersion()); if (updated == 0) { throw new OptimisticLockingFailureException("并发修改冲突"); } // 创建订单... }问题2:订单状态混乱
解决方案:使用状态模式设计订单状态机
public interface OrderState { void pay(Order order); void ship(Order order); void receive(Order order); void cancel(Order order); } public class Order { private OrderState state; public void pay() { state.pay(this); } // 其他方法... }5.3 性能问题
问题1:商品列表查询慢
解决方案:
- 添加合适的索引
- 使用分页查询
- 实现缓存
问题2:图片加载慢
解决方案:
- 使用CDN加速
- 实现图片懒加载
- 使用WebP格式图片
6. 项目扩展方向
这个基础版本完成后,还可以考虑以下扩展方向:
- 即时通讯:集成WebSocket实现买卖家实时沟通
- 推荐系统:基于用户行为实现商品推荐
- 支付集成:对接支付宝、微信支付
- 物流跟踪:对接快递100等物流查询接口
- 移动端适配:开发微信小程序或React Native App
即时通讯实现示例:
@ServerEndpoint("/chat/{userId}") @Component public class ChatEndpoint { private static final Map<Long, Session> sessions = new ConcurrentHashMap<>(); @OnOpen public void onOpen(@PathParam("userId") Long userId, Session session) { sessions.put(userId, session); } @OnMessage public void onMessage(String message, Session session) { // 处理消息并转发 } @OnClose public void onClose(@PathParam("userId") Long userId) { sessions.remove(userId); } }在实际开发这个项目的过程中,我最大的体会是:设计阶段多花时间,编码阶段就能少走弯路。特别是数据库设计和接口定义,一旦确定后再修改成本很高。建议同学们在开始编码前,先画好ER图和接口文档,和导师或组员充分讨论确认。