资讯动态

SpringBoot+Freemarker构建销售团队后台管理系统实战解析

发布时间:2026/10/22 2:54:23 来源:尧图企业网站定制
销售团队最怕什么不是签不下单而是月底对账的时候所有人都抱着一堆Excel表格业绩数据东一个西一个。前阵子帮一家公司做销售团队后台管理系统用的就是标题里这套组合Java SpringBoot Freemarker Bootstrap ECharts MySQL Maven。系统不大但把客户跟进、销售订单、业绩统计、团队看板这些日常管理动作全部放到了线上。下面直接拆解这个项目从需求到落地的完整过程数据库怎么设计、拦截器怎么写、统计图表怎么接以及部署时才暴露出来的那些坑。如果你刚学完JavaWeb想找个完整项目练手或者公司内部正好缺一套轻量级销售数据后台这篇记录应该能帮上忙。1. 需求拆解与功能规划任何项目动手前先不要急着建表写代码。销售团队后台管理系统看起来功能一堆但核心其实只有三件事客户、订单、业绩。所有页面和接口都围绕这三件事转。我在做需求梳理时通常先和业务人员聊半小时把日常操作流程走一遍然后抽象成功能清单。这一步做不好后面返工成本特别高。1.1 销售团队的典型痛点一线销售每天要维护大量客户资料包括公司名称、联系人、电话、跟进阶段、下次跟进时间。没有系统之前这些信息可能散落在Excel、记事本、微信聊天记录里换个人跟进客户就断片了。销售主管更头疼的是业绩汇总月底让每个人报数字数字口径还不统一有的按合同金额有的按回款金额最后只能自己一张一张核。我见过比较夸张的情况是一个销售在表格里把同一个客户录了三遍统计时算成三单。后台管理系统至少要把“客户唯一性”这个问题解决掉通过客户表的主键和唯一索引约束录入时再做去重校验。另外一个痛点是权限边界不清晰普通销售如果能看到全公司订单很容易产生数据安全问题。所以权限设计要从第一天就考虑进去。1.2 功能模块划分与优先级这个项目最终落地时我把它拆成六个模块你可以当成一个最简MVP来参考。模块核心功能说明登录认证账号密码登录、Session会话区分管理员和普通销售客户管理客户增删改查、归属分配按登录人过滤数据跟进记录录入沟通内容、下次跟进时间关联客户和销售订单管理订单金额、回款状态、所属客户支撑业绩统计销售看板本月销售额、团队排名、趋势图使用ECharts展示团队管理销售账号的维护与启停管理员专用第一版建议只做四个核心模块登录、客户、订单、看板。跟进记录可以简单做成客户详情下的一个列表团队管理更是简单到只用一张用户表就行。不要一上来就做复杂的审批流、目标分解、提成计算那会让项目周期拉得很长。先让核心流程跑通让领导和销售都愿意每天打开这个系统后面的优化才有意义。2. 技术选型为什么是这套组合拳这套项目的技术栈看起来是“老面孔”但恰恰是适合内部管理系统的组合。每个组件单独拿出来都不稀奇组合起来却能把开发和部署成本压得很低。下面我把每个选择的理由讲清楚你以后面对类似项目也可以参考这个决策思路。2.1 SpringBoot Freemarker服务端渲染的取舍SpringBoot解决的是JavaWeb开发里的配置地狱。以前SSM项目要写一堆XML、配数据源、配事务、配视图解析器光是环境搭建就能劝退新手。SpringBoot把常用配置变成自动装配还内嵌了Tomcat最终打包成一个可执行的jar拿到服务器上java -jar就能跑。这对销售后台这种内部系统来说非常友好不用单独装Tomcat也不需要运维懂Java。Freemarker是做服务端渲染的模板引擎。为什么不用Vue做前后端分离其实我也考虑过。但仔细想想销售后台的使用场景是公司内网访问量大不到哪去也不需要考虑SEO核心需求是开发快、改起来方便。用Freemarker的话一个Controller返回视图名Model塞数据前端模板直接用list指令渲染后端同学一个人就能搞定全栈。省掉了Node环境、跨域配置、Token鉴权、接口联调这些环节整体开发效率至少能提升30%。2.2 Bootstrap ECharts后台UI与图表的选择Bootstrap的好处不是好看而是稳定和齐全。后台管理页面用到的栅格布局、表格、按钮、表单、模态框Bootstrap全都给你写好了拉一个开源后台模板改一改就能用。ECharts则是目前国内用得最多的可视化方案折线图、柱状图、饼图、地图都有成熟配置官方文档和社区例子非常多。销售趋势、团队排名、客户来源分布用ECharts展示再合适不过。有人可能会问现在流行Vue ElementUI为什么不用对于这个场景Vue ElementUI确实好看但需要增加前端工程化和构建步骤后端同学要额外掌握npm、脚手架、组件通信。如果团队里没有专职前端我强烈建议还是用服务端模板 Bootstrap。技术没有高低贵贱能在项目里快速交付才是第一位的。2.3 MySQL Maven数据持久化与工程管理MySQL是经典的关系型数据库销售订单这种结构化数据用MySQL再稳不过。涉及金额的字段用DECIMAL日期字段用DATETIME索引建在user_id、create_time上单表数据量控制在百万以内性能完全不是问题。Maven则是Java项目的标配工程管理工具统一依赖版本、编译打包、多环境profile切分这些都能让项目在团队协作里保持一致不至于出现“我本地能跑你那边跑不了”的尴尬。我见过一些同学喜欢用最新的数据库和ORM框架比如NoSQL或各种小众ORM出发点可以理解但对于公司内部系统来说团队熟悉度、招聘成本、网上资料量都是现实问题。MySQL Maven SpringBoot这套组合随便一个Java开发都能接手长期维护成本最低。3. 数据库设计与核心表结构数据库设计是这个系统最关键的环节。表如果设计得不合理后面写接口会非常痛苦。我习惯先画一张简单的ER关系图用户管理客户客户产生订单订单聚合出业绩跟进记录挂在客户下面。然后按照这个关系建表。3.1 用户、角色与权限建模用户表是所有业务数据的主人所有销售数据都会关联user_id。这张表不要设计得太复杂字段如下CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 加密后的密码, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, role_id TINYINT NOT NULL DEFAULT 2 COMMENT 1-管理员 2-普通销售, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-启用 0-停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;这里我故意没有做完整的RBAC三张表因为销售系统的角色很固定管理员和销售。用户表里一个role_id就够用。如果你以后要扩展多个角色再拆sys_role和sys_user_role两张表迁移成本也不高。密码字段直接存BCrypt加密串长度设长一点至少100个字符。3.2 客户、订单与跟进记录表客户表是销售管理的核心需要存储基本信息、归属关系和跟进状态CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 归属销售, name VARCHAR(100) NOT NULL COMMENT 客户名称, level TINYINT DEFAULT 0 COMMENT 客户等级 0-潜在 1-重点 2-成交, phone VARCHAR(20), address VARCHAR(255), status TINYINT DEFAULT 0 COMMENT 0-跟进中 1-已成交 2-已流失, next_follow_time DATETIME COMMENT 下次跟进时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户表;订单表的核心字段是金额和回款状态CREATE TABLE sales_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 销售员, customer_id BIGINT NOT NULL COMMENT 关联客户, amount DECIMAL(12,2) NOT NULL COMMENT 合同金额, pay_status TINYINT DEFAULT 0 COMMENT 0-未回款 1-部分回款 2-已回款, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售订单表;订单表只跟用户和客户关联金额统计直接用SUM(amount)即可。如果你后面还要算提成可以再加一个product_detail子表但目前不需要。跟进记录表则比较简单客户ID加销售ID加内容和下次时间查询时按客户ID倒序排列。设计这几个表时我建议把“金额不用FLOAT、时间都用DATETIME、常用的查询字段都建索引”这三条规则记牢。金额用FLOAT会出现精度问题日期只用DATE又丢失了时间维度索引不是越多越好但user_id和create_time这种高频查询条件必须有。3.3 数据初始化与连接配置建好表结构后在工程里配置数据源。我用的是Spring Boot项目配置文件里最关键的几行spring.datasource.urljdbc:mysql://localhost:3306/sales_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse spring.datasource.usernameroot spring.datasource.passwordyour_password spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.freemarker.suffix.ftl spring.freemarker.template-loader-pathclasspath:/templates/第一次连接MySQL 8.x时最常见的就是时区和SSL报错。上面参数里的serverTimezoneAsia/Shanghai和useSSLfalse基本能帮你避开这两个坑。如果你本地库里已经有数据记得把数据库字符集也设置成utf8mb4否则中文写入会变问号。连接池方面SpringBoot默认的HikariCP已经够用不需要额外配置。4. 后端核心模块的落地实现后端代码的骨架并不复杂但是有几个地方需要尤其注意比如登录拦截、数据权限和统计SQL的写法。我按实际开发顺序把核心环节梳理一遍。4.1 SpringBoot项目骨架搭建创建一个空SpringBoot项目然后在pom.xml里加入核心依赖。我习惯使用MyBatis-Plus做持久层它可以省掉大量单表CRUD的Mapper XML。关键依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-freemarker/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency如果你不愿意用MyBatis-Plus用Spring JDBC Template甚至原生JDBC也行但CRUD的代码量会明显增多。MyBatis-Plus的BaseMapper直接提供了selectList、insert、updateById这些方法对做后台系统来说非常划算。4.2 登录鉴权与拦截器登录功能看起来简单但藏着不少细节。首先密码必须用BCrypt单向加密不要用MD5。注册用户时保存BCrypt密文登录时用BCrypt检查。其次登录成功后把用户对象放进Session之后所有请求都通过拦截器校验Session中是否存在用户。核心拦截器代码public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(loginUser) null) { response.sendRedirect(/login); return false; } return true; } }注册拦截器时注意要把登录页和静态资源排除掉Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /static/**, /logout); } }这里有个容易踩的坑静态资源如果也被拦截登录页里的CSS和JS会全部404整个页面看起来就像“裸奔”。所以/static/**一定要加入excludePathPatterns。如果项目里用了自定义的静态资源目录还要配合WebMvcConfigurer的addResourceHandlers一起使用。4.3 业绩统计接口设计业绩统计是销售后台的重点核心逻辑是SQL聚合。比如查询最近12个月的销售趋势可以这样写SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(amount) AS total_amount FROM sales_order WHERE create_time #{startTime} AND pay_status ! 0 GROUP BY month ORDER BY month;Controller中把这个结果放进Model里Freemarker渲染首屏时可以直接用。同时我还会提供一个JSON接口专门给ECharts用这样前端切年份时不用整页刷新。一个MonthAmountDTO包含month和totalAmount两个字段用List返回前端拿数组填进图表即可。统计口径一定要和业务确认清楚。比如订单全额才算业绩还是回款才算业绩如果混着算月底对账又会吵起来。我在这个项目里把“业绩”定义成已回款的订单金额待回款的订单单独展示在“应收”列表里这样账面更清晰。4.4 Freemarker模板渲染与公共布局Freemarker的模板目录通常放在src/main/resources/templates下。我是这样组织的templates/ ├── login.ftl ├── dashboard.ftl ├── customer/ │ ├── list.ftl │ └── edit.ftl ├── order/ │ └── list.ftl └── common/ ├── head.ftl ├── menu.ftl └── footer.ftl公共头部和菜单用include引入比如每个页面都引入common/head.ftl和common/menu.ftl。菜单高亮可以通过模板参数控制后台Controller里把当前菜单标识放进Model菜单模板里做一次if判断即可。写Freemarker模板时最需要注意的是空值判断。比如一个列表页面用#list customerList as customer之前最好先#if customerList??否则集合为空时模板会直接报错连页面都打不开。5. 前端页面与可视化图表的整合后端把数据准备到位后前端的工作就是把数据变成好操作的页面。我用Bootstrap负责布局用ECharts负责图表展示整体开发周期很短。下面把关键部分拆开讲。5.1 Bootstrap布局后台框架后台界面我通常采用经典的“左侧菜单 右侧内容”结构。最外层是container-fluid里面放一个row左侧栏col-md-2右侧内容col-md-10。左侧菜单用Bootstrap的nav-pills或者nav-stacked菜单项绑定链接。右侧内容区放一个panel或card标题栏加操作按钮内容区放表格或图表。引入Bootstrap的方式有两种一种是从CDN引入在线资源一种是下载到本地static目录。内网部署的后台系统我建议先把bootstrap.css、bootstrap.js、jquery和echarts.min.js下载到工程里避免外网断掉时样式全部崩掉。这个细节看起来很小但在公司内网环境里很关键。5.2 ECharts接入销售趋势图ECharts的接入流程其实只有三步放置容器、初始化图表、setOption填充数据。销售趋势图我建议独立定义一个id为trendChart的div并显式设置height样式。div idtrendChart styleheight:400px;width:100%;/div script src/static/lib/echarts.min.js/script script $.get(/api/sales/trend, function(result) { var chart echarts.init(document.getElementById(trendChart)); chart.setOption({ title: { text: 近12个月销售趋势 }, tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: result.months }, yAxis: { type: value }, series: [{ name: 回款金额, type: line, smooth: true, data: result.amounts }] }); }); /script如果是通过Ajax异步获取数据必须等数据返回后再调用echarts.init。很多人遇到的“页面放在div下面初始化却报错找不到容器”大部分原因是脚本在DOM渲染前执行了把script放到div后面就行。窗口大小改变时记得调用chart.resize()否则图表会糊或变形。5.3 表格操作与搜索分页后台管理最常见的页面就是“搜索 表格 分页”。我用MyBatis-Plus的分页插件配置一个PaginationInterceptor然后Controller里接收pageNum和pageSize参数。搜索关键词和归属人过滤也在Service层拼接QueryWrapper。GetMapping(/customer/list) public String customerList(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, Model model) { User loginUser getLoginUser(); LambdaQueryWrapperCustomer wrapper new LambdaQueryWrapper(); if (isAdmin(loginUser)) { // 管理员查所有也可按用户查 } else { wrapper.eq(Customer::getUserId, loginUser.getId()); } if (StringUtils.hasText(keyword)) { wrapper.like(Customer::getName, keyword); } PageCustomer page customerService.page(new Page(pageNum, pageSize), wrapper); model.addAttribute(page, page); return customer/list; }这样通过sellerId的过滤就实现了基础的数据权限。普通销售即使手改URL参数后端也会强制用登录人的ID拼条件不会出现越权看到别人客户的情况。分页导航使用Bootstrap的pagination组件后端把getCurrent、getPages、getTotal传给模板前端循环生成页码即可。6. 常见问题与排查技巧实录这类项目虽然不大但我在开发过程中还是踩了好几个坑。把典型问题整理成速查表可以帮大家少走很多弯路。6.1 静态资源加载404现象是登录页能访问但页面上的CSS、JS全是404。常见原因有两个一是拦截器把/static/**也拦了需要在排除路径中加入静态资源二是Freemarker模板里写的静态资源链接带了上下文路径但配置不对。最快的排查方法是打开浏览器开发者工具看请求URL再把拦截器配置调整到正确路径。还有一个小细节是SpringBoot默认只扫描classpath:/static如果你把资源放在resources根目录需要额外配置ResourceHandler。6.2 MySQL连接报时区错误MySQL 8.x和SpringBoot自带的驱动对时区要求很严格启动报“The server time zone value”时基本就是连接串少了serverTimezone参数。按我前面给出的配置加上Asia/Shanghai即可。如果密码里有特殊字符连接串中要记得URL编码。这个问题在开发环境很容易踩因为很多时候本机MySQL是5.7部署服务器是8.0两边行为还不一样。6.3 ECharts图表不显示图表不显示通常不是数据错了而是容器尺寸问题。有的后台模板会给内容区设置overflow样式导致div高度被压缩成0ECharts就什么都画不出来。解决办法是先给图表容器设置固定高度或min-height。另一个隐蔽问题是Ajax返回的JSON字段名和前端对不上比如后端返回totalAmount前端写成了amount这时图表就会静默失败。打开浏览器Network面板看响应体能很快定位。6.4 Maven依赖冲突引入MyBatis-Plus后偶尔会和SpringBoot的某些依赖版本冲突。最直接的排查命令是mvn dependency:tree把依赖树打出来看是哪个版本不一致。最常见的坑是mysql-connector-java的坐标变了SpringBoot 2.7以后建议使用mysql-connector-j而不是旧的mysql-connector-java。判断标准很简单看项目启动日志里有没有ClassNotFoundException沿着报错类名去查依赖即可。6.5 部署到服务器后运行异常本地跑得好好的服务器上一启动就退出第一件事看日志文件而不是盲目重启。如果内存不够JVM直接OutOfMemoryError打包时可以用mvn clean package启动时加-Xms256m -Xmx512m参数。如果端口被占先看是不是旧进程没杀死。Freemarker模板在生产环境如果被禁用缓存会频繁重新加载模板性能下降明显记得在application-prod.properties里把freemarker的cache配置打开。7. 打包部署与后续扩展经验系统开发完成后打包部署和后续迭代同样有很多门道。这部分我没有太多高深结论但都是实际项目里验证过的方法。7.1 Maven打包与多环境配置工程里创建application-dev.properties和application-prod.properties分别配置不同的数据库连接和日志级别。启动时指定spring.profiles.activeprod即可。打包命令我一般用mvn clean package -Dmaven.test.skiptrue这会生成一个可直接运行的jar放在target目录。服务器上用nohup启动nohup java -jar sales-manage.jar --spring.profiles.activeprod app.log 21 部署前记得验证生产库的账号权限不要用root跑生产应用。数据库初始化脚本单独放一个sql目录发布时按顺序执行。如果需求上线频繁可以用Git的tag来标记版本每次发布打一个新tag回滚也方便。7.2 后续扩展方向与个人踩坑心得这套系统做到后边可以扩展的方向其实很多。比如给销售订单增加Excel导入导出功能把历史Excel数据一键录进来或者增加消息提醒当某客户的下次跟进时间到了系统自动给销售发站内信。数据权限也可以做细改成“部门经理看整个部门普通销售只看自己”实现方案是给部门表和用户表增加关联在查询条件里动态拼接部门ID。我个人在带这类小项目时最大的体会是技术都是成熟组件难的是把销售管理规则理清楚。比如回款金额和订单金额分别统计退款单怎么处理业绩目标怎么分解这些业务口径一定要在设计阶段和用户确认否则代码写完再改逻辑成本极高。另一个体会是后端系统一定要把日志打好至少在拦截器、登录成功失败、订单新增这些关键节点输出一行清晰日志。项目上线后你一定会感谢当初多写的这些日志。对我来说这套小系统最让我满意的不是代码写得多漂亮而是它真正把一堆Excel里的数据盘活了。销售能看清自己的客户主管能看懂团队状态老板能看见走势这就是这套后台管理系统存在的意义。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑