汽服系统主要包括了门店系统、集团系统、运营系统、员工Pad这几个部分。
交互流程
- 公共仓库提供代码的CRUD操作。包括了常用工具类(util),模型对象(domain),服务类(service)。如果出现业务异常,应该在service抛出serviceException
- 运营后台主要是给运营人员使用的
- 门店系统跟集团系统现在都在同一个工程中,后面有可能会拆分。门店系统提供员工pad端的api
- 汽服系统后面考虑做一个用户微信,微信后台目前待定
- carme-qifu-operation域名:qfadm.car-me.com
carme-qifu-admin域名:qfstore.car-me.com
carme-qifu-group域名:qfgroup.car-me.com - 运营系统用的是之前运营平台的那一套
集团系统和门店系统需要重新出页面,做版式
FAQ
-
service为什么不采取接口的形式,而是直接实现类
用接口实现类的方式的好处主要是可以进行多实现,但是目前系统service跟dao只会有一个实现类,所以没有使用接口的方式,这样相对来说简单,而且减少定义无用的接口类。如果要对外暴露接口,可以在carme-qifu-common上面加一层,然后用dubbo的方式对外提供接口。
系统边界
- 对外暴露的接口需要做接口的参数校验
技术框架
- 框架:Spring boot
- orm : spring data jpa+jdbc template
- 模板引擎:velocity
- GitHub地址:http://hjm017.github.io/SpringWheel (觉得不错可以给个star啊 -)
百度脑图地址:http://naotu.baidu.com/file/a49ffc31ca9666ddb4dd99de7c061395?token=601c70341282f465
数据库设计
1、表需要带一个前缀。例如:内容管理(c_)、资源管理(r_)、设备管理(f_)、站点管理(w_)
2、相同功能的字段需要放在一起。例如 order_state,order_price有关订单的需要放在一起
3、每张表中需要有is_delete、created_at、created_by、changed_at、changed_by五个字段
4、有展示列表的数据需要在表中做冗余字段。例如:seller_name
5、表逻辑外键需要带前缀。例如:p_seller_id
6、有关定时任务的字段需要加前缀。例如:cron_state
7、所有的逻辑外键需要加上索引
8、字段尽量不要设置为可空
9、表注释要完整,涉及字典的字段需要在注释上标明dict_type_value。例如:退款状态(car_tkzt )
10、索引命名规范为:idx_(字段名) ** 例如:idx_model_id
11、 建表的时候id都用bigint(20). 字符串都用varchar(255). 大文本用text**
数据库脑图地址:http://naotu.baidu.com/file/fd2d8817fe0c44a68ad24883e84bcf30?token=7e3f0cccf1a9be7f
编码规范
1、代码中不允许出现magic number,应该定义为常量
错误:test.setStatus(1);
正确:test.setStatus(CodeConstant.SUCCESS)
2、用AccountUtil来获取当前登录用户信息
Account account = AccountUtil.getAccountInfo();
String id = account.getId();
String operateName = account.getName();
3、涉及到业务异常的应该在service中抛出serviceException,涉及权限等其他的异常请在Controller抛出
Service:
if(car==null){
throw new ServiceException("该类型的卡没有数据,请检查传入的id是否正确");
}
Controller:
if(CodeConstant.isAdmin==user.getIsAdmin){
throw new ManagerException("该用户没有该权限");
}
如果在底层抛出业务异常,在controller请catch掉,如果不catch ,默认认为为系统异常
controller
type1:正常请求
try{
// do something
}catch(ServiceException e){
return ControllerHelper.showMsg(map.e.getMessage,'/list.do');
}
type2:ajax请求
try{
// do something
}catch(ServiceException e){
AjaxResult res = new AjaxResult();
res.setCode(1211);
res.setMessage(e.getMessage);
return res;
}
有些人直接把底层的异常直接catch掉,然后抛出ServiceException
错误:
try{
//do something
}catch(Exception e){
e.printStackTrace();
throw new ServiceException("xxx失败");
}
此种写法错在两点:
- e.printStackTrace 打印堆栈到控制台,大家都知道,服务器的IO资源是非常宝贵的,如果有很多system.out.print()造成了服务器的io资源被占用,严重的会极大影响服务器性能。
- 既然已经catch了异常,又抛出一个新的异常(把大异常转成了小异常),但是原始的异常没有记录,而且catch的是Exception的异常,容易把其他的异常也catch掉,这样造成很难排查到线上bug造成的原因。
正确的做法
try{
}catch(NullPointException e){
logger.error(e.getMessage(),e);
}
注意:因为我在全局的异常处理中记录了日志,所以RuntimeException可以不用catch
4、在前端调用webUtil来获取css,js,host,img的域名
js域名 :${webUtil.getJsDomain('/js/operate.js')}
img域名:${webUtil.getImgDomain('/img/dd.png')}
css域名:${webUtil.getCssDomain('/css/ddd.css')}
5、前端js绑定事件时需要注意不要直接在class的选择器上绑定(前端可能会修改样式),常用做法
1) 给元素添加一个属性nc_type(推荐)
html
<a href="javascript:void(0);" nc_type="search" />
js
$("a[nc_type="search"]").on("click",function(){
//do something
});
好处:前端更改样式不会影响js事件
2)需要绑定的样式加一个js前缀
html
<a href="javascript:void(0);" class="js-test btn"/>
js
$("a.js-test").on("click",function(){
//do something
});
此种做法不太推荐,但是如果你想在class上面绑定事件,要加上js前缀,这样前端改样式的时候就会注意不会用有js前缀的样式来写css
6、之前由于因为form和dto没有在之前定下规则,导致了很多的问题,故指定命名规范和使用规则。form跟dto的命名规范和使用规则如下:
form分为查询form跟新增编辑form。
1) 查询form(模块名+SearchForm,如:UserSearchForm)可复用。复用时具体的变量命名规则:
1、日期的开始结束时间(字段名+Begin/End),例如:createdAtBegin、createdEnd
2、关键字(模糊查询时使用,查询字段+And+查询字段+Key),例如:关键字(用户名/手机号码)->userNameAndPhoneKey
例如:
if (StringUtils.isNotEmpty(deviceSearchForm.getMeiAndSiteNameKey())) {
Predicate p1 = cb.like(r.get("mei").as(String.class), "%" + deviceSearchForm.getMeiAndSiteNameKey() + "%");
Predicate p2 = cb.like(r.get("siteName").as(String.class), "%" + deviceSearchForm.getMeiAndSiteNameKey() + "%");
predicate.getExpressions().add(cb.or(p1, p2));
}
2)新增/修改 form 不可复用。命名规则:
1、新增(模块+动作+AddForm),例如:UserAddForm,UserStep1AddForm(新增商家第一步)、UserStep2AddForm(新增商家第二步)...
2、编辑(模块+动作+EditForm),例如:UserEditForm、UserStep1EditForm、UserStep2EditForm...
新增和修改form需要加上注解,进行服务端校验
dto的命名规范同form
7、现在项目有三个日志文件,分别是root、request、sql
可以根据自己的需要添加logger。例如:跟金额结算有关的可以增加一个bill的logger
8、关于常量使用
目前系统的常量分为三种,分别是公共常量(common constant)、业务常量(business constant)、系统常量(system constant)
- 公共常量。主要是一些常用不带有业务意义的常量
//公共常量
private static final int FLAG_TRUE=1; //状态标识 true
private static final int FLAG_FALSE=0; //状态标识 false
private static final int PAGE_SIZE=5; //默认分页大小
公共常量放在carme-qifu-common中的Constant类中,正常情况下,类似于是否删除,是否付款这样的简单的标识位无需定义其他的常量,直接使用公共常量中的FLAG_TRUE和FLAG_FALSE
- 业务常量。主要跟业务状态有关的一些常量
//业务常量
public static final String EVALUATION_TAG_STATUS_NORMAL = "0"; // 评价标签表---状态:0,正常
public static final String EVALUATION_TAG_STATUS_SHIELD = "1"; // 评价标签表---状态:1,屏蔽
- 系统常量。主要跟系统的运行有关的一些常量
/** * 编码 */
public static final String I18N_ENCODIND = "UTF-8";
/** * 信息提示页面 */
public static final String I18N_MSG = "classpath:/i18n/messages";
public static final String I18N_VALIDATOR = "classpath:/i18n/validator";
业务常量跟公共常量放置在carme-qifu-common的BusinessConstant.java和Constant.java类中,系统常量放在对应项目的constant包中
9、拦截规则
因为spring boot对(urlPattern=/)的请求进行拦截,所以大家的所有请求都会进入spring boot中,所以不会出现自己定义servlet处理请求的情况。为了之后能够做动态请求做处理,故增加后缀方便日后拓展。
web请求拦截规则:
- 正常请求。后缀加上.do
- ajax异步请求。如果需要返回json数据加上.json后缀,如果需要返回.xml数据,加上.xml后缀
api请求拦截规则: - 如果路径规则符合(/rest/**),为暴露出去的api
- 如果需要返回json数据加上.json后缀,如果需要返回.xml数据,加上.xml后缀
10、在velocity中使用常量类
在velocity中需要进行一些逻辑判断,这个时候需要根据一些业务状态进行逻辑判断
bad practice:
#if($item.modelType==1)
do something
#end
best practice:
#if($item.modelType==$fieldTool.EVALUATION_TAG_STATUS_NORMAL)
do something
#end
这个时候如对应的业务状态更改了(虽然比较少),这个时候就还需要更改velocity中的业务状态判断,而且=1,=2这种可读性太差(今天写了明天就忘了1,2代表什么了,还要去查对应的字典,还是常量比较好理解)
11、关于页面复用
有的时候如果新增页面和修改页面基本一样,这个时候我们就想了,为什么既然页面相同为什么我们不能复用了?其实这个时候新增页面和修改页面是一种很好的方案:
- 如果页面更改了,只要更改一个页面即可
- 提高了开发效率,减少了维护的工作量
但是,关于复用的东西一定要谨慎,标准一定要制定好,不然随时可能因为一个公共方法的修改影响多个页面。
适用范围:两个页面相差不大
操作标识:opType
类型常量:Constant.UPDATE,Constant.ADD
页面命名: xxx_input.vm 例如:card_input.vm
vm
<!--操作标识-->
<input type="hidden" name="opType" id="opType" value="${fieldTool.UPDATE}"/>
js
var opType = $("#opType").val();
if(opType=="update"){
//回填值
...
//js效果显示
...
}
后台
/** * 保存新增的企业信息 * * /
@RequestMapping("/input.do")
public String input(ModelMap modelMap, String opType) {
if(Constant.UPDATE.equals(opType)){
//do something
}
...
}
注意:如果新增后修改页面后面改变很大,两个页面完全不同,不建议复用,具体要自己判断下
12、关于createdBy、createdAt、changedBy,changedAt
WHY:有人可能会疑惑每张表添加这几个字段是干啥用的,举个例子,如果我想知道某个订单是什么时候创建的,谁创建的(createdAt,createdBy)。这个订单是谁审核的,最后审核时间是什么时候(changedBy,changedAt 当然这两个字段不然说明操作类型,如果需要知道具体的操作类型,需要添加相应的操作记录表)。从上面的例子可以看出,这四个字段在我们做数据统计和操作历史的时候是非常有用的。
HOW: 如果添加这几个字段,但是在具体操作的时候没有更新值的话就会有问题了。推荐做法
service
public void update(Card card ,String operaterName){
Card flushData= cardDao.findOne(card.getId());
//将页面传入的对象属性拷贝到flushData中
BeanHelper.copyProperty(flushData,card);
flushData.setChangedBy(operatorName);
flushData.setChangedAt(DateUtil.getDate());
cardDao.saveAndFush(flushData);
}
public void save(Card card,String operatorName){
card.setCreatedBy(operatorName);
card.setCreatedAt(DateUtil.getDate());
cardDao.save(card);
}
直接在sevice参数添加一个operatorName,你不传的话就没办法调用service,妈妈再也不要担心我漏更新时间了,哈哈!!
代码生成工具
GitHub:https://github.com/hjm017/code-generator
generator.xml文件说明:
<!--生成的类的包路径-->
<entry key="basePackage">com.carme</entry>
<!--生成文件目录-->
<entry key="outputDir">output</entry>
<!--模板文件目录-->
<entry key="templateDir">template</entry>
<!--需要去除的表的前缀-->
<entry key="prefix">c,i,p,tb</entry>
<!--生成的表-->
<entry key="tables">c_card</entry>
<!--是否生成数据源中所有的表-->
<entry key="allSwitch">false</entry>
<!--数据库用户名-->
<entry key="jdbc_username">qifuowner</entry>
<!--数据库密码-->
<entry key="jdbc_password">qifuowner_cte</entry>
<!--数据库URL-->
<entry key="jdbc_url">jdbc:mysql://192.168.51.195:3306/qifudbd01?useUnicode=true&characterEncoding=UTF-8</entry>
使用方法:
Step1:配置数据源信息
<entry key="jdbc_username">用户名</entry>
<entry key="jdbc_password">密码</entry>
<entry key="jdbc_url">jdbc:mysql://数据库地址:3306/数据库名?useUnicode=true&characterEncoding=UTF-8</entry>
Step2:配置需要生成的表
<!--需要去除的表的前缀-->
<entry key="prefix">c,i,p,tb</entry>
<!--生成的表-->
<entry key="tables">c_card</entry>
Step3:运行GeneratorServer.java
注意:如果在eclipse中GeneratorServer.java不被识别为java类,请将GeneratorServer.java类放入包路径中
Druid监控平台
URL:http://qfadm.car-me.com/operation/druid/login.html
用户名: admin
密码: admin
代码碎片
1、生成自增长的序号
初始化
--创建序列表
CREATE TABLE tb_sequence (
seq_name VARCHAR (50) NOT NULL,
current_value VARCHAR(15) NOT NULL,
len INT NOT NULL,
increment INT NOT NULL DEFAULT 1,
PRIMARY KEY (seq_name)
) ;
--创建_nextval函数
DELIMITER //
CREATE FUNCTION _nextval(n VARCHAR(50)) RETURNS VARCHAR(15)
BEGIN
DECLARE _cur VARCHAR(15);
DECLARE _len INT;
SET _cur=(SELECT current_value FROM tb_sequence WHERE seq_name= n FOR UPDATE);
SET _len=(SELECT len FROM tb_sequence WHERE seq_name = n FOR UPDATE);
IF LENGTH(_cur)>_len THEN SET _cur='1';END IF;
UPDATE tb_sequence SET current_value = _cur + increment WHERE seq_name=n ;
RETURN LPAD(_cur,_len,'0000000000000000000000000000000');
END;
//
说明:生成的序列可以自定义长度(设置len),如果超过设置的长度将会重置为1
例如:生成3位自增长序号
insert into tb_sequence values('bill_no',3,1);
select _nextval('bill_*no')
在程序中
Class Service{
@Autowire
private SeqHelper seqHelper;
...
String no = seqHelper.getNo("Bill_no");//序列
...
}
注意:获取序列的操作需要放在事务中(在方法上添加@Transactional注解),出现异常时,回滚事务,保证事务的ACID特性(如果在service中自己抛出业务异常,事务会自动回滚)。
2、分页(carme-qifu-operation)(例子:card/list.vm)
1)form格式
<form action="${webUtil.getHostDomain('/card/list.do')}" method="post" id="listForm">
<!--此处添加隐藏参数-->
<input type="hidden"/>
...
#foreach($item in $page.pageItems)
<tr>
...
</tr>
#end
<!--此处添加分页bar-->
#pager("listForm" "pageNo" $page)
</from>
2)marco 说明
#pager("listForm" "pageNo" $page)
1)listForm : form表单id
2)pageNo:分页隐藏域的name
3)$page :后台设置的page参数
3)controller
@RequestMapping("/list.do")
public String index(ModelMap map, CardSearchForm cardSearchForm) {
//分页参数处理:1、初始化的时候pageNo=0->pageNo=1,传入pageNo的时候,pageNo=pageNo-1 2、设置分页的pageSize
ControllerHelper.doDealParam(cardSearchForm);
Page<Card> page = cardService.findAll(cardSearchForm);
//将jpa返回的分页集合包装为view层需要的分页对象
PageInfo<Card> pageInfo = ControllerHelper.getPageInfo(page);
map.addAttribute("page", pageInfo);
return "card/list";
}
分页的大小默认为5,可以更改OperationConstant.PAGE_SIZE的值,更改分页默认大小
3、后台参数检验
在需要进行的方法上添加@ParamCheck注解,使用的是hibernate validator
方式一: 参数中只有对象
@ResponseBody
@ParamCheck
@RequestMapping("/register.do")
public String register(UserRegisterForm userRegisterForm) {
return "";
}
方式二:参数中只有基本类型
@ResponseBody
@ParamCheck
@RequestMapping("/register.do")
public String register(@NotEmpty(message = "设备类型不能为空")
@RequestParam(value = "facilityId", required = false) String facilityId,
@Digits(fraction = 6, integer = 3)
@RequestParam(value = "longitude", required = false) Double longitude
) {
return "";
}
方式三:参数中既有基本类型,又有对象
@ResponseBody
@ParamCheck
@RequestMapping("/register.do")
public String register(UserRegisterForm userRegisterForm, @NotEmpty(message = "设备类型不能为空")
@RequestParam(value = "facilityId", required = false) String facilityId,
@Digits(fraction = 6, integer = 3)
@RequestParam(value = "longitude", required = false) Double longitude
) {
return "";
}
4、使用Beancopier框架来做实体映射
在项目中,经常需要做beancopier的操作,经过测试,使用jdk的set/get效率最高,但是对于字段较多的情况下不太使用,增加了代码量,经过比较,cglib的beancopier效率最高并且容易扩展
我对这些工具做了一个对比:Copy一个简单Bean 1,000,000次,计算总耗时。比较结果如下:
1,000,000 round
jdk set/get takes 17ms
cglib takes 117ms
jodd takes 5309ms
dozer mapper takes 2336ms
apche beanutils takes 6264ms
故在carme-qifu-common中写了一个基于cglib的BeanCopier类。在类中使用
WarehouseAddForm addForm = new WarehouseAddForm();
addForm.setStoreNo("00023011034");
addForm.setName("卡咪汽服");
addForm.setAddress("越达巷");
addForm.setCreatedBy("hxq");
addForm.setPurpose("随便");
SimpleBeanCopier copier = new SimpleBeanCopier(WarehouseAddForm.class, Warehouse.class);
Warehouse warehouse = (Warehouse) copier.copy(addForm);
由于cglib是使用修改字节码的方式实现beancopier的,这种方式如果程序出错了将会很难定位异常位置
所以,推荐使用orika的beancopier框架
productForm.setPrice(NumberUtil.toYuanLong(productForm.getPrice()).toString());
Product product = BeanMapper.map(productForm, Product.class);
Product temp = productService.findOne(product.getId());
product.setCreatedBy(temp.getCreatedBy());
product.setCreatedAt(temp.getCreatedAt());
product.setIsUse(temp.getIsUse());
product.setIsDelete(temp.getIsDelete());
product.setQrcode(temp.getQrcode());
product.setChangedBy(temp.getChangedBy());
product.setChangedAt(temp.getChangedAt());
productService.save(product);
5、前端ajax使用
由于在项目中使用了ajax请求,因此导致在ajax请求回调需要处理因为cookie失效等情况,故按照AOP的思想,基于jquery编写了http.js
define(["jquery"], function($) {
return {
//提交ajax请求
post:function(option){
$.ajax({
type: 'POST',
url: option.uri,
data: option.data,
dataType: "json",
success: function(data) {
//cookie失效判断
if (result.status != undefined && result.status == 4000) {
var redirectURL = result.redirectURL;
window.location.href = redirectURL;
}
option.success(data);
},
error: function(data) {
layer.alert("ajax请求失败",function(index){
layer.close(index);
});
}
});
}
}
});
需要使用http.js的时候需要在require.js中配置
require.config({
baseUrl: getJsBase(),
paths:{
"bootstrap":"bootstrap/bootstrap",
"bootbox":"bootstrap/bootbox.min",
"html5shiv":"bootstrap/html5shiv.min",
"respond":"bootstrap/respond.min",
"http":"http",
...
}
})
然后在头部引入
;require(['domReady!', "http"], function( http) {
在js中使用
http.post({
uri:$("#addProductFragmentUri").val(),
data:{
productId:drag.attr("data-id"),
contentId:$("#contentId").val(),
modelId:$("#modelId").val(),
sort:target.attr("data-flag")
}
});
6、在项目中进行多表查询
方式一:使用jdbc template。优点:方便快捷,直接能够看到sql
vo实体类
public class CardExtendVo {
private Long cardId;
private String companyNo;
public String getCompanyNo() { return companyNo; }
public void setCompanyNo(String companyNo) { this.companyNo = companyNo; }
public Long getCardId() { return cardId; }
public void setCardId(Long cardId) { this.cardId = cardId; } }
service类
@Service
public class CardService {
@Autowired
private CardDao cardDao;
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional(readOnly = true)
public List<CardExtendVo> getMyCardInfo(){
String sql = "select t1.id as cardId,t2.company_no as companyNo from c_card t1,p_company t2 where t1.p_company_id=t2.id";
List<CardExtendVo> cardExtendVo = jdbcTemplate.query(sql,new BeanPropertyRowMapper( CardExtendVo.class));
return cardExtendVo;
}
test类
@Test
public void getMyCardInfo() {
List<CardExtendVo> list = cardService.getMyCardInfo();
for (CardExtendVo cardExtendVo :list){
System.out.println("card id:"+cardExtendVo.getCardId());
System.out.println("company_no:"+cardExtendVo.getCompanyNo());
}
}
注意:查询出来的字段别名需要与实体类的字段一致,否则无法映射
方式二:使用spring data jpa 优点:编码简单 缺点:效率低
多表连接查询稍微麻烦一些,下面演示一下常见的1:M,顺带演示一下1:1
使用Criteria查询实现1对多的查询
Step1:首先要添加一个实体对象DepModel,并设置好UserModel和它的1对多关系,如下:
@Entity
@Table(name="tbl_user")
public class UserModel {
@Id
private Integer uuid;
private String name;
private Integer age;
@OneToMany(mappedBy = "um", fetch = FetchType. *LAZY*, cascade = {CascadeType. *ALL*})
private Set<DepModel> setDep;
//省略getter/setter
}
@Entity
@Table(name="tbl_dep")
public class DepModel {
@Id
private Integer uuid;
private String name;
@ManyToOne()
@JoinColumn(name = "user_id", nullable = false)
//表示在tbl_dep里面有user_id的字段
private UserModel um = new UserModel();
//省略getter/setter
}
Step2:配置好Model及其关系后,就可以在构建Specification的时候使用了,示例如下:
Specification<UserModel> spec = new Specification<UserModel>() {
public Predicate toPredicate(Root<UserModel> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate p1 = cb.like(root.get("name").as(String.class), "%"+um.getName()+"%");
Predicate p2 = cb.equal(root.get("uuid").as(Integer.class), um.getUuid());
Predicate p3 = cb.gt(root.get("age").as(Integer.class), um.getAge());
SetJoin<UserModel,DepModel> depJoin =root.join(root.getModel().getSet("setDep",DepModel.class) , JoinType.LEFT);
Predicate p4 = cb.equal(depJoin.get("name").as(String.class), "ddd");//把Predicate应用到CriteriaQuery去,因为还可以给CriteriaQuery添加其他的功能,比如排序、分组啥的
query.where(cb.and(cb.and(p3,cb.or(p1,p2)),p4));**
//添加分组的功能
query.orderBy(cb.desc(root.get("uuid").as(Integer.class)));
return query.getRestriction();
}};
接下来看看使用Criteria查询实现1:1的查询
Step1:在UserModel中去掉setDep的属性及其配置,然后添加如下的属性和配置:
@OneToOne()
@JoinColumn(name = "depUuid")
private DepModel dep;
public DepModel getDep() { return dep;}
public void setDep(DepModel dep) {this.dep = dep;
}
Step2:在DepModel中um属性上的注解配置去掉,换成如下的配置:
@OneToOne(mappedBy = "dep", fetch = FetchType. *EAGER*, cascade = {CascadeType. *ALL*})
Step3:在Specification实现中,把SetJoin的那句换成如下的语句:
Join<UserModel,DepModel> depJoin =
root.join(root.getModel().getSingularAttribute("dep",DepModel.class),JoinType.LEFT);
//root.join(“dep”,JoinType.LEFT); //这句话和上面一句的功能一样,更简单
7、api接口规范
1)如果是Api接口的控制器需要在controller上面增加@ApiController
2)方法上面需要添加@ApiRequestBody的注解
例如:
/** * @author hjm * @Time 2016/5/1 20:21. */
@ApiController
public class UserEndpoint {
private static Logger logger = LoggerFactory.getLogger(UserEndpoint.class);
@Autowired
private UserService userService;
@RequestMapping(value = "rest/users", produces = MediaTypes.JSON_UTF_8)
public List<UserDto> getList() {
List<User> users = userService.getList();
if (false) {
throw new ApiException("这是一个测试例子", ErrorCode.INTERNAL_SERVER_ERROR);
}
return BeanMapper.mapList(users, UserDto.class);
}
@RequestMapping(value = "rest/user/{id}/modify", produces = MediaTypes.JSON_UTF_8)
public List<UserDto> getList(@ApiRequestBody UserDto userDto) {
List<User> users = userService.getList();
return BeanMapper.mapList(users, UserDto.class);
}
}
8、使用js回填select
html
<select class="w150 beautyselect" name="workState" init-value="1">
...
</select>
js
$.each($(".beautyselect"),function(idx,element){
var initValue = $(element).attr("init-value");
$(element).find("option[value='"+initValue+"']").attr("selected",true);
}) ;
只需要在select中添加init-value即可回填
注意:需要在beautySelect渲染之前添加回填的代码
9、消息提示页面
如果A打开一个列表页面,不操作。这个时候,B也打开了这个列表页面,并且删除了这个列表的某条数据,这个时候如果A如果不刷新,直接点击列表页面的查看按钮,那么就会出现异常,页面也会显示为500。
比较好的做法是在后台对这种情况进行判断
@RequestMapping("/detail.do")
public String index(ModelMap map, @NotEmpty(message = "billId不能为空") @RequestParam("billId") String billId) {
WorkorderBill workorderBill = workorderBillService.findOne(Long.parseLong(billId));
//如果记录不存在,跳转提示页面
if (workorderBill==null){
return ControllerHelper.showMsgPage(map, MsgConstant.FAIL_FIND_RECORD,WORKORDER_LIST);
}
return "finance/settlement/input";
}
10、关于在汽服项目中使用jdbcTemplate
由于在汽服项目中有些复杂查询,这个时候就需要用到jdbcTemplate了,但是如果dao中有了jdbcTemplate,那么我们就无法使用spring data jpa简单好用的功能了。对于这个问题有以下几种解决方式:
1、使用jdbcTemplate的dao全部加个Extend后缀。例如:UserCardExtendDao
这样,我们还可以愉快的使用spring data jpa提供的便利(推荐)
2、直接在service中把jdbcTemplate注入到属性中,在service中使用。
以上两种方式都可以让我们同时使用jdbctemplate和spring data jpa,但是按照java的分层来说,service应该写的是业务逻辑,不应该写数据库操作,所以第一种方式是最优选择
11、关于mysql查询中通配符的问题
在mysql中‘,%’等被识别为通配符(有写过模糊查询的同学应该知道吧)。假如,现在我要查询一个“_”字符串,最终发出的sql会是这样的。
select * from o_workorder where car_no like '%_%'
这样查询出来的结果会很多(因为“_”被识别为通配符)。这样的结果不是我想要的。其实应该这样
select * from o_workorder where car_no like '%\\_%'
利用转义符对“_”进行转义,这样就能得到我们最终想要的结果了,在java中,我将转义的步骤封装成了一个工具类SQLUtil.java
if (StringUtil.isNotEmpty(workorderSearchForm.getCarNo())) {
predicate.getExpressions() .add( cb.like(
r.<String> get("carNo"), "%" + SQLUtil.processWildCard(workorderSearchForm.getCarNo().toString()) + "%"));
}
12、重复提交问题
在页面中,如果多次点击保存,会发出多个请求,这非常容易造成无意中多创建了记录。为了解决这个问题在js增加以下代码
//提交
$("#js-save-btn").on("click",function(){
layer.msg('请稍后...', {icon: 16,shade: [0.6, '#393D49']});
$(this).on("click",submitFail);
$(this).parentsUntil("form").parent().submit();
});
function submitFail(){
layer.alert("网络繁忙,请刷新页面后尝试");
}
效果:
13、业务异常那些事
首先,要理解我们的系统是异步的,这说明了,服务器允许两个或多个人同时登陆系统操作。就拿工单的开单步骤来说,假设一个店员在开单的同时,一个门店管理员删除了门店的某个物料的记录或者将该物料的库存更改为0(这种情况是存在的),那么这个时候该店员已经填写好了工单,像服务器发起了请求。如果我们不对库存不足和物料记录不存在的情况进行判断的话就会造成空指针了。
在店员填好工单,提交请求之前,门店管理员将工单中对应的物料删除或者将库存改为了0,这样后端如果不判断,直接根据前台传的物料id去查询对应物料的数据就会报NullException了
@ParamCheck
@RequestMapping("/add.do")
public String add(ModelMap map, HttpServletRequest request, WorkorderAddForm workorderAddForm) {
if (workorderAddForm != null) {
AccountInfo accountInfo = accountCookieHelper.getAccountInfo(request, BusinessConstant.STORE_TYPE);
//保存workorder记录
Workorder workorder = getWorkorderInfo(workorderAddForm); workorderService.save(workorder, accountInfo.getRealName());
try {
//保存workorderItem记录
List<WorkorderItem> workorderItems = getWorkorderItems(workorderAddForm, workorder.getId());
workorderItemService.save(workorderItems, accountInfo.getRealName());
//保存workorderMaterial记录
List<WorkorderMaterial> workorderMaterials = getWorkorderMaterials(workorderAddForm, workorder.getId());
workorderMaterialService.save(workorderMaterials, accountInfo.getRealName());
} catch (ServiceException e) {
logger.info(e.toString());
return ControllerHelper.showMsgPage(map,e.getMessage(),WORKORDER_LIST);
}
}
return ControllerHelper.showMsgPage(map, MsgConstant.OPT_SUCCESS, WORKORDER_LIST);
}
/**
* 获取工单物料数据
* @param workorderAddForm
* @return
*/
private List<WorkorderMaterial> getWorkorderMaterials(WorkorderAddForm workorderAddForm, Long workorderId) {
List<WorkorderMaterial> materials = new ArrayList<WorkorderMaterial>();
//物料集合
if (workorderAddForm.getMaterialId() != null) {
String[] materialIds = workorderAddForm.getMaterialId();
WorkorderMaterial workorderMaterial = null;
for (int i = 0; i < materialIds.length; i++) {
workorderMaterial = new WorkorderMaterial();
workorderMaterial.setMaterialId(Long.parseLong(materialIds[i]));
workorderMaterial.setWorkorderId(workorderId);
//查询物料数据
Material material = materialService.findOne(workorderMaterial.getId());
if (material == null) {
throw new ServiceException("ID=" + workorderMaterial.getId() + "的物料被删除,请联系管理员");
}
materials.add(workorderMaterial);
}
}
return materials;
}
从代码可以看出,对于物料是否存在和库存不足的情况我们是需要抛出业务异常的,业务异常的处理在controller中(提示用户库存不足或者物料不存在)
14、关于XSS攻击
网站经常会遇到CSXF、XSS、SQL注入攻击(链接:http://itindex.net/blog/2013/10/25/1382688300000.html)。 针对其中的XSS攻击,我在项目中加了一个XssFilter,用于处理输出的脚本,对于输入脚本进行转义处理。
@WebFilter(filterName = "xssFilter", urlPatterns = "/*")
public class XssFilter implements Filter {
FilterConfig filterConfig = null;
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new XssHttpServletRequestWrapper( (HttpServletRequest) request), response);
}
}
15、关于分页操作后回到操作页面的解决方案
在系统中存在这么一个问题,我在第二页操作了一条记录,但是等我操作完之后跳转页面跳到了第一页,这样造成的用户体验很不好。解决方案:
1、在分页列表的form的action加上一个listMode=cache的参数
<form method="post" name="form_workorder" id="form_workorder" action="$!{webUtil.getHostDomain('/workorder/list.do')}?listMode=cache">
2、在返回按钮添加一个listMode=restore参数
<a class="btn btn-primary" href="${webUtil.getHostDomain('/workorder/list.do?listMode=restore')}">返回</a>
当传了listMode=cache的参数时,后台会对这次请求的参数进行缓存,你需要跳回当时的页面只要在list.do后面加上一个listMode=restore(恢复缓存页面)即可
16、系统的中的数据权限
用户登陆系统之后按理说是只能查看属于自己的数据的,但是有这样的一种情况,如果一个门店操作员登陆了,但是直接更改url,那么他就能够访问到另一个门店的记录。
例如:A门店操作员登陆之后,访问工单http://qfstore.car-me.com/store/workorder/detail.do?workorderId=275 ,然后我更改了workorderId=200。在数据库中workorderId=200的记录是存在的,但是这个工单记录不属于改门店,这样这个操作员就查看到了不属于他能看到的记录。对于这种情况我们需要在后台中进行处理
WorkorderVo workorderVo = workorderService.findWorkorderInfo(Long.parseLong(workorderId), accountInfo.getSellerId(), store.getId());
if (workorderVo == null)
{
return ControllerHelper.showMsgPage(map, MsgConstant.FAIL_FIND_RECORD, WORKORDER_LIST);
}
查询单条记录的时候带上sellerId和storeId,如果找不到该记录跳转到消息提示页面
FAQ
1、使用内嵌的tomcat启动的时候提示找不到java.lang.FunctionalInterface这个类
原因:低版本的jdk中没有java.lang.FunctionalInterface这个类
解决方法:在电脑安装高版本(我电脑上jdk版本是1.7_079)的jdk,然后
- eclipse项目jdk指定到高版本
- 更改电脑环境变量的JAVA_HOME
2、关于类循环调用的问题
看到这个堆栈溢出我开始是不知所措的。进一步了解之后明白了,一般出现StackOverflowError的异常是由于调用栈的长度超过了JVM的预设值(参考:http://blog.csdn.net/zhuyijian135757/article/details/38025339 ),一般这种问题都是由于递归调用造成的。进一步观察,通过堆栈发现异常主要出现在CustomerCardItem.java和CustomerCard.java的toString方法,这个地方出现了循环调用的问题。
CustomerCard.java
@Override
public String toString() {
return "CustomerCard [id=" + id + ", customerId=" + customerId + ", cardId=" +
cardId + ", balance=" + balance + ", expireAt=" + expireAt + ", bindCarNo="
+ bindCarNo + ", bindTelephoneNumber=" + bindTelephoneNumber
+ ", cardActiveStatus=" + cardActiveStatus + ", isDelete=" + isDelete +
", createdBy=" + createdBy + ", createdAt=" + createdAt + ", changedBy="
+ changedBy + ", changedAt=" + changedAt + ", version=" + version +
", cardItems=" + cardItems + ", cardTypeInfo=" + cardTypeInfo + "]";
}
CustomerCardItem.java
@Override
public String toString() {
return "CustomerCardItem [id=" + id + ", customerCardId=" + customerCardId +
", itemId=" + itemId + ", itemQuantity=" + itemQuantity + ", discountRatio="
+ discountRatio + ", isDelete=" + isDelete + ", createdBy=" + createdBy +
", createdAt=" + createdAt + ", changedBy=" + changedBy + ", changedAt="
+ changedAt + ", version=" + version + ", customerCard=" +
customerCard + ", item=" + item + "]";}
看出问题了吗?机智的我已经发现了(柯南附体),CustomerCard调用了CardItems(CardItems是CustomerCardItem的集合)的toString方法,在CustomerCardItem的toString方法又调用了CustomerCardItem的toStrng方法
看了图应该秒懂了吧,两个类循环调用直到堆栈的内存耗尽
3、@ServletComponentScan注解的使用
在springBoot中庸@WebFilter注册filter的时候,用内嵌tomcat启动的时候会发现无法进入注册的filter中,这是因为@webfilter并没有注册,@Webfilter没有扫描到。这个时候需要在启动类中加入@ServletComponentScan这个注解。但是如果项目部署在外部的tomcat的时候,这个注解可以不用加,因为外部tomcat会独立扫描