本人负责后端的开发(Java)
CRM系统即客户关系管理系统,是指企业用CRM技术来管理与客户之间的关系。他的目标是缩减销售周期和销售成本,增加收入,寻找扩展业务所需的新的市场和渠道以及提高客户的价值,满意度,营利性和忠实度。CRM项目的实施可以分为3步,即应用业务集成。业务诗句分析和决策执行。
客户关系管理是指企业为提高核心竞争力,利用相应的技术信息以及互联网技术协调企业与顾客间在消费,营销和服务上的交互,从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务的过程,其最终目标是吸引新客户,保留老客户以及将已有客户转为忠实客户,增加市场
项目名称:CRM客户管理系统
系统作用:公司客户关系管理,潜在客户开发及订单合同管理
开发环境:IDEA Windows10 jdk1.8 Maven Mysql8
需要的工具:postman fiddler抓包工具或浏览器开发者工具
前端:LayUI freeMaker
后端:Spring SpringMVC SpringBoot MyBatis Maven MySQL8 Linux CentOS ECharts(折线和饼状图)权限管理 定时任务调度(quartz)CentOS Lombok
1.基础模块:包含系统基本的用户登录,退出,记住我,密码修改等基本操作。
2.营销管理:
营销机会管理:企业客户的质询需求所建立的信息录入功能
客户开发计划:开发计划是根据营销机会而来,对于企业质询的客户,会有相应的销售人员对于该客户
进行具体的沟通交流,此时对于整个 Crm 系统而言,通过营销开发计划来进行相应的信息管理,提高
客户的购买企业产品的可能性。
3.客户管理:
客户信息管理 :Crm 系统中完整记录客户信息来源的数据、企业与客户交往、客户订单查询等信息录
入功能,方便企业与客户进行相应的信息交流与后续合作。
客户流失管理 :Crm 通过一定规则机制所定义的流失客户(无效客户),通过该规则可以有效管理客
户信息资源,提高营销开发的效率。
4.服务管理:服务管理是针对客户而开发的功能,针对客户要求,Crm 提供客户相应的信息质询,反馈与投诉功能,
提高企业对于客户的服务质量。
5.数据报表:
Crm 提供的数据报表功能能够帮助企业了解客户整体分布,了解客户开发结果整体信息,从而帮助企业
整体调整客户开发计划,提高企业的在市场中的竞争力度。
6.系统管理:系统管理包含常量字典维护工作,以及权限管理模块,Crm 权限管理是基于角色的一种权限控制,基于
RBAC 实现基于角色的权限控制,通过不同角色的用户登录该系统后展示系统不同的操作功能,从而达
到对不同角色完成不同操作功能。
1.创建SpringBoot项目,导入依赖(见源代码)
2.在src/main/resources 目录下新建 application.yml 配置文件
## 端口号 上下文路径
server:
port: 8080
servlet:
context-path: /crm
## 数据源配置
spring:
datasource:
type: com.mchange.v2.c3p0.ComboPooledDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/crm?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username:
password:
## freemarker
freemarker:
suffix: .ftl
content-type: text/html
charset: UTF-8
template-loader-path: classpath:/views/
## 启用热部署
devtools:
restart:
enabled: true
additional-paths: src/main/java
## mybatis 配置
mybatis:
mapper-locations: classpath:/mappers/*.xml
type-aliases-package: org.example.crm.vo;org.example.crm.query;org.example.crm.dto
configuration:
map-underscore-to-camel-case: true
## pageHelper 分页
pagehelper:
helper-dialect: mysql
## 设置 dao 日志打印级别
logging:
level:
org:
example:
crm:
dao: debug
3.新建 org.example.crm.controller 包,添加系统登录,主页面转发代码 。
4.添加静态资源:在 src/main/resources 目录下新建 public 目录,存放系统相关静态资源文件,拷贝静态文件内容到
public 目录。
5.添加视图模板:在 src/main/resources 目录下新建 views 目录,添加 index.ftl、main.ftl 等文件。 (具体视图文件详见
相关目录)
6.添加启动类:在 org.example.crm 包下新建 Starter.java ,添加启动项目相关代码如下:
package org.example.crm;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@MapperScan("org.example.crm.dao")
//启用定时任务
@EnableScheduling
public class Starter {
public static void main(String[] args) {
SpringApplication.run(Starter.class);
}
}
7.添加Base包:主要用户对Controller,Service Dao层的统一控制,BaseQuery用于控制按条件搜索的对象,ResultInfo是后端返回的对象的统一封装
项目搭建结构:
8.准备MyBatis代码统一生成工具(generatorConfig.xml)
这里注意:工具有点缺陷,每次需要改工具作用的表名
使用mybatis-generator生成Mybatis代码。能够生成 vo 类、能生成 mapper 映射文件(其中包括基本
的增删改查功能)、能生成 mapper 接口。
命令: mybatis-generator:generate -e
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!--
数据库驱动
在左侧project边栏的External Libraries中找到mysql的驱动,右键选择copy path
-->
<classPathEntry location="D:\Repository\Maven\mysql\mysql-connector-java\5.1.49\mysql-connector-java-5.1.49.jar"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<!-- 是否去除日期那行注释 -->
<property name="suppressDate" value="false"/>
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="false"/>
</commentGenerator>
<!-- 数据库链接地址账号密码 -->
<jdbcConnection
driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/crm?serverTimezone=GMT%2B8"
userId="root"
password="sn20000904">
</jdbcConnection>
<!--
java类型处理器
用于处理DB中的类型到Java中的类型,默认使用JavaTypeResolverDefaultImpl;
注意一点,默认会先尝试使用Integer,Long,Short等来对应DECIMAL和NUMERIC数据类型;
true:使用 BigDecimal对应DECIMAL和NUMERIC数据类型
false:默认,把JDBC DECIMAL和NUMERIC类型解析为Integer
-->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 生成Model类存放位置 -->
<javaModelGenerator targetPackage="org.example.crm.model" targetProject="src/main/java">
<!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->
<property name="enableSubPackages" value="true"/>
<!-- 设置是否在getter方法中,对String类型字段调用trim()方法 -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!--生成映射文件存放位置-->
<sqlMapGenerator targetPackage="mappers" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!--生成Dao类存放位置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="org.example.crm.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!--改表名 -->
<table tableName="t_customer_serve" domainObjectName="CustomerServe"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
</context>
</generatorConfiguration>
9.导入工具类,主要有:根据Cookie获取作用域,登录成功返回userIdStr加密,Md5协议加密,判断电话号码的格式,userID加解密等
主要讲解核心模块的核心代码
定义UserModel类,用于用户登录成功返回的用户信息,用来设置前端的Cookie
@Getter
@Setter
public class UserVo {
//private Integer userId;
//存放在前端cookie中加密后的Id
private String userIdStr;
private String userName;
private String trueName;
}
设置cookie
layer.msg("登录成功!", function () {
// 判断用户是否选择记住密码(判断复选框是否被选中,如果选中,则设置cookie对象7天生效)
if ($("#rememberMe").prop("checked")) {
// 选中,则设置cookie对象7天生效
// 将用户信息设置到cookie中
$.cookie("userIdStr", result.result.userIdStr, {
expires: 7});
$.cookie("userName", result.result.userName, {
expires: 7});
$.cookie("trueName", result.result.trueName, {
expires: 7});
} else {
// 将用户信息设置到cookie中
$.cookie("userIdStr", result.result.userIdStr);
$.cookie("userName", result.result.userName);
$.cookie("trueName", result.result.trueName);
}
退出登录时,删除前端Cookie即可
全局异常实现思路:
控制层的方法返回的内容两种情况
全局异常拦截器的实现,简化了try-catch代码
实现 HandlerExceptionResolver 接口 ,处理应用程序异常信息
对于后端菜单资源,这里要求用户必须进行登录来保护 web 资源的安全性,此时引入非法请求拦截功
能。
实现思路:
判断用户是否是登录状态
获取Cookie对象,解析用户ID的值
如果用户ID不为空,且在数据库中存在对应的用户记录,表示请求合法
否则,请求不合法,进行拦截,重定向到登录页面
定义拦截器:在新建 interceptors 包,创建 NoLoginInterceptor 类,并继承 HandlerInterceptorAdapter 适配器,
实现拦截器功能。
/**
* 非法访问拦截
* 继承HandlerInterceptorAdapter适配器
*/
public class NoLoginInterceptor extends HandlerInterceptorAdapter {
@Autowired
private UserMapper userMapper;
/**
* 拦截用户是否是登录状态
* 在目标方法(资源)执行前执行的方法
* 返回boolean
* 如果为true,表示目标方法可用被执行
* 如果为false,表示阻止目标方法执行
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取cookie中的用户Id
Integer userId = LoginUserUtil.releaseUserIdFromCookie(request);
//判断用户Id是否为空,且数据库中是否存在改userId的记录
if (userId == null || userMapper.selectByPrimaryKey(userId) == null) {
//抛出未登录异常
throw new NoLoginException();
}
return true;
}
}
全局异常类配置:在全局异常处理类中引入未登录异常判断
/**
* 全局异常统一处理
*/
@Component
public class GlobalExceptionResolver implements HandlerExceptionResolver {
/**
* 异常处理方法
* 方法的返回值:
* 1. 返回视图
* 2. 返回数据(JSON数据)
*
* 如何判断方法的返回值?
* 通过方法上是否声明@ResponseBody注解
* 如果未声明,则表示返回视图
* 如果声明了,则表示返回数据
*
* @param request request请求对象
* @param response response响应对象
* @param handler 方法对象
* @param ex 异常对象
* @return org.springframework.web.servlet.ModelAndView
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
/**
* 非法请求拦截
* 判断是否抛出未登录异常
* 如果抛出该异常,则要求用户登录,重定向跳转到登录页面
*/
if (ex instanceof NoLoginException) {
// 重定向到登录页面
ModelAndView mv = new ModelAndView("redirect:/index");
return mv;
}
/**
* 设置默认异常处理(返回视图)
*/
ModelAndView modelAndView = new ModelAndView("error");
// 设置异常信息
modelAndView.addObject("code", 500);
modelAndView.addObject("msg", "系统异常,请重试...");
// 判断HandlerMethod
if (handler instanceof HandlerMethod) {
// 类型转换
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法上声明的@ResponseBody注解对象
ResponseBody responseBody = handlerMethod.getMethod().getDeclaredAnnotation(ResponseBody.class);
// 判断ResponseBody对象是否为空 (如果对象为空,则表示返回的事视图;如果不为空,则表示返回的事数据)
if (responseBody == null) {
/**
* 方法返回视图
*/
// 判断异常类型
if (ex instanceof ParamsException) {
ParamsException p = (ParamsException) ex;
// 设置异常信息
modelAndView.addObject("code", p.getCode());
modelAndView.addObject("msg", p.getMsg());
} else if (ex instanceof AuthException) {
// 认证异常
AuthException a = (AuthException) ex;
// 设置异常信息
modelAndView.addObject("code", a.getCode());
modelAndView.addObject("msg", a.getMsg());
}
return modelAndView;
} else {
/**
* 方法返回数据
*/
// 设置默认的异常处理
ResultInfo resultInfo = new ResultInfo();
resultInfo.setCode(500);
resultInfo.setMsg("异常异常,请重试!");
// 判断异常类型是否是自定义异常
if (ex instanceof ParamsException) {
ParamsException p = (ParamsException) ex;
resultInfo.setCode(p.getCode());
resultInfo.setMsg(p.getMsg());
} else if (ex instanceof AuthException) {
// 认证异常
AuthException a = (AuthException) ex;
resultInfo.setCode(a.getCode());
resultInfo.setMsg(a.getMsg());
}
// 设置响应类型及编码格式(响应JSON格式的数据)
response.setContentType("application/json;charset=UTF-8");
// 得到字符输出流
PrintWriter out = null;
try {
// 得到输出流
out = response.getWriter();
// 将需要返回的对象转换成JOSN格式的字符
String json = JSON.toJSONString(resultInfo);
// 输出数据
out.write(json);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 如果对象不为空,则关闭
if (out != null) {
out.close();
}
}
return null;
}
}
return modelAndView;
}
}
拦截器生效配置:
@Configuration//配置类
public class MvcConfig extends WebMvcConfigurerAdapter {
@Bean//将方法的返回值交给IOC
public NoLoginInterceptor noLoginInterceptor() {
return new NoLoginInterceptor();
}
/**
* 添加拦截器
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//需要实现了拦截器功能的实例对象 NoLoginInterceptor
registry.addInterceptor(noLoginInterceptor())
//设置需要被拦截的资源
.addPathPatterns("/**")
// 设置不需要被拦截的资源
.excludePathPatterns("/css/**", "/images/**", "/js/**", "/lib/**")
.excludePathPatterns("/index", "/user/login");
}
}
测试拦截效果:
当 Cookie 中的用户ID不存在时,访问 main 页面,会自动跳转到登录页面
记住我功能核心在于当用户上次登录时如果点击了记住我,下次在重新打开浏览器时可以不用选择登
录,此时可以借助拦截器 + cookie 来实现,当用户在登录时,如果用户点击了记住我功能,默认设置
cookie存储时间为7天即可。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd") // 如果传递的参数是Date类型,要求传入的时间字符串的格式
private Date planDate;
基本概念:RBAC是基于角色的访问控制( Role-Based Access Control )在RBAC中,权限与角色相关联,用户
通过扮演适当的角色从而得到这些角色的权限。这样管理都是层级相互依赖的,权限赋予给角色,角色
又赋予用户,这样的权限设计很清楚,管理起来很方便。
表结构设计:
从上面实体对应关系分析,权限表设计分为以下基本的五张表结构:用户表(t_user)、角色表(t_role)、
t_user_role(用户角色表)、资源表(t_module)、权限表(t_permission)
用户和角色间一对一关系,角色和权限间一对一关系,建立t_user_role和t_permission中间表
表结构关系如下:
当完成角色权限添加功能后,下一步就是对角色操作的资源进行认证操作,这里对于认证包含两块:
查询出改用户所拥有的角色,然后根据角色查询出拥有的权限码,具体实现如下:
@RequestMapping("main")
public String main(HttpServletRequest request) {
//通过获取cookie用户ID
Integer userId = LoginUserUtil.releaseUserIdFromCookie(request);
//查询用户对象,设置session作用域
User user = userService.selectByPrimaryKey(userId);
request.getSession().setAttribute("user", user);
//通过当前登录用户ID,查询当前登录用户拥有的资源列表(查询对应的资源授权码)
List<String> permissions = null;
permissions = permissionService.queryUserHasRoleHasPermissionByUserId(userId);
//将集合设置作用域中(Session作用域)
request.getSession().setAttribute("permissions", permissions);
return "main";
}
系统根据登录用户扮演的不同角色来对登录用户操作的菜单进行动态控制显示操作,这里显示的控制使
用freemarker指令+内建函数实现
例如:
会根据权限码,来显示菜单内容:
实现了菜单级别显示控制,但最终客户端有可能会通过浏览器来输入资源地址从而越过ui界面来访问后
端资源,所以接下来加入控制方法级别资源的访问控制操作,这里使用aop+自定义注解实现
自定义注解类:表示资源所需的权限码
@Target({
ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
* 定义方法需要的对应资源的权限码
*/
public @interface RequiredPermission {
//权限码
String code() default "";
}
方法级别使用注解:
例如:
定义aop切面类 拦截指定注解标注的方法:
@Component
@Aspect
public class PermissionProxy {
@Resource
private HttpSession session;
/**
* 切面会拦截指定包下的指定注解
* 拦截com.xxxx.crm.annoation的RequiredPermission注解
*
* @param pjp
* @return java.lang.Object
*/
@Around(value = "@annotation(org.example.crm.annotation.RequiredPermission)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
// 得到当前登录用户拥有的权限 (session作用域)
List<String> permissions = (List<String>) session.getAttribute("permissions");
// 判断用户是否拥有权限
if (null == permissions || permissions.size() < 1) {
// 抛出认证异常
throw new AuthException();
}
// 得到对应的目标
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
// 得到方法上的注解
RequiredPermission requiredPermission = methodSignature.getMethod().getDeclaredAnnotation(RequiredPermission.class);
// 判断注解上对应的状态码
if (!(permissions.contains(requiredPermission.code()))) {
// 如果权限中不包含当前方法上注解指定的权限码,则抛出异常
throw new AuthException();
}
result = pjp.proceed();
return result;
}
}
模块功能:
表结构设计:
t_customer 客户表、t_customer_contact 客户交往记录表、t_customer_linkman 客户联系人表、t_customer_order 客户订单表、t_order_details 订单详情表
当实现了客户数据转移业务逻辑代码后,这里需要思考一个问题:客户数据量的问题随着时间的积累,流失的客户数据可能就比较大,如果数据的获取在用户查询时进行,此时后端对于数据的查询就会变得很慢,此时可以使用我们之前讲到的定时任务来处理,后台通过定时器来对流失客户数据定时进行转移处理,从而当前端用户查询时只需到客户流失表查询流失数据即可。
增加定时器服务:
/**
* 定时任务的执行
*/
@Component
public class JobTask {
@Autowired
private CustomerService customerService;
//cron表达式
//每两秒执行一次
//@Scheduled(cron = "0/2 * * * * ?")
//从六月开始,每个月执行一次
@Scheduled(cron = "* * * * 6/1 ? ")
public void job() {
//调用需要被执行的方法
//开始执行定时任务
System.out.println("开始执行定时器任务");
customerService.updateCustomerState();
System.out.println("定时器任务执行完成");
}
}
Starter开启定时任务环境配置:
@SpringBootApplication
@MapperScan("org.example.crm.dao")
//启用定时任务
@EnableScheduling
public class Starter {
public static void main(String[] args) {
SpringApplication.run(Starter.class);
}
}
这里对于服务管理服务的创建,分配,处理与反馈后端代码实现放在同一个方法中进行处理,同时方便对于服务状态值统一处理,这里定义 CustomerServeStatus 枚举类来实现。
/**
* 客户服务状态枚举类
*/
public enum CustomerServeStatus {
// 创建
CREATED("fw_001"),
// 分配
ASSIGNED("fw_002"),
// 处理
PROCED("fw_003"),
// 反馈
FEED_BACK("fw_004"),
// 归档
ARCHIVED("fw_005");
private String state;
CustomerServeStatus(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
ECharts官网:添加链接描述
折线图数据返回实现:
/**
* 查询客户构成 (折线图)
* @return
*/
public Map<String, Object> countCustomerMake() {
Map<String, Object> map = new HashMap<>();
// 查询客户构成数据的列表
List<Map<String, Object>> dataList = customerMapper.countCustomerMake();
// 折线图X轴数据 数组
List<String> data1 = new ArrayList<>();
// 折线图Y轴数据 数组
List<Integer> data2 = new ArrayList<>();
// 判断数据列表 循环设置数据
if (dataList != null && dataList.size() > 0) {
for (int i = 0; i < dataList.size(); i++) {
data1.add(dataList.get(i).get("level").toString());
data2.add(Integer.parseInt(dataList.get(i).get("total").toString()));
}
}
// 将X轴的数据集合与Y轴的数据集合,设置到map中
map.put("data1", data1);
map.put("data2", data2);
return map;
}
public Map<String, Object> countCustomerMake02() {
Map<String, Object> map = new HashMap<>();
// 查询客户构成数据的列表
List<Map<String, Object>> dataList = customerMapper.countCustomerMake();
// 饼状图数据 数组(数组中是字符串)
List<String> data1 = new ArrayList<>();
// 饼状图的数据 数组(数组中是对象)
List<Map<String, Object>> data2 = new ArrayList<>();
// 判断数据列表 循环设置数据
if (dataList != null && dataList.size() > 0) {
// 遍历集合
for (int i = 0; i < dataList.size(); i++) {
//饼状图数据, 数组(数组中是字符串
data1.add(dataList.get(i).get("level").toString());
//饼状图数据 数组(数组中是对象)
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("name", dataList.get(i).get("level"));
dataMap.put("value", dataList.get(i).get("total"));
data2.add(dataMap);
}
}
// 将X轴的数据集合与Y轴的数据集合,设置到map中
map.put("data1", data1);
map.put("data2", data2);
return map;
}
本项目以jar包方式部署到阿里云
项目演示:添加链接描述
(账号admin 密码123456) 看看就好,不要删改数据。
源码请加q:2690534598