1. 项目背景与核心价值
网上租赁系统是当前数字化转型浪潮中的典型应用场景。我去年为一家本地设备租赁公司开发类似系统时,发现传统租赁业务普遍存在三个痛点:手工登记效率低下、库存状态更新延迟、财务对账困难。这套基于SpringBoot+Vue的技术方案,正是针对这些业务痛点设计的现代化解决方案。
从技术选型角度看,SpringBoot+Vue的组合在2023年StackOverflow开发者调查中,已成为全栈开发最主流的技术栈之一。后端采用Java生态的SpringBoot框架,配合MyBatis持久层,既能保证系统稳定性,又具备良好的可扩展性;前端选用Vue.js框架,其响应式特性和组件化开发模式,特别适合租赁系统这类需要频繁交互的业务场景。
2. 系统架构设计
2.1 技术栈组成
本系统采用经典的三层架构:
- 前端层:Vue 3.x + Element Plus + Axios
- 后端层:SpringBoot 2.7 + MyBatis-Plus 3.5
- 数据层:MySQL 8.0 + Redis缓存
特别说明版本选择的原因:SpringBoot 2.7是长期支持版本(LTS),而Vue 3.x的组合式API相比Options API更适合复杂业务逻辑的封装。在数据库选型上,MySQL 8.0的窗口函数和CTE特性可以简化很多租赁业务中的复杂统计查询。
2.2 核心模块划分
系统主要包含六大功能模块:
- 用户中心:基于RBAC模型的权限控制系统
- 商品管理:支持多级分类的设备信息维护
- 租赁管理:包含预约、签约、续租全流程
- 库存管理:实时库存状态追踪与预警
- 支付系统:集成支付宝/微信支付沙箱环境
- 统计报表:基于ECharts的可视化数据分析
3. 关键技术实现细节
3.1 前后端分离架构实践
项目采用严格的前后端分离开发模式,通过自定义axios封装实现以下特性:
javascript复制// api/request.js
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000,
withCredentials: true
})
// 请求拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
},
error => {
return Promise.reject(error)
}
)
后端则通过Spring Security配置CORS策略:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE"));
config.setAllowCredentials(true);
config.addAllowedHeader("*");
return config;
});
}
}
3.2 租赁业务状态机设计
租赁订单的状态流转是本系统的核心业务逻辑。我们采用状态模式实现:
java复制public interface LeaseState {
void handle(LeaseContext context);
}
@Component
public class PendingState implements LeaseState {
@Override
public void handle(LeaseContext context) {
if ("PAY_SUCCESS".equals(context.getEvent())) {
context.setState(new ActiveState());
// 更新库存等操作
}
}
}
// 在Service层应用状态机
public class LeaseServiceImpl {
@Autowired
private Map<String, LeaseState> stateMap;
public void changeState(Long leaseId, String event) {
Lease lease = leaseMapper.selectById(leaseId);
LeaseContext context = new LeaseContext(lease);
stateMap.get(lease.getStatus()).handle(context);
}
}
3.3 库存并发控制方案
针对租赁系统常见的超卖问题,我们实现了三种防护机制:
- 乐观锁:通过version字段控制
sql复制UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE item_id = ? AND version = ?
- Redis分布式锁:针对集群环境
java复制public boolean tryLock(String key, long expire) {
String value = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expire, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
// 加锁成功,设置线程局部变量
lockHolder.set(value);
return true;
}
return false;
}
- 库存预占队列:使用Redis的List结构实现预占队列,配合定时任务处理
4. 开发环境搭建指南
4.1 后端环境配置
- JDK选择:推荐Amazon Corretto 11
bash复制# 验证安装
java -version
- Maven配置:settings.xml需配置阿里云镜像
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
- 数据库初始化:建议使用Flyway管理数据库迁移
properties复制# application.properties
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
4.2 前端环境准备
- Node.js版本:推荐16.x LTS版本
bash复制nvm install 16
nvm use 16
- Vue CLI安装:
bash复制npm install -g @vue/cli
vue create rental-frontend
- Element Plus引入:
javascript复制import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
5. 典型业务场景实现
5.1 租赁合同生成
使用Apache POI实现动态合同生成:
java复制public void generateContract(Lease lease, HttpServletResponse response) {
try (InputStream is = getClass().getResourceAsStream("/templates/contract.docx");
XWPFDocument doc = new XWPFDocument(is)) {
// 替换文档中的占位符
for (XWPFParagraph p : doc.getParagraphs()) {
String text = p.getText();
if (text != null) {
text = text.replace("${lesseeName}", lease.getLesseeName())
.replace("${startDate}", lease.getStartDate().toString());
p.getRuns().get(0).setText(text, 0);
}
}
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment; filename=contract.docx");
doc.write(response.getOutputStream());
}
}
5.2 支付结果异步通知
处理支付宝回调的防重入设计:
java复制@Transactional
public String handleAlipayNotify(Map<String, String> params) {
// 1. 验签
if (!AlipaySignature.rsaCheckV1(params, alipayPublicKey, "UTF-8", "RSA2")) {
throw new RuntimeException("验签失败");
}
// 2. 幂等控制
String tradeNo = params.get("out_trade_no");
if (paymentLogMapper.existsByTradeNo(tradeNo)) {
return "success";
}
// 3. 业务处理
PaymentLog log = new PaymentLog();
// ...填充字段
paymentLogMapper.insert(log);
// 4. 更新订单状态
orderService.updateStatus(tradeNo, "PAID");
return "success";
}
5.3 库存预警功能
基于Spring Scheduler的定时检查:
java复制@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void checkInventoryWarning() {
List<Inventory> list = inventoryMapper.selectList(
new QueryWrapper<Inventory>()
.lt("stock", "min_stock"));
if (!list.isEmpty()) {
String content = list.stream()
.map(item -> item.getItemName() + "剩余" + item.getStock() + "件")
.collect(Collectors.joining("\n"));
emailService.sendWarningEmail("库存预警", content);
}
}
6. 部署与运维方案
6.1 生产环境部署
推荐使用Docker Compose编排:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
volumes:
mysql_data:
6.2 性能优化建议
-
数据库层面:
- 为租赁表的user_id和item_id创建联合索引
- 大文本字段使用垂直分表
-
缓存策略:
java复制@Cacheable(value = "items", key = "#id", unless = "#result == null") public Item getItemById(Long id) { return itemMapper.selectById(id); } -
前端优化:
- 使用Vue的异步组件加载
- 配置Webpack的SplitChunksPlugin
7. 项目扩展方向
7.1 微服务化改造
当业务规模扩大时,可考虑按功能拆分为:
- 用户服务
- 商品服务
- 订单服务
- 支付服务
- 通知服务
使用Spring Cloud Alibaba套件:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
7.2 多租户支持
通过TenantFilter实现租户隔离:
java复制public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
String tenantId = req.getHeader("X-Tenant-ID");
TenantContext.setCurrentTenant(tenantId);
chain.doFilter(request, response);
}
}
7.3 数据分析增强
集成Apache Druid实现OLAP分析:
java复制@RestController
@RequestMapping("/analytics")
public class AnalyticsController {
@Autowired
private JdbcTemplate jdbcTemplate;
@GetMapping("/lease-trend")
public List<Map<String, Object>> getLeaseTrend(
@RequestParam String granularity) {
String sql = "SELECT TIME_FLOOR(__time, 'PT1H') AS time, "
+ "COUNT(*) AS leases FROM leases GROUP BY 1";
return jdbcTemplate.queryForList(sql);
}
}
8. 开发经验与避坑指南
-
日期处理陷阱:
- 前端传参使用ISO8601格式:
2023-08-20T10:00:00+08:00 - 数据库存储统一用UTC时间
- 展示时根据用户时区转换
- 前端传参使用ISO8601格式:
-
MyBatis映射注意:
xml复制<!-- 错误示范 --> <resultMap id="BaseResultMap" type="User"> <result column="create_time" property="createTime"/> </resultMap> <!-- 正确做法 --> <resultMap id="BaseResultMap" type="User"> <result column="create_time" property="createTime" jdbcType="TIMESTAMP" javaType="java.time.LocalDateTime"/> </resultMap> -
Vue响应式问题:
javascript复制// 数组更新需注意 this.items[index] = newItem // 不会触发视图更新 this.$set(this.items, index, newItem) // 正确方式 -
事务失效场景:
- 同类方法内调用:
this.methodB()会使@Transactional失效 - 异常被捕获未抛出
- 方法非public修饰
- 同类方法内调用:
-
线上问题排查工具:
- Arthas诊断工具
- SkyWalking链路追踪
- Prometheus + Grafana监控
