对于三层架构,一个模块对应一个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可以看作代码。
请求与响应
提示性信息(可能性):对于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();
}
市场活动模块activity
线索模块: clue(整个CRM项目最重要的,线索就是潜在客户)
交易模块: transaction
统计图表
为了提高用户体验,一般在页面加载完毕后,将光标定位在账号输入框。使用$(“#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都在。
关于乱码:前端发请求,后端发响应都会经过过滤器增强,即设置字符编码,但是一般只滤*.do。因为jsp文件一般用<%@ page contentType=“” %>设置
页面中的所有按钮、窗口都应该由我们来控制,改它的id,我们js来触发。
模态窗口
日历控件
分页查询
分页查询的步骤是:点击入口、发送请求、获取列表、分页展示到相应位置。
参数问题:
前端发送pageNo、pageSize。接收 data:{“total”:total, “dataList”:[{市场活动1},{2}]} 其中total是分页插件、dataList拼活动列表需要
后端sql语句分页需要两个参数:skipCount、pageSize。skipCount=(pageNo-1)*pageSize
分页存在问题
隐藏域(解决上述问题)
注意:
使用分页插件前,要把前端模型中原有的分页插件干掉,引入一个div,我们把自己引入的分页插件写div里
复选框的全选和取消全选
//为全选的复选框绑定事件,触发全选操作
$("#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);
})
注意:
CURD**(最重要的是id,后台查数据只根据id)**
创建市场活动
删除市场活动
修改市场活动
注意问题:
对于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+'\';"
注意应该发送的传统请求,原因如下:
备注列表的展示写到一个函数里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”);
线索模块所用数据库表结构及关系
数据字典所用到的表及关系,并将数据字典中数据导入
将“线索”、“客户”、“联系人”、“交易”模块的html修改为jsp,解决404错误。包括内部超链接的404问题
搭建“线索”、“客户”、“联系人”、“交易”相关后端结构(domain,dao,service,controller)
数据字典是指将一些固定不变的数据作为数据字典。常将数据字典保存在服务器缓存中,每次取数据不走数据库,快。一般表单中有关选择的相关的数据(下拉框,单选框,复选框)都从数据字典中取。例如,城市下拉列表中的城市,职位下拉列表中的职位。对于表单元素中选择的数据一定都是要写活的,来自数据字典。注意如何取数据字典,数据字典保存在map中Map
使用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+'';
线索模块核心页面是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该记录,也可以重新刷一下市场活动列表,因为该列表是在中
关联市场活动(重点)
<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);
}
线索转换(重点)
线索转换的核心:将线索转换真正客户,即将线索中与公司相关的信息转化为客户,与人相关的信息转化人联系人。
转换步骤:
在线索的详细页detail.jsp,点击转换,发送传统请求(因为是子页面的刷新),不需要过后台。传参的格式:
//参数从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%>
注意:
转换页面convert,可以为用户创建交易,也可以不创建。但是无论怎样都需要过后台,因为线索转换要涉及多张表。因此,要根据用户时候选中创建交易复选框去发不同的请求。
给“创建交易”的表单的“日期”添加日历插件;“阶段”从数据字典中用“jstl+el”取值;
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
“市场活动源”的放大镜绑事件,发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表单中给一个标识,不能用“有没有值”去判断,因为交易中有些信息可能是没有填写。