思想
-
对于三层架构,一个模块对应一个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等。
-
后端数据可以封装成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这样每一次刷新不会过后台,只是刷页面。具体选择应该看该页面有没有修改操作,例如在页面修改完,刷新直接过后台然后重新取数据,前端铺数据,保证是最新的数据。
- 简单判断就是,携带数据用转发,否则一律重定向。
- 请求的方式有两种:ajax请求,传统请求。使用局部刷必定用ajax请求,如果刷整个页面通常发传统请求;
-
提示性信息(可能性):对于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文件,注意路径没有.propertiesResourceBundle bundle = ResourceBundle.getBundle("Stage2Possibility");//获取properties文件中所有的keyEnumeration<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表达式,必须是字符串
- 模态窗口的出现是:KaTeX parse error: Expected 'EOF', got '#' at position 3: (“#̲xxxModal”).moda…(“#xxxModal”).modal(“hide”);清空模态窗口内容:$(“#activityAddForm”)[0].reset();
-
日历控件
- 填写日期,一般情况下都是需要相关的日历控件的。这里使用的是: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 = "<option></option>"; $.each(data.userList, function (i, n){html += "<option value='"+n.id+"'>"+n.name+"</option>"; }) $("#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_clue)、线索备注表(tbl_clue_remark)、线索市场活动关系表(tbl_clue_activity_relation)
-
数据字典所用到的表及关系,并将数据字典中数据导入
- 字典类型表(tbl_dic_type)、字典值表(tbl_dic_value)
- tbl_dic_type与tbl_dic_value是一对多关系
- 字典类型表(tbl_dic_type)、字典值表(tbl_dic_value)
-
将“线索”、“客户”、“联系人”、“交易”模块的html修改为jsp,解决404错误。包括内部超链接的404问题
-
搭建“线索”、“客户”、“联系人”、“交易”相关后端结构(domain,dao,service,controller)
-
数据字典是指将一些固定不变的数据作为数据字典。常将数据字典保存在服务器缓存中,每次取数据不走数据库,快。一般表单中有关选择的相关的数据(下拉框,单选框,复选框)都从数据字典中取。例如,城市下拉列表中的城市,职位下拉列表中的职位。对于表单元素中选择的数据一定都是要写活的,来自数据字典。注意如何取数据字典,数据字典保存在map中Map<String, List>,String代表DicType,有几个类型,就有几个List。在将map里的value放到application域中,name就是DicType。
-
使用jstl语法,将application域中的数据拿出来,拼到下拉列表里。jstl和el表达式一起使用,方便对域对象中的数据操作
<select class="form-control" id="create-status"><option></option><c:forEach items="${clueStateList}" var="c"><option value="${c.value}">${c.text}</option></c:forEach> </select>
-
点击线索,走后台获取线索列表拼到列表表格中,这个是动态生成的,要给每条线索的名称、复选框绑定该线索的id。与市场活动相同。
-
点击线索上的名称,发传统请求走后台,注意该传统请求是拼在动态生成的线索列表中的,想要触发动态生成的元素,只能用on的方式,并将该条线索的id发给后台,后台根据id查单条线索,并将线索保存到request域中,转发至详情页面detail.jsp,在该页面用el表达式从request域中取出数据,拼到对应位置。
onclick="window.location.href=\'workbench/clue/detail.do?id='+n.id+'\';">'+n.fullname+'</a></td>';
- 注意:线索模块的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">selecta.id,u.name as owner,a.name,a.startDate,a.endDatefrom tbl_activity ajoin tbl_user uon a.owner=u.id/*这里模糊查询不需要用动态sql,如果没有条件就是都查。注意该子查询,即要查询出的市场活动的id不被clueId所关联*/where a.name like '%' #{name} '%' and a.id not in(selectactivityIdfrom tbl_clue_activity_relationwhere 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=xxxString 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); }
-
- 点击“关联市场活动超链接”在模态窗口出现前,过后台取出所有与该线索非关联的市场活动(重要)。并且在该模态窗口中可以根据市场活动的name模糊查询。查询时,发两个参数:clueId、name。关联时,发两个参数:clueId、avtivityId。
-
线索转换(重点)
线索转换的核心:将线索转换真正客户,即将线索中与公司相关的信息转化为客户,与人相关的信息转化人联系人。
转换步骤:
- 前端
-
在线索的详细页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}
-
转换页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个内置对象
-
转换页面convert,可以为用户创建交易,也可以不创建。但是无论怎样都需要过后台,因为线索转换要涉及多张表。因此,要根据用户时候选中创建交易复选框去发不同的请求。
-
给“创建交易”的表单的“日期”添加日历插件;“阶段”从数据字典中用“jstl+el”取值;
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <select id="stage" class="form-control" name="stage"><option></option><c:forEach items="${stageList}" var="s"><option value="${s.value}">${s.text}</option></c:forEach> </select>
-
“市场活动源”的放大镜绑事件,发ajax请求将市场活动名字作为参数,后端根据name进行模糊查询,将查询到的市场活动相应给前端,前端将结果拼到市场活动列表中。这个展示列表应该抽出一个函数,即在点击放大镜是调该函数,在搜索框输入内容敲回车后也调该函数。每一条市场活动前面的单选框绑定了该市场活动的id。点击保存按钮,将市场活动名称保存在“市场活动源”的文本框中,将市场活动id放到“交易表单”的隐藏域中。
-
给“转换”按钮绑定事件。这里需要考虑,有没有创建交易,如果没有创建交易,只用发个传统请求,把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表单中给一个标识,不能用“有没有值”去判断,因为交易中有些信息可能是没有填写。
<!--以表单的方式提交这里要想到,以后该表单可能需要扩展,比如要把一个交易的完整内容都展示在表单里,这时候传统get请求就不行了因为对参数长度都要求。这样就使用form表单提交数据,这样的好处是:1、方便,只用设置name,提交表单就把参数都提交了。2、传统请求中只有form表单可以设置请求方式为post,对长度不限制。workbench/clue/convert.do?clueId=xxx&money=xxx&...--> <form action="workbench/clue/convert.do" method="post" id="tranForm"><!--标识--><input type="hidden" name="flag" value="a"/><input type="hidden" name="clueId" value="${param.id}"/><div class="form-group" style="width: 400px; position: relative; left: 20px;"><label for="amountOfMoney">金额</label><input type="text" class="form-control" id="amountOfMoney" name="money"> ... /form>
后端
-
获取到线索id,通过线索id获取线索对象(线索对象当中封装了线索的信息)
-
通过线索对象提取客户信息,当该客户不存在的时候,新建客户(根据公司的名称精确匹配,判断该客户是否存在!因为该客户之前可能存在)
-
通过线索对象提取联系人信息,保存联系人
-
线索备注转换到客户备注以及联系人备注
-
“线索和市场活动”的关系转换到“联系人和市场活动”的关系
-
如果有创建交易需求(判断tran是否为null,不为null,创建交易),创建一条交易
-
如果创建了交易,则创建一条该交易下的交易历史
-
删除线索备注
-
删除线索和市场活动的关系
-
删除线索
注意:代码很简单,但是要对业务逻辑清晰,即线索转换都需要创建那些记录,删除线索,要先删除那些信息。
交易模块
处理交易添加页
-
搭建后台结构(TranController、TranService、TranServiceImpl)
-
点击创建,跳转到添加页。注意发送的是一个传统请求到add.do,过后台取用户列表,然后讲用户列表保存在request域,转发到添加页save.jsp
-
在save.jsp页面加载时,使用jstl、el将“用户列表”、“阶段”、“类型”、“来源”下拉列表拼好。其中“用户列表”是从request域取,其它是数据字典中的从application中取;将“预计成交日期”、“下次联系时间”添加日期控件;“市场活动源”与“联系人名称”可以过后台从tbl_activity与tbl_contacts中取,然后动态拼到下拉列表,这里写死。线索模块练过。
-
注意:用户列表是通过转发发给前端的,前端通过jstl和el取,默认选择当前用户,用el表达式的三目运算符。以往是点在模态窗口打开前过后台取用户列表,然后拼下拉列表,再将下拉列表的默认值设置为当前用户的id。
<c:forEach items="${userList}" var="u"><!--注意:EL表达式中${"selected"}的意思就是把字符串"selected"输出到浏览器,如果三目运算符为ture就代表<option value="${u.id}" selected>${u.name}</option> 即符合条件,选中该条--><option value="${u.id}" ${user.id eq u.id ? "selected" : ""}>${u.name}</option> </c:forEach>
-
-
“客户名称”支持自动补全,使用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处理
- 注意:
-
“阶段”下拉列表选取阶段,自动生成“可能性”。
-
阶段和可能性是一种一一对应的关系,一个阶段对应一个可能性,因此以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文件,注意路径没有.propertiesResourceBundle bundle = ResourceBundle.getBundle("Stage2Possibility");//获取properties文件中所有的keyEnumeration<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中取得valuevar possibility = json[stage];//为可能性的文本框赋值$("#create-possibility").val(possibility); })
-
-
点击保存,以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操作。
- controller:表单中提交的是客户名称customerName,因此在控制器中取到数据后,先不要对customerId赋值,将交易对象t和customerName作为参数发给serivce。此外,业务层有大量的添加操作,如客户的创建、交易的创建、交易历史的创建。如果业务层涉及到大量的创建,还需要在控制器发给业务层createBy,创建一定要给createBy与createTime赋值。但是这里createBy在交易对象t中,不需要额外传,直接t.getCreateBy()。控制器从业务层获取flag,为true代表保存成功。这时候使用重定向到index.jsp。原因如下:
交易模块详细信息页处理
-
点击交易列表的名称,以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">selecttran.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.nextContactTimefrom tbl_tran tranjoin tbl_user useron tran.owner=user.idjoin tbl_customer cuson tran.customerId=cus.idleft join tbl_activity acton tran.activityId=act.idleft join tbl_contacts conon tran.contactsId=con.idwhere tran.id=#{id} </select>
-
这里后端响应数据是以转发的方式,原因如下:
- 数据:使用转发,因为是传统请求,因此将数据保存在request域中。request域中有数据用转发
- 路径:此外转发后的路径还是该路径即,detail.do这样前端在详细页,进行了修改,只要刷新就会调detail.do过后台。
-
-
detail.jsp中用EL表达式从request域中取数据,将数据铺到页面上。
-
关于可能性的处理,有多种处理方式:
- 可能性没有保存在数据库中,它以json串的形式保存在了application域中,这里要根据该条交易的阶段stage,将对应的可能性显示在前端。
<div style="width: 300px;position: relative; left: 450px; top: -40px; color: gray;">阶段</div> <div style="width: 300px;position: relative; left: 650px; top: -60px;" id="stage"><b>${t.stage}</b></div><div style="width: 300px;position: relative; left: 450px; top: -40px; color: gray;">可能性</div><div style="width: 300px;position: relative; left: 650px; top: -60px;" id="possibility"><b><%--后台将stage-possibility的对应关系转化成了json串保存在了application域中这里先获取json串,由于stage是动态的,所以从json中取数据以json[stage]的方式然后将取到的数据保存在对应的框中。这样的优点是,jsp文件中没有出现java代码,不需要写java脚本。--%><script type="text/javascript">var stage = $("#stage").text();var json = ${possibilityMap};var possibility = json[stage];$("#possibility").text(possibility);</script> </b></div>
- 在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);}}
-
展示交易阶段历史列表:在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);
动态展现交易阶段内容及图标
- 先对图标进行逻辑设计,使其动态化。这里使用的是java脚本,因为用js对图标的样式支持不是很好,每次先从当前页面的request域中获取到交易t对象,然后获取其stage,在从application域中获取到pMap集合,根据stage获取当前阶段的可能性。然后就是逻辑判断
- 给每个图标的div块绑定id和事件,当点击图标,会调响应的函数changeStage(),发送参数stage与i,i代表当前阶段的下标。在函数内发送ajax请求,走后台更改数据库tbl_tran表中的stage字段,并且还要创建一条交易历史记录。将结果t响应给前端,前端从t中取值,将修改后的stage、possibility、editBy、editTime修改,并且修改阶段图标调changeIcon()函数,参数也是stage与i。
- 在changeIcon()函数中也是先获取当前阶段的可能性,根据可能性去刷新图标。
Echarts统计图
- Echarts统计图是由百度研发出来的,现在已经被apache运维,echarts是现在最好的统计图绘制工具
- 可以上官网上copy模板,然后该响应的代码就可以
- 需要注意的是echarts统计图需要的数据是一个json数据,因此要发ajax请求过后台拼串,并且除了dataList之外,还需要total总数,与分页工具差不多
- 一般用统计图展示数据对客服更友好,人们跟喜欢看图。因此,一个项目中通常有多个统计图,所以后端数据一般封装到一个VO类中
重点
条件查询和分页查询的市场活动列表
<select id="getActivityListByCondition" resultType="com.xd.workbench.domain.Activity">selecta.id,a.name,u.name as owner,a.startDate,a.endDatefrom tbl_activity a join tbl_user u on a.owner=u.id<where><if test="name!=null and name!=''">a.name like '%' #{name} '%'</if><if test="owner!=null and owner!=''">and u.name like '%' #{owner} '%'</if><if test="startDate!=null and startDate!=''">and a.startDate > #{startDate}</if><if test="endDate!=null and endDate!=''">and a.endDate < #{endDate}</if></where>order by a.createTime desclimit #{skipCount},#{pageSize}</select>
重点:关联市场活动,查询出与当前线索未关联的市场活动