CRM项目实现过程中的细节记录(一)
文章目录
- CRM项目实现过程中的细节记录(一)
- 一、数据库相关细节
- 1. 表名
- 2. 表字段说明
- 3. 不使用主外键约束
- 4. 不使用主键自动增长
- 5. 日期(时间)
- 6. 实体类
- 7. MD5密码加密
- 8. 动态SQL
- 9. 字符串数字排序
- 10. Limit
- 11. MyBatis动态SQL(模糊查询)
- 12. 表关系
- 13. 多对多关系的删除与添加
- 14. 注意使用外连接
- 二、JSP相关细节
- 三、登录相关细节
- 1. 考虑用户名与密码框中前后可能输入有空格
- 2. JS中的函数
- 3. 请求发送方式
- 4. 关于网站的初始页面
- 5. 刷新maven
- 6. 登录失败时处理思路
- 7. 拦截器
- 8. IP地址
- 9. Session
- 10. Cookie
- 11. 将顶层窗口设置为当前窗口
- 四、工作区相关
- 1. 页面中加入别的页面
- 2. Bootstrap的模态窗口只支持发送AJAX请求
- 3. JS操作模态窗口
- 4. 清空模态窗口表单的时机
- 5. 循环拼接请求参数
- 6. 在controller层抛异常自定义异常
- 五、数据字典实现相关细节
- 六、代码中的一些方法
- 七、前端
- 八、踩过的坑
- 九、市场活动模块
下一篇:Java实现CRM项目过程中的细节记录(二)
一、数据库相关细节
1. 表名
在建表时,表名一般以t_
、tb_
、 tbl_
开头
2. 表字段说明
在该项目中,所有的表字段均为字符串,包括数字(在银行等的项目中,字段该是什么类型,就是什么类型)
- 在创建表时,可以确定是定长(长度固定)的字符串,在表中使用char(),如(性别,UUID生成的唯一id值)
- 不固定的字符串在表中使用varchar()
3. 不使用主外键约束
表中有外键,但是不使用外键约束,即在创建表时,外键的字段同其它字段的创建方式一致。在表的说明文档中,要说明谁是谁的外键
4. 不使用主键自动增长
当主键字段属性为 int / long 整型时,可以自动递增
- 主键自动递增涉及到表的添加删除的效率问题(实际操作的是两张表)
- 数据库移植相关的问题
在实际项目开发中,更推荐使用字符串类型的主键,即不自动递增。
UUID
如何确保字符串主键的唯一性?
UUID是未来最常见的主键生成机制,该机制是由java.util包为我们提供的工具类,该形式生成的是由数字
,字母
和-
组成的36位的随机串,这36位的随机串一定是全世界唯一的
- UUID为什么是全世界唯一的?
UUID用了随机数+时间+当前生成UUID所在硬件的机器编码进行计算产生的 - 在数据库表中,应该为UUID赋予什么类型?
由于UUID生成的字符串中-
的位置是固定的,所以可以将-
去掉,即在数据库中使用char(32)
来存储 - UUID字符串的生成方式:
public static String getUUID(){
return UUID.randomUUID().toString().replaceAll("-","");
}
5. 日期(时间)
在项目开发中,使用字符串来表示时间,共有两种方式
- 纯日期:yyyy-MM-dd 在表中使用 char(10) 来存储
- 日期+时间:yyyy-MM-dd HH:mm:ss 在表中使用 char(19) 来存储
6. 实体类
实体类中的属性名与字段名一致,且类型均为String
7. MD5密码加密
public class MD5Util {
public static String getMD5(String password) {
try {
// 得到一个信息摘要器
MessageDigest digest = MessageDigest.getInstance("md5");
byte[] result = digest.digest(password.getBytes());
StringBuffer buffer = new StringBuffer();
// 把每一个byte 做一个与运算 0xff;
for (byte b : result) {
// 与运算
int number = b & 0xff;// 加盐
String str = Integer.toHexString(number);
if (str.length() == 1) {
buffer.append("0");
}
buffer.append(str);
}
// 标准的md5加密后的结果
return buffer.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
}
8. 动态SQL
动态sql中的<foreach>
标签中的collection
属性指的是集合的属性,有array
和list
两个值
9. 字符串数字排序
在字符串数字后+0
或者*1
select * from tbl_dic_value order by typeCode,orderNo+0
10. Limit
Limit 1, 2
后的第一个参数可以简单的记为:略过的记录数
11. MyBatis动态SQL(模糊查询)
"%"
与#{name}
之间要有空格,相当于字符串拼接中的加号"+"
<if>
标签中的条件要用"and"
- 第一个
<if>
标签对中的and可有可无,之后的<if>
标签对中的and不能省略
<select id="getActivity" resultType="int">
select count(*)
from tbl_activity
<where>
<if test="name!=null and name!=''">
and name like '%' #{name} '%'
</if>
</where>
</select>
12. 表关系
- 一对一(如人员表与证件表)、一对多、多对一(如学生表与班级表):
- 永远在多的一方建立外键,换名话说,有外键的表一定是多的一方(死记)
- 一对一可以理解成特殊的一对多,在哪一边建外键视具体情况而定
- 多对多(如学生表与课程表):
- 建一个单独的表,叫关联关系表,该表有三个字段,分别是该表的主键、多的一方表的外键、另一个多的一方表的外键。
13. 多对多关系的删除与添加
- 一对多时,删除一方表中的数据时,关联的多方表中的数据也要删除
- 多对多时,删除的是关联关系表中的数据,添加时也是在关联关系表中进行添加
14. 注意使用外连接
当表中存在非必填的外键时,要极其注意外键为null
的情况,使用外连接来避免查不到数据
二、JSP相关细节
1. HTML文件转JSP文件
<%
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/";
%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<head>
<base href="<%=basePath%>">
</head>
<base>
标签一定要加在<head>
标签中的第一行- 如果页面中使用了
../
,将所有的../
全部去掉 - 定个约定:对于前端文件,写路径前面一律不加
/
,对于后端文件,写路径时一律要加/
- 前端的图片、jQuery的文件或者文件夹要放在
webapp
的根路径下
三、登录相关细节
1. 考虑用户名与密码框中前后可能输入有空格
去除空格:
$.trim("字符串");
2. JS中的函数
如果要在绑定事件中引入外部函数,函数不能写在$( )
中,要写在$( )
之外(为什么?)
3. 请求发送方式
-
get:拿、取
- 以查询为主要目的的需求
-
post:邮寄、邮递
- 以添加,修改,删除为目的的需求
- 如果涉及到参数安全性相关的问题,用post请求
4. 关于网站的初始页面
网站的初始页面约定是index.html
,但是登录页面在login.jsp
,
- 做法:在
index.html
中进行重定向到login.jsp
页面
<script type="text/javascript">
document.location.href = "login.jsp";
</script>
5. 刷新maven
在maven中加入任何外部资源时都需要刷新一下maven
6. 登录失败时处理思路
登录失败时,直接抛出异常,通过SpringMVC来统一处理异常,在异常中返回ajax请求
7. 拦截器
如果登录成功后响应为200,但是返回的是空白页,说明拦截器暂时没有设置为true
8. IP地址
获取ip地址,如果是自己访问自己,则用的是localhost,则下面的方法返回的是 0:0:0:0:0:0:0:1不是一个有效的ip地址,本地访问要用127.0.0.1
String ip = request.getRemoteAddr();
9. Session
Session的销毁方式:
session.invalidate();
10. Cookie
- Cookie机制中,是通过path来控制权限的。只有
servlet
的<url-pattern>
和该path相同或是它的子路径,servlet才能够访问该Cookie。如果把一个Cookie的path设为项目根目录,那么该项目下的所有servlet都能够访问它 - 如果不设置setMaxAge(过期时间),则浏览器一关闭就过期,默认单位为秒
Cookie loginActCookie = new Cookie("loginAct", loginAct);
loginActCookie.setMaxAge(60*60*24*10);//设置过期时间
loginActCookie.setPath("/");//设置权限
- Java中,没有对Cookie的销毁提供类似session的销毁方式,所以用新的Cookie替换旧的Cookie
Cookie loginActCookie = new Cookie("loginAct", null);//Cookie的key一定要与要销毁的Cookie键相同
cookie1.setPath("/");
cookie1.setMaxAge(0);//过期时间设置为0
11. 将顶层窗口设置为当前窗口
如果顶层窗口不是当前窗口,则将顶层窗口设置为当前窗口,防止出现登录页显示在<iframe>
标签中的bug
$(function(){
if(window.top != window){
window.top.location = window.location;
}
})
四、工作区相关
1. 页面中加入别的页面
使用<iframe></iframe>
标签
<div id="workarea" style="position: absolute; top : 0px; left: 18%; width: 82%; height: 100%;">
<iframe style="border-width: 0px; width: 100%; height: 100%;" name="workareaFrame"></iframe>
</div>
$(function(){
//在页面加载完成后,默认在工作区中自动打开
//参数1:打开哪个页面
//参数2:在哪儿打开(工作区的name属性)
window.open("workbench/main/toIndex.do","workareaFrame");
})
2. Bootstrap的模态窗口只支持发送AJAX请求
3. JS操作模态窗口
取得指定模态窗口的jquery对象,由该对象调用modal方法,该modal方法有两种取值
- show:打开模态窗口
- hide:关闭模态窗口
4. 清空模态窗口表单的时机
添加操作执行之后,要清空表单,可以在打开的时候清,也可以在点击保存添加成功之后清,为了防止失误操作让表单关闭后填写的数据丢失,应该在处理了添加操作之后清空表单
5. 循环拼接请求参数
var attr = "";
var $create = $("#create-form :input");//选中表单下的所有input表单
for (var i = 0; i < $create.length; i++) {
attr += $create[i].name + ":'"+ $.trim($create[i].value) + "',";
}
attr = attr.substr(0, attr.length - 1);//去除最后一位的逗号
var obj = eval("({"+attr+"})");//将JSON字符串转为JS对象,AJAX中的data属性要使用对象,不是字符串
6. 在controller层抛异常自定义异常
五、数据字典实现相关细节
1. 增删改后不能用请求转发(*)
增删改后应该使用重定向,返回修改后的页面,使地址栏中的地址与当前页的地址一致。
2. 前端页面弹框后不加感叹号
3. EL表达式取值取出空串
EL表达式取值取出NULL
,会自动改为空字符串""
六、代码中的一些方法
1. jQuary
-
模拟鼠标点击
$(“#code”)
元素,click可以换成其它任意事件$("#code").trigger("click");
-
给dom对象赋值:
$("#span-code").prop("class", spanError);
-
失去焦点:
blur()
-
删除时,弹窗用
confirm("消息")
-
el表达式的取值是能够用在js代码中的,只不过需要套用在字符串的引号中
-
使用JS来实现表单的提交与清空
submit()
是jQuary
提供的,而reset()
是JavaScript
提供的,故他们的使用方法不同:$("#activitySaveForm").submit(); $("#activitySaveForm")[0].reset();
-
往前追加同级元素(兄弟标签)用
兄弟元素.before
$("p").before("<b>追加</b>"); $("#remarkDiv").before(html);
结果:
<b>追加</b><p>段落</p>
-
父级元素中的最后追加子元素(子标签)用
父级元素.append
$("p").append("<b>追加</b>");
结果:
<p>段落<b>追加</b></p>
2. json
- json字符串是键值对,当只有一个键,值是一个数组时,键可省略
{"key":[{"id":"001","name":"zhangsan"},{"id":"002","name":"lisi"}]}
可以简写成:[{"id":"001","name":"zhangsan"},{"id":"002","name":"lisi"}]
3. AJAX
(1) 模态窗口的带值打开*
$("#toSaveActivityBtn").click(function () {
$.ajax({
url: "workbench/activity/getUserList.do",
type: "get",
dataType: "json",//这一步将json字符串转为js对象
success: function (data) {
var html = "";
$.each(data, function (i, n) {
html += "<option value='" + n.id+"'>n.name</option>";
});
$("#create-owner").html(html);
//将当前用户默认为被选中的用户
$("#create-owner").val("${user.id}");
//通过js打开模态窗口
//这句话要写在ajax的响应成功函数中
$("#createActivityModal").modal("show");//show打开,hide关闭
}
});
});
(2) AJAX请求排错
点击按钮发送AJAX请求,没有反应,具体排查流程:
-
先点击F12,再点击"创建"按钮
- 观察你的Network,看看请求有没有发出去
- 如果请求没有发出去,一定是该方法中有js代码写错语法了,排查语法,可以使用简单的alert弹框去做标记
- 如果请求发出去了,请看第二步
-
在你的ajax的回调函数中第一行,加入alert(data)
- 观察结果
- 如果弹框没反应 说明是后台代码出错了,进入到第三步
- 如果弹出了 object Object,说明我们是有json数据的,打印你拼接的html,看看是不是我们想要的拼接后的option
- 如果弹出了 [{},{},{}],说明这个json字符串并没有解析为json对象,查看你的dataType
- (a.看看你是不是写的是dateType b.看看你是不是写的是datatype)
-
观察后台
- 从Controller开始排查
(1)看参数是不是我们想要接收的
(2)观察业务层有没有报错(错误信息大多数情况比较模糊,不容易排查,需要总结异常经验) - 观察Service层
如果是比较复杂的业务逻辑,需要你打断点一步一步进行调试,每走一步,都需要观察结果,是不是我们想要的
如果是比较简单的业务逻辑,则不需要观察 - 观察dao层
主要观察的是log4j日志为我们打印的sql语句
观察sql语句本身
观察参数
观察返回记录数 - 如果dao层没有问题,service层也没有问题,controller也没问题(这3层都没有抛异常)
肯定是你的业务层写了一个return null;
- 从Controller开始排查
七、前端
1. 前端页面弹框后不加感叹号
alert("弹框的内容,内容最后不要随便加感叹号,就像这样!!!")
2. 全选框实现
- 将多选框的checked属性赋值给单选框
- 判断单选框的选中的个数(数组长度)与所有单选框的个数是否一致,若一致就让多选框选中
$("#qx").click(function () {
//将多选框的checked属性赋值给单选框
$("input[name=xz]").prop("checked", this.checked);
})
$("input[name=xz]").click(function () {
$("#qx").prop("checked", $("input[name=xz]:checked").length === $("input[name=xz]").length);
});
3. 日历控件
引入BootStrap的日历控件,给所有class
中带有time
的表单加入日历控件
<link href="jquery/bootstrap_3.3.0/css/bootstrap.min.css" type="text/css" rel="stylesheet"/>
<link href="jquery/bootstrap-datetimepicker-master/css/bootstrap-datetimepicker.min.css" type="text/css" rel="stylesheet"/>
<script type="text/javascript" src="jquery/jquery-1.11.1-min.js"></script>
<script type="text/javascript" src="jquery/bootstrap_3.3.0/js/bootstrap.min.js"></script>
<script type="text/javascript" src="jquery/bootstrap-datetimepicker-master/js/bootstrap-datetimepicker.js"></script>
<script type="text/javascript" src="jquery/bootstrap-datetimepicker-master/locale/bootstrap-datetimepicker.zh-CN.js"></script>
<script type="text/javascript">
$(".time").datetimepicker({
language: "zh-CN",
format: "yyyy-mm-dd",//显示格式
minView: "month",//设置只显示到月份
// initialDate: new Date(),//初始化当前日期
autoclose: true,//选中自动关闭
todayBtn: true, //显示今日按钮
clearBtn : true,//显示游击队按钮
pickerPosition: "bottom-left"//对齐样式
});
</script>
4. JavaScript字符串中引号问题
原则:在双引号中如果要再使用引号的话,要使用单引号,在单引号中如果要再使用引号的话,要使用双引号,为了与最外层的双引号做区分,双引号要加上转义"\
"
- 最外层为双引号:
" ' \" \" ' "
- 最外层为单引号:
' " \' \' " '
八、踩过的坑
1. @ResponseBody
每次在Controller中return值的时候,都要想一下注解是否加全(有一次忘记加@ResponseBody找了一个多小时bug)
2. 自动注入
Service层使用事务时,Controller层给Service属性自动赋值,属性只能是Service的接口类型
,不能是Service的实现类类型(我和另一个人合伙找了快三个小时bug……)
3. JSTL表达式
items属性中的参数一定要从作用域对象中取出来(多次犯错这个错)
<c:forEach items="${dtList}" var="dt" varStatus="i">
</c:forEach>
4. 路径问题
总是在不该加“/
”的地方加上"/
"(要多注意)
5. input标签disable与readonly
- disable不会往后台发送表单数据
- readonly会往后台发送表单数据
6. SQL语句老写错
- 更新、删除、插入语句
from
写成form
九、市场活动模块
1. 用哪个模块的Controller处理,响应什么参数
- 请求是在市场活动模块发起的,所以由市场活动模块的控制器
ActivityController
处理请求 - 但是进入到控制器后,取得用户信息列表,是属于用户的业务范畴,所以业务层要使用的是
UserService
来处理业务 - 业务层为控制器返回什么,完全在于控制器想要什么
- 控制器想要什么,完全在于前端发出的请求,想要响应什么
2. 异常的处理方式
异常处理方式:在控制层捕获业务层所有的异常,然后抛出自定义异常
@RequestMapping("/saveActivity.do")
@ResponseBody
public Map<String,Object> saveActivity(Activity a)throws AjaxRequestException {
try {
activityService.saveActivity(a);
} catch (Exception e) {
e.printStackTrace();
throw new AjaxRequestException();
}
return HandleFlag.successTrue();
}
3. 使用隐藏域
<input>
标签type
属性为"hidden
"时,为隐藏域,可以保存一些值,而且不会被浏览器界面看到
<input type="hidden" id="hidden-name"/>
<input type="hidden" id="hidden-owner"/>
应用:
- 当查询框中加入值时,不点击查询按钮,而是点击下一页,会把查询框中的值传到后台,出现实现查询结果的bug,
- 解决:当点击查询按钮时,将查询框中的值备份一份到隐藏域中,在往后台查找数据时,将隐藏域中的备份的值恢复到查询框中。
4. 给动态生成的标签绑定事件(全选框高级)
使用的函数:
$(需要绑定的元素的有效的父级元素).on(绑定事件的方式,需要绑定的元素,回调函数)
- 有效的父级元素是指:不是动态生成的标签元素,如果父级不行,则继续向上扩,直到是有效的父级为止。
- 原理:
on
这个方法会为有效的父级元素这个区域添加一个监视器,只要有新的元素进来都会添加上一个事件,如click
等//这里的有效父元素为#activityBody $("#activityBody").on("click",$("input[name=xz]"),function () { $("#qx").prop("checked",$("input[name=xz]").length==$("input[name=xz]:checked").length); }); //另一种绑定方式,与上面的区别是这里用的是CSS选择器 $("#contentRemarkDiv").on("mouseout",".myHref",function(){ $(this).children("span").css("color","#E6E6E6"); });
5. BootStrap分页框架(bs_pagination)
一个完整的ajax查询显示流程:
//pageNo表示当前页,pageSize表示一页显示几条数据
function pageList(pageNo, pageSize) {
//将全选的复选框的√灭掉,小细节
$("#qx").prop("checked", false);
//将隐藏域中的信息取出,重新赋值给搜索框,前面讲的9-3
$("#search-name").val($.trim($("#hidden-name").val()));
$("#search-owner").val($.trim($("#hidden-owner").val()));
$.ajax({
//这里来发送请求
success: function (data) {
//这里来展现后台返回的数据
//计算总页数
var totalPages = data.total % pageSize == 0 ? data.total / pageSize : parseInt(data.total / pageSize) + 1;
$("#activityPage").bs_pagination({
currentPage: pageNo, // 页码
rowsPerPage: pageSize, // 每页显示的记录条数
maxRowsPerPage: 20, // 每页最多显示的记录条数
totalPages: totalPages, // 总页数
totalRows: data.total, // 总记录条数
visiblePageLinks: 3, // 显示几个卡片(显示几个1,2,3,4...这种的页码)
showGoToPage: true,
showRowsPerPage: true,
showRowsInfo: true,
showRowsDefaultInfo: true,
//该函数的触发时机:在我们点击分页组件的时候(上一页,下一页,首页,尾页,12345...页)
onChangePage: function (event, data) {
/*
data.currentPage:点击分页组件后的当前页码
data.rowsPerPage:点击分页组件后每页展现的记录数
以上这两个值,是分页插件为我们提供的,我们千万不要去改动
*/
pageList(data.currentPage, data.rowsPerPage);
}
});
}
})
}
调用函数时,有两个框架已经提供好的参数,当实参传入即可,根据具体情况进行使用:
$("#activityPage").bs_pagination('getOption', 'currentPage')
:维持当前页$("#activityPage").bs_pagination('getOption', 'rowsPerPage')
:维持每页展现的记录数
使用:
pageList($("#activityPage").bs_pagination('getOption', 'currentPage'),
$("#activityPage").bs_pagination('getOption', 'rowsPerPage'));
6. VO类
下一层往上一层做值的返回,如果返回的有多个不同类型的值,那么使用的一般都是map
类型来处理,当返回的信息复用率比较高,为了方便可读性以及可维护性,我们也可以使用vo
类技术来代替传统的map
技术来处理
VO:Value Object 用来表现值的对象,用来展现值的类,用法与传统的实体类是一样的,将需要展现的值的信息,一项一项的列在VO类的属性当中,这些属性都是私有的,所有的存取值操作一律使用setter
和getter
来实现;使用泛型式的方式,让每一个模块都能够使用(集合中存不同的实体类)
public class PaginationVo<T> {
private List<T> dataList;
private int total;
//省略getter和setter
}
7. @RequestParam
- 使用
Map
来接收前端发过来的参数,将前端传过来的所有参数以键值对的形式存储到map中 - 可以使用
value
属性来给传递过来的参数起别名,功能和@Param
相同,只不过@Param
用在dao
层,而@RequestParam
用在controller
层
public PaginationVo<Activity> pageList(@RequestParam Map<String,Object> map){}
8. textarea文本域*
textarea
文本域是表单元素,但是他操作值的方式与其他元素有些差别,其他的表单元素都有其value
值,但是文本域没有value
值,而是以textarea
标签对中的信息去操作表单元素的值。
虽然textarea
操作的是标签对中的内容,但是他也是属于表单元素范畴,所以我们必须以val()
方法的方式去操作文本域的值,而不是html()
方法
9. AJAX传多个同名参数
data属性规定要发送到服务器的数据, 类型可以是:string
,数组,多数是 json
,当有多个徤相同
的值要往后台发送时,就不能使用json
了,而要使用string
,string
的格式"id=A001&id=A002&id=A003
",后台使用string数组接收,变量名与前台发回的名称一样(id
)
public PaginationVo<Activity> pageList2(String[] id){}//id这个变量名要与发送回来的参数名一样
10. 数据导入/导出(excel格式)
11. 评论(备注)加载时机
在页面加载完毕后,发出ajax
请求取得备注信息列表,采用这种方式的原因:
- 评论区数据量大,加载时间长,防止网页响应变慢
- 页面上处理备注的添加,修改,删除操作,每一次操作完毕后,都需要"局部刷新"备注信息列表
12. 使用before追加的方式实现
增、删、改之后要注意一下要定位到更改的那个元素位置,再单独对它进行刷新。不能全部刷
13. JS中使用EL表达式
在js
中用el
表达式一定要加引号,已经在引号中可以直接用(在字符串中)
- 要加引号:
$.ajax({data : {"activityId" : "${a.id}"})
- 不需要加引号:
var html = "</font> <b>${a.name}</b>"
14. 禁用超链接
href="javascript:void(0);"
:禁用超链接,超链接必须以触发(绑定)事件的形式执行操作
目的:将鼠标移到超链接上,鼠标会变成变手形,只有超链接能办到。
15. 为for循环动态拼接中的元素绑定事件
如果元素是通过在js
或者java
脚本动态拼接的for
循环中的元素,那么我们一般的做法都是以直接触发事件(例如标签中加入onclick="f('id')"
)的方式来进行操作,而不是绑定事件。
直接触发事件传递的参数(如前面的id
)必须要包含在引号当中
16. 浮动的下划线
前端设置了浮动的下划线,当后台没有取到数据时,下划线会跑到上面
- 解决办法:加空格将下划线压下来,即使没有数据,下划线也不会乱跑