销售团队最怕什么?不是签不下单,而是月底对账的时候,所有人都抱着一堆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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售订单表';订单表只跟用户和客户关联,金额统计直接用SUM(amount)即可。如果你后面还要算提成,可以再加一个product_detail子表,但目前不需要。跟进记录表则比较简单,客户ID加销售ID加内容和下次时间,查询时按客户ID倒序排列。
设计这几个表时,我建议把“金额不用FLOAT、时间都用DATETIME、常用的查询字段都建索引”这三条规则记牢。金额用FLOAT会出现精度问题,日期只用DATE又丢失了时间维度,索引不是越多越好,但user_id和create_time这种高频查询条件必须有。
3.3 数据初始化与连接配置
建好表结构后,在工程里配置数据源。我用的是Spring Boot项目,配置文件里最关键的几行:
spring.datasource.url=jdbc:mysql://localhost:3306/sales_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false spring.datasource.username=root spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.freemarker.suffix=.ftl spring.freemarker.template-loader-path=classpath:/templates/第一次连接MySQL 8.x时,最常见的就是时区和SSL报错。上面参数里的serverTimezone=Asia/Shanghai和useSSL=false基本能帮你避开这两个坑。如果你本地库里已经有数据,记得把数据库字符集也设置成utf8mb4,否则中文写入会变问号。连接池方面,SpringBoot默认的HikariCP已经够用,不需要额外配置。
4. 后端核心模块的落地实现
后端代码的骨架并不复杂,但是有几个地方需要尤其注意,比如登录拦截、数据权限和统计SQL的写法。我按实际开发顺序把核心环节梳理一遍。
4.1 SpringBoot项目骨架搭建
创建一个空SpringBoot项目,然后在pom.xml里加入核心依赖。我习惯使用MyBatis-Plus做持久层,它可以省掉大量单表CRUD的Mapper XML。关键依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</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 id="trendChart" style="height: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(); LambdaQueryWrapper<Customer> wrapper = new LambdaQueryWrapper<>(); if (isAdmin(loginUser)) { // 管理员查所有,也可按用户查 } else { wrapper.eq(Customer::getUserId, loginUser.getId()); } if (StringUtils.hasText(keyword)) { wrapper.like(Customer::getName, keyword); } Page<Customer> 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高度被压缩成0,ECharts就什么都画不出来。解决办法是先给图表容器设置固定高度或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.active=prod即可。打包命令我一般用:
mvn clean package -Dmaven.test.skip=true这会生成一个可直接运行的jar,放在target目录。服务器上用nohup启动:
nohup java -jar sales-manage.jar --spring.profiles.active=prod > app.log 2>&1 &部署前记得验证生产库的账号权限,不要用root跑生产应用。数据库初始化脚本单独放一个sql目录,发布时按顺序执行。如果需求上线频繁,可以用Git的tag来标记版本,每次发布打一个新tag,回滚也方便。
7.2 后续扩展方向与个人踩坑心得
这套系统做到后边,可以扩展的方向其实很多。比如给销售订单增加Excel导入导出功能,把历史Excel数据一键录进来;或者增加消息提醒,当某客户的下次跟进时间到了,系统自动给销售发站内信。数据权限也可以做细,改成“部门经理看整个部门,普通销售只看自己”,实现方案是给部门表和用户表增加关联,在查询条件里动态拼接部门ID。
我个人在带这类小项目时最大的体会是,技术都是成熟组件,难的是把销售管理规则理清楚。比如回款金额和订单金额分别统计,退款单怎么处理,业绩目标怎么分解,这些业务口径一定要在设计阶段和用户确认,否则代码写完再改逻辑成本极高。另一个体会是,后端系统一定要把日志打好,至少在拦截器、登录成功失败、订单新增这些关键节点输出一行清晰日志。项目上线后,你一定会感谢当初多写的这些日志。对我来说,这套小系统最让我满意的不是代码写得多漂亮,而是它真正把一堆Excel里的数据盘活了。销售能看清自己的客户,主管能看懂团队状态,老板能看见走势,这就是这套后台管理系统存在的意义。