去年接手某家电品牌售后系统重构时,我面临一个典型的技术选型困境——既要快速交付又要保证长期可维护性。最终采用的SpringBoot+Vue3+MyBatis组合,在3个月迭代周期内实现了客户要求的全业务流程数字化。这套技术栈如今已成为中后台系统的黄金搭配,尤其适合需要快速响应业务变化的售后管理场景。
这个全栈解决方案包含前端Vue3构建的管理门户、后端SpringBoot提供的RESTful API、MyBatis处理的数据持久层,以及MySQL存储的业务数据。前后端完全分离的架构让我们的前端团队可以独立开发交互功能,而后端专注于业务逻辑和性能优化。特别在工单流转模块,这种架构优势体现得淋漓尽致——前端实时渲染工单状态变更,后端则通过事件驱动机制保证数据一致性。
工单状态机是整个系统的核心,我们采用Spring StateMachine实现状态流转控制。以下是定义在TicketStateMachineConfig中的关键状态转换:
java复制@Configuration
@EnableStateMachineFactory
public class TicketStateMachineConfig {
@Bean
public StateMachineTransitionConfigurer<TicketState, TicketEvent> transitions(
StateMachineTransitionConfigurer<TicketState, TicketEvent> transitions) {
return transitions
.withExternal()
.source(TicketState.NEW)
.target(TicketState.ASSIGNED)
.event(TicketEvent.ASSIGN)
.and()
.withExternal()
.source(TicketState.ASSIGNED)
.target(TicketState.PROCESSING)
.event(TicketEvent.START_PROCESS);
}
}
前端对应使用Vue3的Composition API管理工单状态,通过WebSocket接收后端状态变更通知:
javascript复制// 工单状态组件
const ticketState = ref('NEW')
const socket = new WebSocket('wss://api.example.com/ticket-events')
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
if(data.type === 'STATE_CHANGE') {
ticketState.value = data.newState
}
}
踩坑提示:WebSocket连接需要处理断线重连,建议使用指数退避算法。我们在生产环境曾因网络抖动导致状态同步失败,后来增加了心跳检测机制。
库存模块采用乐观锁解决并发扣减问题,MyBatis的@Version注解配合MySQL事务实现:
java复制public class Inventory {
@Version
private Integer version;
@Update("UPDATE inventory SET quantity=quantity-#{count}, version=version+1
WHERE part_id=#{partId} AND version=#{version}")
int deductInventory(@Param("partId") Long partId,
@Param("count") Integer count,
@Param("version") Integer version);
}
前端库存看板使用ECharts实现实时可视化,通过Vue3的watchEffect自动更新:
javascript复制const inventoryData = ref([])
watchEffect(async () => {
const res = await fetch('/api/inventory')
inventoryData.value = await res.json()
})
集成高德地图API实现网点半径覆盖分析,后端使用MySQL的空间函数计算距离:
sql复制SELECT
id,
ST_Distance_Sphere(
point(#{lng}, #{lat}),
point(longitude, latitude)
) AS distance
FROM service_center
HAVING distance < 5000
ORDER BY distance
前端地图组件采用Vue3的自定义指令封装AMap:
javascript复制// 地图指令
app.directive('amap', {
mounted(el, binding) {
const map = new AMap.Map(el, binding.value)
binding.instance.$amap = map
}
})
我们使用OpenAPI 3.0规范定义接口契约,后端通过SpringDoc自动生成文档:
java复制@Operation(summary = "创建工单")
@PostMapping("/tickets")
public ResponseEntity<TicketDTO> createTicket(
@RequestBody @Valid CreateTicketRequest request) {
// 实现逻辑
}
前端则根据swagger.json生成TypeScript类型定义和API客户端:
bash复制npx openapi-typescript https://api.example.com/v3/api-docs -o src/api/types.ts
采用RBAC模型,后端通过Spring Security的@PreAuthorize注解控制:
java复制@PreAuthorize("hasRole('SERVICE_MANAGER') ||
(hasRole('TECHNICIAN') && #ticket.assignedTo == authentication.name)")
@PutMapping("/tickets/{id}")
public void updateTicket(@PathVariable Long id, @RequestBody TicketUpdate update) {
// 实现逻辑
}
前端权限指令封装了角色校验逻辑:
javascript复制app.directive('permission', {
mounted(el, binding) {
const { hasPermission } = useAuth()
if (!hasPermission(binding.value)) {
el.parentNode?.removeChild(el)
}
}
})
针对工单列表的复杂查询,我们采用以下优化手段:
xml复制<cache type="org.mybatis.caches.redis.RedisCache"
eviction="LRU"
flushInterval="60000"
size="1024"/>
SELECT *,只获取必要字段sql复制SELECT
t.id, t.title, t.status,
u.name AS customer_name
FROM ticket t
JOIN user u ON t.customer_id = u.id
vue复制<template>
<VirtualList :items="tickets" :item-size="72">
<template #default="{ item }">
<TicketCard :ticket="item" />
</template>
</VirtualList>
</template>
工单表按月份分表,使用MyBatis动态表名:
java复制@Intercepts({
@Signature(type= StatementHandler.class, method="prepare", args={Connection.class, Integer.class})
})
public class TableShardInterceptor implements Interceptor {
// 根据日期路由到对应月份表
private String getTableName(String baseName, Date date) {
return baseName + "_" + new SimpleDateFormat("yyyyMM").format(date);
}
}
Docker Compose编排文件示例:
yaml复制version: '3'
services:
backend:
image: springboot-app:1.0
ports:
- "8080:8080"
depends_on:
- redis
- mysql
frontend:
image: nginx:1.19
ports:
- "80:80"
volumes:
- ./dist:/usr/share/nginx/html
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
Spring Boot Actuator暴露的指标通过Prometheus采集:
yaml复制# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
前端性能监控使用Sentry捕获异常:
javascript复制import * as Sentry from '@sentry/vue'
app.use(Sentry, {
dsn: 'YOUR_DSN',
integrations: [
new Sentry.BrowserTracing(),
],
tracesSampleRate: 0.2
})
初期工单列表出现严重性能问题,排查发现是关联查询导致的N+1问题。解决方案:
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
java复制@Query("SELECT t FROM Ticket t JOIN FETCH t.customer WHERE t.status = :status")
List<Ticket> findByStatusWithCustomer(@Param("status") String status);
技术人员反馈工单表单数据有时不更新,原因是直接修改了响应式数组:
javascript复制// 错误做法
ticket.parts = newPartsArray
// 正确做法
ticket.parts.splice(0, ticket.parts.length, ...newPartsArray)
当前架构已支持以下扩展:
在最近一次压力测试中,这套架构成功支撑了5000+并发工单操作,平均响应时间保持在200ms以内。特别值得一提的是,通过前端组件按需加载和后端接口缓存优化,系统首屏加载时间从最初的4.2秒降低到1.8秒。