1. 项目概述
去年在开发校园二手交易平台时,我选择了SpringBoot作为后端框架。这个决定让整个开发周期缩短了40%,特别是在处理高并发商品查询请求时,SpringBoot的自动配置和内置Tomcat让我们团队避开了很多传统Spring项目的配置陷阱。
二手交易平台本质上是个典型的C2C电商系统,但相比传统电商,它有几个独特的技术挑战:商品信息非标准化、交易流程简单化、用户信用体系轻量化。基于这些特点,我们采用SpringBoot 2.7 + Vue3的技术栈,用6周时间就完成了从零到上线的全过程。
2. 技术架构设计
2.1 为什么选择SpringBoot
在技术选型阶段,我们对比了三种方案:
- 传统Spring MVC + 手动配置Tomcat
- SpringBoot + 内嵌Tomcat
- 微服务架构
最终选择方案2基于三个实际考量:
- 开发效率:SpringBoot的starter依赖让项目初始化时间从3天缩短到3小时
- 运维成本:内嵌Tomcat避免了服务器环境配置差异导致的问题
- 性能需求:预估日均UV不超过1万,单体架构完全够用
经验提示:对于中小型二手平台,除非预计半年内UV会突破5万,否则不要过早引入微服务,这会让开发复杂度成倍增加。
2.2 核心组件依赖
我们的pom.xml关键配置如下(简化版):
xml复制<dependencies>
<!-- SpringBoot基础套件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 缓存优化 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
这套组合实现了:
- 开箱即用的Web MVC支持
- 基于JPA的快速数据访问层开发
- 基于Spring Security的RBAC权限控制
- 本地缓存+二级缓存的混合缓存策略
3. 核心功能实现
3.1 用户系统的技术细节
用户模块看似简单,但藏着不少技术点:
java复制@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // 实际存储的是BCrypt哈希值
@Enumerated(EnumType.STRING)
private UserStatus status = UserStatus.ACTIVE;
// 信用评分系统
private Integer creditScore = 100;
@OneToMany(mappedBy = "owner")
private List<Commodity> commodities = new ArrayList<>();
// 省略getter/setter
}
密码安全处理是关键点之一。我们在SecurityConfig中配置了如下密码编码器:
java复制@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 成本因子设为12,兼顾安全与性能
}
踩坑记录:初期使用SHA-256加密,后被安全审计指出不足。BCrypt的自动加盐机制和自适应成本因子更安全,虽然加密耗时比SHA-256多约15ms,但在登录场景下完全可以接受。
3.2 商品系统的设计要点
商品模型设计考虑了二手商品的特殊性:
java复制@Entity
public class Commodity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
@Enumerated(EnumType.STRING)
private CommodityCategory category;
@ElementCollection
@CollectionTable(name = "commodity_images")
private List<String> imageUrls = new ArrayList<>();
private BigDecimal price;
private BigDecimal originalPrice;
@Enumerated(EnumType.STRING)
private CommodityStatus status;
@ManyToOne
@JoinColumn(name = "owner_id")
private User owner;
// 省略其他字段和方法
}
特别要注意的是:
- 使用@ElementCollection处理多图存储,避免创建单独的表实体
- 价格使用BigDecimal而非double,防止浮点数精度问题
- 商品状态机设计:
java复制public enum CommodityStatus {
DRAFT, // 草稿
PUBLISHED, // 已发布
RESERVED, // 已预订
SOLD, // 已售出
DELETED // 已删除
}
3.3 交易流程的实现
交易核心流程的状态转换:
java复制public enum TransactionStatus {
INITIATED, // 买家发起
PAYMENT_PENDING, // 待支付
PAID, // 已支付
DELIVERING, // 交付中
COMPLETED, // 已完成
CANCELLED // 已取消
}
支付接口的简化实现:
java复制@RestController
@RequestMapping("/api/payment")
public class PaymentController {
@PostMapping
public ResponseEntity<?> createPayment(@RequestBody PaymentRequest request) {
// 1. 验证交易存在且属于当前用户
Transaction transaction = transactionService.validate(request.getTransactionId());
// 2. 调用第三方支付网关(模拟)
PaymentGatewayResponse response = paymentGateway.process(
transaction.getAmount(),
request.getPaymentMethod()
);
// 3. 更新交易状态
if (response.isSuccess()) {
transactionService.updateStatus(
transaction.getId(),
TransactionStatus.PAID
);
return ResponseEntity.ok(new PaymentResult(true));
}
return ResponseEntity.badRequest().body(new PaymentResult(false));
}
}
4. 性能优化实践
4.1 缓存策略设计
商品列表接口的缓存实现:
java复制@Service
@CacheConfig(cacheNames = "commodities")
public class CommodityService {
@Cacheable(key = "#root.methodName + ':' + #page + ':' + #size")
public Page<Commodity> listPublished(int page, int size) {
// 数据库查询逻辑
return commodityRepository.findByStatus(
CommodityStatus.PUBLISHED,
PageRequest.of(page, size, Sort.by("createTime").descending())
);
}
@CacheEvict(allEntries = true)
public void refreshCache() {
// 清空所有商品缓存
}
}
缓存配置(application.yml):
yaml复制spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
我们测试发现:
- 无缓存时,商品列表API平均响应时间:320ms
- 启用缓存后,首次请求:350ms(含缓存写入开销)
- 缓存命中时:28ms
4.2 数据库优化技巧
- 索引策略:
sql复制ALTER TABLE commodity
ADD INDEX idx_status_category (status, category);
- 查询优化示例:
java复制// 错误写法 - N+1查询问题
List<Commodity> commodities = commodityRepository.findAll();
commodities.forEach(c -> System.out.println(c.getOwner().getUsername()));
// 正确写法 - 使用JOIN FETCH
@Query("SELECT c FROM Commodity c JOIN FETCH c.owner WHERE c.status = 'PUBLISHED'")
List<Commodity> findAllPublishedWithOwner();
5. 安全防护措施
5.1 接口安全配置
Spring Security的核心配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离项目可禁用
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/commodities/search").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
5.2 敏感数据处理
用户手机号脱敏处理:
java复制@Converter
public class PhoneNumberConverter implements AttributeConverter<String, String> {
@Override
public String convertToDatabaseColumn(String attribute) {
return attribute; // 数据库存原始数据
}
@Override
public String convertToEntityAttribute(String dbData) {
if (dbData == null) return null;
// 显示效果:138****1234
return dbData.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
6. 部署与监控
6.1 生产环境配置
application-prod.yml关键配置:
yaml复制spring:
datasource:
url: jdbc:mysql://prod-db:3306/secondhand?useSSL=false
username: prod_user
password: ${DB_PASSWORD}
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
show-sql: false # 生产环境关闭SQL日志
server:
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,application/json
6.2 健康检查端点
SpringBoot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
关键监控指标:
- 数据库连接池使用率
- JVM内存和GC情况
- API响应时间P99值
- 缓存命中率
7. 踩坑与解决方案
7.1 图片上传的坑
初期直接保存文件到服务器本地,导致:
- 扩容时多节点文件不同步
- 备份困难
最终方案:使用MinIO搭建私有对象存储
java复制public String uploadImage(MultipartFile file) {
String objectName = "images/" + UUID.randomUUID() +
FilenameUtils.getExtension(file.getOriginalFilename());
minioClient.putObject(
PutObjectArgs.builder()
.bucket("secondhand")
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
return "/" + objectName;
}
7.2 事务失效场景
发现商品下架操作有时不会回滚:
java复制// 错误示例 - 自调用导致事务失效
public void disableCommodity(Long id) {
updateStatus(id, CommodityStatus.DELETED);
}
@Transactional
public void updateStatus(Long id, CommodityStatus status) {
// ...
}
解决方案:
- 将事务方法移到另一个Service
- 使用AopContext.currentProxy()获取代理对象
8. 扩展思考
这套架构在以下场景可能需要调整:
- 跨校区交易:引入分布式事务处理
- 拍卖功能:需要集成WebSocket实时竞价
- 智能推荐:增加机器学习模块
我在实际开发中发现,SpringBoot的自动配置虽然方便,但在复杂业务场景下,适当关闭自动配置反而更可控。比如手动配置Jackson的日期格式,而不是依赖SpringBoot的默认配置。