公司用了這個叫做jeecg的快速開發框架,我不知道有多少公司在用這個框架,園子里有的可以吱一聲。個人覺得這框架唯一優勢就是可以讓不會ssh的人也能進行開發,只要你會J2SE,有web后台發開經驗即可。
框架的優劣這里不做說明,但是官方文檔真的寫的很粗糙,很多時候需要自己額外添加一些功能的時候會有一點無處下手的感覺。接觸了一段時間后,也踩了不少的坑,現在記錄一下,以饗讀者。
jeecg版本:3.7.1
Tips
前端
- 權限管理設置中,按鈕權限需要對相應的按鈕設置
OperateCode
字段,然后在后台菜單管理-頁面權限控制中配置相應的規則,接着去角色管理中分配權限,注意checkbox選中狀態下為顯示該按鈕(此處與文檔中描述的相反!)。 dgToolBar
中對應的funname
中的方法(例如add、update),都在curdtools_zh-cn.js
文件中,寫新的方法時可以去那里面復制。- 針對於
<t:datagrid>
中的顯示,<t:dgCol/>
中如果有表示狀態的字段,數據庫可能存int,而顯示需要中文,可以使用dictionary
屬性,如果對應的中文直接添加在系統后台的數據字典中(系統管理-數據字典),則直接dictionary=[字典名稱]
;如果數據庫中存在代碼表,則dictionary=[表名,編碼,顯示文本]
- 針對於表單中的顯示,狀態選擇可以使用下拉控件
<t:dictSelect>
,其中typeGroupCode
屬性填寫數據字典名稱。 - 文件上傳推薦使用
<t:webUploader>
控件,具體代碼見Snippets。t:webUploader是h5的,兼容性較好。 - 在
<t:formvalid>
表單中,需要手動提交表單,需要一個id為btn_sub
的按鈕。 - 表單頁面中,設置input設置
disabled="disabled"
后,該元素的內容不會提交表單,如果需要提交,但不可編輯,請使用readonly="readonly"
- 使用
<t:dgOpenOpt>
時注意,默認的openMode
為OpenWin
,需要為其設置width和height,否則報錯;OpenTab
時則不需要設置。 - jeecg所有封裝的控件的
urlfont
屬性為圖標設置,可以更換Font Awesome中的所有圖標。
后台
- SpringMVC路由默認采用
param
形式,即xxController.do?getList
曾經一度想改成xx/getList
,嘗試多次后失敗,事實證明代碼關聯太強,不推薦修改。 - 數據表設計中如果包含添加人,添加時間的可以直接使用jeecg指定字段
(create_time,create_by,create_name,update_time,update_by,update_name等)
,jeecg自帶aop綁定,更新時會 自動賦值。具體查看DataBaseConstant.java和HiberAspect.java
。 - GUI代碼生成器中,若pk為uuid,主鍵生成策略選擇uuid,若為自增的id,則選擇identity。
- GUI代碼生成器中,form風格個人推薦選擇div風格,使用表格時,Validform會有坑。
- GUI代碼生成器中,推薦使用一對一關系來建表,需要一對多等別的關系時,可以添加注解來實現(
@OneToMany,@ManyToOne
) - 路由的全局攔截器文件為
AuthInterceptor.java和SignInterceptor.java
,在里面添加系統的攔截規則。 - 后台可以配置過濾器來解決全局跨域問題。代碼見Snippets。
- 清理jeecg自帶版本號和logo信息,注意他的國際化內容,文字信息均存在數據表
t_s_muti_lang
中,無法直接在源代碼中搜索到。 - 定時任務有bug,暫未解決,存在實例化多次的情況。
- 事務處理,添加注解
@Transactional(rollbackFor=Exception.class)
。 t:datagrid
查詢問題,對時間查詢,如果時間格式是年月日+時分秒,則無法查詢,需要修改代碼文件,將原來的區間格式由xxxbegin1、xxxend2
改成xxxbegin1、xxxend2
。t:datagrid
查詢問題,如果使用多對多的關系進行查詢,直接對字段添加query=true
,如果關系多於二級則無法查詢。例如放款表中有一個借款表外鍵,借款表有一個用戶表的外鍵。在顯示的時候,顯示字段的field=borrow.user.name
,那么,就算設置了query=true
,查詢也是無效的。
Snippets
1.跨域過濾器
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Appkey");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
<!-- web.xml中配置-->
<filter>
<filter-name>cors</filter-name>
<filter-class>cn.crenative.afloan.core.controller.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.文件上傳
@RequestMapping(params = "doUpload", method = RequestMethod.POST)
@ResponseBody
public AjaxJson doUpload(MultipartHttpServletRequest request,
String path) {
logger.info("后台上傳文件");
AjaxJson j = new AjaxJson();
String fileName = null;
String ctxPath = request.getSession().getServletContext().getRealPath(path);
File file = new File(ctxPath);
if (!file.exists()) {
file.mkdir();// 創建文件根目錄
}
Map<String, MultipartFile> fileMap = request.getFileMap();
for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
MultipartFile mf = entity.getValue();// 獲取上傳文件對象
fileName = mf.getOriginalFilename();// 獲取文件名
String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
String newFileName = df.format(new Date()) + "_" + new Random().nextInt(1000) + "." + fileExt;
String savePath = file.getPath() + "/" + newFileName;// 上傳后的文件絕對路徑
System.out.println("上傳后路徑:" + savePath);
File savefile = new File(savePath);
try {
// String imageUrl = "http://" + request.getServerName() + ":" + request.getLocalPort() + request.getContextPath() + path + "/" + newFileName;
String imageUrl = request.getContextPath() + path + "/" + newFileName;
logger.info("輸出路徑:" + imageUrl);
mf.transferTo(savefile);
j.setObj(imageUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
j.setMsg("上傳成功");
return j;
}
<t:webUploader url="upload.do?doUpload&path=[相對路徑]" name="[數據庫字段]" extensions="" auto="true" pathValues="${后端set的attribute名稱}"/>
<!-- eg:-->
<t:webUploader url="upload.do?doUpload&path=/upload/afloan/users/attachment" name="credentialPhoto" extensions="" auto="true" pathValues="${attachmentPage.credentialPhoto}"/>
3.dictSelect
<t:dictSelect field="credentialType" type="select" defaultVal="${attachmentPage.credentialType}" typeGroupCode="attachment" hasLabel="false"/>
4.全局表單元素的隱藏
$(":input").attr("disabled", "true");
$('select').attr('disabled', true);
5.添加一個提示的窗口
layer.open({
title: title, //彈窗title
content: content, //彈窗內容
icon: 7,
yes: function (index) {
//回調函數
},
btn: ['確定', '取消'],
btn2: function (index) {
layer.close(index);
}
});
6.選擇datagrid中選中的行。
var rowsData = $('#' + id).datagrid('getSelections');
//獲取具體的字段名,推薦第二種取值形式,如果使用一對多,name字段名可能長這樣(user.id),使用第一種方式會報錯
console.log(rowData[0].fieldname)
console.log(rowData[0]['fieldname')
7. 選擇datagrid中選中的行
// 在方法中添加index,控件會自動添加選擇的行號
<t:dgFunOpt title="刪除" funname="deleteOne()" urlclass="ace_button" urlfont="fa-trash-o"/>
function deleteOne(index) {
console.log(index);
var row = $("#usersList").datagrid('getData').rows[index];
}
8.添加一個新的標簽頁
//function addOneTab(subtitle, url, icon),該方法定義在curdtools_zh-cn.js中
function openAuditTab(id, mobile) {
addOneTab("用戶" + mobile + "的檔案", "userInfo?userInfo&mode=claim&userId=" + id);
}
9.popup,彈框選擇相應的記錄,並回調到父頁面。
//設置表單內容
function setUser(obj, rowTag, selected) {
if (selected == '' || selected == null) {
alert("請選擇");
return false;
} else {
var str = "";
var name = "";
var idNo = "";
$.each(selected, function (i, n) {
str += n.mobile;
name += n.realName;
idNo += n.idcardNo;
});
$("input[id='" + rowTag + ".mobile']").val(str);
$("input[id='" + rowTag + ".realName']").val(name);
$("input[id='" + rowTag + ".idcardNo']").val(idNo);
return true;
}
}
/**
* 彈出popup窗口獲取
* @param obj
* @param rowTag 行標記
* @param code 動態報表配置ID
*/
function selectUser(obj, rowTag) {
if (rowTag == null) {
alert("popup參數配置不全");
return;
}
console.log($('#mobile').val());
var inputClickUrl = basePath + "/users?userSelect";
if (typeof (windowapi) == 'undefined') { //頁面彈出popup
$.dialog({
content: "url:" + inputClickUrl,
zIndex: getzIndex(),
lock: true,
title: "選擇客戶",
width: 1000,
height: 300,
cache: false,
ok: function () {
iframe = this.iframe.contentWindow;
var selected = iframe.getSelectRows(); //重要,此處獲取行數據
return setUserMobile(obj, rowTag, selected);
},
cancelVal: '關閉',
cancel: true //為true等價於function(){}
});
} else { //popup內彈出popup
$.dialog({
content: "url:" + inputClickUrl,
zIndex: getzIndex(),
lock: true,
title: "選擇客戶",
width: 1000,
height: 300,
parent: windowapi, //設置彈出popup的openner
cache: false,
ok: function () {
iframe = this.iframe.contentWindow;
var selected = iframe.getSelectRows(); //重要,此處獲取行數據
return setUserMobile(obj, rowTag, selected);
},
cancelVal: '關閉',
cancel: true //為true等價於function(){}
});
}
}
<!-- 方法綁定 -->
<input class="inputxt" onclick="selectUser(this,'user');" placeholder="點擊選擇客戶" id="user.mobile"
name="appUser.mobile" value="${borrowInfoPage.appUser.mobile}"/>
10.一對多關系的使用
具體例子:借款訂單(afl_borrow_info)中存在用戶表(afl_user)外鍵,通過user_id關聯。
@Entity
@Table(name = "afl_borrow_info", schema = "")
@DynamicUpdate(true)
@DynamicInsert(true)
@SuppressWarnings("serial")
public class BorrowInfoEntity implements java.io.Serializable {
private UsersEntity appUser;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
public UsersEntity getAppUser() {
return appUser;
}
public void setAppUser(UsersEntity appUser) {
this.appUser = appUser;
}
}
關聯之后,所有的查詢(service層)和頁面渲染(jsp),均不再使用user_id
而是使用appUser.id
,別的字段同理。