Jeecg踩坑不完全指南
公司用了这个叫做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);![Alt text](./popup.gif)
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
,别的字段同理。