作为一名长期奋战在企业信息化建设一线的开发者,我深知客户管理对于企业发展的重要性。在最近为某中型制造企业实施的数字化改造项目中,我们团队基于SpringBoot+Vue技术栈开发了一套完整的客户管理系统,成功将企业原本分散在20多个Excel文件中的客户数据实现了统一管理。系统上线后,客户信息查询效率提升300%,销售团队跟进及时率提高45%,这个案例让我深刻体会到一套设计良好的客户管理系统对企业运营的实际价值。
传统手工管理模式下,企业常面临以下痛点:
我们设计的这套系统正是为了解决这些实际问题而生。系统采用前后端分离架构,后端基于SpringBoot 2.7提供RESTful API服务,前端使用Vue 3组合式API开发,数据库选用MySQL 8.0,整体技术选型兼顾了性能、可维护性和开发效率。下面我将从架构设计到具体实现,详细分享这个项目的开发经验。
系统采用经典的三层架构模式,但在实现上做了针对性优化:
code复制客户端层(Web前端)
↑↓ HTTP/HTTPS
应用服务层(SpringBoot)
↑↓ JDBC
数据存储层(MySQL)
前端采用Vue 3 + TypeScript + Pinia状态管理,配合Element Plus组件库,这种组合的选择主要基于:
后端架构特别设计了双安全校验机制:
这种设计确保即使前端校验被绕过,后端仍有完善的安全防护。我们在压力测试中验证,单节点配置(4核8G)可支持800+并发请求,满足中型企业使用需求。
数据库设计遵循第三范式但适当做了反范式优化。以客户基础信息表为例,我们做了这些特殊设计:
sql复制CREATE TABLE `client_info` (
`client_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '客户编号',
`client_name` VARCHAR(100) NOT NULL COMMENT '客户名称',
`contact_person` VARCHAR(50) COMMENT '联系人',
`contact_phone` VARCHAR(20) NOT NULL COMMENT '联系电话',
`industry_type` VARCHAR(50) COMMENT '行业分类',
`client_level` TINYINT DEFAULT 1 COMMENT '客户等级(1-5)',
`client_address` VARCHAR(200) COMMENT '地址',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除标记',
PRIMARY KEY (`client_id`),
UNIQUE KEY `idx_phone` (`contact_phone`),
KEY `idx_industry` (`industry_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
几个关键设计决策:
前端采用动态表单设计,根据客户类型显示不同字段。关键实现代码:
vue复制<template>
<el-form :model="clientForm" :rules="rules" ref="formRef">
<el-form-item label="客户类型" prop="clientType">
<el-radio-group v-model="clientForm.clientType" @change="changeClientType">
<el-radio-button label="ENTERPRISE">企业客户</el-radio-button>
<el-radio-button label="PERSONAL">个人客户</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="clientForm.clientType === 'ENTERPRISE'">
<el-form-item label="企业名称" prop="clientName">
<el-input v-model="clientForm.clientName" />
</el-form-item>
<el-form-item label="统一社会信用代码" prop="creditCode">
<el-input v-model="clientForm.creditCode" />
</el-form-item>
</template>
</el-form>
</template>
<script setup>
// 根据客户类型动态变更校验规则
const rules = computed(() => ({
clientName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
creditCode: clientForm.clientType === 'ENTERPRISE'
? [{ required: true, validator: checkCreditCode, trigger: 'blur' }]
: []
}))
</script>
后端采用Spring Validation进行二次校验:
java复制@PostMapping("/clients")
public Result addClient(@Valid @RequestBody ClientDTO clientDTO) {
// 校验信用代码唯一性
if (clientService.existsByCreditCode(clientDTO.getCreditCode())) {
throw new BusinessException("该统一社会信用代码已存在");
}
return Result.success(clientService.saveClient(clientDTO));
}
通过自定义注解实现字段级权限控制:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface PermissionField {
String[] roles() default {};
String[] permissions() default {};
}
// 在DTO中使用
public class ClientDTO {
@PermissionField(roles = {"ADMIN", "MANAGER"})
private BigDecimal contractAmount;
@PermissionField(permissions = {"client:view:contact"})
private String contactPhone;
}
通过AOP拦截处理:
java复制@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public Object checkFieldPermission(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
if (result instanceof Result) {
Object data = ((Result) result).getData();
// 过滤无权限字段
FieldPermissionUtils.filterFields(data, getCurrentUser());
}
return result;
}
前端使用el-timeline组件展示交互记录:
vue复制<el-timeline>
<el-timeline-item
v-for="(record, index) in records"
:key="index"
:timestamp="formatTime(record.interactTime)"
placement="top"
>
<el-card>
<template #header>
<div class="flex-between">
<span>{{ record.interactType | interactTypeFilter }}</span>
<el-tag :type="record.followUpFlag ? 'danger' : 'success'">
{{ record.followUpFlag ? '需跟进' : '已完成' }}
</el-tag>
</div>
</template>
<div v-html="record.interactContent" />
</el-card>
</el-timeline-item>
</el-timeline>
后端采用MyBatis动态SQL实现复杂查询:
xml复制<select id="selectInteractionRecords" resultType="InteractionRecordVO">
SELECT
r.record_id, r.interact_type, r.interact_content,
r.interact_time, r.follow_up_flag,
s.staff_name, s.avatar,
c.client_name
FROM interaction_record r
LEFT JOIN staff_info s ON r.staff_id = s.staff_id
LEFT JOIN client_info c ON r.client_id = c.client_id
<where>
<if test="clientId != null">
AND r.client_id = #{clientId}
</if>
<if test="staffId != null">
AND r.staff_id = #{staffId}
</if>
<if test="startTime != null and endTime != null">
AND r.interact_time BETWEEN #{startTime} AND #{endTime}
</if>
<if test="followUpFlag != null">
AND r.follow_up_flag = #{followUpFlag}
</if>
</where>
ORDER BY r.interact_time DESC
</select>
使用Spring Scheduled实现定时提醒:
java复制@Scheduled(cron = "0 0 9 * * ?") // 每天9点执行
public void checkFollowUpTasks() {
List<InteractionRecord> records = interactionMapper.selectNeedFollowUp();
records.forEach(record -> {
String title = "待跟进提醒:" + record.getClientName();
String content = String.format(
"您与客户【%s】的%s记录需要跟进,交互时间:%s",
record.getClientName(),
record.getInteractType(),
DateFormatUtils.format(record.getInteractTime(), "yyyy-MM-dd HH:mm")
);
messageService.pushNotification(
record.getStaffId(),
title,
content
);
});
}
系统实现了五层安全防护:
JWT刷新机制实现代码:
java复制public String refreshToken(String oldToken) {
String username = jwtUtil.getUsernameFromToken(oldToken);
UserDetails userDetails = userService.loadUserByUsername(username);
if (!jwtUtil.validateToken(oldToken, userDetails)) {
throw new AuthenticationException("无效的token");
}
if (jwtUtil.canTokenBeRefreshed(oldToken)) {
return jwtUtil.generateToken(userDetails);
}
throw new AuthenticationException("token已过期,需重新登录");
}
采用多级缓存架构:
Redis缓存配置示例:
java复制@Cacheable(value = "client", key = "#clientId", unless = "#result == null")
public ClientDetailVO getClientDetail(Long clientId) {
return clientMapper.selectDetailById(clientId);
}
@CacheEvict(value = "client", key = "#clientId")
public void updateClient(ClientDTO clientDTO) {
clientMapper.updateById(clientDTO);
}
针对大数据量查询做了以下优化:
游标分页实现:
sql复制-- 第一页
SELECT * FROM client_info
WHERE client_id > 0 AND industry_type = 'IT'
ORDER BY client_id ASC
LIMIT 20;
-- 后续页
SELECT * FROM client_info
WHERE client_id > 上一页最后一条记录的ID AND industry_type = 'IT'
ORDER BY client_id ASC
LIMIT 20;
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
MYSQL_DATABASE: crm
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
mysql_data:
redis_data:
集成Prometheus + Grafana监控体系:
java复制@Configuration
@EnablePrometheusEndpoint
@EnableSpringBootMetricsCollector
public class PrometheusConfig {
@Bean
public CollectorRegistry collectorRegistry() {
return CollectorRegistry.defaultRegistry;
}
}
监控指标包括:
接口文档示例:
yaml复制paths:
/api/v1/clients:
get:
tags: [Client]
summary: 获取客户列表
parameters:
- $ref: '#/components/parameters/pageNum'
- $ref: '#/components/parameters/pageSize'
responses:
200:
description: 客户列表
content:
application/json:
schema:
$ref: '#/components/schemas/PageResult«ClientVO»'
问题1:MyBatis查询结果映射不全
xml复制<resultMap id="clientMap" type="ClientVO">
<result column="client_name" property="clientName" />
<result column="contact_phone" property="phone" />
</resultMap>
问题2:Vue响应式数据不更新
javascript复制// 错误方式
this.list[0] = newValue
// 正确方式
this.$set(this.list, 0, newValue)
问题3:Spring事务失效
java复制@Transactional(rollbackFor = Exception.class)
public void updateClient(ClientDTO dto) {
// 业务逻辑
}
技术选型建议:
在项目实际开发过程中,我们团队总结出最重要的经验是:不要过度追求技术新颖性,而应该选择团队熟悉且社区支持良好的技术栈。比如我们最初考虑使用GraphQL替代RESTful API,但评估团队学习成本后还是选择了更成熟的RESTful风格,这个决策为项目节省了大量调试时间。