1. 项目概述
在共享经济蓬勃发展的当下,物品租赁业务正经历着前所未有的增长。作为一名长期从事企业级应用开发的工程师,我最近完成了一个基于SpringBoot+Vue的全栈式物品租赁管理系统。这个项目从需求分析到最终上线历时3个月,期间遇到了不少技术挑战,也积累了许多值得分享的实践经验。
这个系统主要解决传统租赁业务中存在的几个痛点:
- 手工记录导致的效率低下问题
- 库存状态更新不及时引发的超租风险
- 财务对账困难
- 用户体验不佳
系统采用前后端分离架构,后端基于SpringBoot 2.7提供RESTful API,前端使用Vue 3组合式API开发管理界面。数据库选用MySQL 8.0,利用其JSON字段特性存储灵活的租赁规则。特别值得一提的是,我们实现了基于Redis的分布式锁机制,有效解决了高并发场景下的库存超卖问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 后端技术栈解析
选择SpringBoot作为后端框架主要基于以下几个考量:
- 自动配置:大幅减少XML配置,内置Tomcat服务器简化部署
- 起步依赖:通过spring-boot-starter-*系列依赖快速集成常用组件
- 生产就绪:自带健康检查、指标监控等生产级特性
数据库访问层我们采用了MyBatis-Plus 3.5,相比原生MyBatis,它提供了诸多开箱即用的功能:
java复制// 示例:使用MyBatis-Plus的Lambda查询
List<Equipment> availableList = equipmentMapper.selectList(
Wrappers.<Equipment>lambdaQuery()
.eq(Equipment::getStatus, 0) // 0表示可用状态
.ge(Equipment::getValue, 1000) // 价值大于1000元的设备
);
2.2 前端技术方案
Vue 3的组合式API让我们能够更好地组织前端逻辑代码。项目中使用的主要技术点包括:
- Pinia状态管理:替代Vuex的轻量级方案
- Element Plus:UI组件库加速开发
- Axios拦截器:统一处理HTTP请求/响应
一个典型的设备列表组件实现:
vue复制<script setup>
import { ref, onMounted } from 'vue'
import { useEquipmentStore } from '@/stores/equipment'
const equipmentStore = useEquipmentStore()
const tableData = ref([])
onMounted(async () => {
await equipmentStore.fetchEquipments()
tableData.value = equipmentStore.list
})
</script>
2.3 系统架构图
整个系统采用分层架构设计:
code复制┌───────────────────────────────────────┐
│ 客户端层 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Web前端 │ │ 移动端APP │ │
│ └─────────────┘ └─────────────┘ │
└───────────────────┬───────────────────┘
│ HTTP/HTTPS
┌───────────────────▼───────────────────┐
│ 网关层 │
│ ┌─────────────────────────────────┐ │
│ │ Spring Cloud Gateway │ │
│ └─────────────────────────────────┘ │
└───────────────────┬───────────────────┘
│
┌───────────────────▼───────────────────┐
│ 应用层 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 租赁服务模块 │ │ 支付服务模块 │ │
│ └─────────────┘ └─────────────┘ │
└───────────────────┬───────────────────┘
│
┌───────────────────▼───────────────────┐
│ 数据层 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ MySQL │ │ Redis │ │
│ └─────────────┘ └─────────────┘ │
└───────────────────────────────────────┘
3. 核心功能实现细节
3.1 租赁业务流程实现
完整的租赁流程包含以下关键步骤:
- 库存检查 → 2. 用户信用验证 → 3. 生成订单 → 4. 支付处理 → 5. 库存扣减
我们使用Spring的@Transactional注解确保数据一致性:
java复制@Transactional(rollbackFor = Exception.class)
public RentalOrder createOrder(CreateOrderDTO dto) {
// 1. 检查设备可用性
Equipment equipment = checkAvailability(dto.getEquipmentId());
// 2. 验证用户信用
validateUserCredit(dto.getUserId());
// 3. 生成订单
RentalOrder order = buildOrder(dto, equipment);
orderMapper.insert(order);
// 4. 处理支付
paymentService.processPayment(order);
// 5. 更新设备状态
updateEquipmentStatus(equipment.getId(), 1); // 1表示已出租
return order;
}
3.2 库存并发控制
为解决高并发下的库存超卖问题,我们实现了两种方案:
方案一:乐观锁实现
java复制public boolean rentWithOptimisticLock(Long equipmentId) {
Equipment equipment = equipmentMapper.selectById(equipmentId);
if (equipment.getStock() <= 0) {
return false;
}
int updated = equipmentMapper.updateStock(
equipmentId,
equipment.getStock() - 1,
equipment.getVersion()
);
return updated > 0;
}
方案二:Redis分布式锁
java复制public boolean rentWithDistributedLock(Long equipmentId) {
String lockKey = "equipment_lock:" + equipmentId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 执行业务逻辑
Equipment equipment = equipmentMapper.selectById(equipmentId);
if (equipment.getStock() <= 0) {
return false;
}
equipment.setStock(equipment.getStock() - 1);
return equipmentMapper.updateById(equipment) > 0;
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
3.3 文件上传下载实现
文件上传模块支持多种存储策略,通过策略模式实现灵活切换:
java复制public interface FileStorageStrategy {
String upload(MultipartFile file, String bizType);
ResponseEntity<byte[]> download(String fileKey);
}
@Service
public class FileService {
@Autowired
private FileStorageStrategy strategy;
public String uploadFile(MultipartFile file, String bizType) {
// 文件校验逻辑
validateFile(file);
// 调用具体策略实现
return strategy.upload(file, bizType);
}
}
// 本地存储策略实现
@Service
@Profile("local")
public class LocalFileStorage implements FileStorageStrategy {
@Value("${file.upload-dir}")
private String uploadDir;
@Override
public String upload(MultipartFile file, String bizType) {
String filename = generateFilename(file.getOriginalFilename());
Path path = Paths.get(uploadDir, bizType, filename);
try {
Files.createDirectories(path.getParent());
file.transferTo(path);
return filename;
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
}
4. 关键问题与解决方案
4.1 跨域问题处理
在前后端分离架构下,我们遇到了跨域访问问题。解决方案是在后端配置全局CORS:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.maxAge(3600);
}
}
对于更复杂的需求,可以使用Spring Cloud Gateway统一处理:
yaml复制spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-methods:
- GET
- POST
- PUT
- DELETE
allowed-headers: "*"
4.2 接口幂等性设计
对于支付等关键操作,我们实现了基于token的幂等控制:
- 服务端生成token并存入Redis
java复制public String createIdempotentToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(
"idempotent:" + token,
"1",
2,
TimeUnit.HOURS
);
return token;
}
- 客户端在请求头中携带token
javascript复制async function payOrder(orderId) {
const token = await getTokenFromServer();
return axios.post(`/api/payments`, { orderId }, {
headers: { 'X-Idempotent-Token': token }
});
}
- 服务端校验token
java复制@PostMapping("/payments")
public Result processPayment(@RequestBody PaymentDTO dto,
@RequestHeader("X-Idempotent-Token") String token) {
if (!idempotentService.validateToken(token)) {
throw new BusinessException("请勿重复提交");
}
// 处理支付逻辑
}
4.3 性能优化实践
数据库优化:
- 为高频查询字段添加索引
sql复制ALTER TABLE rental_orders
ADD INDEX idx_user_status (user_id, status);
- 使用EXPLAIN分析慢查询
- 合理设置连接池参数
缓存策略:
- 多级缓存架构
java复制public Equipment getEquipmentWithCache(Long id) {
// 1. 查询本地缓存
Equipment equipment = localCache.get(id);
if (equipment != null) {
return equipment;
}
// 2. 查询Redis
equipment = redisTemplate.opsForValue().get("equipment:" + id);
if (equipment != null) {
localCache.put(id, equipment);
return equipment;
}
// 3. 查询数据库
equipment = equipmentMapper.selectById(id);
if (equipment != null) {
redisTemplate.opsForValue().set(
"equipment:" + id,
equipment,
1,
TimeUnit.HOURS
);
localCache.put(id, equipment);
}
return equipment;
}
- 缓存击穿防护
java复制public Equipment getEquipmentSafely(Long id) {
String cacheKey = "equipment:" + id;
Equipment equipment = redisTemplate.opsForValue().get(cacheKey);
if (equipment == null) {
synchronized (this) {
equipment = redisTemplate.opsForValue().get(cacheKey);
if (equipment == null) {
equipment = equipmentMapper.selectById(id);
if (equipment != null) {
redisTemplate.opsForValue().set(
cacheKey,
equipment,
30 + new Random().nextInt(30), // 随机过期时间
TimeUnit.MINUTES
);
} else {
// 应对缓存穿透
redisTemplate.opsForValue().set(
cacheKey,
new Equipment(),
5,
TimeUnit.MINUTES
);
}
}
}
}
return equipment.getId() != null ? equipment : null;
}
5. 安全防护措施
5.1 认证与授权
我们采用JWT进行身份认证,结合Spring Security实现细粒度权限控制:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
JWT工具类实现:
java复制public class JwtUtils {
private static final String SECRET = "your-256-bit-secret";
private static final long EXPIRATION = 86400000L; // 24小时
public static String generateToken(UserDetails user) {
return Jwts.builder()
.setSubject(user.getUsername())
.claim("roles", user.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
public static UserDetails parseToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
List<String> roles = claims.get("roles", List.class);
return new User(username, "",
roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList())
);
}
}
5.2 数据安全
敏感数据加密:
java复制public class AesUtils {
private static final String KEY = "your-32-byte-key";
private static final String IV = "your-16-byte-iv";
public static String encrypt(String data) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(KEY.getBytes(), "AES"),
new IvParameterSpec(IV.getBytes()));
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
public static String decrypt(String encrypted) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(KEY.getBytes(), "AES"),
new IvParameterSpec(IV.getBytes()));
byte[] original = cipher.doFinal(
Base64.getDecoder().decode(encrypted));
return new String(original);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
}
SQL注入防护:
- 始终使用预编译语句
- MyBatis中使用#{}而非${}
xml复制<!-- 安全的方式 -->
<select id="findByStatus" resultType="Order">
SELECT * FROM orders WHERE status = #{status}
</select>
<!-- 不安全的方式(不要这样用) -->
<select id="findByStatusUnsafe" resultType="Order">
SELECT * FROM orders WHERE status = ${status}
</select>
6. 部署与监控
6.1 容器化部署
我们使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
app:
build: .
image: rental-system:1.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/rental
- REDIS_HOST=redis
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=rental
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3000:3000"
volumes:
mysql_data:
6.2 监控配置
Spring Boot Actuator提供健康检查端点:
properties复制# application.properties
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always
management.metrics.export.prometheus.enabled=true
对应的Prometheus配置:
yaml复制# prometheus.yml
scrape_configs:
- job_name: 'rental-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['app:8080']
7. 项目总结与经验分享
在开发这个租赁系统的过程中,有几个关键经验值得分享:
-
领域模型设计:初期花费足够时间进行领域分析,建立准确的实体关系模型,能显著减少后期的返工。我们使用事件风暴(Event Storming)方法梳理业务流程,效果很好。
-
并发控制:对于库存类系统,一定要在开发早期考虑并发场景。我们最初只实现了乐观锁,在生产环境出现超卖后才补充了Redis分布式锁方案。
-
监控告警:系统上线后,完善的监控体系能快速发现问题。我们配置了以下告警规则:
- 订单创建失败率 > 1%
- 平均响应时间 > 500ms
- JVM内存使用率 > 80%
-
文档维护:使用Swagger维护API文档,并通过Git Hook确保文档随代码更新:
java复制@Configuration
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.rental.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("租赁系统API文档")
.description("物品租赁管理系统接口说明")
.version("1.0")
.build();
}
}
- 测试策略:建立多层次的自动化测试:
- 单元测试:核心业务逻辑
- 集成测试:API接口
- E2E测试:关键业务流程
- 性能测试:使用JMeter模拟高并发场景
这个项目让我深刻体会到,一个好的系统不仅需要完善的功能实现,更需要考虑性能、安全、可维护性等非功能性需求。特别是在租赁这类涉及财务的业务场景中,数据一致性和安全性必须放在首位。
