Java+HTML轻量级CRM系统实战:Spring Boot 3与纯前端落地指南
发布时间:2026/9/14 2:19:29来源:尧图网络
简介本资源是一套基于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. 这不是“JavaHTML就能跑”的静态页面而是一套需分层解耦、可落地运维的轻量级CRM系统很多刚接触企业级开发的工程师看到“基于Java和HTML的CRM客户关系管理系统设计源码”这个标题第一反应是用Servlet写个增删改查前端套几页HTML表单再连个MySQL——完事。但真实场景中这类系统一旦上线立刻面临三类硬需求销售团队要实时看到客户跟进状态不能刷新才更新客服主管要导出近30天未联系客户清单SQL得带时间窗口和关联统计IT运维要确认某次客户信息修改是谁在什么时间触发的操作日志必须结构化可检索。这意味着所谓“JavaHTML”本质是以Java为服务端核心逻辑载体、HTML为标准化呈现层接口、前后端职责边界清晰的三层架构实践。它不追求高并发或微服务化但必须满足中小团队对数据一致性、操作可追溯、界面可维护的底线要求。本文面向两类人一是正在做课程设计或毕业设计、需要从零搭建可演示系统的Java初学者二是已有Spring Boot经验、想快速复用经典分层模式落地内部工具的中级开发者。我们不讲抽象理论只拆解从项目初始化到上线验证的每一步关键决策。2. 用MavenSpring Boot 3.x构建后端骨架为什么选Spring Web而非原生Servlet2.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?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue 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. 用纯HTMLCSSJavaScript实现前端为什么拒绝框架坚持语义化结构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 htmlhtml langzh-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 langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 meta namedescription content轻量级CRM客户关系管理系统支持客户管理、跟进记录、状态追踪 titleCRM客户管理系统 - 首页/title link relstylesheet hrefcss/main.css /head body header classapp-header h1CRM客户管理系统/h1 nav a hrefindex.html classactive客户列表/a a hrefcustomer-detail.html?idnew新增客户/a /nav /header main classcontainer div idcustomer-list/div !-- 动态渲染区域 -- /main script typemodule srcjs/api.js/script script typemodule srcjs/customer-list.js/script /body /html提示typemodule启用ES6模块使import语法生效。meta nameviewport确保移动端正常缩放这是“永久在线的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 classcustomer-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 前端状态操作组件用原生HTMLCSS实现状态切换按钮在customer-detail.html中状态切换区不使用下拉框而用一组互斥按钮视觉上更符合CRM操作直觉!-- customer-detail.html 片段 -- div classstatus-controls h3当前状态span idcurrent-status classstatus-badgePOTENTIAL/span/h3 div classstatus-buttons button classstatus-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 classfollow-up-section h3跟进记录/h3 div classfollow-up-input textarea idfollow-content placeholder输入本次跟进内容.../textarea button idadd-follow-btn添加到列表/button /div ul idfollow-list classfollow-list/ul button idsubmit-follows-btn批量提交/button /divJavaScript逻辑// 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 classfollow-item span classfollow-content${f.content}/span span classfollow-meta${f.operator} · ${f.occurredAt}/span button classremove-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三容器协同MySQLBackendNginx在项目根目录创建docker-compose.ymlversion: 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.activedocker -jar /app.jar ports: - 8080:8080 environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/crm_db?useSSLfalseserverTimezoneAsia/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?idnew且表单可提交。跨域代理生效打开浏览器开发者工具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:MaxGCPauseMillis200 -jar /app.jar-Xms256m -Xmx512m堆内存固定为256MB起始、512MB上限避免动态扩容抖动-XX:UseG1GC启用G1垃圾收集器适合小堆内存场景-XX:MaxGCPauseMillis200目标GC停顿时间200毫秒平衡吞吐与响应。验证方式docker-compose exec backend jstat -gc $(pgrep java)观察G1 Eden Space和G1 Old Gen使用率是否平稳FGCFull 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_timeout180030分钟与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查看Networkindex.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?idnew但页面空白customer-detail.html中JavaScript报错浏览器F12 Console查看错误检查script typemodule路径是否正确js/api.js是否404提交客户时报HTTP 500 Internal Server ErrorMySQL连接失败或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配置。本文还有配套的精品资源点击获取
网站建设高端定制企业官网