简介:本资源是一套基于Java后端与HTML前端实现的轻量级CRM客户关系管理系统源码,面向Java初学者、Web开发入门者及中小企业信息化建设学习者,聚焦客户信息采集、服务流程管理与基础数据分析等核心场景。压缩包共38个文件(51KB),含29个Java源文件(承载业务逻辑与数据处理)、5个XML配置文件(管理数据库连接与框架参数)、1个YAML配置文件(提升配置可读性)、1个HTML页面(提供简洁用户交互界面),以及.gitignore和readme.txt等工程规范文件。已有302人学习下载,适合通过完整项目理解Maven构建流程、三层架构设计思想与前后端基础协同机制。读者可直接导入IDE运行调试,掌握从环境配置、模块划分到客户信息增删改查的全流程实现,同时参考pom.xml依赖管理与src目录标准结构,夯实企业级Java Web开发实践基础。
1. 这不是“Java+HTML就能跑”的静态页面,而是一套需分层解耦、可落地运维的轻量级CRM系统
很多刚接触企业级开发的工程师看到“基于Java和HTML的CRM客户关系管理系统设计源码”这个标题,第一反应是:用Servlet写个增删改查,前端套几页HTML表单,再连个MySQL——完事。但真实场景中,这类系统一旦上线,立刻面临三类硬需求:销售团队要实时看到客户跟进状态(不能刷新才更新),客服主管要导出近30天未联系客户清单(SQL得带时间窗口和关联统计),IT运维要确认某次客户信息修改是谁在什么时间触发的(操作日志必须结构化可检索)。这意味着,所谓“Java+HTML”,本质是以Java为服务端核心逻辑载体、HTML为标准化呈现层接口、前后端职责边界清晰的三层架构实践。它不追求高并发或微服务化,但必须满足中小团队对数据一致性、操作可追溯、界面可维护的底线要求。本文面向两类人:一是正在做课程设计或毕业设计、需要从零搭建可演示系统的Java初学者;二是已有Spring Boot经验、想快速复用经典分层模式落地内部工具的中级开发者。我们不讲抽象理论,只拆解从项目初始化到上线验证的每一步关键决策。
2. 用Maven+Spring Boot 3.x构建后端骨架:为什么选Spring Web而非原生Servlet
2.1 选型依据:避免重复造轮子,把精力聚焦在CRM业务逻辑上
CRM系统的核心复杂度不在HTTP协议解析或线程池管理,而在客户生命周期状态流转(如“潜在客户→意向客户→签约客户→流失客户”)、联系记录与客户实体的强关联约束、以及多角色数据权限控制(销售只能看自己客户,主管可看全组)。若用原生Servlet,需手动处理JSON序列化、参数校验、事务边界、异常统一响应——这些与CRM业务无关的胶水代码会占去60%以上开发时间。Spring Boot 3.x(基于Jakarta EE 9+)通过@RestController自动完成HTTP请求/响应映射,@Valid注解驱动JSR-380校验,@Transactional声明式事务管理,直接将开发重心拉回CustomerService.updateStatus()这样的业务方法。更重要的是,其内嵌Tomcat支持热部署,调试阶段改一行Java代码即可刷新生效,这对快速验证客户状态变更逻辑至关重要。
2.2 初始化工程:用Spring Initializr生成最小可行依赖集
访问 https://start.spring.io (官方在线生成器),按以下配置生成crm-backend项目:
# 项目元数据 Project: Maven Project Language: Java Spring Boot: 3.2.7 (LTS) # 依赖项(仅勾选以下4项,拒绝“全家桶”) [✓] Spring Web [✓] Spring Data JDBC (轻量替代JPA,避免Hibernate学习成本) [✓] MySQL Driver [✓] Lombok (消除getter/setter样板代码)生成后解压,导入IDEA。关键配置文件application.yml需明确指定数据库连接与Web路径:
# src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/crm_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: crm_user password: crm_pass_2024 driver-class-name: com.mysql.cj.jdbc.Driver sql: init: mode: always schema-locations: classpath:schema.sql >// Customer.java @Data // Lombok注解,自动生成getter/setter/toString public class Customer { private Long id; private String name; // 客户公司名 private String industry; // 所属行业 private String status; // 状态:LEAD/POTENTIAL/CONTRACTED/LOST private LocalDateTime createdAt; private LocalDateTime updatedAt; } // FollowUp.java - 跟进记录,含外键关联 @Data public class FollowUp { private Long id; private Long customerId; // 外键指向Customer.id private String content; // 跟进内容 private String operator; // 操作人姓名 private LocalDateTime occurredAt; // 发生时间 }对应建表SQL存于src/main/resources/schema.sql:
-- 创建数据库 CREATE DATABASE IF NOT EXISTS crm_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 客户表 CREATE TABLE customer ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, industry VARCHAR(50), status ENUM('LEAD','POTENTIAL','CONTRACTED','LOST') DEFAULT 'LEAD', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 跟进记录表(无外键约束,由应用层保证一致性) CREATE TABLE follow_up ( id BIGINT AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT NOT NULL, content TEXT NOT NULL, operator VARCHAR(50) NOT NULL, occurred_at DATETIME DEFAULT CURRENT_TIMESTAMP );注意:
follow_up.customer_id未设FOREIGN KEY,因JDBC不强制依赖数据库级外键。实际开发中,CustomerRepository.findById(id)先查客户是否存在,再执行followUpRepository.insert(),既保证数据一致性,又避免数据库锁表风险。这是轻量级CRM的务实选择。
3. 用纯HTML+CSS+JavaScript实现前端:为什么拒绝框架,坚持语义化结构
3.1 架构定位:HTML是交付物,不是开发过程
标题中“HTML”二字常被误解为“用记事本写网页”。实则指最终部署形态为标准HTML文档,通过浏览器原生能力渲染,不依赖Node.js运行时或Webpack打包环境。这带来三大优势:① 运维只需拷贝dist/目录到Nginx静态服务器;② 销售人员用手机浏览器扫码即可访问,无APP安装门槛;③ 页面加载速度极快(首屏<300ms),因无JavaScript框架启动开销。因此,前端技术栈锁定为:HTML5语义化标签 + CSS3 Flex/Grid布局 + 原生Fetch API调用后端REST接口。所有交互逻辑用ES6模块化组织,拒绝jQuery等历史库。
3.2 目录结构与核心HTML模板:<!doctype html><html lang="zh-cn">是起点而非装饰
在项目根目录新建frontend/文件夹,结构如下:
frontend/ ├── index.html # 首页(客户列表) ├── customer-detail.html # 客户详情页 ├── css/ │ └── main.css # 全局样式,含响应式断点 ├── js/ │ ├── api.js # 封装fetch调用后端API │ ├── customer-list.js # 客户列表页逻辑 │ └── utils.js # 工具函数(日期格式化、状态转中文) └── assets/ └── logo.svg # 静态资源index.html必须包含SEO友好元信息与中文语言声明:
<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="轻量级CRM客户关系管理系统,支持客户管理、跟进记录、状态追踪"> <title>CRM客户管理系统 - 首页</title> <link rel="stylesheet" href="css/main.css"> </head> <body> <header class="app-header"> <h1>CRM客户管理系统</h1> <nav> <a href="index.html" class="active">客户列表</a> <a href="customer-detail.html?id=new">新增客户</a> </nav> </header> <main class="container"> <div id="customer-list"></div> <!-- 动态渲染区域 --> </main> <script type="module" src="js/api.js"></script> <script type="module" src="js/customer-list.js"></script> </body> </html>提示:
type="module"启用ES6模块,使import语法生效。<meta name="viewport">确保移动端正常缩放,这是“永久在线的CRM网站”能被手机访问的基础。
3.3 用Fetch API对接后端:零依赖实现数据双向同步
js/api.js封装统一请求入口,处理认证(此处简化为无Token)、错误提示:
// js/api.js const API_BASE = '/api'; // 后端Spring Boot默认context-path export async function getCustomers() { const res = await fetch(`${API_BASE}/customers`); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); return res.json(); } export async function createCustomer(customerData) { const res = await fetch(`${API_BASE}/customers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(customerData) }); if (!res.ok) { const err = await res.json(); throw new Error(err.message || `创建失败: ${res.status}`); } return res.json(); } // 导出其他API函数...js/customer-list.js负责渲染客户列表并绑定事件:
// js/customer-list.js import { getCustomers, createCustomer } from './api.js'; import { formatDate, statusToChinese } from './utils.js'; async function renderCustomerList() { try { const customers = await getCustomers(); const listEl = document.getElementById('customer-list'); listEl.innerHTML = ` <table class="customer-table"> <thead> <tr> <th>客户名称</th> <th>行业</th> <th>状态</th> <th>创建时间</th> <th>操作</th> </tr> </thead> <tbody> ${customers.map(c => ` <tr>public enum CustomerStatus { LEAD("潜在客户"), POTENTIAL("意向客户"), CONTRACTED("已签约"), LOST("已流失"); private final String desc; CustomerStatus(String desc) { this.desc = desc; } // 定义合法状态转移路径 public boolean canTransitionTo(CustomerStatus next) { return switch (this) { case LEAD -> next == POTENTIAL || next == LOST; case POTENTIAL -> next == CONTRACTED || next == LOST; case CONTRACTED -> next == LOST; // 签约后仅允许流失 case LOST -> false; // 流失状态不可逆 }; } }CustomerController中校验状态变更:
// CustomerController.java @PostMapping("/customers/{id}/status") public ResponseEntity<?> updateStatus(@PathVariable Long id, @RequestBody StatusUpdateRequest request) { Customer existing = customerRepository.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "客户不存在")); CustomerStatus targetStatus = CustomerStatus.valueOf(request.getStatus()); if (!existing.getStatus().canTransitionTo(targetStatus)) { return ResponseEntity.badRequest() .body(Map.of("error", String.format("状态非法:从%s不可变为%s", existing.getStatus(), targetStatus))); } existing.setStatus(request.getStatus()); existing.setUpdatedAt(LocalDateTime.now()); customerRepository.update(existing); return ResponseEntity.ok(Map.of("success", true)); }4.2 前端状态操作组件:用原生HTML+CSS实现状态切换按钮
在customer-detail.html中,状态切换区不使用下拉框,而用一组互斥按钮,视觉上更符合CRM操作直觉:
<!-- customer-detail.html 片段 --> <div class="status-controls"> <h3>当前状态:<span id="current-status" class="status-badge">POTENTIAL</span></h3> <div class="status-buttons"> <button class="status-btn">// js/customer-detail.js document.querySelectorAll('.status-btn').forEach(btn => { btn.addEventListener('click', async function() { const status = this.dataset.status; const customerId = new URLSearchParams(window.location.search).get('id'); try { const res = await fetch(`/api/customers/${customerId}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (res.ok) { const data = await res.json(); document.getElementById('current-status').textContent = status; document.getElementById('current-status').className = 'status-badge'; alert('状态更新成功!'); } else { const err = await res.json(); alert(`更新失败:${err.error}`); } } catch (err) { alert(`网络错误:${err.message}`); } }); });提示:按钮初始
disabled,需在页面加载客户详情后根据当前状态动态启用合法选项。例如客户当前为POTENTIAL,则启用CONTRACTED和LOST按钮,禁用其余。这通过JavaScript读取#current-status文本后计算canTransitionTo()结果实现,将业务规则从前端UI层就拦截。
4.3 跟进记录的批量提交与防抖:解决销售高频录入痛点
销售每天可能录入10+条跟进,若每输一条就发一次HTTP请求,既慢又易出错。前端实现“输入框+添加按钮”组合,所有记录暂存内存,点击“批量提交”才发送:
<!-- customer-detail.html 跟进记录区 --> <div class="follow-up-section"> <h3>跟进记录</h3> <div class="follow-up-input"> <textarea id="follow-content" placeholder="输入本次跟进内容..."></textarea> <button id="add-follow-btn">添加到列表</button> </div> <ul id="follow-list" class="follow-list"></ul> <button id="submit-follows-btn">批量提交</button> </div>JavaScript逻辑:
// js/customer-detail.js 片段 let pendingFollows = []; document.getElementById('add-follow-btn').addEventListener('click', function() { const content = document.getElementById('follow-content').value.trim(); if (!content) return; pendingFollows.push({ content, operator: '张三', // 实际应从登录态获取 occurredAt: new Date().toISOString().slice(0, 19).replace('T', ' ') }); // 清空输入框并刷新列表显示 document.getElementById('follow-content').value = ''; renderFollowList(); }); function renderFollowList() { const listEl = document.getElementById('follow-list'); listEl.innerHTML = pendingFollows.map((f, i) => ` <li class="follow-item"> <span class="follow-content">${f.content}</span> <span class="follow-meta">${f.operator} · ${f.occurredAt}</span> <button class="remove-btn"># frontend/ 下执行(无需安装全局工具) npx html-minifier-terser --input-dir . --output-dir dist --file-ext html --collapse-whitespace --remove-comments cp -r css js assets dist/此时frontend/dist/即为可部署的纯静态文件。
5.2 编写docker-compose.yml:三容器协同(MySQL+Backend+Nginx)
在项目根目录创建docker-compose.yml:
version: '3.8' services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: crm_db MYSQL_USER: crm_user MYSQL_PASSWORD: crm_pass_2024 volumes: - ./mysql-data:/var/lib/mysql ports: - "3306:3306" backend: image: openjdk:17-jre-slim depends_on: - db volumes: - ./crm-backend/target/crm-backend-0.0.1-SNAPSHOT.jar:/app.jar - ./frontend/dist:/app/static command: java -Dspring.profiles.active=docker -jar /app.jar ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/crm_db?useSSL=false&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: crm_user SPRING_DATASOURCE_PASSWORD: crm_pass_2024 nginx: image: nginx:alpine depends_on: - backend volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./frontend/dist:/usr/share/nginx/html ports: - "80:80"其中nginx.conf配置反向代理,使前端HTML能跨域调用后端API:
# nginx.conf events { worker_connections 1024; } http { server { listen 80; location / { root /usr/share/nginx/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 一键启动与健康检查:验证CRM是否真正“永久在线”
执行docker-compose up -d,等待30秒后访问http://localhost。验证步骤:
- 数据库连通性:
docker-compose exec db mysql -u crm_user -p crm_pass_2024 crm_db -e "SELECT COUNT(*) FROM customer;"应返回0(初始无数据)。 - 后端API可用性:
curl http://localhost:8080/api/customers应返回[](空JSON数组)。 - 前端路由正确性:在浏览器打开
http://localhost,点击“新增客户”,地址栏应变为http://localhost/customer-detail.html?id=new,且表单可提交。 - 跨域代理生效:打开浏览器开发者工具Network面板,提交客户后,请求URL应为
http://localhost/api/customers(而非http://localhost:8080/api/customers),证明Nginx反向代理工作正常。
提示:若遇到
ERR_CONNECTION_REFUSED,先执行docker-compose logs backend查看Java进程是否因数据库连接超时而崩溃。常见原因是backend容器启动快于db,此时需在backend的command中加入重试逻辑,或使用docker-compose的healthcheck机制。但对本CRM系统,手动docker-compose restart backend一次即可解决,符合“轻量级”定位。
6. 关键参数调优与排错技巧:让CRM在低配服务器上稳定运行
6.1 后端JVM参数:针对1核2G服务器的最小化配置
Spring Boot默认JVM参数在低配环境易触发OOM。在backend服务的command中追加:
# docker-compose.yml 中 backend 服务的 command command: java -Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar /app.jar-Xms256m -Xmx512m:堆内存固定为256MB起始、512MB上限,避免动态扩容抖动;-XX:+UseG1GC:启用G1垃圾收集器,适合小堆内存场景;-XX:MaxGCPauseMillis=200:目标GC停顿时间200毫秒,平衡吞吐与响应。
验证方式:docker-compose exec backend jstat -gc $(pgrep java),观察G1 Eden Space和G1 Old Gen使用率是否平稳,FGC(Full GC)次数应为0。
6.2 MySQL连接池与超时参数:防止销售高峰期连接耗尽
在application.yml中补充HikariCP配置:
spring: datasource: hikari: maximum-pool-size: 10 # 最大连接数,匹配MySQL max_connections minimum-idle: 2 # 最小空闲连接,避免冷启动延迟 connection-timeout: 30000 # 连接获取超时30秒 idle-timeout: 600000 # 空闲连接存活600秒 max-lifetime: 1800000 # 连接最大寿命30分钟(防MySQL wait_timeout)同时在MySQL容器中设置wait_timeout=1800(30分钟),与max-lifetime对齐,避免连接被MySQL主动断开后HikariCP未感知。
6.3 前端资源加载优化:用HTTP缓存头提升二次访问速度
Nginx需为静态资源添加缓存策略,在nginx.conf的location块中追加:
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } location / { # ... 原有配置 add_header Cache-Control "no-cache"; }- JS/CSS等资源设为
1年缓存,因文件名哈希后不变; - HTML设为
no-cache,确保用户总能获取最新版本; - 验证:访问
http://localhost/index.html后,F12查看Network,index.html的Cache-Control应为no-cache,而main.css应为public, immutable。
6.4 常见报错速查表:5分钟定位问题根源
| 现象 | 可能原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
访问http://localhost显示Nginx欢迎页 | Nginx未挂载dist/目录 | docker-compose exec nginx ls /usr/share/nginx/html | 检查docker-compose.yml中volumes路径是否正确 |
点击“新增客户”跳转customer-detail.html?id=new但页面空白 | customer-detail.html中JavaScript报错 | 浏览器F12 Console查看错误 | 检查<script type="module">路径是否正确,js/api.js是否404 |
提交客户时报HTTP 500 Internal Server Error | MySQL连接失败或SQL语法错误 | docker-compose logs backend | grep -i "failed|exception" | 检查application.yml中spring.datasource.url是否指向db:3306而非localhost:3306 |
| 跟进记录提交后数据库无数据 | follow_up表未创建或字段名不匹配 | docker-compose exec db mysql -u crm_user -p crm_pass_2024 crm_db -e "DESCRIBE follow_up;" | 核对schema.sql中字段名与FollowUp.java属性名是否完全一致(大小写敏感) |
注意:所有排查均基于“Java后端+HTML前端”物理分离架构。若将前端代码直接写入
src/main/resources/static/,则失去独立构建与CDN加速能力,违背标题中“HTML”作为交付形态的本质。真正的CRM系统,应让销售团队只关心http://crm.yourcompany.com这个URL,而非背后的Java版本或Tomcat配置。
本文还有配套的精品资源,点击获取