一.对CRM的项目的简单描述1.什么是CRM
CRM系统即客户关系管理系统,是指企业用CRM技术来管理与客户之间的关系。他的目标是缩减销售周期和销售成本,增加收入,寻找扩展业务所需的新的市场和渠道以及提高客户的价值,满意度,营利性和忠实度。CRM项目的实施可以分为3步,即应用业务集成。业务诗句分析和决策执行。
2.CRM开发环境和技术
<1> 项目业务介绍
客户关系管理是指企业为提高核心竞争力,利用相应的技术信息以及互联网技术协调企业与顾客间在消费,营销和服务上的交互,从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务的过程,其最终目标是吸引新客户,保留老客户以及将已有客户转为忠实客户,增加市场
<2>开发环境
项目名称:CRM客户管理系统系统作用:公司客户关系管理,潜在客户开发及订单合同管理开发环境:IDEA Windows10 jdk1.8 Maven Mysql8需要的工具:postman fiddler抓包工具或浏览器开发者工具
<3>开发技术
前端:LayUI freeMaker后端:Spring SpringMVC SpringBoot MyBatis Maven MySQL8 Linux CentOS ECharts(折线和饼状图)权限管理 定时任务调度(quartz)CentOS Lombok二.项目准备及模块分析1.模块分析总览
1.基础模块:包含系统基本的用户登录,退出,记住我,密码修改等基本操作。
2.营销管理:
营销机会管理:企业客户的质询需求所建立的信息录入功能客户开发计划:开发计划是根据营销机会而来,对于企业质询的客户,会有相应的销售人员对于该客户进行具体的沟通交流,此时对于整个 Crm 系统而言,通过营销开发计划来进行相应的信息管理,提高客户的购买企业产品的可能性。
3.客户管理:
4.服务管理:
服务管理是针对客户而开发的功能,针对客户要求,Crm 提供客户相应的信息质询,反馈与投诉功能,提高企业对于客户的服务质量。
5.数据报表:
Crm 提供的数据报表功能能够帮助企业了解客户整体分布,了解客户开发结果整体信息,从而帮助企业整体调整客户开发计划,提高企业的在市场中的竞争力度。
6.系统管理:
系统管理包含常量字典维护工作,以及权限管理模块,Crm 权限管理是基于角色的一种权限控制,基于RBAC 实现基于角色的权限控制,通过不同角色的用户登录该系统后展示系统不同的操作功能,从而达到对不同角色完成不同操作功能。
2.项目前期环境的搭建1.创建SpringBoot项目,导入依赖(见源代码)2.在src/main/resources 目录下新建 application.yml 配置文件##端口号上下文路径server:port:8080servlet:context-path:/crm##数据源配置spring:datasource:type:com.mchange.v2.c3p0.ComboPooledDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverurl:jdbc:mysql://127.0.0.1:3306/crm?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT+8username:password:##freemarkerfreemarker:suffix:.ftlcontent-type:text/htmlcharset:UTF-8template-loader-path:classpath:/views/##启用热部署devtools:restart:enabled:trueadditional-paths:src/main/java##mybatis配置mybatis:mapper-locations:classpath:/mappers/*.xmltype-aliases-package:org.example.crm.vo;org.example.crm.query;org.example.crm.dtoconfiguration:map-underscore-to-camel-case:true##pageHelper分页pagehelper:helper-dialect:mysql##设置dao日志打印级别logging:level:org:example:crm:dao:debug3.新建 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 ,添加启动项目相关代码如下:packageorg.example.crm;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication@MapperScan(“org.example.crm.dao”)//启用定时任务@EnableSchedulingpublicclassStarter{publicstaticvoidmain(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
三.项目的正式开发
主要讲解核心模块的核心代码
1.用户管理模块<1>.表结构分析
<2>.用户登录
定义UserModel类,用于用户登录成功返回的用户信息,用来设置前端的Cookie
@Getter@SetterpublicclassUserVo{//privateIntegeruserId;//存放在前端cookie中加密后的IdprivateStringuserIdStr;privateStringuserName;privateStringtrueName;}
设置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即可
<3>全局统一的异常处理及非法请求的拦截(1)统一的异常处理
全局异常实现思路:
控制层的方法返回的内容两种情况
视图:视图异常Json:方法执行错误 返回错误json信息
全局异常拦截器的实现,简化了try-catch代码
实现 HandlerExceptionResolver 接口 ,处理应用程序异常信息
(2)非法请求拦截
对于后端菜单资源,这里要求用户必须进行登录来保护 web 资源的安全性,此时引入非法请求拦截功能。
实现思路:
判断用户是否是登录状态获取Cookie对象,解析用户ID的值如果用户ID不为空,且在数据库中存在对应的用户记录,表示请求合法。否则,请求不合法,进行拦截,重定向到登录页面
定义拦截器:在新建 interceptors 包,创建 NoLoginInterceptor 类,并继承 HandlerInterceptorAdapter 适配器,实现拦截器功能。
/***非法访问拦截*继承HandlerInterceptorAdapter适配器*/publicclassNoLoginInterceptorextendsHandlerInterceptorAdapter{@AutowiredprivateUserMapperuserMapper;/***拦截用户是否是登录状态*在目标方法(资源)执行前执行的方法*返回boolean*如果为true,表示目标方法可用被执行*如果为false,表示阻止目标方法执行**@paramrequest*@paramresponse*@paramhandler*@return*@throwsException*/@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{//获取cookie中的用户IdIntegeruserId=LoginUserUtil.releaseUserIdFromCookie(request);//判断用户Id是否为空,且数据库中是否存在改userId的记录if(userId==null||userMapper.selectByPrimaryKey(userId)==null){//抛出未登录异常thrownewNoLoginException();}returntrue;}}
全局异常类配置:在全局异常处理类中引入未登录异常判断
/***全局异常统一处理*/@ComponentpublicclassGlobalExceptionResolverimplementsHandlerExceptionResolver{/***异常处理方法*方法的返回值:*1.返回视图*2.返回数据(JSON数据)*<p>*如何判断方法的返回值?*通过方法上是否声明@ResponseBody注解*如果未声明,则表示返回视图*如果声明了,则表示返回数据**@paramrequestrequest请求对象*@paramresponseresponse响应对象*@paramhandler方法对象*@paramex异常对象*@returnorg.springframework.web.servlet.ModelAndView*/@OverridepublicModelAndViewresolveException(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){/***非法请求拦截*判断是否抛出未登录异常*如果抛出该异常,则要求用户登录,重定向跳转到登录页面*/if(exinstanceofNoLoginException){//重定向到登录页面ModelAndViewmv=newModelAndView(“redirect:/index”);returnmv;}/***设置默认异常处理(返回视图)*/ModelAndViewmodelAndView=newModelAndView(“error”);//设置异常信息modelAndView.addObject(“code”,500);modelAndView.addObject(“msg”,”系统异常,请重试…”);//判断HandlerMethodif(handlerinstanceofHandlerMethod){//类型转换HandlerMethodhandlerMethod=(HandlerMethod)handler;//获取方法上声明的@ResponseBody注解对象ResponseBodyresponseBody=handlerMethod.getMethod().getDeclaredAnnotation(ResponseBody.class);//判断ResponseBody对象是否为空(如果对象为空,则表示返回的事视图;如果不为空,则表示返回的事数据)if(responseBody==null){/***方法返回视图*///判断异常类型if(exinstanceofParamsException){ParamsExceptionp=(ParamsException)ex;//设置异常信息modelAndView.addObject(“code”,p.getCode());modelAndView.addObject(“msg”,p.getMsg());}elseif(exinstanceofAuthException){//认证异常AuthExceptiona=(AuthException)ex;//设置异常信息modelAndView.addObject(“code”,a.getCode());modelAndView.addObject(“msg”,a.getMsg());}returnmodelAndView;}else{/***方法返回数据*///设置默认的异常处理ResultInforesultInfo=newResultInfo();resultInfo.setCode(500);resultInfo.setMsg(“异常异常,请重试!”);//判断异常类型是否是自定义异常if(exinstanceofParamsException){ParamsExceptionp=(ParamsException)ex;resultInfo.setCode(p.getCode());resultInfo.setMsg(p.getMsg());}elseif(exinstanceofAuthException){//认证异常AuthExceptiona=(AuthException)ex;resultInfo.setCode(a.getCode());resultInfo.setMsg(a.getMsg());}//设置响应类型及编码格式(响应JSON格式的数据)response.setContentType(“application/json;charset=UTF-8”);//得到字符输出流PrintWriterout=null;try{//得到输出流out=response.getWriter();//将需要返回的对象转换成JOSN格式的字符Stringjson=JSON.toJSONString(resultInfo);//输出数据out.write(json);}catch(IOExceptione){e.printStackTrace();}finally{//如果对象不为空,则关闭if(out!=null){out.close();}}returnnull;}}returnmodelAndView;}}
拦截器生效配置:
@Configuration//配置类publicclassMvcConfigextendsWebMvcConfigurerAdapter{@Bean//将方法的返回值交给IOCpublicNoLoginInterceptornoLoginInterceptor(){returnnewNoLoginInterceptor();}/***添加拦截器**@paramregistry*/@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){//需要实现了拦截器功能的实例对象NoLoginInterceptorregistry.addInterceptor(noLoginInterceptor())//设置需要被拦截的资源.addPathPatterns(“/**”)//设置不需要被拦截的资源.excludePathPatterns(“/css/**”,”/images/**”,”/js/**”,”/lib/**”).excludePathPatterns(“/index”,”/user/login”);}}
测试拦截效果:
当 Cookie 中的用户ID不存在时,访问 main 页面,会自动跳转到登录页面
<3>记住我功能
2.营销管理模块(CRUD操作,见源码)<1>功能开发及表结构分析
功能开发:
表结构:
这里注意:时间格式化:
@JsonFormat(pattern=”yyyy-MM-ddHH:mm:ss”,timezone=”GMT 8″)@DateTimeFormat(pattern=”yyyy-MM-dd”)//如果传递的参数是Date类型,要求传入的时间字符串的格式privateDateplanDate;3.权限管理模块(CRUD操作见源码)
基本概念:RBAC是基于角色的访问控制( Role-Based Access Control )在RBAC中,权限与角色相关联,用户通过扮演适当的角色从而得到这些角色的权限。这样管理都是层级相互依赖的,权限赋予给角色,角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
<1>.模块功能及表的结构设计
功能模块:
表结构设计:
从上面实体对应关系分析,权限表设计分为以下基本的五张表结构:用户表(t_user)、角色表(t_role)、t_user_role(用户角色表)、资源表(t_module)、权限表(t_permission)用户和角色间一对一关系,角色和权限间一对一关系,建立t_user_role和t_permission中间表表结构关系如下:
<2>角色权限功能
当完成角色权限添加功能后,下一步就是对角色操作的资源进行认证操作,这里对于认证包含两块:
菜单级别显示控制后端方法访问控制
查询出改用户所拥有的角色,然后根据角色查询出拥有的权限码,具体实现如下:
@RequestMapping(“main”)publicStringmain(HttpServletRequestrequest){//通过获取cookie用户IDIntegeruserId=LoginUserUtil.releaseUserIdFromCookie(request);//查询用户对象,设置session作用域Useruser=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”;}(1).菜单级别显示控制
系统根据登录用户扮演的不同角色来对登录用户操作的菜单进行动态控制显示操作,这里显示的控制使用freemarker指令 内建函数实现,例如:
会根据权限码,来显示菜单内容:
(2).后端方法级别访问控制(AOP 注解实现)
@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented/***定义方法需要的对应资源的权限码*/public@interfaceRequiredPermission{//权限码Stringcode()default””;}
方法级别使用注解:
例如:
定义aop切面类 拦截指定注解标注的方法:
@Component@AspectpublicclassPermissionProxy{@ResourceprivateHttpSessionsession;/***切面会拦截指定包下的指定注解*拦截com.xxxx.crm.annoation的RequiredPermission注解**@parampjp*@returnjava.lang.Object*/@Around(value=”@annotation(org.example.crm.annotation.RequiredPermission)”)publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{Objectresult=null;//得到当前登录用户拥有的权限(session作用域)List<String>permissions=(List<String>)session.getAttribute(“permissions”);//判断用户是否拥有权限if(null==permissions||permissions.size()<1){//抛出认证异常thrownewAuthException();}//得到对应的目标MethodSignaturemethodSignature=(MethodSignature)pjp.getSignature();//得到方法上的注解RequiredPermissionrequiredPermission=methodSignature.getMethod().getDeclaredAnnotation(RequiredPermission.class);//判断注解上对应的状态码if(!(permissions.contains(requiredPermission.code()))){//如果权限中不包含当前方法上注解指定的权限码,则抛出异常thrownewAuthException();}result=pjp.proceed();returnresult;}}4.客户管理模块(CRUD操作见源码)<1>.模块功能及表结构设计
模块功能:
表结构设计:
t_customer 客户表、t_customer_contact 客户交往记录表、t_customer_linkman 客户联系人表、t_customer_order 客户订单表、t_order_details 订单详情表
<2>.定时器
当实现了客户数据转移业务逻辑代码后,这里需要思考一个问题:客户数据量的问题随着时间的积累,流失的客户数据可能就比较大,如果数据的获取在用户查询时进行,此时后端对于数据的查询就会变得很慢,此时可以使用我们之前讲到的定时任务来处理,后台通过定时器来对流失客户数据定时进行转移处理,从而当前端用户查询时只需到客户流失表查询流失数据即可。
增加定时器服务:
/***定时任务的执行*/@ComponentpublicclassJobTask{@AutowiredprivateCustomerServicecustomerService;//cron表达式//每两秒执行一次//@Scheduled(cron=”0/2****?”)//从六月开始,每个月执行一次@Scheduled(cron=”****6/1?”)publicvoidjob(){//调用需要被执行的方法//开始执行定时任务System.out.println(“开始执行定时器任务”);customerService.updateCustomerState();System.out.println(“定时器任务执行完成”);}}
Starter开启定时任务环境配置:
@SpringBootApplication@MapperScan(“org.example.crm.dao”)//启用定时任务@EnableSchedulingpublicclassStarter{publicstaticvoidmain(String[]args){SpringApplication.run(Starter.class);}}5.服务管理(CRUD操作见源码)<1>功能实现及表结构设计
功能实现:
表结构设计:
<2>服务实现
这里对于服务管理服务的创建,分配,处理与反馈后端代码实现放在同一个方法中进行处理,同时方便对于服务状态值统一处理,这里定义 CustomerServeStatus 枚举类来实现。
/***客户服务状态枚举类*/publicenumCustomerServeStatus{//创建CREATED(“fw_001”),//分配ASSIGNED(“fw_002”),//处理PROCED(“fw_003”),//反馈FEED_BACK(“fw_004”),//归档ARCHIVED(“fw_005”);privateStringstate;CustomerServeStatus(Stringstate){this.state=state;}publicStringgetState(){returnstate;}}6.统计报表管理(CRUD见源码)<1>.功能实现
功能实现:
<2>.使用ECharts对数据进行分析
折线图数据返回实现:
/***查询客户构成(折线图)*@return*/publicMap<String,Object>countCustomerMake(){Map<String,Object>map=newHashMap<>();//查询客户构成数据的列表List<Map<String,Object>>dataList=customerMapper.countCustomerMake();//折线图X轴数据数组List<String>data1=newArrayList<>();//折线图Y轴数据数组List<Integer>data2=newArrayList<>();//判断数据列表循环设置数据if(dataList!=null&&dataList.size()>0){for(inti=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);returnmap;}
饼状图数据返回实现:
publicMap<String,Object>countCustomerMake02(){Map<String,Object>map=newHashMap<>();//查询客户构成数据的列表List<Map<String,Object>>dataList=customerMapper.countCustomerMake();//饼状图数据数组(数组中是字符串)List<String>data1=newArrayList<>();//饼状图的数据数组(数组中是对象)List<Map<String,Object>>data2=newArrayList<>()//判断数据列表循环设置数据if(dataList!=null&&dataList.size()>0){//遍历集合for(inti=0;i<dataList.size();i ){//饼状图数据,数组(数组中是字符串data1.add(dataList.get(i).get(“level”).toString());//饼状图数据数组(数组中是对象)Map<String,Object>dataMap=newHashMap<>();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)returnmap;}
blog.csdn.net/qq_45704528/article/details/117451506