汽服系統(tǒng)主要包括了門店系統(tǒng)、集團(tuán)系統(tǒng)急前、運(yùn)營(yíng)系統(tǒng)、員工Pad這幾個(gè)部分瀑构。
交互流程
- 公共倉(cāng)庫(kù)提供代碼的CRUD操作裆针。包括了常用工具類(util),模型對(duì)象(domain)检碗,服務(wù)類(service)据块。如果出現(xiàn)業(yè)務(wù)異常,應(yīng)該在service拋出serviceException
- 運(yùn)營(yíng)后臺(tái)主要是給運(yùn)營(yíng)人員使用的
- 門店系統(tǒng)跟集團(tuán)系統(tǒng)現(xiàn)在都在同一個(gè)工程中折剃,后面有可能會(huì)拆分另假。門店系統(tǒng)提供員工pad端的api
- 汽服系統(tǒng)后面考慮做一個(gè)用戶微信,微信后臺(tái)目前待定
- carme-qifu-operation域名:qfadm.car-me.com
carme-qifu-admin域名:qfstore.car-me.com
carme-qifu-group域名:qfgroup.car-me.com - 運(yùn)營(yíng)系統(tǒng)用的是之前運(yùn)營(yíng)平臺(tái)的那一套
集團(tuán)系統(tǒng)和門店系統(tǒng)需要重新出頁(yè)面怕犁,做版式
FAQ
-
service為什么不采取接口的形式边篮,而是直接實(shí)現(xiàn)類
用接口實(shí)現(xiàn)類的方式的好處主要是可以進(jìn)行多實(shí)現(xiàn)己莺,但是目前系統(tǒng)service跟dao只會(huì)有一個(gè)實(shí)現(xiàn)類,所以沒(méi)有使用接口的方式戈轿,這樣相對(duì)來(lái)說(shuō)簡(jiǎn)單凌受,而且減少定義無(wú)用的接口類。如果要對(duì)外暴露接口思杯,可以在carme-qifu-common上面加一層胜蛉,然后用dubbo的方式對(duì)外提供接口。
系統(tǒng)邊界
- 對(duì)外暴露的接口需要做接口的參數(shù)校驗(yàn)
技術(shù)框架
- 框架:Spring boot
- orm : spring data jpa+jdbc template
- 模板引擎:velocity
- GitHub地址:http://hjm017.github.io/SpringWheel (覺(jué)得不錯(cuò)可以給個(gè)star啊 -)
百度腦圖地址:http://naotu.baidu.com/file/a49ffc31ca9666ddb4dd99de7c061395?token=601c70341282f465
數(shù)據(jù)庫(kù)設(shè)計(jì)
1色乾、表需要帶一個(gè)前綴誊册。例如:內(nèi)容管理(c_)、資源管理(r_)暖璧、設(shè)備管理(f_)案怯、站點(diǎn)管理(w_)
2、相同功能的字段需要放在一起澎办。例如 order_state,order_price有關(guān)訂單的需要放在一起
3嘲碱、每張表中需要有is_delete、created_at局蚀、created_by麦锯、changed_at、changed_by五個(gè)字段
4至会、有展示列表的數(shù)據(jù)需要在表中做冗余字段离咐。例如:seller_name
5谱俭、表邏輯外鍵需要帶前綴奉件。例如:p_seller_id
6、有關(guān)定時(shí)任務(wù)的字段需要加前綴昆著。例如:cron_state
7县貌、所有的邏輯外鍵需要加上索引
8、字段盡量不要設(shè)置為可空
9凑懂、表注釋要完整煤痕,涉及字典的字段需要在注釋上標(biāo)明dict_type_value。例如:退款狀態(tài)(car_tkzt )
10接谨、索引命名規(guī)范為:idx_(字段名) ** 例如:idx_model_id
11摆碉、 建表的時(shí)候id都用bigint(20). 字符串都用varchar(255). 大文本用text**
數(shù)據(jù)庫(kù)腦圖地址:http://naotu.baidu.com/file/fd2d8817fe0c44a68ad24883e84bcf30?token=7e3f0cccf1a9be7f
編碼規(guī)范
1、代碼中不允許出現(xiàn)magic number脓豪,應(yīng)該定義為常量
錯(cuò)誤:test.setStatus(1);
正確:test.setStatus(CodeConstant.SUCCESS)
2巷帝、用AccountUtil來(lái)獲取當(dāng)前登錄用戶信息
Account account = AccountUtil.getAccountInfo();
String id = account.getId();
String operateName = account.getName();
3、涉及到業(yè)務(wù)異常的應(yīng)該在service中拋出serviceException扫夜,涉及權(quán)限等其他的異常請(qǐng)?jiān)贑ontroller拋出
Service:
if(car==null){
throw new ServiceException("該類型的卡沒(méi)有數(shù)據(jù),請(qǐng)檢查傳入的id是否正確");
}
Controller:
if(CodeConstant.isAdmin==user.getIsAdmin){
throw new ManagerException("該用戶沒(méi)有該權(quán)限");
}
如果在底層拋出業(yè)務(wù)異常楞泼,在controller請(qǐng)catch掉驰徊,如果不catch ,默認(rèn)認(rèn)為為系統(tǒng)異常
controller
type1:正常請(qǐng)求
try{
// do something
}catch(ServiceException e){
return ControllerHelper.showMsg(map.e.getMessage,'/list.do');
}
type2:ajax請(qǐng)求
try{
// do something
}catch(ServiceException e){
AjaxResult res = new AjaxResult();
res.setCode(1211);
res.setMessage(e.getMessage);
return res;
}
有些人直接把底層的異常直接catch掉堕阔,然后拋出ServiceException
錯(cuò)誤:
try{
//do something
}catch(Exception e){
e.printStackTrace();
throw new ServiceException("xxx失敗");
}
此種寫法錯(cuò)在兩點(diǎn):
- e.printStackTrace 打印堆棧到控制臺(tái)棍厂,大家都知道,服務(wù)器的IO資源是非常寶貴的超陆,如果有很多system.out.print()造成了服務(wù)器的io資源被占用牺弹,嚴(yán)重的會(huì)極大影響服務(wù)器性能。
- 既然已經(jīng)catch了異常时呀,又拋出一個(gè)新的異常(把大異常轉(zhuǎn)成了小異常)例驹,但是原始的異常沒(méi)有記錄,而且catch的是Exception的異常退唠,容易把其他的異常也catch掉鹃锈,這樣造成很難排查到線上bug造成的原因。
正確的做法
try{
}catch(NullPointException e){
logger.error(e.getMessage(),e);
}
注意:因?yàn)槲以谌值漠惓L幚碇杏涗浟巳罩厩圃ぃ訰untimeException可以不用catch
4屎债、在前端調(diào)用webUtil來(lái)獲取css,js垢油,host盆驹,img的域名
js域名 :${webUtil.getJsDomain('/js/operate.js')}
img域名:${webUtil.getImgDomain('/img/dd.png')}
css域名:${webUtil.getCssDomain('/css/ddd.css')}
5、前端js綁定事件時(shí)需要注意不要直接在class的選擇器上綁定(前端可能會(huì)修改樣式)滩愁,常用做法
1) 給元素添加一個(gè)屬性nc_type(推薦)
html
<a href="javascript:void(0);" nc_type="search" />
js
$("a[nc_type="search"]").on("click",function(){
//do something
});
好處:前端更改樣式不會(huì)影響js事件
2)需要綁定的樣式加一個(gè)js前綴
html
<a href="javascript:void(0);" class="js-test btn"/>
js
$("a.js-test").on("click",function(){
//do something
});
此種做法不太推薦躯喇,但是如果你想在class上面綁定事件,要加上js前綴硝枉,這樣前端改樣式的時(shí)候就會(huì)注意不會(huì)用有js前綴的樣式來(lái)寫css
6廉丽、之前由于因?yàn)閒orm和dto沒(méi)有在之前定下規(guī)則,導(dǎo)致了很多的問(wèn)題妻味,故指定命名規(guī)范和使用規(guī)則正压。form跟dto的命名規(guī)范和使用規(guī)則如下:
form分為查詢form跟新增編輯form。
1) 查詢form(模塊名+SearchForm,如:UserSearchForm)可復(fù)用责球。復(fù)用時(shí)具體的變量命名規(guī)則:
1焦履、日期的開(kāi)始結(jié)束時(shí)間(字段名+Begin/End),例如:createdAtBegin雏逾、createdEnd
2嘉裤、關(guān)鍵字(模糊查詢時(shí)使用,查詢字段+And+查詢字段+Key)栖博,例如:關(guān)鍵字(用戶名/手機(jī)號(hào)碼)->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 不可復(fù)用屑宠。命名規(guī)則:
1、新增(模塊+動(dòng)作+AddForm)笛匙,例如:UserAddForm侨把,UserStep1AddForm(新增商家第一步)犀变、UserStep2AddForm(新增商家第二步)...
2、編輯(模塊+動(dòng)作+EditForm)秋柄,例如:UserEditForm获枝、UserStep1EditForm、UserStep2EditForm...
新增和修改form需要加上注解骇笔,進(jìn)行服務(wù)端校驗(yàn)
dto的命名規(guī)范同form
7省店、現(xiàn)在項(xiàng)目有三個(gè)日志文件,分別是root笨触、request懦傍、sql
可以根據(jù)自己的需要添加logger。例如:跟金額結(jié)算有關(guān)的可以增加一個(gè)bill的logger
8芦劣、關(guān)于常量使用
目前系統(tǒng)的常量分為三種粗俱,分別是公共常量(common constant)、業(yè)務(wù)常量(business constant)虚吟、系統(tǒng)常量(system constant)
- 公共常量寸认。主要是一些常用不帶有業(yè)務(wù)意義的常量
//公共常量
private static final int FLAG_TRUE=1; //狀態(tài)標(biāo)識(shí) true
private static final int FLAG_FALSE=0; //狀態(tài)標(biāo)識(shí) false
private static final int PAGE_SIZE=5; //默認(rèn)分頁(yè)大小
公共常量放在carme-qifu-common中的Constant類中,正常情況下串慰,類似于是否刪除偏塞,是否付款這樣的簡(jiǎn)單的標(biāo)識(shí)位無(wú)需定義其他的常量,直接使用公共常量中的FLAG_TRUE和FLAG_FALSE
- 業(yè)務(wù)常量邦鲫。主要跟業(yè)務(wù)狀態(tài)有關(guān)的一些常量
//業(yè)務(wù)常量
public static final String EVALUATION_TAG_STATUS_NORMAL = "0"; // 評(píng)價(jià)標(biāo)簽表---狀態(tài):0灸叼,正常
public static final String EVALUATION_TAG_STATUS_SHIELD = "1"; // 評(píng)價(jià)標(biāo)簽表---狀態(tài):1,屏蔽
- 系統(tǒng)常量庆捺。主要跟系統(tǒng)的運(yùn)行有關(guān)的一些常量
/** * 編碼 */
public static final String I18N_ENCODIND = "UTF-8";
/** * 信息提示頁(yè)面 */
public static final String I18N_MSG = "classpath:/i18n/messages";
public static final String I18N_VALIDATOR = "classpath:/i18n/validator";
業(yè)務(wù)常量跟公共常量放置在carme-qifu-common的BusinessConstant.java和Constant.java類中古今,系統(tǒng)常量放在對(duì)應(yīng)項(xiàng)目的constant包中
9、攔截規(guī)則
因?yàn)閟pring boot對(duì)(urlPattern=/)的請(qǐng)求進(jìn)行攔截疼燥,所以大家的所有請(qǐng)求都會(huì)進(jìn)入spring boot中沧卢,所以不會(huì)出現(xiàn)自己定義servlet處理請(qǐng)求的情況。為了之后能夠做動(dòng)態(tài)請(qǐng)求做處理醉者,故增加后綴方便日后拓展。
web請(qǐng)求攔截規(guī)則:
- 正常請(qǐng)求披诗。后綴加上.do
- ajax異步請(qǐng)求撬即。如果需要返回json數(shù)據(jù)加上.json后綴,如果需要返回.xml數(shù)據(jù)呈队,加上.xml后綴
api請(qǐng)求攔截規(guī)則: - 如果路徑規(guī)則符合(/rest/**)剥槐,為暴露出去的api
- 如果需要返回json數(shù)據(jù)加上.json后綴,如果需要返回.xml數(shù)據(jù)宪摧,加上.xml后綴
10粒竖、在velocity中使用常量類
在velocity中需要進(jìn)行一些邏輯判斷颅崩,這個(gè)時(shí)候需要根據(jù)一些業(yè)務(wù)狀態(tài)進(jìn)行邏輯判斷
bad practice:
#if($item.modelType==1)
do something
#end
best practice:
#if($item.modelType==$fieldTool.EVALUATION_TAG_STATUS_NORMAL)
do something
#end
這個(gè)時(shí)候如對(duì)應(yīng)的業(yè)務(wù)狀態(tài)更改了(雖然比較少),這個(gè)時(shí)候就還需要更改velocity中的業(yè)務(wù)狀態(tài)判斷蕊苗,而且=1沿后,=2這種可讀性太差(今天寫了明天就忘了1,2代表什么了,還要去查對(duì)應(yīng)的字典朽砰,還是常量比較好理解)
11尖滚、關(guān)于頁(yè)面復(fù)用
有的時(shí)候如果新增頁(yè)面和修改頁(yè)面基本一樣,這個(gè)時(shí)候我們就想了瞧柔,為什么既然頁(yè)面相同為什么我們不能復(fù)用了漆弄?其實(shí)這個(gè)時(shí)候新增頁(yè)面和修改頁(yè)面是一種很好的方案:
- 如果頁(yè)面更改了,只要更改一個(gè)頁(yè)面即可
- 提高了開(kāi)發(fā)效率造锅,減少了維護(hù)的工作量
但是撼唾,關(guān)于復(fù)用的東西一定要謹(jǐn)慎,標(biāo)準(zhǔn)一定要制定好哥蔚,不然隨時(shí)可能因?yàn)橐粋€(gè)公共方法的修改影響多個(gè)頁(yè)面券坞。
適用范圍:兩個(gè)頁(yè)面相差不大
操作標(biāo)識(shí):opType
類型常量:Constant.UPDATE,Constant.ADD
頁(yè)面命名: xxx_input.vm 例如:card_input.vm
vm
<!--操作標(biāo)識(shí)-->
<input type="hidden" name="opType" id="opType" value="${fieldTool.UPDATE}"/>
js
var opType = $("#opType").val();
if(opType=="update"){
//回填值
...
//js效果顯示
...
}
后臺(tái)
/** * 保存新增的企業(yè)信息 * * /
@RequestMapping("/input.do")
public String input(ModelMap modelMap, String opType) {
if(Constant.UPDATE.equals(opType)){
//do something
}
...
}
注意:如果新增后修改頁(yè)面后面改變很大肺素,兩個(gè)頁(yè)面完全不同恨锚,不建議復(fù)用,具體要自己判斷下
12倍靡、關(guān)于createdBy猴伶、createdAt、changedBy塌西,changedAt
WHY:有人可能會(huì)疑惑每張表添加這幾個(gè)字段是干啥用的他挎,舉個(gè)例子,如果我想知道某個(gè)訂單是什么時(shí)候創(chuàng)建的捡需,誰(shuí)創(chuàng)建的(createdAt办桨,createdBy)。這個(gè)訂單是誰(shuí)審核的站辉,最后審核時(shí)間是什么時(shí)候(changedBy呢撞,changedAt 當(dāng)然這兩個(gè)字段不然說(shuō)明操作類型,如果需要知道具體的操作類型饰剥,需要添加相應(yīng)的操作記錄表)殊霞。從上面的例子可以看出,這四個(gè)字段在我們做數(shù)據(jù)統(tǒng)計(jì)和操作歷史的時(shí)候是非常有用的汰蓉。
HOW: 如果添加這幾個(gè)字段绷蹲,但是在具體操作的時(shí)候沒(méi)有更新值的話就會(huì)有問(wèn)題了。推薦做法
service
public void update(Card card ,String operaterName){
Card flushData= cardDao.findOne(card.getId());
//將頁(yè)面?zhèn)魅氲膶?duì)象屬性拷貝到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參數(shù)添加一個(gè)operatorName,你不傳的話就沒(méi)辦法調(diào)用service祝钢,媽媽再也不要擔(dān)心我漏更新時(shí)間了比规,哈哈!拦英!
代碼生成工具
GitHub:https://github.com/hjm017/code-generator
generator.xml文件說(shuō)明:
<!--生成的類的包路徑-->
<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>
<!--是否生成數(shù)據(jù)源中所有的表-->
<entry key="allSwitch">false</entry>
<!--數(shù)據(jù)庫(kù)用戶名-->
<entry key="jdbc_username">qifuowner</entry>
<!--數(shù)據(jù)庫(kù)密碼-->
<entry key="jdbc_password">qifuowner_cte</entry>
<!--數(shù)據(jù)庫(kù)URL-->
<entry key="jdbc_url">jdbc:mysql://192.168.51.195:3306/qifudbd01?useUnicode=true&characterEncoding=UTF-8</entry>
使用方法:
Step1:配置數(shù)據(jù)源信息
<entry key="jdbc_username">用戶名</entry>
<entry key="jdbc_password">密碼</entry>
<entry key="jdbc_url">jdbc:mysql://數(shù)據(jù)庫(kù)地址:3306/數(shù)據(jù)庫(kù)名?useUnicode=true&characterEncoding=UTF-8</entry>
Step2:配置需要生成的表
<!--需要去除的表的前綴-->
<entry key="prefix">c,i,p,tb</entry>
<!--生成的表-->
<entry key="tables">c_card</entry>
Step3:運(yùn)行GeneratorServer.java
注意:如果在eclipse中GeneratorServer.java不被識(shí)別為java類蜒什,請(qǐng)將GeneratorServer.java類放入包路徑中
Druid監(jiān)控平臺(tái)
URL:http://qfadm.car-me.com/operation/druid/login.html
用戶名: admin
密碼: admin
代碼碎片
1、生成自增長(zhǎng)的序號(hào)
初始化
--創(chuàng)建序列表
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)
) ;
--創(chuàng)建_nextval函數(shù)
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;
//
說(shuō)明:生成的序列可以自定義長(zhǎng)度(設(shè)置len)龄章,如果超過(guò)設(shè)置的長(zhǎng)度將會(huì)重置為1
例如:生成3位自增長(zhǎng)序號(hào)
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");//序列
...
}
注意:獲取序列的操作需要放在事務(wù)中(在方法上添加@Transactional注解)吃谣,出現(xiàn)異常時(shí),回滾事務(wù)做裙,保證事務(wù)的ACID特性(如果在service中自己拋出業(yè)務(wù)異常岗憋,事務(wù)會(huì)自動(dòng)回滾)。
2锚贱、分頁(yè)(carme-qifu-operation)(例子:card/list.vm)
1)form格式
<form action="${webUtil.getHostDomain('/card/list.do')}" method="post" id="listForm">
<!--此處添加隱藏參數(shù)-->
<input type="hidden"/>
...
#foreach($item in $page.pageItems)
<tr>
...
</tr>
#end
<!--此處添加分頁(yè)bar-->
#pager("listForm" "pageNo" $page)
</from>
2)marco 說(shuō)明
#pager("listForm" "pageNo" $page)
1)listForm : form表單id
2)pageNo:分頁(yè)隱藏域的name
3)$page :后臺(tái)設(shè)置的page參數(shù)
3)controller
@RequestMapping("/list.do")
public String index(ModelMap map, CardSearchForm cardSearchForm) {
//分頁(yè)參數(shù)處理:1仔戈、初始化的時(shí)候pageNo=0->pageNo=1,傳入pageNo的時(shí)候,pageNo=pageNo-1 2拧廊、設(shè)置分頁(yè)的pageSize
ControllerHelper.doDealParam(cardSearchForm);
Page<Card> page = cardService.findAll(cardSearchForm);
//將jpa返回的分頁(yè)集合包裝為view層需要的分頁(yè)對(duì)象
PageInfo<Card> pageInfo = ControllerHelper.getPageInfo(page);
map.addAttribute("page", pageInfo);
return "card/list";
}
分頁(yè)的大小默認(rèn)為5监徘,可以更改OperationConstant.PAGE_SIZE的值,更改分頁(yè)默認(rèn)大小
3吧碾、后臺(tái)參數(shù)檢驗(yàn)
在需要進(jìn)行的方法上添加@ParamCheck注解凰盔,使用的是hibernate validator
方式一: 參數(shù)中只有對(duì)象
@ResponseBody
@ParamCheck
@RequestMapping("/register.do")
public String register(UserRegisterForm userRegisterForm) {
return "";
}
方式二:參數(shù)中只有基本類型
@ResponseBody
@ParamCheck
@RequestMapping("/register.do")
public String register(@NotEmpty(message = "設(shè)備類型不能為空")
@RequestParam(value = "facilityId", required = false) String facilityId,
@Digits(fraction = 6, integer = 3)
@RequestParam(value = "longitude", required = false) Double longitude
) {
return "";
}
方式三:參數(shù)中既有基本類型,又有對(duì)象
@ResponseBody
@ParamCheck
@RequestMapping("/register.do")
public String register(UserRegisterForm userRegisterForm, @NotEmpty(message = "設(shè)備類型不能為空")
@RequestParam(value = "facilityId", required = false) String facilityId,
@Digits(fraction = 6, integer = 3)
@RequestParam(value = "longitude", required = false) Double longitude
) {
return "";
}
4倦春、使用Beancopier框架來(lái)做實(shí)體映射
在項(xiàng)目中户敬,經(jīng)常需要做beancopier的操作,經(jīng)過(guò)測(cè)試睁本,使用jdk的set/get效率最高尿庐,但是對(duì)于字段較多的情況下不太使用,增加了代碼量呢堰,經(jīng)過(guò)比較抄瑟,cglib的beancopier效率最高并且容易擴(kuò)展
我對(duì)這些工具做了一個(gè)對(duì)比:Copy一個(gè)簡(jiǎn)單Bean 1,000,000次,計(jì)算總耗時(shí)枉疼。比較結(jié)果如下:
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中寫了一個(gè)基于cglib的BeanCopier類皮假。在類中使用
WarehouseAddForm addForm = new WarehouseAddForm();
addForm.setStoreNo("00023011034");
addForm.setName("卡咪汽服");
addForm.setAddress("越達(dá)巷");
addForm.setCreatedBy("hxq");
addForm.setPurpose("隨便");
SimpleBeanCopier copier = new SimpleBeanCopier(WarehouseAddForm.class, Warehouse.class);
Warehouse warehouse = (Warehouse) copier.copy(addForm);
由于cglib是使用修改字節(jié)碼的方式實(shí)現(xiàn)beancopier的,這種方式如果程序出錯(cuò)了將會(huì)很難定位異常位置
所以往衷,推薦使用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使用
由于在項(xiàng)目中使用了ajax請(qǐng)求,因此導(dǎo)致在ajax請(qǐng)求回調(diào)需要處理因?yàn)閏ookie失效等情況席舍,故按照AOP的思想,基于jquery編寫了http.js
define(["jquery"], function($) {
return {
//提交ajax請(qǐng)求
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請(qǐng)求失敗",function(index){
layer.close(index);
});
}
});
}
}
});
需要使用http.js的時(shí)候需要在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哮笆、在項(xiàng)目中進(jìn)行多表查詢
方式一:使用jdbc template来颤。優(yōu)點(diǎn):方便快捷汰扭,直接能夠看到sql
vo實(shí)體類
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());
}
}
注意:查詢出來(lái)的字段別名需要與實(shí)體類的字段一致,否則無(wú)法映射
方式二:使用spring data jpa 優(yōu)點(diǎn):編碼簡(jiǎn)單 缺點(diǎn):效率低
多表連接查詢稍微麻煩一些福铅,下面演示一下常見(jiàn)的1:M萝毛,順帶演示一下1:1
使用Criteria查詢實(shí)現(xiàn)1對(duì)多的查詢
Step1:首先要添加一個(gè)實(shí)體對(duì)象DepModel,并設(shè)置好UserModel和它的1對(duì)多關(guān)系滑黔,如下:
@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及其關(guān)系后笆包,就可以在構(gòu)建Specification的時(shí)候使用了,示例如下:
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應(yīng)用到CriteriaQuery去,因?yàn)檫€可以給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();
}};
接下來(lái)看看使用Criteria查詢實(shí)現(xiàn)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實(shí)現(xiàn)中巴粪,把SetJoin的那句換成如下的語(yǔ)句:
Join<UserModel,DepModel> depJoin =
root.join(root.getModel().getSingularAttribute("dep",DepModel.class),JoinType.LEFT);
//root.join(“dep”,JoinType.LEFT); //這句話和上面一句的功能一樣,更簡(jiǎn)單
7粥谬、api接口規(guī)范
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("這是一個(gè)測(cè)試?yán)?, 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掸茅、消息提示頁(yè)面
如果A打開(kāi)一個(gè)列表頁(yè)面艇纺,不操作轧拄。這個(gè)時(shí)候筋夏,B也打開(kāi)了這個(gè)列表頁(yè)面透葛,并且刪除了這個(gè)列表的某條數(shù)據(jù)秽梅,這個(gè)時(shí)候如果A如果不刷新烁巫,直接點(diǎn)擊列表頁(yè)面的查看按鈕赖舟,那么就會(huì)出現(xiàn)異常巢寡,頁(yè)面也會(huì)顯示為500喉脖。
比較好的做法是在后臺(tái)對(duì)這種情況進(jìn)行判斷
@RequestMapping("/detail.do")
public String index(ModelMap map, @NotEmpty(message = "billId不能為空") @RequestParam("billId") String billId) {
WorkorderBill workorderBill = workorderBillService.findOne(Long.parseLong(billId));
//如果記錄不存在,跳轉(zhuǎn)提示頁(yè)面
if (workorderBill==null){
return ControllerHelper.showMsgPage(map, MsgConstant.FAIL_FIND_RECORD,WORKORDER_LIST);
}
return "finance/settlement/input";
}
10、關(guān)于在汽服項(xiàng)目中使用jdbcTemplate
由于在汽服項(xiàng)目中有些復(fù)雜查詢抑月,這個(gè)時(shí)候就需要用到j(luò)dbcTemplate了树叽,但是如果dao中有了jdbcTemplate,那么我們就無(wú)法使用spring data jpa簡(jiǎn)單好用的功能了谦絮。對(duì)于這個(gè)問(wèn)題有以下幾種解決方式:
1题诵、使用jdbcTemplate的dao全部加個(gè)Extend后綴。例如:UserCardExtendDao
這樣层皱,我們還可以愉快的使用spring data jpa提供的便利(推薦)
2性锭、直接在service中把jdbcTemplate注入到屬性中,在service中使用叫胖。
以上兩種方式都可以讓我們同時(shí)使用jdbctemplate和spring data jpa草冈,但是按照java的分層來(lái)說(shuō),service應(yīng)該寫的是業(yè)務(wù)邏輯,不應(yīng)該寫數(shù)據(jù)庫(kù)操作怎棱,所以第一種方式是最優(yōu)選擇
11哩俭、關(guān)于mysql查詢中通配符的問(wèn)題
在mysql中‘,%’等被識(shí)別為通配符(有寫過(guò)模糊查詢的同學(xué)應(yīng)該知道吧)拳恋。假如凡资,現(xiàn)在我要查詢一個(gè)“_”字符串,最終發(fā)出的sql會(huì)是這樣的谬运。
select * from o_workorder where car_no like '%_%'
這樣查詢出來(lái)的結(jié)果會(huì)很多(因?yàn)椤癬”被識(shí)別為通配符)隙赁。這樣的結(jié)果不是我想要的。其實(shí)應(yīng)該這樣
select * from o_workorder where car_no like '%\\_%'
利用轉(zhuǎn)義符對(duì)“_”進(jìn)行轉(zhuǎn)義梆暖,這樣就能得到我們最終想要的結(jié)果了伞访,在java中,我將轉(zhuǎn)義的步驟封裝成了一個(gè)工具類SQLUtil.java
if (StringUtil.isNotEmpty(workorderSearchForm.getCarNo())) {
predicate.getExpressions() .add( cb.like(
r.<String> get("carNo"), "%" + SQLUtil.processWildCard(workorderSearchForm.getCarNo().toString()) + "%"));
}
12式廷、重復(fù)提交問(wèn)題
在頁(yè)面中咐扭,如果多次點(diǎn)擊保存,會(huì)發(fā)出多個(gè)請(qǐng)求滑废,這非常容易造成無(wú)意中多創(chuàng)建了記錄蝗肪。為了解決這個(gè)問(wèn)題在js增加以下代碼
//提交
$("#js-save-btn").on("click",function(){
layer.msg('請(qǐng)稍后...', {icon: 16,shade: [0.6, '#393D49']});
$(this).on("click",submitFail);
$(this).parentsUntil("form").parent().submit();
});
function submitFail(){
layer.alert("網(wǎng)絡(luò)繁忙,請(qǐng)刷新頁(yè)面后嘗試");
}
效果:
13蠕趁、業(yè)務(wù)異常那些事
首先薛闪,要理解我們的系統(tǒng)是異步的,這說(shuō)明了俺陋,服務(wù)器允許兩個(gè)或多個(gè)人同時(shí)登陸系統(tǒng)操作豁延。就拿工單的開(kāi)單步驟來(lái)說(shuō),假設(shè)一個(gè)店員在開(kāi)單的同時(shí)腊状,一個(gè)門店管理員刪除了門店的某個(gè)物料的記錄或者將該物料的庫(kù)存更改為0(這種情況是存在的)诱咏,那么這個(gè)時(shí)候該店員已經(jīng)填寫好了工單,像服務(wù)器發(fā)起了請(qǐng)求缴挖。如果我們不對(duì)庫(kù)存不足和物料記錄不存在的情況進(jìn)行判斷的話就會(huì)造成空指針了袋狞。
在店員填好工單,提交請(qǐng)求之前映屋,門店管理員將工單中對(duì)應(yīng)的物料刪除或者將庫(kù)存改為了0苟鸯,這樣后端如果不判斷,直接根據(jù)前臺(tái)傳的物料id去查詢對(duì)應(yīng)物料的數(shù)據(jù)就會(huì)報(bào)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);
}
/**
* 獲取工單物料數(shù)據(jù)
* @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);
//查詢物料數(shù)據(jù)
Material material = materialService.findOne(workorderMaterial.getId());
if (material == null) {
throw new ServiceException("ID=" + workorderMaterial.getId() + "的物料被刪除,請(qǐng)聯(lián)系管理員");
}
materials.add(workorderMaterial);
}
}
return materials;
}
從代碼可以看出棚点,對(duì)于物料是否存在和庫(kù)存不足的情況我們是需要拋出業(yè)務(wù)異常的早处,業(yè)務(wù)異常的處理在controller中(提示用戶庫(kù)存不足或者物料不存在)
14、關(guān)于XSS攻擊
網(wǎng)站經(jīng)常會(huì)遇到CSXF瘫析、XSS砌梆、SQL注入攻擊(鏈接:http://itindex.net/blog/2013/10/25/1382688300000.html)默责。 針對(duì)其中的XSS攻擊,我在項(xiàng)目中加了一個(gè)XssFilter么库,用于處理輸出的腳本傻丝,對(duì)于輸入腳本進(jìn)行轉(zhuǎn)義處理甘有。
@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诉儒、關(guān)于分頁(yè)操作后回到操作頁(yè)面的解決方案
在系統(tǒng)中存在這么一個(gè)問(wèn)題,我在第二頁(yè)操作了一條記錄亏掀,但是等我操作完之后跳轉(zhuǎn)頁(yè)面跳到了第一頁(yè)忱反,這樣造成的用戶體驗(yàn)很不好。解決方案:
1滤愕、在分頁(yè)列表的form的action加上一個(gè)listMode=cache的參數(shù)
<form method="post" name="form_workorder" id="form_workorder" action="$!{webUtil.getHostDomain('/workorder/list.do')}?listMode=cache">
2温算、在返回按鈕添加一個(gè)listMode=restore參數(shù)
<a class="btn btn-primary" href="${webUtil.getHostDomain('/workorder/list.do?listMode=restore')}">返回</a>
當(dāng)傳了listMode=cache的參數(shù)時(shí),后臺(tái)會(huì)對(duì)這次請(qǐng)求的參數(shù)進(jìn)行緩存间影,你需要跳回當(dāng)時(shí)的頁(yè)面只要在list.do后面加上一個(gè)listMode=restore(恢復(fù)緩存頁(yè)面)即可
16注竿、系統(tǒng)的中的數(shù)據(jù)權(quán)限
用戶登陸系統(tǒng)之后按理說(shuō)是只能查看屬于自己的數(shù)據(jù)的,但是有這樣的一種情況魂贬,如果一個(gè)門店操作員登陸了巩割,但是直接更改url,那么他就能夠訪問(wèn)到另一個(gè)門店的記錄付燥。
例如:A門店操作員登陸之后宣谈,訪問(wèn)工單http://qfstore.car-me.com/store/workorder/detail.do?workorderId=275 ,然后我更改了workorderId=200键科。在數(shù)據(jù)庫(kù)中workorderId=200的記錄是存在的闻丑,但是這個(gè)工單記錄不屬于改門店,這樣這個(gè)操作員就查看到了不屬于他能看到的記錄勋颖。對(duì)于這種情況我們需要在后臺(tái)中進(jìn)行處理
WorkorderVo workorderVo = workorderService.findWorkorderInfo(Long.parseLong(workorderId), accountInfo.getSellerId(), store.getId());
if (workorderVo == null)
{
return ControllerHelper.showMsgPage(map, MsgConstant.FAIL_FIND_RECORD, WORKORDER_LIST);
}
查詢單條記錄的時(shí)候帶上sellerId和storeId嗦嗡,如果找不到該記錄跳轉(zhuǎn)到消息提示頁(yè)面
FAQ
1、使用內(nèi)嵌的tomcat啟動(dòng)的時(shí)候提示找不到j(luò)ava.lang.FunctionalInterface這個(gè)類
原因:低版本的jdk中沒(méi)有java.lang.FunctionalInterface這個(gè)類
解決方法:在電腦安裝高版本(我電腦上jdk版本是1.7_079)的jdk饭玲,然后
- eclipse項(xiàng)目jdk指定到高版本
- 更改電腦環(huán)境變量的JAVA_HOME
2侥祭、關(guān)于類循環(huán)調(diào)用的問(wèn)題
看到這個(gè)堆棧溢出我開(kāi)始是不知所措的。進(jìn)一步了解之后明白了咱枉,一般出現(xiàn)StackOverflowError的異常是由于調(diào)用棧的長(zhǎng)度超過(guò)了JVM的預(yù)設(shè)值(參考:http://blog.csdn.net/zhuyijian135757/article/details/38025339 )卑硫,一般這種問(wèn)題都是由于遞歸調(diào)用造成的。進(jìn)一步觀察蚕断,通過(guò)堆棧發(fā)現(xiàn)異常主要出現(xiàn)在CustomerCardItem.java和CustomerCard.java的toString方法欢伏,這個(gè)地方出現(xiàn)了循環(huán)調(diào)用的問(wèn)題。
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 + "]";}
看出問(wèn)題了嗎亿乳?機(jī)智的我已經(jīng)發(fā)現(xiàn)了(柯南附體)硝拧,CustomerCard調(diào)用了CardItems(CardItems是CustomerCardItem的集合)的toString方法径筏,在CustomerCardItem的toString方法又調(diào)用了CustomerCardItem的toStrng方法
看了圖應(yīng)該秒懂了吧,兩個(gè)類循環(huán)調(diào)用直到堆棧的內(nèi)存耗盡
3障陶、@ServletComponentScan注解的使用
在springBoot中庸@WebFilter注冊(cè)filter的時(shí)候滋恬,用內(nèi)嵌tomcat啟動(dòng)的時(shí)候會(huì)發(fā)現(xiàn)無(wú)法進(jìn)入注冊(cè)的filter中,這是因?yàn)锧webfilter并沒(méi)有注冊(cè)抱究,@Webfilter沒(méi)有掃描到恢氯。這個(gè)時(shí)候需要在啟動(dòng)類中加入@ServletComponentScan這個(gè)注解。但是如果項(xiàng)目部署在外部的tomcat的時(shí)候鼓寺,這個(gè)注解可以不用加勋拟,因?yàn)橥獠縯omcat會(huì)獨(dú)立掃描