简介:这份资源是面向计算机专业学生与Java初学者的一份完整毕业设计文档,主题为基于Java的客户关系管理系统,适合需要完成课程设计、毕业设计或学习B/S架构开发的人群参考。压缩包内共1个docx文件,约3.84MB,内容涵盖系统从需求分析到实现测试的完整论述。文档详细介绍了采用B/S开发模式、在IDEA环境下使用Java语言编码、以MySQL管理数据、通过JSP技术搭建功能架构并借助Tomcat发布系统的全过程,并展示了主要功能模块的设计界面与操作界面。读者可从中获取数据库表结构规划、客户信息管理、销售机会跟踪、服务请求处理等功能模块的划分思路,以及单元测试、集成测试和系统测试的排错方法,对理解Java Web项目开发流程与论文写作结构具有实际参考价值。目前已有71人学习下载。
1. 从一份 docx 需求文档到能跑的 CRM:Java 技术选型的真实取舍
很多人的毕业设计或课程设计题目里都躺着「基于 Java 的客户关系管理系统设计与实现」这几个字,但真正动手时才发现,难点从来不是写几个增删改查,而是想清楚客户、联系人、商机、跟进记录这几张表怎么串起来,以及权限和数据隔离怎么落地。CRM 的本质是把「客户信息」变成「可追踪的销售过程」,它要解决的是线索散落在 Excel、微信、纸质名片里,销售离职就带走一片客户的问题。这套系统适合中小企业内部使用,也适合作为 Java Web 全栈练手项目——它覆盖了权限模型、多表关联、分页查询、数据统计这些真实业务里绕不开的东西。下面我按自己做过几版的思路,把选型、建表、编码、踩坑一条条讲清楚,你照着能跑起来一套最小可用的版本。
2. 需求拆解与技术栈定型:别一上来就堆框架
2.1 先分清 CRM 里到底有哪几类角色和动作
拿到题目别急着建工程,先把业务角色和动作列清楚,否则后面表结构一定返工。一个最小可用的 CRM 通常有三类角色:管理员、销售主管、普通销售。管理员管账号和权限,销售主管看团队业绩、分配客户,普通销售只管自己名下的客户和跟进记录。对应的核心动作有:录入客户、给客户打标签和分级、记录每次跟进、把客户推进到不同商机阶段、按条件筛选和统计。
这里有个反直觉的点:客户和联系人不该合成一张表。一个客户公司可能对应多个联系人,销售打电话找的是人,签合同看的是公司。把两者分开,跟进记录挂在联系人上,商机挂在客户上,后面统计「某客户一共跟进了几次」才不会算错。常见做法是四张主表打底:customer(客户)、contact(联系人)、follow_record(跟进记录)、opportunity(商机),再加 user、role、permission 做权限。
2.2 技术栈怎么选才不给自己挖坑
题目写的是「基于 Java」,那后端就是 Java。但 Java 生态里能选的组合太多,选错了写到一半就想重来。我的建议是:Spring Boot + MyBatis-Plus + MySQL + Spring Security + Thymeleaf 或前后端分离的 Vue。为什么这么配,逐条说理由。
Spring Boot 省掉大量 XML 配置,内嵌 Tomcat 直接java -jar就能跑,对课程设计和中小项目最友好。持久层用 MyBatis-Plus 而不是纯 JPA,是因为 CRM 里大量是「按条件动态拼查询」——今天按客户等级筛,明天按跟进时间筛,MyBatis 的动态 SQL 写起来直观,分页插件也现成。权限用 Spring Security,虽然学习曲线陡,但它是 Java 里最标准的方案,面试和实际工作都用得上,比手写拦截器靠谱。前端如果时间紧就用 Thymeleaf 服务端渲染,想练前后端分离就上 Vue,接口用 RESTful 风格。
数据库选 MySQL 8,字符集用 utf8mb4,别用 utf8——后者存不了 emoji,客户备注里一个表情就能让插入报错,这是血泪经验。JDK 用 17 或 21 都行,Spring Boot 3.x 要求 JDK 17 起步,如果学校机房还是 JDK 8,那就锁 Spring Boot 2.7.x,别硬上新版本。
2.3 工程骨架和依赖清单
确定技术栈后,用 Spring Initializr 生成骨架,或者在 IDE 里新建 Maven 项目。核心依赖如下,直接抄进 pom.xml:
<!-- Spring Boot 父依赖,锁定版本,避免依赖冲突 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <!-- Web 层:提供 REST 接口和内嵌 Tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 安全框架:登录认证 + 方法级权限 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- MyBatis-Plus:简化 CRUD 和分页 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- 参数校验,@NotBlank 这类注解靠它 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>这段配置里,spring-boot-starter-parent的版本决定了整个项目的依赖基线,选 2.7.18 是为了兼容 JDK 8 到 17 的过渡期。MyBatis-Plus 的版本要和你 Spring Boot 版本匹配,3.5.x 对应 Boot 2.x,如果升到 Boot 3.x 要换成 mybatis-plus-spring-boot3-starter。MySQL 驱动从 8.x 开始 groupId 变成了com.mysql,老教程里的mysql-connector-java已经废弃,照抄老配置会报找不到驱动。
配置文件 application.yml 里几个关键项:
spring: datasource: url: jdbc:mysql://localhost:3306/crm?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true # 数据库下划线字段自动映射 Java 驼峰 global-config: db-config: logic-delete-field: deleted # 逻辑删除字段,删数据不真删 logic-delete-value: 1 logic-not-delete-value: 0serverTimezone必须显式指定,否则 MySQL 8 连接时会因为时区问题报错。map-underscore-to-camel-case打开后,数据库的create_time会自动映射到 Java 的createTime,省掉一堆手动映射。逻辑删除是 CRM 的刚需——客户被「删除」后其实要留档,销售主管可能还要查历史,所以用deleted字段标记而不是物理删除。
3. 数据库设计:客户、联系人、跟进记录怎么关联才不返工
3.1 核心表结构与字段说明
表设计是 CRM 的地基,改表比改代码痛苦十倍。下面给出四张核心表的建表语句,字段都加了注释说明用途。
-- 客户表:一家公司或一个客户主体 CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '客户名称', industry VARCHAR(50) COMMENT '所属行业', level TINYINT DEFAULT 3 COMMENT '客户等级 1重要 2普通 3潜在', owner_id BIGINT NOT NULL COMMENT '负责销售的 user_id', remark VARCHAR(500) COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标记', INDEX idx_owner (owner_id) -- 按负责人查客户是高频操作,必须建索引 ); -- 联系人表:挂在客户下的具体人 CREATE TABLE contact ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL COMMENT '所属客户', name VARCHAR(50) NOT NULL COMMENT '联系人姓名', phone VARCHAR(20) COMMENT '电话', position VARCHAR(50) COMMENT '职位', deleted TINYINT DEFAULT 0, INDEX idx_customer (customer_id) ); -- 跟进记录表:每次沟通留痕 CREATE TABLE follow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, contact_id BIGINT COMMENT '本次跟进的联系人,可为空', content VARCHAR(1000) NOT NULL COMMENT '跟进内容', follow_time DATETIME NOT NULL COMMENT '跟进时间', create_by BIGINT NOT NULL COMMENT '记录人', deleted TINYINT DEFAULT 0, INDEX idx_customer_time (customer_id, follow_time) -- 联合索引,按客户查时间线 );customer表的owner_id是数据隔离的关键——普通销售只能查owner_id = 自己的客户,主管能查全部。follow_record上建了(customer_id, follow_time)联合索引,因为最常用的查询是「某客户按时间倒序的跟进记录」,联合索引能直接命中,不用回表排序。level用 TINYINT 而不是字符串,省空间且排序快,前端展示时再转成「重要/普通/潜在」。
3.2 数据隔离和权限的落地方式
数据隔离不能只靠前端隐藏按钮,必须在 SQL 层强制过滤。MyBatis-Plus 提供了@InterceptorIgnore和自定义拦截器,但更简单可靠的做法是在 Service 层统一拼条件。我一般会写一个DataScopeHelper,根据当前登录用户的角色决定加不加owner_id条件:
// 根据当前用户角色,给查询条件追加数据范围限制 public static <T extends QueryWrapper<Customer>> T applyScope(T wrapper) { LoginUser user = SecurityUtils.getLoginUser(); // 管理员和主管看全部,普通销售只看自己 if (user.hasRole("ADMIN") || user.hasRole("MANAGER")) { return wrapper; } return wrapper.eq("owner_id", user.getUserId()); }这段逻辑的核心是:权限判断放在服务端,前端传什么参数都不影响。SecurityUtils.getLoginUser()从 Spring Security 的上下文里取当前用户,角色判断用hasRole。注意别把这段逻辑写在 Controller 里,否则每个接口都要复制一遍,漏一个就是越权漏洞。放在 Service 的查询入口统一调用,改规则时只改一处。
3.3 分页查询和动态条件的实现
CRM 里最常见的操作是「按客户名、等级、负责人筛选,分页展示」。用 MyBatis-Plus 的分页插件,先配置拦截器:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件,指定数据库类型为 MySQL interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后 Service 里这样写查询:
public Page<Customer> pageQuery(CustomerQuery query, int pageNum, int pageSize) { QueryWrapper<Customer> wrapper = new QueryWrapper<>(); // 动态条件:参数不为空才拼进 SQL,避免全表扫描 wrapper.like(StringUtils.hasText(query.getName()), "name", query.getName()); wrapper.eq(query.getLevel() != null, "level", query.getLevel()); // 应用数据隔离 DataScopeHelper.applyScope(wrapper); wrapper.orderByDesc("create_time"); return customerMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); }like和eq的第一个参数是布尔条件,只有为 true 时才拼进 SQL,这是 MyBatis-Plus 处理动态条件的标准写法,比在 XML 里写一堆<if>清爽。orderByDesc("create_time")保证新客户排前面。分页参数pageNum从 1 开始,pageSize建议限制上限(比如最大 100),否则前端传个 100000 就能把数据库拖垮。
4. 避坑与排查:那些让我加班到凌晨的细节
4.1 中文乱码:从数据库到响应全链路排查
现象:客户名存进去是「张三」,查出来变成「å¼ ä¸‰」。原因通常是字符集在某一环没统一。排查顺序是:数据库和表的字符集、连接 URL 的characterEncoding、Tomcat 的 URI 编码。解决:建库时CREATE DATABASE crm DEFAULT CHARACTER SET utf8mb4,连接 URL 加characterEncoding=utf8mb4,Spring Boot 2.x 默认响应编码就是 UTF-8 不用额外配。如果还乱,检查是不是用了utf8而不是utf8mb4,前者只支持三字节,存不了部分生僻字和 emoji。
4.2 逻辑删除后唯一索引冲突
现象:删掉一个客户后,想用同样的名字再建一个,报唯一键冲突。原因:逻辑删除只是把deleted置 1,数据行还在,如果name上建了唯一索引,新数据就插不进去。解决:要么唯一索引改成(name, deleted)联合唯一,要么干脆不在name上建唯一索引,改在业务层做重名校验。我一般选后者,因为客户重名在现实里是允许的,硬卡唯一反而不合理。
4.3 分页插件不生效,查出来还是全量
现象:明明配了分页插件,selectPage返回的还是所有数据。原因:拦截器没注册,或者注册顺序不对。MyBatis-Plus 的分页依赖PaginationInnerInterceptor,必须加进MybatisPlusInterceptor并注册成 Bean。另一个常见原因是自己写了 XML 查询但没走 MyBatis-Plus 的分页方法,那插件管不到。解决:确认配置类被@Configuration扫描到,XML 查询要么改用selectPage,要么手动传IPage参数。
4.4 Spring Security 登录后接口还是 403
现象:登录成功拿到 Cookie,但访问业务接口返回 403。原因:CSRF 防护默认开启,POST 请求没带 CSRF token。前后端分离项目里通常直接关掉 CSRF,因为用 JWT 或 Session + 同源策略已经够用。解决:在 Security 配置里http.csrf().disable(),同时确认接口的权限注解@PreAuthorize里的角色名和数据库里存的一致——hasRole("ADMIN")实际匹配的是ROLE_ADMIN,数据库存ADMIN的话要加前缀或改配置。
4.5 时间字段差 8 小时
现象:存进去的时间是对的,查出来少了 8 小时。原因:MySQL 连接时区没指定,JDBC 按 UTC 解析。解决:连接 URL 加serverTimezone=Asia/Shanghai,同时确认实体类里时间字段用LocalDateTime而不是java.util.Date,前者配合新版驱动更省心。如果用了@JsonFormat注解,pattern 和 timezone 都要写对,否则前端展示又会偏。
5. 统计报表与进阶技巧:让 CRM 从能用变成好用
5.1 用一条 SQL 出销售业绩看板
CRM 的价值一半在记录,一半在统计。销售主管最想看的是「每个销售这个月跟进了多少客户、转化了多少商机」。与其在 Java 里循环查,不如一条 SQL 搞定:
-- 按销售统计本月跟进量和客户数 SELECT u.username, COUNT(DISTINCT f.customer_id) AS customer_count, COUNT(f.id) AS follow_count FROM user u LEFT JOIN follow_record f ON f.create_by = u.id AND f.follow_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') WHERE u.deleted = 0 GROUP BY u.id, u.username ORDER BY follow_count DESC;LEFT JOIN保证没跟进的销售也出现在结果里,显示为 0,否则主管会以为人漏了。DATE_FORMAT(CURDATE(), '%Y-%m-01')取本月第一天,比在 Java 里算好再传参更简洁。COUNT(DISTINCT f.customer_id)去重,因为一个客户可能被跟进多次,要的是覆盖客户数而不是记录数。这条 SQL 直接映射到一个 DTO 返回给前端,ECharts 渲染成柱状图就是业绩看板。
5.2 客户跟进时间线的接口设计
客户详情页要展示「这个客户从建档到现在,谁在什么时候跟进了什么」。接口设计成GET /api/customer/{id}/timeline,返回按时间倒序的跟进记录,每条带上记录人姓名。实现时用 MyBatis-Plus 的selectList加orderByDesc("follow_time"),再在 Service 里把create_by批量换成用户名,避免 N+1 查询。如果记录多,加个limit 50,前端做「加载更多」。这个接口是销售每天打开最多的页面,响应速度要控制在 200ms 内,索引和缓存该加就加。
5.3 导出 Excel 时别踩 POI 的坑
客户列表经常要导出 Excel 给领导。用 Apache POI 或 EasyExcel 都行,但注意两点:一是数据量大时别用XSSFWorkbook一次性写内存,几万行就 OOM,改用 EasyExcel 的流式写;二是日期和数字要设单元格格式,否则导出来是一串数字。我一般用 EasyExcel,注解式定义表头,代码量少一半。导出接口记得加权限校验,不是谁都能导全量客户——这本身就是数据泄露风险点。
5.4 一个我坚持了很久的习惯
每加一个新功能,先写一条对应的集成测试,用@SpringBootTest加MockMvc跑一遍接口,确认权限过滤和分页都对。CRM 这类系统最怕的是「功能能跑但数据串了」——A 销售看到 B 销售的客户,这种 bug 测试不覆盖就发现不了,上线后是事故。我吃过一次亏,后来所有涉及owner_id的查询都强制走DataScopeHelper,并在测试里专门造两个用户互相查,确认隔离生效。这套习惯让返工少了很多,希望帮到你。
本文还有配套的精品资源,点击获取