CRM简单小结

思想

  • 对于三层架构,一个模块对应一个controller,controller实际就是Servlet;一张表对应一个domain类对应一个dao接口对应一个mapper文件;service层没有严格规定,如果两张表内容相近,用一个service接口也可以,如ActicityService,也可以用俩。

  • **web目录下有controller、filter、listener。这三个都是web顶层内容,都可以根据需求访问任何业务层service。如在市场活动模态窗口中展示用户列表;同时,在业务层service也可能调多个dao层接口。只要知道:那个模块发的请求,那个模块的controller接收,根据前端要的数据,控制器调不同业务层去完成,业务层根据要查询的数据去调不同表的dao层。**此外,controller层对业务层变量命名用简写,不会重复。业务层对dao接口要写全,因为可能用到多张表,即多个dao层

  • controller层尽可能少写代码,即少处理业务,接收的即使是个数组,也不要在这里封装数据,而是接收到啥,就发给业务层啥,让业务层去处理业务,控制器只要结果,前端要啥,控制器问业务层要啥。

  • MVC思想,只有控制器里能用request和response。不要把这俩传给业务层,业务层只是处理业务。业务层需要的数据在控制器中就拿出来,只传数据。

  • 在前端写完ajax请求时,就应该考虑需要什么数据,前端需要什么数据,后端发送什么数据,并且确定数据的格式,是JSON数组,还是JSON,还是JSON里又套了个JSON数组。同理,后端controller也要考虑问service层要什么数据,是java对象,还是普通类型。这些都想明白了,在往下走。

  • 不是一个模块一个包结构,而是按照大功能分包结构。如系统设置settings分个包结构,workbench分个包结构。workbench下有市场活动、线索等模块,这些模块在一个包下,只不过他们的controller不同,但都在一个包下。整个项目共用的等级高,如utils、vo等。

    image-20211214081747093
  • 后端数据可以封装成map/vo响应给前端:具体选取依据就是看这个数据使用率高不,高就用VO,以后别的模块也能用到,并且一般整个模块的VO类用泛型

  • 在实际开发中,有关“改”操作的代码直接复制“增”的代码,再其基础上进行修改。因为大部分代码一致。

  • 使用ajax请求,数据以json格式响应,前端处理json数据;使用传统请求,可以将数据保存在域对象中,前端使用jstl、el表达式,更方便操作数据。但是要注意,jq中使用EL表达式必须带双引号。EL表达式的xxxScope可以忽略,默认从页面域pageContext找、request、session、application。能用小的用小的。

  • 对于动态拼接出来dom元素注意,一般用主键id绑定,保证唯一。用onclick绑定事件,执行某个函数,或直接发传统请求给后台。

  • 实际开发中,一般将不变的数据,如池、数据字典保存在服务器缓存中,当服务器启动时,将这些数据保存在application域对象当中,只要服务器正常启动,就直接从application中取数据。这样的优点是:不走数据库,快。但是不是所有数据都适合放缓存中,只有不变的数据以及池才适合放缓存中,因为缓存中的数据只有服务器重启时会更新,将经常变化的数据放进去,会导致每次取得都是旧数据。

  • 对于多对多,三张表,关系表,俩外键。一般处理关系表中的业务时,可以随便用关联的两张表的service层,不过一般从那个模块发出请求,用那个模块的service层。如tbl_clue_activity_relation。在通过clueId获取activityList时,从线索模块发出,就用ClueService。

  • 单元测试:junit组件,junit是多线程一起测方法,比main好用,main中测试代码不好管理。junit一起测,若模块之间的耦合出现问题也能测出来,例如改了A模块,B可以受影响,那就一起测,没问题就说明改动是可以的。而main中不行,因为main只是一个线程,不能一起测。断言:提前预言结果,如果是对的,拿junit测试就是对的,否则就错的,语法:Assert.assertEquals(flag, true)。junit是注解式开发,注解Annotation可以看作代码。

  • 请求与响应

    • 请求的方式有两种:ajax请求,传统请求。使用局部刷必定用ajax请求,如果刷整个页面通常发传统请求;
      • 发ajax请求,可以发json串,也可以自己拼个url,以name=value的方式发,这种一般用在复选框,即有多个相同的name时。如根据id批量删除。
      • 发传统请求也有多种方式,如果不携带数据或少量数据(id),通常直接绑定一个单击事件,该地址栏:window.location.href=“url”。如果数据多,通常以form表单的形式发数据,主要表单中需要发送的数据要有name。
    • 响应的方式有三种:如果前端发的是ajax请求,后端必定响应json字符串:response.getWriter().print(json);如果前端发的是传统请求,后端用转发或重定向,使用哪种判断如下:
      • 数据:如果响应需要携带数据,将数据保存在request域中以转发的方式响应给前端。不需要携带数据,直接重定向。
      • 路径:转发后的路径是当前路径xxx.do,这样前端每刷一次,就会过一次后台。而使用重定向后的路径通常是xxx.jsp这样每一次刷新不会过后台,只是刷页面。具体选择应该看该页面有没有修改操作,例如在页面修改完,刷新直接过后台然后重新取数据,前端铺数据,保证是最新的数据。
      • 简单判断就是,携带数据用转发,否则一律重定向。

    CRM简单小结_第1张图片

  • 提示性信息(可能性):对于stage-possibility这种数量少的对应关系,保存在application中,而不是在数据库中。并且这种数据也是不会变的,一般将对应关系写在properties文件中,通过ResourceBundle去解析该文件,将解析出的数据保存在map集合中。可以将该map集合直接保存在application域中;也可以将该map集合通过jackson转化为json串保存在域中,目的都是在项目其他模块中使用。两者各有优点,以map集合形式保存在后端容易获取数据;以json串前端容易操作,通过EL就可以。注意:可以将这种信息设置为一个类的扩展属性,不在业务层与dao层操作,只是在控制器中获取对应的值,然后set就可以,这样的好处是,前端取值统一。但是,扩展属性慎用,一个domain类中不能超过3个,否则会破坏实体类结构

    Map<String, String> pMap = new HashMap<>();
        //使用ResourceBundle工具类处理properties文件,注意路径没有.properties
        ResourceBundle bundle = ResourceBundle.getBundle("Stage2Possibility");
        //获取properties文件中所有的key
        Enumeration<String> stageList = bundle.getKeys();
        while(stageList.hasMoreElements()){
            String stage = stageList.nextElement();
            String possibility = bundle.getString(stage);
            pMap.put(stage, possibility);
        }
    
        application.setAttribute("pMap", pMap);
    
        //在这里使用jackson将pMap转化为json串,保存在application域中。
        ObjectMapper om = new ObjectMapper();
        try {
            String possibilityMap = om.writeValueAsString(pMap);
            //前端直接从该json串中取值就可以
            application.setAttribute("possibilityMap", possibilityMap);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
    }
    

功能介绍

系统设置模块settings

  • 用户模块:登录操作
  • 涉及到数据字典模块信息的查询

工作台(核心业务)workbench

  • 市场活动模块activity

    • 点击创建按钮,打开模态窗口添加操作
    • 展现市场活动信息列表(结合条件查询+分页查询)全选/反选
    • 执行删除操作((可批量删除)
    • 点击修改按钮打开修改操作的模态窗口执行修改操作
    • 点击市场活动名称跳转到详细信息页,展现详细信息详细信息页加载完毕后,展现备注信息列表
    • 备注添加,修改,删除
  • 线索模块: clue(整个CRM项目最重要的,线索就是潜在客户)

    • 点击创建按钮,打开添加操作模态窗口(窗口中对于下拉框的处理有服务器缓存中的数据字典来填充)
    • 点击线索名称进入到详细信息页,展现线索的详细信息
    • 在页面加载完毕后,展现关联的市场活动列表
    • 解除关联操作
    • 关联市场活动操作
    • 点击转换按钮跳转到线索转换页面
    • 执行线索转换的操作(可同时添加交易)
  • 交易模块: transaction

    • 点击创建按钮,跳转到交易添加页
    • 点击交易名称进入到交易详细信息页,展现详细信息在页面加载完毕后,展现交易历史列表
    • 动态展现交易阶段内容及图标
    • 点击阶段图标,更改交易阶段
  • 统计图表

    • 交易阶段统计图Echarts

登录模块

  • 为了提高用户体验,一般在页面加载完毕后,将光标定位在账号输入框。使用$(“#id“).focus();用户点击登录按钮可以发送请求,同时敲回车也可以发送。综上两条,应该将login抽成一个函数,发生以上任意事件调用login函数

    $(document).keydown(function(event){
    	if(event.keyCode == 13){
    		login();
    	}
    })
    
  • 登录失败的具体信息也要返回给前端,后端采用异常的方法将失败信息抛出。注意代理类对异常的捕捉会导致controller接收不到异常,从而导致错误。throw e.getCause()

  • 前端需要注意的是如果往这种单个的标签里赋值用val()。如果给

    这种中间夹着的地方赋值用text()或html()

  • 使用window.location.href可以修改网页地址栏,达到跳转的目的。js里写EL表达式${sessionScope.user.name} sessionScope可忽略。jq中EL必须被括起来

  • 登录页面要每次以顶级窗口形式出现

    if(window.top != window){
    	window.top.location = window.location;
    }
    
  • 整个项目中的资源需要防,即加拦截器(拦*.do与*.jsp);以及使用过滤器(滤所有.do)将所有请求与响应的字符集进行统一设置。url写某一类文件只能*.xxx不能a/*.xxx*

  • 关于令牌:访问任何后台资源.do/.jsp都先过拦截器要令牌(session),除了与login相关的,有令牌提供服务,没有直接返回登录页面。用户登录成功,创建一个session并保存在session域中,之后用户每次请求都有令牌。session保存在后端,默认30分钟。Cookie保存在前端浏览器缓存,除了用户有10天免登录等操作。否则用户只要关闭浏览器,Cookie释放,导致用户再次访问需要重新登陆。但是只要用户不关闭浏览器,就还是一次会话,Cookie和Session都在。

    • Session做令牌的原理:用户登录成功,后端创建一个Session对象,创建Session对象的同时会创建一个Cookie对象,这个Cookie的name是“JSESSIONID”,其value是“32位序列号,全球唯一”。之后服务器将Cookie响应给前端,并将Session对象与“32位序列号”绑定,保存在session列表中。
    • 拦截器具体操作:前端访问任何.do/.jsp资源,经过拦截器,拦截器要session,这个底层其实是看前端有没有发来一个name为JSESSION的Cookie。如果发了,然后会去那这个Cookie的value,即32位的序列号,去后端session列表中进行equals,如果返回true,代表找到了绑定的session对象,即获取到了session,放行。除了login相关操作不要令牌,其它没令牌的全重定向到login.jsp。
  • 关于乱码:前端发请求,后端发响应都会经过过滤器增强,即设置字符编码,但是一般只滤*.do。因为jsp文件一般用<%@ page contentType=“” %>设置

市场活动模块

  • 页面中的所有按钮、窗口都应该由我们来控制,改它的id,我们js来触发。

  • 模态窗口

    • 模态窗口的出现是:KaTeX parse error: Expected 'EOF', got '#' at position 3: (“#̲xxxModal”).moda…(“#xxxModal”).modal(“hide”);清空模态窗口内容:$(“#activityAddForm”)[0].reset();
      • 注意要清空模态窗口中内容,用reset函数重置模态窗口里的表单,并且reset函数是dom对象的函数,要先将jq对象转换为dom对象
    • 下拉列表默认选中一项:KaTeX parse error: Expected 'EOF', got '#' at position 3: ("#̲create-owner").…{user.id}"); //${user.id}是EL表达式,注意在jq中使用EL表达式,必须是字符串
  • 日历控件

    • 填写日期,一般情况下都是需要相关的日历控件的。这里使用的是:bootstrap datetimepicker(日期拾取器)
    • 步骤:导入日历控件库->引入库->直接cv日期控件代码在模态窗口弹出前->在需要的地方添加class
  • 分页查询

    • 分页查询有多个入口:查询、创建、修改、删除、分页组件(分页插件写好了调用方法)、市场活动因此,要将列表展示抽成一个函数pageList(),数据处理完毕后结合分页插件展示列表,达到分页的效果。分页代码直接CV。
  • 分页查询的步骤是:点击入口、发送请求、获取列表、分页展示到相应位置。

    • 分页需要用到分页插件pagination,只要使用插件就要将插件资源导入项目,在需要用到分页插件的页面在引入资源的路径,注意顺序。如pagination分页插件是bootstrap下的,因此必须放到bootstrap的引入资源下面。凡是使用到插件,都要导入插件、引入路径
  • 参数问题:

    • 前端发送pageNo、pageSize。接收 data:{“total”:total, “dataList”:[{市场活动1},{2}]} 其中total是分页插件、dataList拼活动列表需要

      • 分页插件需要的两个参数:totalPages(总页数)、pageSize(每页展示记录数)。totalPages=Math.ceil(data.total/pageSize)
    • 后端sql语句分页需要两个参数:skipCount、pageSize。skipCount=(pageNo-1)*pageSize

      • total是数据库查出来的总记录条数;totalPages是拿到总记录条数算出来的;skipCount是根据前端的参数在后端算出来给sql语句用的
    • 分页存在问题

      • 在查询框中输入内容,不点击查询按钮,点击分页按钮,结果为查询框中的内容生效了;
      • 在查询框中输入内容,点击查询按钮,再在查询框中输入内容,不点击查询按钮,点击分页按钮,结果为新的查询框中的内容生效了;
    • 隐藏域(解决上述问题)

      • 将查询条件放到隐藏域当中,每一次翻页的时候,条件都从隐藏域当中取。
      • 点击查询按钮的时候将查询框中的内容更新(保存内容到)隐藏域;执行pageList的时候,将隐藏域中的内容更新到查询框。
  • 注意:

    • 使用分页插件前,要把前端模型中原有的分页插件干掉,引入一个div,我们把自己引入的分页插件写div里

      • 展示的列表都是在ajax执行成功后根据响应的数据出来的。分页是在这之后的操作。
  • 复选框的全选和取消全选

    • 使用jQuery实现。jQuery支持多个元素一块处理。很多地方是不需要使用each遍历的。为后期ajax动态生成的元素绑定事件,必须用on。
    //为全选的复选框绑定事件,触发全选操作
    $("#qx").click(function (){
    	//input[name=xz]代表选中input标签中所有name为xd的dom对象;prop函数是设置或返回被选元素的属性和值。
    	$("input[name=xz]").prop("checked", this.checked);
    
    })
    
    //以下代码错误
    /*
    $("input[name=xz]").click(function (){
    	alert(123);
    })
    */
    /*
        因为动态生成的元素,是不能以普通绑定事件的形式来操作
        动态生成的元素,要以on的形式来触发事件
    
        语法:
        $(需要绑定元素的有效的外层元素).on(绑定事件,需要绑定的jquery对象,回调函数)
    */
    
    $("#activityBody").on("click", $("input[name=xz]"), function (){
    	//比较原理:xz框总数量与xz框checked的数量一致,就选中qx
    	$("#qx").prop("checked", $("input[name=xz]").length == $("input[name=xz]:checked").length);
    })
    
  • 注意:

    • **动态生成的元素,是不能以普通绑定事件的形式来操作,动态生成的元素,要以on的形式来触发事件。**语法:$(需要绑定元素的有效的外层元素).on(绑定事件,需要绑定的jquery对象,回调函数)
    • “需要绑定元素的有效的外层元素”是指,不是动态生成的元素。如果是动态生成的元素外层还是动态生成的,那就再往外找。
  • CURD**(最重要的是id,后台查数据只根据id)**

    • 创建市场活动

      • 点击“创建”按钮->走后台取数据->将数据平铺到模态窗口中->展示模态窗口
      • 点击保存->将“创建”模态窗口中的所有数据,以及创建人的id发送给后台->控制器生成该市场活动的uuid并将数据封装发送->数据库作insert操作
    • 删除市场活动

      • 由于tbl_activity和tbl_activity_remark表存在父子关系,因此要删除市场活动,首先要删除掉它该市场活动下的所有市场活动备注。
      • 删除市场活动最重要的是发送市场活动的id,但是一次可能删多条,因此发送的是一个key都为id的串,这个串不能用json拼,因为json要求key不相同,因此只能自己拼,形式:“id=value&id=value”,但是发送的还是ajax请求,只不过data,直接填这个串Param就可以。
      • 市场活动controller通过getParameterValues(“key”)获取ids数组,将ids数组作为参数发送给service
      • 业务层删除市场活动前先进行验证。查询要删除的市场活动备注的总数count1,获取删除市场活动备注的总数count2,如果两者相等,删除该市场活动。因此,要查询和删除市场活动备注表,就要调市场活动备注表的dao接口;删除市场活动,就调市场活动的dao接口。
    • 修改市场活动

      • 在实际开发中,有关“改”操作的代码直接复制“增”的代码,再其基础上进行修改。因为大部分代码一致。
      • 点击“修改”按钮->走后台取数据->将数据平铺到模态窗口中->展示模态窗口
      • 修改最终要的是获取待修改市场活动的id,这个id保存在模态窗口的form表单的隐藏域当中,提交表单时一并提交,因为id不需要用户知道,所以放隐藏域里。同时应注意,将所有者的id也要发送给后台,因为后端owner保存的是uuid,而前端展示的是真实姓名。
    • 注意问题:

      • 对于CURD而言,id是很重要的数据,必须发给后台,这样才知道操作的那条记录。市场活动id的处理:创建时,后台创建一个uuid;删除时,id就是每条记录前checkbox的value;修改时,直接从隐藏域里获取uuid就可以。所有者id的处理:创建时,将当前用户的id赋值给下拉列表;删除时,所有者id不重要;修改时,获取下拉列表的value。总之,只要保证下拉列表赋值的是所有者的uuid,后面直接获取下拉列表的value发送就可以了。

        //拼用户下拉列表
        var html = "";
        $.each(data.userList, function (i, n){
        	html += "+n.name+"";
        })
        $("#create-owner").val("40f6cdea0bd34aceb77492a1656d9fb3"); //给下拉列表赋值uuid,展示的是name
        var id = $.trim($("#edit-id").val()); //只要下拉列表的value是uuid,直接获取就好,数据库中也是uuid。不能是html()
        
    • CURD操作后,有关分页插件的pageList()函数参数的设置

      • 新建市场活动后,应该回到第一页,维持每页展示的记录数
      • 删除市场活动后,应该回到第一页,维持每页展示的记录数
      • 修改市场活动后,应该维持当前页,维持每页展示的记录数
      • 搜索市场活动后,应该回到第一页,维持每页展示的记录数
      修改后停留在当前页,修改后维持已经设置好的每页展示的记录数
      pageList($("#activityPage").bs_pagination('getOption', 'currentPage')
      		,$("#activityPage").bs_pagination('getOption', 'rowsPerPage'));
      
  • 市场活动备注

    • 点击市场活动的名称跳转到detail.do,市场活动名称是在pageList中动态拼出来的,对于动态拼出来的对象想要绑定事件,采用onclick的方式。注意:这也是要走后台的,因为detail.jsp页面需要该市场活动的信息。后台取到数据保存在request域,然后转发到detail.jsp。在detail.jsp中用EL表达式赋值

      onclick="window.location.href=\'workbench/activity/detail.do?id='+n.id+'\';"	
      
    • 注意应该发送的传统请求,原因如下:

      • 市场活动备注是一个新的页面,是整个子页面的刷新,不需要局部刷新
      • 该页面下有备注列表,备注列表有CURD操作,并且需要局部刷新。可以将备注列表与页面一同发ajax展示出来,但是之后每对备注进行一次CURD就要刷新上面不变的部分。因此好的方式是:页面用传统请求展示,备注列表用ajax请求。当页面加载完毕,自动调一次ajax把该市场活动的备注列表刷出来。
    • 备注列表的展示写到一个函数里showRemarkList(),如同pageList。因为展示该列表有多个入口,如CURD操作也要刷新备注列表。并且,如果备注里列表的外层div里有不能删的代码,如前端动画等,就不能用val的方式赋值。解决办法有两个,方案1:在div里再建一个div,起个id,对这个div操作,将拼好的备注列表放里面;方案2:再前面动画的div里用append,或者在最后的div里用before追加。

      //remarkDiv是最后一个div,在它之间追加。
      $("#remarkDiv").before(html);
      
    • 注意备注列表是动态拼出来的,而备注div中的图标也是动态拼出来的。对于动态拼出来的dom对象要注意:

      • 在遍历中动态生成的dom对象都采用直接触发的方式 onclick。通常在onclick中写一个回调函数,注意函数中的参数必须在字符串中。

        //最外层是先转移,保证在字符串中,然后再拼串,外层拼串用的是'',因此拼串也用''
        onclick="deleteRemark(\''+n.id+'\');"
        
      • 动态拼出来的dom对象,一般用该“对象的id”作为其id。因为动态拼出来的,不能写死。

        //注意,这里不用将id写在字符串里,只有函数中的参数需要写在字符串中
        <div id="'+n.id+'" class="remarkDiv" style="height: 60px;">
        
      • 一个超链接的href=“javascript:void(0);”,表示将超链接禁用,只能以触发事件的形式来操作。

    • CURD:CURD步骤都差不多,不过要注意的是对于备注列表CRUD完不能调showRemarkList(),因为该函数是通过before追加上去的。因此,创建时:直接用before追加;删除时:删除当前备注;修改:修改当前备注;操作当前备注时,要通过id绑定事件,而当前备注是通过动态拼出来的,因此id不能写死,一般用该条备注信息的id作为绑定的id,即$(“#xxx”);

线索模块

  • 线索模块所用数据库表结构及关系

    • 线索表(tbl_clue)、线索备注表(tbl_clue_remark)、线索市场活动关系表(tbl_clue_activity_relation)
      • tbl_clue与tbl_clue_remark(多)是一对多关系;tbl_clue与tbl_clue_activity是多对多关系
    • 客户表(tbl_customer)、客户备注表(tbl_customer_remark)
      • tbl_customer与tbl_customer_remark是一对多关系
    • 联系人表(tbl_contacts)、联系人备注表(tbl_contacts_remark)、联系人市场活动关系表(tbl_contacts_activity_relation)
      • tbl_contacts与tbl_contacts_remark是一对多;tbl_contacts与tbl_activity是多对多
  • 数据字典所用到的表及关系,并将数据字典中数据导入

    • 字典类型表(tbl_dic_type)、字典值表(tbl_dic_value)
      • tbl_dic_type与tbl_dic_value是一对多关系
  • 将“线索”、“客户”、“联系人”、“交易”模块的html修改为jsp,解决404错误。包括内部超链接的404问题

  • 搭建“线索”、“客户”、“联系人”、“交易”相关后端结构(domain,dao,service,controller)

  • 数据字典是指将一些固定不变的数据作为数据字典。常将数据字典保存在服务器缓存中,每次取数据不走数据库,快。一般表单中有关选择的相关的数据(下拉框,单选框,复选框)都从数据字典中取。例如,城市下拉列表中的城市,职位下拉列表中的职位。对于表单元素中选择的数据一定都是要写活的,来自数据字典。注意如何取数据字典,数据字典保存在map中Map,String代表DicType,有几个类型,就有几个List。在将map里的value放到application域中,name就是DicType。

    CRM简单小结_第2张图片

  • 使用jstl语法,将application域中的数据拿出来,拼到下拉列表里。jstl和el表达式一起使用,方便对域对象中的数据操作

    
    
  • 点击线索,走后台获取线索列表拼到列表表格中,这个是动态生成的,要给每条线索的名称、复选框绑定该线索的id。与市场活动相同。

  • 点击线索上的名称,发传统请求走后台,注意该传统请求是拼在动态生成的线索列表中的,想要触发动态生成的元素,只能用on的方式,并将该条线索的id发给后台,后台根据id查单条线索,并将线索保存到request域中,转发至详情页面detail.jsp,在该页面用el表达式从request域中取出数据,拼到对应位置。

    onclick="window.location.href=\'workbench/clue/detail.do?id='+n.id+'\';">'+n.fullname+'';
    
    • 注意:线索模块的CURD没做全,以及备注没做,只做了线索的创建、拼线索列表、点名称发传统请求、最终转发到详情页面。

线索模块的核心

线索模块核心页面是detail.jsp。对于CURD不练了

  • 根据clueId获取市场活动列表:通过线索id去线索市场活动关系表中查出所有该线索下的市场活动,将其拼到市场活动列表中。线索与市场活动是多对多的关系,这个功能主要练的是3张表联查,即tbl_activity、tbl_clue_activity_relation、tbl_user。本质上还是CURD,找到市场活动列表拼串。注意:该功能是在detail.jsp页面加载时就走后台,然后拼好展示。

  • 解除关联:点击”解除关联“超链接,可以将该条市场活动与当前线索解除关联。解除关联的本质就是在tbl_clue_activity_relation删去对应的关系记录。而市场活动列表是动态拼出来的,给该超链接绑定一个事件,里面是unbund(n.id)注意动态生成的,函数内的参数要写在字符串中,并且该id是关系表的的id,不是市场记录的id,这样方便操作,在查市场活动列表时:car.id as id。解除关联,可以直接remove该记录,也可以重新刷一下市场活动列表,因为该列表是在中

  • 关联市场活动(重点)

    • 点击“关联市场活动超链接”在模态窗口出现前,过后台取出所有与该线索非关联的市场活动(重要)。并且在该模态窗口中可以根据市场活动的name模糊查询。查询时,发两个参数:clueId、name。关联时,发两个参数:clueId、avtivityId。
      • 注意,该模态窗口有根据市场活动name模糊查询,敲回车触发。在模态窗口中敲回车会触发默认刷新页面事件,使用“return fasle”解决。
    <select id="getActivityListByNameAndNotClueId" resultType="com.xd.workbench.domain.Activity">
    
        select
    
        a.id,
        u.name as owner,
        a.name,
        a.startDate,
        a.endDate
    
        from tbl_activity a
        join tbl_user u
        on a.owner=u.id
    	/*这里模糊查询不需要用动态sql,如果没有条件就是都查。注意该子查询,即要查询出的市场活动的id不被clueId所关联*/
        where a.name like '%' #{name} '%' and a.id not in(
            select
    
            activityId
    
            from tbl_clue_activity_relation
            where clueId=#{clueId}
        )
    
    </select>
    
    • 因为要批量关联,所以前端发的数据是“cid=xxx&aid=xxx&aid=xxx”,后端获取到cid,还有aids数组后,还要生成根据aids数组的长度生成UUID,注意要封装到ClueActivityRelation类,最终发给dao层一个List类型的carList。sql语句如下:

      <insert id="bund">
          insert into tbl_clue_activity_relation(id,clueId,activityId)
          values
          <foreach collection="list" item="car" separator=",">
          	(#{car.id},#{car.clueId},#{car.activityId})
          </foreach>
      </insert>
      
      //service
      public boolean bund(String cid, String[] aids) {
          boolean flag = true;
          List<ClueActivityRelation> carList = new ArrayList<>();
          for(String aid : aids){
              ClueActivityRelation car = new ClueActivityRelation();
              car.setId(UUIDUtil.getUUID());
              car.setClueId(cid);
              car.setActivityId(aid);
      
          	carList.add(car);
          }
      
          int count = clueActivityRelationdao.bund(carList);
          if(count != aids.length){
         		flag = false;
          }
      
          return flag;
      }
      

      注意:

      • 封装成List的好处是只访问一次数据库,后端foreach拼出多个插入的值。也可以不用List,即没生成的car对象,就走一次dao层,不过不推荐。

      • controller层尽可能少写代码,即少处理业务,接收的即使是个aids数组,也不要在这里封装数据,而是接收到啥,就发给业务层啥,让业务层去处理业务,控制器只要结果,前端要啥,控制器要啥。

        //controller
        private void bund(HttpServletRequest request, HttpServletResponse response) {
        
            System.out.println("通过clueId和activityId创建关联");
        	//cid=xxx&aid=xxx&aid=xxx&aid=xxx
            String cid = request.getParameter("cid");
            String[] aids = request.getParameterValues("aid");
        
            ClueService cs = (ClueService) ServiceFactory.getService(new ClueServiceImpl());
        
            boolean flag = cs.bund(cid, aids);
        
            PrintJson.printJsonFlag(response, flag);
        }
        
  • 线索转换(重点)

    线索转换的核心:将线索转换真正客户,即将线索中与公司相关的信息转化为客户,与人相关的信息转化人联系人。

    转换步骤:

    • 前端
    1. 在线索的详细页detail.jsp,点击转换,发送传统请求(因为是子页面的刷新),不需要过后台。传参的格式:

      • 传参数的方式转发,不用过后台。要求:这种方式传的信息不能涉及用户隐私;字符长度不能很长,传统get请求传参对字符数有限制。
      //参数从request域中取,request域保存了该条clue的所有信息
      onclick="window.location.href='workbench/clue/convert.jsp?id=${c.id}&fullname=${c.fullname}&appellation=${c.appellation}&company=${c.company}&owner=${c.owner}
      
    2. 转换页convert可以直接用EL表达式从参数中取值。也可以用jsp取值(jsp本质就是servlet,有9大内置对象)。

      //EL表达式取数据,使用该方式
      ${param.id} ${param.fullname} ${param.appellation} ${param.company}
      
      //JSP中取
      <%
      	var id = request.getParameter("id");
      %>
      <%=id%> 
      

      注意:

      • EL表达式从域对象中取值可以省略前面的xxxScope,默认从小开始找。而从参数中取值不能省略,格式:${param.参数名}
      • jsp九大内置对象的考点:
        • 列写9大内置对象:pageContext、request、session、application、page、response、out、exception、config
        • PageContext的作用:当成普通页面域对象用、可以随时随处变成其它域对象用、PageContext域对象可以取得另外8个内置对象
    3. 转换页面convert,可以为用户创建交易,也可以不创建。但是无论怎样都需要过后台,因为线索转换要涉及多张表。因此,要根据用户时候选中创建交易复选框去发不同的请求。

      CRM简单小结_第3张图片

    4. 给“创建交易”的表单的“日期”添加日历插件;“阶段”从数据字典中用“jstl+el”取值;

      <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
      
      
    5. “市场活动源”的放大镜绑事件,发ajax请求将市场活动名字作为参数,后端根据name进行模糊查询,将查询到的市场活动相应给前端,前端将结果拼到市场活动列表中。这个展示列表应该抽出一个函数,即在点击放大镜是调该函数,在搜索框输入内容敲回车后也调该函数。每一条市场活动前面的单选框绑定了该市场活动的id。点击保存按钮,将市场活动名称保存在“市场活动源”的文本框中,将市场活动id放到“交易表单”的隐藏域中。

      image-20211220184406892 image-20211220184205830
    6. 给“转换”按钮绑定事件。这里需要考虑,有没有创建交易,如果没有创建交易,只用发个传统请求,把clueId发过去就可以;如果创建了交易,就要将交易信息发过去,这里再考虑,“交易”表单中的信息虽然不涉及隐私,长度不长,但是将来可能扩展将这个交易信息都填写。因此,如果创建了交易就不能发get请求,就必须发post请求,而想要用传统请求发post请求,只有一种方式:表单。这样的好处是,表单是现成的,提交表单直接用submit就可以。

      $("#convertBtn").click(function (){
      
          //prop函数,可以获取复选框的选中状态;也可以为复选框设置选中状态
          if($("#isCreateTransaction").prop("checked")){
          	//创建交易,提交form表单
          	$("#tranForm").submit();
          }else{
          	//没有创建交易,直接将clueId作为参数发给后台
          	window.location.href = "workbench/clue/convert.do?clueId=${param.id}";
          }
      
      })
      

      注意:由于,无论是否创建了交易,走的都是“workbench/clue/convert.do”这个请求路径,只不过后面的参数不同。但是要让后台知道是否创建了交易,需要在form表单中给一个标识,不能用“有没有值”去判断,因为交易中有些信息可能是没有填写。

      
      
      ... /form>

后端

  1. 获取到线索id,通过线索id获取线索对象(线索对象当中封装了线索的信息)

  2. 通过线索对象提取客户信息,当该客户不存在的时候,新建客户(根据公司的名称精确匹配,判断该客户是否存在!因为该客户之前可能存在)

  3. 通过线索对象提取联系人信息,保存联系人

  4. 线索备注转换到客户备注以及联系人备注

  5. “线索和市场活动”的关系转换到“联系人和市场活动”的关系

  6. 如果有创建交易需求(判断tran是否为null,不为null,创建交易),创建一条交易

  7. 如果创建了交易,则创建一条该交易下的交易历史

  8. 删除线索备注

  9. 删除线索和市场活动的关系

  10. 删除线索

    注意:代码很简单,但是要对业务逻辑清晰,即线索转换都需要创建那些记录,删除线索,要先删除那些信息。

交易模块

处理交易添加页

  1. 搭建后台结构(TranController、TranService、TranServiceImpl)

  2. 点击创建,跳转到添加页。注意发送的是一个传统请求到add.do,过后台取用户列表,然后讲用户列表保存在request域,转发到添加页save.jsp

  3. 在save.jsp页面加载时,使用jstl、el将“用户列表”、“阶段”、“类型”、“来源”下拉列表拼好。其中“用户列表”是从request域取,其它是数据字典中的从application中取;将“预计成交日期”、“下次联系时间”添加日期控件;“市场活动源”与“联系人名称”可以过后台从tbl_activity与tbl_contacts中取,然后动态拼到下拉列表,这里写死。线索模块练过。

    • 注意:用户列表是通过转发发给前端的,前端通过jstl和el取,默认选择当前用户,用el表达式的三目运算符。以往是点在模态窗口打开前过后台取用户列表,然后拼下拉列表,再将下拉列表的默认值设置为当前用户的id。

      
      	
      	
      
      
  4. “客户名称”支持自动补全,使用bootstrap下的插件。使用步骤:导包、引入、cv代码。注意:该插件只要绑定了对象,在框里输入就会发请求,不过有延迟。延迟建议1500,慢了用户体验差,快了没必要,体验也差。发getCustomerName.do就是根据输入的name模糊匹配查所有客户名称,类型List

    $("#create-customerName").typeahead({
        source: function (query, process) {
            $.get(
                "workbench/transaction/getCustomerName.do",
                { "name" : query },
                function (data) {
                    //alert(data);
    
                    // data [{客户名称1},{客户名称2}]
                    process(data);
                },
                "json"
            );
        },
        delay: 1500
    });
    
    • 注意:
      • 只要发ajax请求,后端响应的数据一定是json格式数据。前端不一定非要发json数据。例如之前的根据id删多条市场活动,因为json中key不能相同
      • 前端发传统请求,后端不能发json数据,一般将数据保存在request域中,通过转发的方式响应给前端,前端通过jstl+el处理
  5. “阶段”下拉列表选取阶段,自动生成“可能性”。

    • 阶段和可能性是一种一一对应的关系,一个阶段对应一个可能性,因此以key-value键值对的形式保存。阶段为key,通过选中的阶段,触发可能性value。对于这种数据:数据量不是很大;存在一种键值对的对应关系。将这样的数据保存在properties配置文件中,而不是数据库中。

      #注意properties文件中最好不出现中文,因为有的ide不支持,一般将中文用jdk的native2ascii.exe转换为ASCII码保存,2表示To
      01\u8D44\u8D28\u5BA1\u67E5=10
      02\u9700\u6C42\u5206\u6790=25
      ...
      
    • stage2Possibility.properties这个文件表示的是阶段和键值对之间的对应关系,我们通过stage,以及对应关系,来取得可能性这个值这种需求在交易模块中需要大量的使用到。因此我们就需要将该文件解析在服务器缓存中application.setAttribute(stage2Possibility.properties文件内容)。在系统初始化监听器中,之前监听application对象的创建,并将数据字典保存在application域中。下来再处理stage2Possibility.properties文件,将该文件的kv关系解析出来保存在map集合中,再通过jackson将其转为json串,把这个json串保存在application域中。

      Map<String, String> pMap = new HashMap<>();
          //使用ResourceBundle工具类处理properties文件,注意路径没有.properties
          ResourceBundle bundle = ResourceBundle.getBundle("Stage2Possibility");
          //获取properties文件中所有的key
          Enumeration<String> stageList = bundle.getKeys();
          while(stageList.hasMoreElements()){
              String stage = stageList.nextElement();
              String possibility = bundle.getString(stage);
              pMap.put(stage, possibility);
          }
      
          //在这里使用jackson将pMap转化为json串,保存在application域中。
          ObjectMapper om = new ObjectMapper();
          try {
              String possibilityMap = om.writeValueAsString(pMap);
              //前端直接从该json串中取值就可以
              application.setAttribute("possibilityMap", possibilityMap);
          } catch (JsonProcessingException e) {
              e.printStackTrace();
      }
      
    • 前端直接通过EL表达式从applicationScope中获取possibilityMap这个json:{“01资质审查”:10,“02需求分析”:25…},直接根据stage获取对应的possibility。注意:json字符串就是以 key-value 键值对的形式保存数据,json中根据key获取value有两种方式:

      • var value = json.key;
      • var value = json[key];

      一般通过第一种方式获取value,但是如果key是可变的变量(例如stage就是在下拉列表中选取不同的),就要通过json[key]的方式取值

      //给阶段下拉列表绑定变化事件,当下拉列表变化,自动给可能性拦添加数据。
      $("#create-stage").change(function (){
          //取得阶段
          var stage = $("#create-stage").val();
          //从application域中取得possibilityMap的json串:{"01资质审查":10,"02需求分析":25...}
          var json = ${possibilityMap};
      	//根据key从json中取得value
          var possibility = json[stage];
          //为可能性的文本框赋值
          $("#create-possibility").val(possibility);
      })
      
  6. 点击保存,以form表单形式发送传统post请求到save.do,后台进行数据保存。注意:

    • controller:表单中提交的是客户名称customerName,因此在控制器中取到数据后,先不要对customerId赋值,将交易对象t和customerName作为参数发给serivce。此外,业务层有大量的添加操作,如客户的创建、交易的创建、交易历史的创建。如果业务层涉及到大量的创建,还需要在控制器发给业务层createBy,创建一定要给createBy与createTime赋值。但是这里createBy在交易对象t中,不需要额外传,直接t.getCreateBy()。控制器从业务层获取flag,为true代表保存成功。这时候使用重定向到index.jsp。原因如下:
      • 数据:save不需要给前端响应数据,保存成功直接跳转index.jsp页面就可以。不给前端传数据就用重定向。
      • 路径:重定向后路径为index.jsp,如果是转发就是save.do,这样每刷一次就会过后端添加一次。
    • service:业务层首先要根据customerName精确查询客户,如果没有查到就要新建一个Customer对象,并完成customerDao.save(cus);有了用户对象后,将其id保存在交易对象t中,即t.setCustomerId(cus.getId()),之后保存交易tranDao.save(t);每创建一条交易,就要对应生成一条交易历史,因此要创建交易历史对象,从交易中那值,然后tranHistoryDao.save(th)。以上都成功,返回控制器flag(true)。
    • dao:dao层作对应的save操作。

交易模块详细信息页处理

  1. 点击交易列表的名称,以url的方式发送传统请求到detail.do,同时将该条线索的id发送给后台。后端根据id查单条,然后将交易对象t保存在request域中,以转发的方式响应给前端detail.jsp。注意:

    • 发传统请求是因为这个页面刷新了,直接跳转到了detail.jsp,并且要发id

    • 后端的sql语句需要注意,因为tran中的属性是owner、customerId、activityId、contactsId,而页面需要展示的是name,因此sql中要五表联查。使用name as 对应的id。同时,需要注意,创建交易时,activityId、contactsId是非必填项,因此不能用内连接,要用外连接,否则如果没有这俩信息会导致tran也查不出来。

      <select id="detail" resultType="com.xd.workbench.domain.Tran">
              select
      
                  tran.id,
                  user.name as owner,
                  tran.money,
                  tran.name,
                  tran.expectedDate,
                  cus.name as customerId,
                  tran.stage,
                  tran.type,
                  tran.source,
                  act.name as activityId,
                  con.fullname as contactsId,
                  tran.createBy,
                  tran.createTime,
                  tran.editBy,
                  tran.editTime,
                  tran.description,
                  tran.contactSummary,
                  tran.nextContactTime
      
              from tbl_tran tran
              join tbl_user user
              on tran.owner=user.id
              join tbl_customer cus
              on tran.customerId=cus.id
              left join tbl_activity act
              on tran.activityId=act.id
              left join tbl_contacts con
              on tran.contactsId=con.id
      
              where tran.id=#{id}
      </select>
      
    • 这里后端响应数据是以转发的方式,原因如下:

      • 数据:使用转发,因为是传统请求,因此将数据保存在request域中。request域中有数据用转发
      • 路径:此外转发后的路径还是该路径即,detail.do这样前端在详细页,进行了修改,只要刷新就会调detail.do过后台。
  2. detail.jsp中用EL表达式从request域中取数据,将数据铺到页面上。

  3. 关于可能性的处理,有多种处理方式:

    • 可能性没有保存在数据库中,它以json串的形式保存在了application域中,这里要根据该条交易的阶段stage,将对应的可能性显示在前端。
    阶段
    ${t.stage}
    可能性
    <%-- 后台将stage-possibility的对应关系转化成了json串保存在了application域中 这里先获取json串,由于stage是动态的,所以从json中取数据以json[stage]的方式 然后将取到的数据保存在对应的框中。 这样的优点是,jsp文件中没有出现java代码,不需要写java脚本。 --%>
    • 在application创建时将对应关系的map集合保存在application域中,控制器从域中取出数据,与当前要响应的交易对象t的stage属性进行equals获取对应的possibility。可以将它直接保存在request域中响应给前端以 p o s s i b i l i t y 获取数据。也可以给交易类添加一个扩展属性 p o s s i b i l i t y ,将该值赋值给扩展属性,然后前端通过 {possibility}获取数据。也可以给交易类添加一个扩展属性possibility,将该值赋值给扩展属性,然后前端通过 possibility获取数据。也可以给交易类添加一个扩展属性possibility,将该值赋值给扩展属性,然后前端通过{t.possibility}获取,这样的优点是:前端统一,都是${t.possibility}获取数据。但是要注意,扩展实体类的属性,一定要少,否则会影响到实体类的结构。
    TranService ts = (TranService) ServiceFactory.getService(new TranServiceImpl());
        Tran t = ts.detail(id);
    	
        ServletContext application = request.getServletContext();
        Map<String, String> pMap = (Map<String, String>) application.getAttribute("pMap");
        Set<String> sets = pMap.keySet();
        for(String stage : sets){
            if(stage.equals(t.getStage())){
                String possibility = pMap.get(stage);
                request.setAttribute("possibility", possibility);
        	}
    	}
    
  4. 展示交易阶段历史列表:在detail.jsp页面加载时发送ajax请求,走后台获取历史列表,通过tranId去tbl_tran_history中查交易历史列表,按照stage降序查。由于交易历史列表中也有“可能性”信息,这里采用给TranHistory类添加扩展属性possibility的方式,要在控制器中,从application域中获取pMap集合,然后遍历历史列表,取stage,通过stage去pMap中获取对应的possibility,将其赋值给交易历史th对象。然后前端就通过${n.possibility}获取,与其它属性一致。

    List<TranHistory> tranHistoryList = ts.getTranHistoryList(tranId);
    
    Map<String, String> pMap = (Map<String, String>)request.getServletContext().getAttribute("pMap");
    for(TranHistory th : tranHistoryList){
        String possibility = pMap.get(th.getStage());
        th.setPossibility(possibility);
    }
    
    PrintJson.printJsonObj(response, tranHistoryList);
    

动态展现交易阶段内容及图标

  1. 先对图标进行逻辑设计,使其动态化。这里使用的是java脚本,因为用js对图标的样式支持不是很好,每次先从当前页面的request域中获取到交易t对象,然后获取其stage,在从application域中获取到pMap集合,根据stage获取当前阶段的可能性。然后就是逻辑判断
  2. 给每个图标的div块绑定id和事件,当点击图标,会调响应的函数changeStage(),发送参数stage与i,i代表当前阶段的下标。在函数内发送ajax请求,走后台更改数据库tbl_tran表中的stage字段,并且还要创建一条交易历史记录。将结果t响应给前端,前端从t中取值,将修改后的stage、possibility、editBy、editTime修改,并且修改阶段图标调changeIcon()函数,参数也是stage与i。
  3. 在changeIcon()函数中也是先获取当前阶段的可能性,根据可能性去刷新图标。

Echarts统计图

  • Echarts统计图是由百度研发出来的,现在已经被apache运维,echarts是现在最好的统计图绘制工具
  • 可以上官网上copy模板,然后该响应的代码就可以
  • 需要注意的是echarts统计图需要的数据是一个json数据,因此要发ajax请求过后台拼串,并且除了dataList之外,还需要total总数,与分页工具差不多
  • 一般用统计图展示数据对客服更友好,人们跟喜欢看图。因此,一个项目中通常有多个统计图,所以后端数据一般封装到一个VO类中

重点

条件查询和分页查询的市场活动列表


重点:关联市场活动,查询出与当前线索未关联的市场活动

CRM简单小结_第4张图片

CRM简单小结_第5张图片

CRM简单小结_第6张图片

你可能感兴趣的:(okhttp,spring,cloud,spring,boot,分布式)