1. 项目概述与架构设计
这套基于SpringBoot+Vue+MyBatis+MySQL的客户关系管理系统(CRM)采用了典型的前后端分离架构。作为一名经历过多个企业级项目开发的老手,我认为这种架构选择在当前开发环境下具有显著优势。前端用Vue.js构建响应式界面,后端通过SpringBoot提供RESTful API,两者通过HTTP协议进行数据交互,这种解耦方式让团队可以并行开发,大幅提升交付效率。
系统核心功能模块包括:
- 客户信息管理(基础信息、交互记录、标签分类)
- 销售机会跟踪(商机阶段、预计金额、成交概率)
- 数据分析看板(客户分布、转化率、业绩趋势)
- 权限控制系统(RBAC模型、JWT认证、动态路由)
技术栈选型经过了我们团队的多次技术论证:
- 后端选择SpringBoot 2.7.x版本,这是目前企业开发中最稳定的选择
- 前端采用Vue 3.x组合Element Plus,比React更适合快速开发管理后台
- 持久层使用MyBatis而非JPA,因为CRM系统存在大量复杂查询场景
- MySQL 8.0作为主数据库,其窗口函数对分析型查询非常友好
实际开发中发现,Vue 3的Composition API相比Options API更适合复杂业务逻辑的组织,建议新项目直接采用setup语法糖写法。
2. 核心数据模型设计
2.1 客户主数据表结构
客户表(client_info)的设计经历了三次迭代优化,最终版包含以下关键字段:
sql复制CREATE TABLE `client_info` (
`client_id` varchar(32) NOT NULL COMMENT '客户UUID主键',
`client_name` varchar(100) NOT NULL COMMENT '客户全称',
`short_name` varchar(50) DEFAULT NULL COMMENT '客户简称',
`credit_code` varchar(18) DEFAULT NULL COMMENT '统一社会信用代码',
`industry_id` int DEFAULT NULL COMMENT '行业分类ID',
`region_path` varchar(255) DEFAULT NULL COMMENT '地区路径(省-市-区)',
`address` varchar(255) DEFAULT NULL COMMENT '详细地址',
`contact_person` varchar(50) DEFAULT NULL COMMENT '主要联系人',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`email` varchar(100) DEFAULT NULL COMMENT '电子邮箱',
`client_level` tinyint DEFAULT '1' COMMENT '客户等级(1-5)',
`client_status` tinyint DEFAULT '1' COMMENT '状态(1正常2流失3黑名单)',
`creator_id` varchar(32) DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`client_id`),
KEY `idx_industry` (`industry_id`),
KEY `idx_region` (`region_path`(20)),
KEY `idx_level_status` (`client_level`,`client_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
几个设计要点:
- 使用varchar(32)存储UUID而非自增ID,便于数据迁移和分布式部署
- region_path采用"省-市-区"格式存储,比单独存三个字段更易查询
- 创建时间和更新时间自动维护,避免业务代码处理
- 建立了复合索引提升常用查询效率
2.2 交互记录表设计
客户交互表(client_interaction)的设计特别注意了审计追踪需求:
sql复制CREATE TABLE `client_interaction` (
`interaction_id` varchar(32) NOT NULL,
`client_id` varchar(32) NOT NULL,
`interaction_type` tinyint NOT NULL COMMENT '1电话2邮件3拜访4线上',
`subject` varchar(200) NOT NULL COMMENT '交互主题',
`content` text COMMENT '详细内容',
`next_contact_time` datetime DEFAULT NULL COMMENT '下次联系时间',
`important_level` tinyint DEFAULT '1' COMMENT '重要程度1-5',
`attachment_ids` varchar(512) DEFAULT NULL COMMENT '附件ID列表,逗号分隔',
`operator_id` varchar(32) NOT NULL COMMENT '操作人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`interaction_id`),
KEY `idx_client` (`client_id`),
KEY `idx_next_time` (`next_contact_time`),
KEY `idx_operator` (`operator_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
实际使用中发现text类型字段在列表查询时会影响性能,后来优化为:
- 主表只存content的前200字符作为摘要
- 详细内容移到单独的client_interaction_detail表
- 通过interaction_id关联查询
3. 前后端接口规范
3.1 RESTful API设计
后端接口严格遵循RESTful规范,以客户资源为例:
code复制GET /api/clients - 客户列表查询
POST /api/clients - 创建新客户
GET /api/clients/{id} - 获取客户详情
PUT /api/clients/{id} - 全量更新客户
PATCH /api/clients/{id} - 部分更新客户
DELETE /api/clients/{id} - 删除客户
GET /api/clients/export - 导出客户列表
列表查询支持以下通用参数:
- page:页码(默认1)
- size:每页条数(默认20)
- sort:排序字段(如sort=createTime,desc)
- search:模糊搜索(所有字符串字段)
- filter:精确过滤(如filter=clientLevel=3,status=1)
响应统一采用以下格式:
json复制{
"code": 200,
"message": "success",
"data": {...},
"timestamp": 1630000000000
}
3.2 前端请求封装
前端对axios进行了二次封装,主要增强功能包括:
- 请求拦截器 - 自动添加JWT Token
javascript复制service.interceptors.request.use(config => {
const token = store.getters.token
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
- 响应拦截器 - 统一错误处理
javascript复制service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || 'Error')
return Promise.reject(new Error(res.message || 'Error'))
}
return res
},
error => {
if (error.response.status === 401) {
// token过期处理
}
ElMessage.error(error.message)
return Promise.reject(error)
}
)
- API模块化组织
javascript复制// src/api/client.js
import request from '@/utils/request'
export function listClients(params) {
return request({
url: '/api/clients',
method: 'get',
params
})
}
export function createClient(data) {
return request({
url: '/api/clients',
method: 'post',
data
})
}
4. 权限控制系统实现
4.1 后端权限控制
采用Spring Security + JWT实现安全认证,核心配置如下:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll();
http.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
自定义权限注解实现方法级控制:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@permissionService.hasPermission(#root, 'client:manage')")
public @interface RequireClientPermission {}
4.2 前端动态路由
根据用户权限动态生成路由表:
javascript复制// 过滤异步路由表
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = {...route}
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
菜单数据格式示例:
json复制{
"path": "/client",
"component": "Layout",
"meta": {
"title": "客户管理",
"icon": "user",
"permission": ["client:view"]
},
"children": [
{
"path": "list",
"component": "client/list",
"meta": {
"title": "客户列表",
"permission": ["client:list"]
}
}
]
}
5. 系统部署方案
5.1 生产环境部署架构
推荐采用以下部署方案:
code复制客户端 → Nginx(负载均衡) → 前端静态资源
↓
SpringBoot应用集群(2-4节点)
↓
MySQL主从集群(1主2从)
↓
Redis哨兵集群(3节点)
5.2 Docker部署配置
后端Dockerfile示例:
dockerfile复制FROM openjdk:11-jre
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
前端Dockerfile示例:
dockerfile复制FROM nginx:alpine
COPY dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx配置关键点:
nginx复制server {
listen 80;
server_name crm.example.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.3 性能优化实践
- 数据库层面:
- 使用连接池控制连接数(HikariCP推荐配置)
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
- JVM参数优化:
bash复制java -Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 ...
- 前端懒加载:
javascript复制const ClientList = () => import('@/views/client/list')
6. 典型问题解决方案
6.1 跨域问题处理
SpringBoot后端配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.allowCredentials(true)
.maxAge(3600);
}
}
开发环境Vue代理配置(vue.config.js):
javascript复制devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
6.2 大文件上传处理
前端采用分片上传:
javascript复制const chunkSize = 2 * 1024 * 1024 // 2MB
async function uploadFile(file) {
const chunks = Math.ceil(file.size / chunkSize)
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize
const end = Math.min(file.size, start + chunkSize)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkNumber', i + 1)
formData.append('totalChunks', chunks)
formData.append('identifier', file.uniqueIdentifier)
await uploadChunk(formData)
}
await mergeChunks(file.name, file.uniqueIdentifier, chunks)
}
后端接收处理:
java复制@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file,
@RequestParam int chunkNumber,
@RequestParam int totalChunks,
@RequestParam String identifier) {
// 存储分片到临时目录
// 返回当前上传进度
}
@PostMapping("/merge")
public Result merge(@RequestParam String filename,
@RequestParam String identifier,
@RequestParam int totalChunks) {
// 合并所有分片
// 返回最终文件路径
}
6.3 数据导出性能优化
对于大数据量导出,采用以下方案:
- 后端使用POI的SXSSFWorkbook进行流式导出
java复制SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 缓存100行到内存
Sheet sheet = workbook.createSheet("客户数据");
// 分批查询数据写入
workbook.write(outputStream);
workbook.dispose(); // 清理临时文件
- 前端通过Blob对象下载:
javascript复制axios({
method: 'get',
url: '/api/clients/export',
responseType: 'blob'
}).then(response => {
const url = window.URL.createObjectURL(new Blob([response.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'clients.xlsx')
document.body.appendChild(link)
link.click()
})
- 服务端添加异步导出任务:
- 用户触发导出请求
- 后端生成任务ID并放入消息队列
- 异步处理完成后通知前端下载
- 下载链接保留24小时自动失效
7. 项目开发经验总结
7.1 技术选型反思
- Vue 3 + TypeScript组合在实际开发中显著提升了代码质量,特别是对于复杂表单验证场景
- MyBatis-Plus简化了大量CRUD操作,但对于复杂联表查询仍需要手写XML
- Element Plus的Pro版本表格组件在处理万级数据时性能明显优于基础版
- 下次类似项目会考虑引入WebSocket实现实时通知功能
7.2 团队协作建议
- 前后端定义好API契约后使用Swagger/YAPI等工具维护文档
- 制定严格的Git分支管理策略(我们采用Git Flow变种)
- 代码规范检查必须前置到提交阶段(Husky + ESLint)
- 每日构建的Docker镜像有助于及早发现集成问题
7.3 性能监控方案
生产环境建议部署:
- Spring Boot Actuator + Prometheus监控JVM指标
- SkyWalking或Zipkin进行分布式链路追踪
- 关键业务接口添加@Log注解记录执行时间
- 前端使用Sentry捕获运行时错误
日志收集方案:
yaml复制logging:
level:
root: info
com.example.crm: debug
file:
name: logs/app.log
max-size: 50MB
max-history: 30
logback:
rollingpolicy:
max-file-size: 50MB
total-size-cap: 1GB
这套系统经过三个月的开发和优化,目前已在多家中小型企业稳定运行,日均处理客户交互记录超过2万条。最大的收获是验证了前后端分离架构在中型管理系统中的可行性,特别是在需要频繁迭代的业务场景下,这种架构的优势更加明显。