1. 项目概述:前后端分离物品租赁系统
这个基于SpringBoot+Vue+MyBatis+MySQL技术栈的物品租赁系统,是我去年为一个校园设备共享平台开发的核心项目。不同于传统的单体架构,我们采用了彻底的前后端分离模式,后端提供RESTful API,前端通过Axios进行数据交互,实现了真正意义上的解耦开发。
系统主要解决了三个核心痛点:
- 租赁流程数字化:将线下纸质登记转为线上自动化流程
- 库存状态实时可视化:通过Vue的动态绑定实现设备状态的秒级更新
- 多角色权限隔离:采用RBAC模型区分管理员、租户、审核员等不同权限
技术选型上,我们选择了SpringBoot 2.7作为后端框架,Vue 3作为前端框架,MyBatis-Plus 3.5作为ORM工具,MySQL 8.0作为数据库。这套组合在保证开发效率的同时,也兼顾了性能需求——在校园内网环境下,系统能稳定支撑300+并发请求。
提示:选择MyBatis-Plus而非JPA是考虑到项目中有大量复杂查询场景,需要更灵活的SQL控制能力
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计解析
2.1 后端技术栈实现
SpringBoot的自动配置让我们快速搭建起了项目骨架。几个关键配置点:
java复制@SpringBootApplication
@MapperScan("com.rental.mapper")
@EnableTransactionManagement
public class RentalApplication {
public static void main(String[] args) {
SpringApplication.run(RentalApplication.class, args);
}
}
数据库层采用MyBatis-Plus的代码生成器自动生成基础CRUD操作:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
特别值得分享的是我们处理租赁时间冲突的方案。在RentalMapper.xml中,这个SQL确保了同一设备在相同时段不会被重复租赁:
sql复制<select id="checkTimeConflict" resultType="int">
SELECT COUNT(*) FROM rental_record
WHERE equipment_id = #{equipmentId}
AND (
(start_time <= #{endTime} AND end_time >= #{startTime})
OR (start_time >= #{startTime} AND start_time <= #{endTime})
)
</select>
2.2 前端工程化实践
Vue 3的组合式API大大提升了代码组织效率。我们使用Pinia进行状态管理,典型store结构如下:
javascript复制// stores/rental.js
export const useRentalStore = defineStore('rental', {
state: () => ({
currentRentals: [],
historyRentals: []
}),
actions: {
async fetchCurrentRentals() {
const res = await axios.get('/api/rentals/current')
this.currentRentals = res.data
}
}
})
路由配置采用懒加载提升首屏速度:
javascript复制const routes = [
{
path: '/equipment',
component: () => import('../views/EquipmentView.vue'),
meta: { requiresAuth: true }
}
]
3. 核心业务模块实现
3.1 租赁流程状态机
租赁业务涉及多个状态转换,我们采用状态模式实现:
java复制public interface RentalState {
void confirm(RentalContext context);
void cancel(RentalContext context);
void complete(RentalContext context);
}
@Component
@Scope("prototype")
public class PendingState implements RentalState {
@Override
public void confirm(RentalContext context) {
context.setState(new ConfirmedState());
// 发送通知邮件
emailService.sendConfirmation(context.getRental());
}
}
状态转换通过Spring的ApplicationEvent实现解耦:
java复制@EventListener
public void handleStateChange(RentalStateChangeEvent event) {
log.info("状态变更: {} -> {}",
event.getPreviousState(),
event.getNewState());
// 更新ES索引
elasticsearchTemplate.update(...);
}
3.2 库存预警机制
基于MySQL的事件调度器实现低库存预警:
sql复制CREATE EVENT check_inventory
ON SCHEDULE EVERY 1 DAY
DO
BEGIN
INSERT INTO notifications(user_id, content)
SELECT manager_id, CONCAT('设备 ', name, ' 库存不足')
FROM equipment
WHERE stock < threshold;
END
前端通过WebSocket实时接收预警信息:
javascript复制const socket = new WebSocket(`wss://${location.host}/api/notifications`)
socket.onmessage = (event) => {
const notification = JSON.parse(event.data)
useNotificationStore().add(notification)
}
4. 部署方案详解
4.1 后端部署要点
采用Docker Compose编排服务:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- mysql
environment:
- SPRING_PROFILES_ACTIVE=prod
mysql:
image: mysql:8.0
volumes:
- db_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=rental123
- MYSQL_DATABASE=rental_db
关键的生产环境配置:
properties复制# application-prod.properties
spring.datasource.hikari.maximum-pool-size=20
spring.jpa.hibernate.ddl-auto=validate
spring.cache.type=redis
4.2 前端优化部署
使用Nginx配置gzip压缩和缓存:
nginx复制server {
gzip on;
gzip_types text/plain application/javascript application/x-javascript text/css;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
expires 30d;
}
}
通过Jenkins实现CI/CD流水线:
groovy复制pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('Deploy') {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: 'production-server',
transfers: [
sshTransfer(
sourceFiles: 'dist/**',
remoteDirectory: '/var/www/rental'
)
]
)
]
)
}
}
}
}
5. 开发中的典型问题与解决方案
5.1 MyBatis动态SQL陷阱
在实现复杂查询过滤器时,遇到过条件拼接问题。正确做法是:
xml复制<select id="searchEquipment" resultType="Equipment">
SELECT * FROM equipment
<where>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
</where>
ORDER BY
<choose>
<when test="sortBy == 'price'">rental_price</when>
<otherwise>id</otherwise>
</choose>
</select>
5.2 Vue响应式数据更新坑
当直接修改数组元素时,Vue无法检测变化。解决方案:
javascript复制// 错误做法
this.items[0] = newItem
// 正确做法
this.items.splice(0, 1, newItem)
// 或者使用Vue.set
Vue.set(this.items, 0, newItem)
5.3 SpringBoot事务失效场景
在私有方法上使用@Transactional会失效。我们的处理方案:
java复制@Service
public class RentalService {
@Autowired
private RentalService self; // 注入自身代理对象
public void processRental() {
self.privateMethod(); // 通过代理对象调用
}
@Transactional
private void privateMethod() {
// 事务操作
}
}
6. 性能优化实践
6.1 数据库查询优化
针对租赁记录分页查询,采用覆盖索引:
sql复制ALTER TABLE rental_record ADD INDEX idx_user_status (user_id, status);
MyBatis-Plus的分页插件配置:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
6.2 前端性能提升
实施组件级懒加载:
javascript复制const EquipmentList = defineAsyncComponent(() =>
import('./components/EquipmentList.vue')
)
使用虚拟滚动处理长列表:
vue复制<template>
<RecycleScroller
class="scroller"
:items="equipments"
:item-size="72"
key-field="id"
>
<template #default="{ item }">
<EquipmentItem :item="item" />
</template>
</RecycleScroller>
</template>
7. 安全防护措施
7.1 接口安全
Spring Security配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
7.2 数据安全
敏感字段加密存储:
java复制@ColumnTransformer(
read = "AES_DECRYPT(credit_card, '${encryption.key}')",
write = "AES_ENCRYPT(?, '${encryption.key}')"
)
@Column(name = "credit_card")
private String creditCard;
审计日志记录:
java复制@EntityListeners(AuditingEntityListener.class)
@Entity
public class RentalRecord {
@CreatedBy
private String createdBy;
@LastModifiedDate
private LocalDateTime modifiedAt;
}
8. 监控与运维
8.1 健康检查端点
SpringBoot Actuator配置:
properties复制management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=when_authorized
自定义健康检查指标:
java复制@Component
public class InventoryHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int lowStockCount = equipmentRepository.countByStockLessThan(5);
return lowStockCount > 10 ?
Health.down().withDetail("message", "低库存设备过多").build() :
Health.up().build();
}
}
8.2 日志收集方案
Logback日志配置:
xml复制<appender name="ELK" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"rental-system"}</customFields>
</encoder>
</appender>
前端错误监控:
javascript复制Vue.config.errorHandler = (err, vm, info) => {
Sentry.captureException(err, {
extra: {
component: vm.$options.name,
propsData: vm.$options.propsData,
info
}
})
}
9. 项目扩展方向
9.1 微服务化改造
考虑将系统拆分为:
- 租赁服务
- 用户服务
- 支付服务
- 通知服务
使用Spring Cloud Alibaba组件:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
9.2 多租户支持
基于Schema的多租户实现:
java复制public class TenantSchemaProvider implements SchemaBasedMultiTenantConnectionProvider {
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
Connection conn = getDataSource().getConnection();
conn.createStatement().execute("USE " + tenantIdentifier);
return conn;
}
}
9.3 移动端适配
使用Capacitor打包为移动应用:
javascript复制// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.rental',
appName: '设备租赁',
webDir: 'dist',
server: {
androidScheme: 'https'
}
};
10. 开发心得与建议
经过这个项目的实战,有几个关键经验值得分享:
- API文档先行:使用Swagger或YAPI先定义好接口规范,前后端并行开发效率提升明显。我们采用OpenAPI 3.0规范,通过springdoc-openapi自动生成文档:
java复制@Operation(summary = "获取设备列表")
@GetMapping("/equipments")
public PageResult<EquipmentVO> listEquipments(
@Parameter(description = "分类ID") @RequestParam(required = false) Long categoryId,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum) {
// ...
}
- 类型安全优先:前后端都使用TypeScript/Java等强类型语言,配合DTO验证能减少30%以上的低级错误:
typescript复制interface RentalForm {
equipmentId: number
startTime: string
endTime: string
purpose: string
}
const form = ref<RentalForm>({
equipmentId: 0,
startTime: '',
endTime: '',
purpose: ''
})
- 监控要尽早接入:在开发阶段就集成Prometheus和Grafana,能快速定位性能瓶颈。我们的监控指标包括:
- 平均响应时间
- JVM内存使用
- 慢SQL数量
- 活跃用户数
- 测试策略建议:
- 单元测试覆盖核心业务逻辑
- 集成测试验证API契约
- E2E测试关键用户旅程
- 压力测试评估系统极限
这个项目从技术选型到最终上线历时3个月,期间遇到的每个挑战都成为了团队技术成长的契机。特别在解决MyBatis动态SQL性能问题时,我们通过分析执行计划最终优化了查询效率,这个经验后来被应用到其他项目中,产生了连锁效益。
