1. 项目概述与技术选型
这个CRM客户管理系统是我去年为一家中型贸易公司开发的内部管理工具,核心目标是解决他们客户信息分散、跟进记录混乱的问题。系统采用JavaWeb技术栈,基于SSM(Spring+SpringMVC+MyBatis)框架搭建,数据存储使用MySQL,项目管理采用Maven。这种技术组合在当前企业级Java开发中非常典型,特别适合需要快速迭代的中小型项目。
为什么选择SSM而不是Spring Boot?在项目启动的2022年,该公司IT部门已有成熟的Tomcat运维体系,且团队成员对SSM更熟悉。虽然Spring Boot的自动化配置更便捷,但SSM的显式配置反而让部署和问题排查更透明。数据库选择MySQL 5.7而非8.0版本,主要是考虑到与现有其他系统的兼容性。
提示:新手常犯的错误是盲目追求最新技术版本。在实际企业环境中,稳定性、团队熟悉度和周边系统兼容性往往比技术新颖性更重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建与项目初始化
2.1 基础环境配置
开发机器配置了JDK 1.8(注意不是更高版本,因为部分依赖库对Java 11+支持不完善)、Apache Maven 3.6.3和MySQL 5.7。使用IntelliJ IDEA作为IDE,相比Eclipse对Maven项目的支持更友好。以下是关键的环境验证命令:
bash复制# Java版本验证
java -version
# 输出应包含"1.8.0_xxx"
# Maven验证
mvn -v
# 应显示Apache Maven 3.6.3
# MySQL登录
mysql -u root -p
# 成功登录后执行
SELECT VERSION();
# 应返回5.7.x版本号
2.2 Maven项目骨架构建
使用maven-archetype-webapp原型创建项目骨架:
bash复制mvn archetype:generate -DgroupId=com.company.crm
-DartifactId=crm-system
-DarchetypeArtifactId=maven-archetype-webapp
-DinteractiveMode=false
生成的pom.xml需要添加SSM相关依赖。特别注意Spring版本统一管理,避免不同子模块版本冲突:
xml复制<properties>
<spring.version>5.2.8.RELEASE</spring.version>
<mybatis.version>3.5.6</mybatis.version>
</properties>
<dependencies>
<!-- Spring核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 其他必要依赖... -->
</dependencies>
3. 数据库设计与实现
3.1 核心表结构设计
系统包含12张核心表,其中客户(customer)和跟进记录(follow_up)是最关键的表:
sql复制CREATE TABLE `customer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '客户名称',
`industry` varchar(20) DEFAULT NULL COMMENT '所属行业',
`credit_rating` enum('A','B','C','D') DEFAULT 'B' COMMENT '信用评级',
`contact_person` varchar(20) DEFAULT NULL COMMENT '联系人',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`address` varchar(200) DEFAULT NULL COMMENT '地址',
`creator_id` int(11) NOT 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 (`id`),
KEY `idx_industry` (`industry`),
KEY `idx_creator` (`creator_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户基本信息表';
CREATE TABLE `follow_up` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`customer_id` int(11) NOT NULL COMMENT '客户ID',
`content` text NOT NULL COMMENT '跟进内容',
`next_contact_time` datetime DEFAULT NULL COMMENT '下次联系时间',
`status` enum('PENDING','COMPLETED','CANCELLED') DEFAULT 'PENDING',
`creator_id` int(11) NOT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_customer` (`customer_id`),
KEY `idx_next_time` (`next_contact_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户跟进记录表';
3.2 数据库连接池配置
在Spring配置文件中配置Druid连接池(比传统的HikariCP更适合监控需求):
xml复制<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/crm_db?useSSL=false&characterEncoding=utf8"/>
<property name="username" value="crm_user"/>
<property name="password" value="Crm@1234"/>
<property name="initialSize" value="5"/>
<property name="minIdle" value="5"/>
<property name="maxActive" value="20"/>
<property name="maxWait" value="60000"/>
<property name="validationQuery" value="SELECT 1"/>
<property name="testWhileIdle" value="true"/>
<property name="timeBetweenEvictionRunsMillis" value="3600000"/>
</bean>
注意:生产环境必须将密码加密存储,可以使用Druid的ConfigFilter配合加密工具实现。
4. SSM框架整合关键点
4.1 Spring与MyBatis整合
在applicationContext.xml中配置SqlSessionFactoryBean时,特别注意mapperLocations的配置方式:
xml复制<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations">
<array>
<!-- 这种Ant风格路径比直接指定包名更可靠 -->
<value>classpath*:mapper/**/*Mapper.xml</value>
</array>
</property>
<property name="typeAliasesPackage" value="com.company.crm.model"/>
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true"/>
<property name="defaultFetchSize" value="100"/>
<property name="logPrefix" value="CRM_DAO_"/>
</bean>
</property>
</bean>
4.2 事务管理配置
声明式事务配置中特别注意隔离级别和超时设置:
xml复制<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true" timeout="10"/>
<tx:method name="query*" read-only="true" timeout="10"/>
<tx:method name="list*" read-only="true" timeout="10"/>
<tx:method name="*" isolation="READ_COMMITTED"
timeout="30" propagation="REQUIRED"
rollback-for="java.lang.Exception"/>
</tx:attributes>
</tx:advice>
4.3 Spring MVC配置要点
在spring-mvc.xml中配置静态资源处理时,特别注意缓存策略:
xml复制<mvc:resources mapping="/static/**" location="/static/"
cache-period="2592000" cache-control="max-age=2592000"/>
<!-- 避免Jackson的日期序列化问题 -->
<bean id="jacksonObjectMapper" class="com.company.crm.config.CustomObjectMapper"/>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="jacksonObjectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
5. 核心功能实现细节
5.1 客户分页查询实现
CustomerServiceImpl中的分页查询方法结合了MyBatis物理分页和业务逻辑:
java复制@Override
public PageResult<CustomerVO> queryCustomers(CustomerQuery query, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
try {
List<Customer> customers = customerMapper.selectByQuery(query);
PageInfo<Customer> pageInfo = new PageInfo<>(customers);
// 转换VO并处理敏感信息
List<CustomerVO> voList = customers.stream().map(c -> {
CustomerVO vo = new CustomerVO();
BeanUtils.copyProperties(c, vo);
if (!SecurityUtils.isAdmin()) {
vo.setContactPhone(StringUtils.maskPhone(vo.getContactPhone()));
}
return vo;
}).collect(Collectors.toList());
return new PageResult<>(voList, pageInfo.getTotal());
} finally {
PageHelper.clearPage(); // 确保ThreadLocal被清除
}
}
对应的MyBatis动态SQL:
xml复制<select id="selectByQuery" resultMap="BaseResultMap" parameterType="com.company.crm.query.CustomerQuery">
SELECT * FROM customer
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="industry != null and industry != ''">
AND industry = #{industry}
</if>
<if test="creatorId != null">
AND creator_id = #{creatorId}
</if>
<if test="creditRating != null and creditRating != ''">
AND credit_rating = #{creditRating}
</if>
</where>
ORDER BY update_time DESC
</select>
5.2 跟进记录的时间轴展示
前端采用Timeline组件展示客户跟进记录时,后端需要特殊处理时间分组:
java复制public List<FollowUpGroupVO> getFollowUpTimeline(Integer customerId) {
List<FollowUp> records = followUpMapper.selectByCustomerId(customerId);
// 按自然日分组
Map<LocalDate, List<FollowUpVO>> groupMap = records.stream()
.collect(Collectors.groupingBy(
r -> r.getCreateTime().toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate(),
Collectors.mapping(this::convertToVO, Collectors.toList())
));
// 转换为前端需要的结构
return groupMap.entrySet().stream()
.sorted(Map.Entry.<LocalDate, List<FollowUpVO>>comparingByKey().reversed())
.map(entry -> new FollowUpGroupVO(
entry.getKey().format(DateTimeFormatter.ISO_DATE),
entry.getValue()))
.collect(Collectors.toList());
}
6. 项目部署与性能优化
6.1 Tomcat生产环境配置
在server.xml中优化Connector配置:
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="200"
minSpareThreads="20"
acceptCount="100"
maxConnections="1000"
compression="on"
compressionMinSize="2048"
compressableMimeType="text/html,text/xml,text/css,application/javascript,application/json"
URIEncoding="UTF-8"/>
6.2 JVM参数调优
在catalina.sh中添加JVM参数:
bash复制JAVA_OPTS="-server -Xms2g -Xmx2g -XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m -XX:+UseG1GC
-XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4
-XX:ConcGCThreads=2 -XX:InitiatingHeapOccupancyPercent=70
-XX:+DisableExplicitGC -Djava.awt.headless=true"
6.3 MySQL性能优化
针对CRM系统的特点,在my.cnf中添加以下优化参数:
ini复制[mysqld]
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_read_io_threads = 8
innodb_write_io_threads = 4
query_cache_type = 0
# 针对CRM的特定优化
join_buffer_size = 2M
sort_buffer_size = 2M
read_rnd_buffer_size = 2M
7. 常见问题排查与解决
7.1 中文乱码问题全解决方案
-
数据库层面:
- 确认建表时使用utf8mb4字符集
- 连接字符串添加参数:
useUnicode=true&characterEncoding=UTF-8
-
Java应用层面:
- 在JVM启动参数添加:
-Dfile.encoding=UTF-8 - 检查所有Filter是否设置了request/response的编码
- 在JVM启动参数添加:
-
JSP页面:
- 页面顶部添加:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> - HTML中添加:
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- 页面顶部添加:
7.2 Maven依赖冲突解决
使用mvn dependency:tree分析依赖树,常见冲突解决方式:
- 排除特定传递依赖:
xml复制<dependency>
<groupId>com.some.group</groupId>
<artifactId>some-artifact</artifactId>
<exclusions>
<exclusion>
<groupId>conflict.group</groupId>
<artifactId>conflict-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
- 使用dependencyManagement统一版本:
xml复制<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
</dependencies>
</dependencyManagement>
7.3 生产环境问题排查清单
-
突然响应变慢:
- 检查数据库连接池状态:
SHOW STATUS LIKE 'Threads_connected' - 分析慢查询日志:
mysqldumpslow -s t /var/log/mysql/mysql-slow.log - 检查JVM内存使用:
jstat -gcutil <pid> 1000 5
- 检查数据库连接池状态:
-
定时任务不执行:
- 确认Quartz表锁是否正确释放:
SELECT * FROM QRTZ_LOCKS - 检查服务器时间是否同步:
ntpdate -q pool.ntp.org
- 确认Quartz表锁是否正确释放:
-
文件上传失败:
- 检查Tomcat临时目录权限
- 确认spring配置的maxUploadSize足够大
- 检查磁盘空间:
df -h
