JeeSite 4.0 规划(一)

JeeSiteJava EEJava企业架构快速开发平台

时隔3年,偶得OSC举办的2016最优秀的开源项目之一,让Gem兴奋了一下,也再次燃起了对JeeSite升级的强烈欲望。感谢OSC提供码云这么好的平台,感谢红薯大哥的亲笔祝福!

很抱歉,由于近年来工作原因比较忙,JeeSite得到的是极少维护,在这样的情况下,依然得到了大家的深深青睐,这让Gem感到非常羞愧,在此忠诚感谢您们的鼓励和支持,否则也没有JeeSite的今天。

JeeSite自开源以来被用到了企业、政府、医疗、金融、互联网等各个领域中,JeeSite的设计思想和开发模式也深入支持者的内心,也帮助了不少刚毕业的大学生去快速学习和实践。这次升级的规划Gem也结合了以往的经验和总结各方面的应用案例,对架构做一次全部重构,也纳入一些新的思想。从开发者模式、底层架构、逻辑处理到用户界面都有很大的进步,最重要的是安全稳定,降低学习成本,提高开发效率。

由于时代的变革,技术的演变,这次规划架构的变化很大,所以将不考虑之前版本的兼容。既然有了规划,就的去实现,在不影响正常工作的情况下,所有计划均安排在本人业余时间,这样也就意味着,我会牺牲掉业余时间去学习新的知识。话说回来,规划结果也是我非常期待的,可以把我近年来的一个个的想法,一个个的目标,在这里实现。由于时间上不能保证,也很抱歉不能给大家一个准确的公布时间,如果大家有什么建议,可在此留言。

关于开源许可,目前孪生产品或直接拿来改名销售的太多,有可理解的也不可理解,在这里不说太多了,也不发表什么看法,总之新版Licence应该会有下变更,从之前的开源协议ALv2变更为GPLv3或AGPL,核心代码暂不开放,不影响二次开发。

关于版本号,感谢江南白衣大哥的springside4对我的启发和帮助很大,所以4作为jeesite的第二个里程碑版本号。

JeeSite 4.0 规划(二)

JeeSite 4.0 简化MyBatis持久层开发

引言

在做这一方面研究的时候,本人参考了很多资料和框架,如MyBatis-Mapper、MyBatis-Plus等等,这些都做的很不错,本来想集成一个,尝试了下还是有多处地方不顺手,不易扩展,不能解决我的本意,既能使用方便又不能失灵活,所以决定自己试着完成一套Dao层架构,精简开发。

在此之前我先考虑API的写法,通俗易懂,大众思维。持久层实体类采用 @Table注解配置,自动生成增删改通用SQL,不需要在mapper.xml里写重复又费时的SQL,遇见复杂的情况下支持扩展。而报表统计分析的情况下又能支持mybatis原生写法,在写sql的时候,又能调用之前实体配置的一些参数。从而减少开发和后期维护成本。

众多的持久层框架@Column注解定义都是分布到get或属性上,或者干脆直接使用属性作为字段名,这在JeeSite是不推荐的,JeeSite的实体不仅仅是物理实体,它是与Model实体结合的一个产物,视乎记得JFinal作者也说过这一点,也是推荐的一个做法。总合考虑,将@Column所有定义到类头,而不是分布到各个属性或方法上,主要是有以下三点原因:

  1. 可一览熟知该实体类对应的物理表结构是什么样,引领开发者思维从物理表结构到对象的映射转换,都是基于物理表结构的,@Column中的name指定物理字段名,而不是指定类上的属性名,也是这个原因;
  2. 自动化生成的SQL和查询条件,是有序的,可方便核查定义,优化查询;
  3. 方便@JoinTable关联表和其它扩展信息的设置,如果分布到类的属性上就需要来回滚动屏幕查找,不利于管理字段列。

下面举例说明,最后附上API:

以定义员工实体举例,配置如下:(注意代码上的注释)

@Table(name="${_prefix}sys_employee", alias="a", columns={@Column(includeEntity=BaseEntity.class),        // 支持Include@Column(includeEntity=DataEntity.class),        // 支持Include,如:自动导入status、create_by、create_date等字段@Column(name="emp_code", label="员工编码", isPK=true),  // 支持设置主键PK字段,调用get方法时自动加入主键唯一条件@Column(name="emp_name", label="名称", queryType=QueryType.LIKE),  // 支持设置查询字段类型,如LIKE自动在查询值前后加 % 符号。@Column(name="emp_name_en", label="英文名", queryType=QueryType.LIKE),@Column(name="emp_no", label="工号"),   // 字段名到Java属性名的转换,采用驼峰命名法规则自动进行转换// 驼峰命名法转换不了的,支持设置特殊对象属性,如mapper.xml的sql中 a.office_code AS "office.officeCode" 的写法@Column(name="office_code", attrName="office.officeCode", label="机构编码"),  @Column(name="office_name", attrName="office.officeName", label="机构名称", queryType=QueryType.LIKE),@Column(name="company_code", attrName="company.companyCode", label="公司编码"),@Column(name="company_name", attrName="company.companyName", label="公司名称", queryType=QueryType.LIKE),@Column(name="sex", label="性别"),@Column(name="birthday", label="生日"),@Column(name="photo", label="员工照片", isQuery=false), // 支持设置非查询字段,添加查询条件时忽略该字段@Column(name="email", label="电子邮件"),@Column(name="mobile", label="手机号码"),@Column(name="phone", label="办公电话"),@Column(name="fax", label="传真号码"),@Column(name="qq", label="QQ号"),@Column(name="weixin", label="微信号"),@Column(name="stations", label="岗位"),},// 支持联合查询,如左右连接查询,支持设置查询自定义关联表的返回字段列joinTable={@JoinTable(type=Type.LEFT_JOIN, entity=Office.class, alias="o", on="o.office_code = a.office_name",columns={@Column(includeEntity=Office.class)}),@JoinTable(type=Type.LEFT_JOIN, entity=Company.class, alias="c", on="c.company_code = a.company_name",columns={@Column(includeEntity=Company.class)}),},// 支持扩展Column、Form、Where等,主要用于该注解实现不了的复杂情况,扩展SQL写法,这里设置的是sqlMap的keyextWhereKeys="dsfOffice, dsfCompany"// 自动设置默认排序orderBy="a.update_date DESC"
)
public class Employee extends DataEntity<Employee> {private static final long serialVersionUID = 1L;private String empCode;      // 员工编码private String empName;      // 名称private String empNameEn;  // 英文名private String empNo;     // 工号private Office office;     // 机构编码private Company company; // 公司编码private String sex;          // 性别private Date birthday;     // 生日private String photo;      // 员工照片private String email;        // 电子邮件private String mobile;       // 手机号码private String phone;        // 办公电话private String fax;          // 传真号码private String qq;           // QQ号private String weixin;        // 微信号private String stations;  // 岗位/// 省略  get  set 方法}

请仔细看上面的代码和注释,其以上之外,还支持是否为插入字段,是否为更新字段等等。

再举一个例子,扩展上面介绍的Employee表,与用户表联合查询单独定义实体,用户员工实体:

@Table(name="${_prefix}sys_user", alias="a", columns={@Column(includeEntity=User.class),}, joinTable={@JoinTable(type=Type.JOIN, entity=Employee.class, alias="e",on="e.emp_code = a.ref_code AND a.user_type=#{USER_TYPE_EMPLOYEE}",columns={@Column(includeEntity=Employee.class)}),@JoinTable(type=Type.LEFT_JOIN, entity=Office.class, alias="o", on="o.office_code = a.office_name", attrName="employee.office",columns={@Column(includeEntity=Office.class)}),@JoinTable(type=Type.LEFT_JOIN, entity=Company.class, alias="c", on="c.company_code = a.company_name", attrName="employee.company",columns={@Column(includeEntity=Company.class)}),}, extWhereKeys="dsfOffice, dsfCompany", orderBy="a.update_date DESC"
)
public class EmpUser extends User {private static final long serialVersionUID = 1L;public EmpUser() {this(null);}public EmpUser(String id){super(id);}@Validpublic Employee getEmployee(){Employee employee = (Employee)super.getRefObj();if (employee == null){employee = new Employee();}return employee;}public void setEmployee(Employee employee){super.setRefObj(employee);}
}

注解配置完成了,下面看看如何使用

如何使用

贴了这么多配置代码,下面介绍下用法。

你的Dao只需要继承CrudDao即可享受便捷体验,是不是特Easy,如下:

/*** 员工管理DAO接口* @author ThinkGem*/
@MyBatisDao(entity = Employee.class)
public interface EmployeeDao extends CrudDao<Employee> {}

EmployeeDao继承CrudDao后,里面的方法你都可以调用,如下方法:

/*** DAO实现增删改接口* @author ThinkGem*/
public interface CrudDao<T> extends QueryDao<T> {/*** 插入数据*/public int insert(T entity);/*** 批量插入数据*/public int insertBatch(List<T> entityList);/*** 更新数据  By PK*/public int update(T entity);/*** 更新数据  By Entity*/public int updateByEntity(T entity, T whereEntity);/*** 更新状态数据  By PK*/public int updateStatus(T entity);/*** 更新状态数据  By Entity*/public int updateStatusByEntity(T entity, T whereEntity);/*** 删除数据  By PK(如果有status字段,则为逻辑删除,更新status字段为1,否则物理删除)*/public int delete(T whereEntity);/*** 删除数据  By Entity(如果有status字段,则为逻辑删除,更新status字段为1,否则物理删除)*/public int deleteByEntity(T whereEntity);/*** 获取单条数据* @param entity* @return entity*/public T get(T entity);/*** 获取单条数据* @param entity* @return entity*/public T getByEntity(T entity);/*** 查询数据列表,如果需要分页,请设置分页对象,如:entity.setPage(new Page<T>(pageNo, pageSize));* @param entity* @return*/public List<T> findList(T entity);/*** 查询数据总数* @param entity* @return*/public long findCount(T entity);}

调用举例:

// 查询一条,更新
Employee employee = new Employee();
employee.setEmpCode('E001');
employee = employeeDao.get(employee);
employee.setMobile('18666666666');
employeeDao.update(employee);// 列表查询、统计
Employee employee = new Employee();
employee.setEmpName('小王');
employee.setPage(new Page(1, 20)); // 分页查询
List<Employee> list = employeeDao.findList(employee);
Long count = employeeDao.findCount(employee);// 批量插入
employeeDao.insertBatch(list);

是不是有种事半功倍的感觉,小小的配置,可以实现几乎可以完成原来需要写代码的80%时间。

也许你会觉着配置复杂,难以理解,只要你用上了相信你就会爱不释手。

还有一个惊喜,这些配置也可以通过代码生成工具快速生成,喜欢不喜欢。

嗯!基本增删改查,批量操作,按实体属性查询,按实体属性更新,以及统计都有了 ↓↓↓ 可是 ↓↓↓

可是

这么多还是还不够,比如,我们想实现,日期范围查询怎么办?某个实体属性,实现双重查询(如那么既能eq又能like)怎么办?想实现or、is null,括号查询怎么办?这些都么关系,已经替你考虑了,如下:


// 日期范围查询,gte,lte public Date getCreateDate_gte(){return sqlMap.getWhere().getValue("create_date", QueryType.GTE);
}public void setCreateDate_gte(Date createDate){createDate = DateUtils.getOfDayFirst(createDate); // 将日期的时间改为0点0分0秒sqlMap.getWhere().and("create_date", QueryType.GTE, createDate);
}public Date getCreateDate_lte(){return sqlMap.getWhere().getValue("create_date", QueryType.LTE);
}public void setCreateDate_lte(Date createDate){createDate = DateUtils.getOfDayLast(createDate); // 将日期的时间改为23点59分59秒sqlMap.getWhere().and("create_date", QueryType.LTE, createDate);
}// 双重字段查询,支持eq,支持like public String getTableName() {return StringUtils.lowerCase(tableName);
}public void setTableName(String tableName) {this.tableName = tableName;
}public String getTableName_like() {return sqlMap.getWhere().getValue("table_name", QueryType.LIKE);
}public void setTableName_like(String tableName) {sqlMap.getWhere().and("table_name", QueryType.LIKE, tableName);
}// 实现 or、is null,括号 public String getParentTableName_isNull() {return this.getParentTableName();
}public void setParentTableName_isNull(String parentTableName) {if (StringUtils.isBlank(parentTableName)){sqlMap.getWhere().andBracket("parent_table_name", QueryType.IS_NULL, null, 2).or("parent_table_name", QueryType.EQ_FORCE, "", 3).endBracket();this.setParentTableName(null);}else{this.setParentTableName(parentTableName);}
}

还有一种情况,如所有的配置都配置好了,我只需要在sql返回值里加一个简单的统计数,多返回一列,你可以这样写:

// 实体类定义
@Table(name="${_prefix}gen_table", alias="a", columns={// @Column 。。。此处省略 。。。},// 扩展Column里指定一个Key名字,类里并定义一个需要返回的属性和get setextColumnKeys="extColumn"
)
public class GenTable extends DataEntity<GenTable> {private Long childNum;                // 子表个数public Long getChildNum() {return childNum;}public void setChildNum(Long childNum) {this.childNum = childNum;}
}// Service 里,通过sqlMap设置你刚定义的Key即可,如下
public Page<GenTable> findPage(Page<GenTable> page, GenTable genTable) {// 添加扩展列,查询子表个数String extColumn = "(SELECT count(1) FROM "+MapperHelper.getTableName(genTable)+" WHERE parent_table_name=a.table_name) AS \"childNum\"";genTable.getSqlMap().add("extColumn", extColumn);return super.findPage(page, genTable);
}

如果以上仍得不到你的满足,怎么办,那你可以写Mapper.xml了,比如EmployeeDao.xml一些通用的字段、条件,你就不需要在xml再写一遍了,你只需要补充SQL即可(相同id,如id="findList"则会自动覆盖默认设置):

<select id="findList" resultType="Employee">SELECT ${sqlMap.column.toSql('a')}FROM ${_prefix}sys_employee a<where>${sqlMap.where.toSql('a')}</where>ORDER BY ${sqlMap.order.toSql('a')}
</select>

这样的Dao,你满意吗?编码原来如此简单,提高了效率,又不损失灵活,是不是很有趣呢。

附:API

@Table

/*** 指定实体的物理表属性 * @author ThinkGem*/
public @interface Table {/*** 物理表名*/String name() default "";/*** 当前表别名*/String alias() default "a";/*** 表列定义*/Column[] columns();/*** 查询,关联表*/JoinTable[] joinTable() default {};/*** 指定排序*/String orderBy() default "";/*** 表说明*/String comment() default "";/*** 扩展ColumnSQL,在这里指定sqlMap的key。<br>* 例如:\@Table(extColumnKeys="dataScopeColumn");<br>* Service里设置:sqlMap.put("extColumn", "column_name AS \"columnName\"");<br>* 在执行查询的时候,该语句放到自动会加到Where最后并执行。<br>* <b>注意:</b>如果设置,必须后台代码中设置,否则可能造成sql注入漏洞<br>*/String extColumnKeys() default "";/*** 扩展FromSQL,在这里指定sqlMap的key。<br>* 例如:\@Table(extFromKeys="dataScopeFrom");<br>* Service里设置:sqlMap.put("dataScopeFrom", "JOIN table_name t on t.pk=a.pk");<br>* 在执行查询的时候,该语句放到自动会加到Where最后并执行。<br>* <b>注意:</b>如果设置,必须后台代码中设置,否则可能造成sql注入漏洞<br>*/String extFromKeys() default "";/*** 扩展WhereSQL,在这里指定sqlMap的key。<br>* 例如:\@Table(extWhereKeys="dataScopeWhere");<br>* Service里设置:sqlMap.put("dataScopeWhere", "AND column_name='value'");<br>* 在执行查询的时候,该语句放到自动会加到Where最后并执行。<br>* <b>注意:</b>如果设置,必须后台代码中设置,否则可能造成sql注入漏洞<br>*/String extWhereKeys() default "";}

@Column

/*** 定义物理表列属性(不继承父类注解)* @author ThinkGem*/
public @interface Column {/*** 字段名(例如:config_key)*/String name() default "";/*** 属性名,若不指定,则根据name()字段名进行驼峰命名法转换(例如:config_key 转换为   configKey)*/String attrName() default "";/*** 属性名,定义的类型*/Class<?> type() default Class.class;/*** 标签名*/String label() default "";/*** 字段备注*/String comment() default "";/*** 是否主键(update、delete时的条件)*/boolean isPK() default false;/*** 是否插入字段*/boolean isInsert() default true;/*** 是否更新字段*/boolean isUpdate() default true;/*** 是否是查询字段*/boolean isQuery() default true;/*** 查询类型*/QueryType queryType() default QueryType.EQ;/*** 包含嵌入一个实体*/Class<?> includeEntity() default Class.class;
}

@JoinTable

/*** 指定实体的物理表的关联表属性 * @author ThinkGem*/
public @interface JoinTable {/*** 连接类型*/Type type() default Type.JOIN;public enum Type{JOIN("JOIN"), // INNER JOINLEFT_JOIN("LEFT JOIN"),RIGHT_JOIN("RIGHT JOIN");private final String value;Type(String value) { this.value = value; }public String value() { return this.value; }}/*** 连接的表,指定实体Class*/Class<?> entity();/*** 当前表别名*/String alias();/*** 连接表条件*/String on();/*** 对应主表中对应的属性名,若不指定,则根据entity()进行首字母小写得到属性名(例如:Config 转换为   config)*/String attrName() default "";/*** 连接表,返回的列,若不指定,则读取entity()的所有列。*/Column[] columns() default {};}

QueryType

/*** 查询类型* @author ThinkGem*/
public enum QueryType{EQ("="),NE("!="),GT(">"),GTE(">="),LT("<"),LTE("<="),IN("IN"),NOT_IN("NOT IN"),LIKE("LIKE", "%", "%"),LEFT_LIKE("LIKE", "%", ""),RIGHT_LIKE("LIKE", "", "%"),IS_NULL("IS NULL"),IS_NOT_NULL("IS NOT NULL"),// 强制条件,不管值是不是空字符串都加载这个查询条件EQ_FORCE("=", true),NE_FORCE("!=", true),;
}

© 著作权归作者所有

JeeSite 4.0 简化业务逻辑层开发

引言

对于业务逻辑层的开发重复代码很多,尽管有代码生成器,但从代码量总的来说还是比较多,所以就有了以下抽象类及工具,对一些常用操作进行封装。

对通用新增、删除、编辑、查询,代码操作进行封装简化。你只需要写你的业务逻辑代码就可以了。

对特有树状结构特有字段如(所有父级编码、所有排序号编码、是否是叶子节点、当前节点层次)进行更新,比如,通过所有父级编码可快速查询到所有子级的数据;通过所有排序号,可快速对整个树结构进行排序;通过是否叶子节点快速得知是否有下级;根据当前层次快速知道当前节点在树中的级别。

对通用数据权限进行简化封装,将颗粒度降到人员身上,支持人员与数据,角色与数据权限定制。数据权限不仅仅支持公司、部门、角色,还可以通过配置支持你的业务字段数据信息过滤,如订单的区域、内容管理的栏目、故障单类型等等。

对事务处理使用Spring事务@Transactional注解,进行方法级别的事务控制,不用单独处理事务及回滚。如配置传播行为,进行事务继承,子事务,事务回滚行为等,配置隔离级别读取未提交的数据等。

基类及接口的继承关系

TreeService -> CrudService -> QueryService -> BaseService

TreeDao -> CrudDao -> QueryDao -> BaseDao

TreeEntity -> DataEntity -> BaseEntity

QueryService查询抽象基类

/*** 新建实体对象* @return*/
protected T newEntity();/*** 新建实体对象(带一个String构造参数)* @return*/
protected T newEntity(String id);/*** 获取单条数据* @param id 主键* @return*/
public T get(String id);/*** 获取单条数据* @param entity* @return*/
public T get(T entity);/*** 获取单条数据,如果获取不到,则实例化一个空实体* @param id 主键编号* @param isNewRecord 如果是新记录,则验证主键编号是否存在。*                          如果存在抛出ValidationException异常。* @return*/
public T get(String id, boolean isNewRecord);/*** 获取单条数据,如果获取不到,则实例化一个空实体(多个主键情况下调用)* @param pkClass 主键类型数组* @param pkValue 主键数据值数组* @param isNewRecord 如果是新记录,则验证主键编号是否存在。*                       如果存在抛出ValidationException异常。* @return*/
public T get(Class<?>[] pkClass, Object[] pkValue, boolean isNewRecord);/*** 列表查询数据* @param entity* @return*/
public List<T> findList(T entity);/*** 分页查询数据* @param page 分页对象* @param entity* @return*/
public Page<T> findPage(Page<T> page, T entity);/*** 查询列表总数* @param entity* @return*/
public long findCount(T entity);

CrudService增删改抽象基类

该类继承QueryService抽象类

/*** 保存数据(插入或更新)* @param entity*/
@Transactional(readOnly = false)
public void save(T entity)/*** 插入数据* @param entity*/
@Transactional(readOnly = false)
public void insert(T entity);/*** 更新数据* @param entity*/
@Transactional(readOnly = false)
public void update(T entity);/*** 更新状态(级联更新子节点)* @param entity*/
@Transactional(readOnly = false)
public void updateStatus(T entity);/*** 删除数据* @param entity*/
@Transactional(readOnly = false)
public void delete(T entity);

TreeService树结构抽象基类

该类继承CrudService抽象类

/*** 根据父节点获取子节点最后一条记录*/
public T getLastByParentCode(T entity);/*** 保存数据(插入或更新)* 实现自动保存字段:所有父级编号、所有排序号、是否是叶子节点、节点的层次级别等数据* 实现级联更新所有子节点数据:同父级自动保存字段*/
@Transactional(readOnly = false)
public void save(T entity);/*** 更新parent_codes、tree_sorts、tree_level字段值*/
@Transactional(readOnly = false, isolation = Isolation.READ_UNCOMMITTED) // 可读取未提交数据
private void updateParentCodes(T entity);/*** 更新当前节点排序号*/
@Transactional(readOnly = false)
public void updateTreeSort(T entity);/*** 更新tree_leaf字段值*/
@Transactional(readOnly = false, isolation = Isolation.READ_UNCOMMITTED) // 可读取未提交数据
private void updateTreeLeaf(T entity);/*** 修正本表树结构的所有父级编号* 包含:数据修复(parentCodes、treeLeaf、treeLevel)字段*/
@Transactional(readOnly = false) // 可读取未提交数据
public void updateFixParentCodes();/*** 按父级编码修正树结构的所有父级编号* 包含:数据修复(parentCodes、treeLeaf、treeLevel)字段*/
@Transactional(readOnly = false) // 可读取未提交数据
public void updateFixParentCodes(String parentCode);/*** 预留接口事件,更新子节点* @param childEntity 当前操作节点的子节点* @param parentEntity 当前操作节点*/
protected void updateChildNode(T childEntity, T parentEntity);/*** 更新状态(级联更新子节点)* @param entity*/
@Transactional(readOnly = false)
public void updateStatus(T entity);/*** 删除数据(级联删除子节点)* @param entity*/
@Transactional(readOnly = false)
public void delete(T entity);/*** 将不同级别无序的树列表进行排序,前提是sourcelist每一级是有序的<br>* 举例如下:<br>*    List<T> targetList = Lists.newArrayList();<br>*    List<T> sourcelist = service.findList(category);<br>*  service.execTreeSort(targetList, sourcelist, "0");<br>* @param sourceList 源数据列表* @param targetList 目标数据列表* @param parentCode 目标数据列表的顶级节点*/
public void execTreeSort(List<T> sourceList, List<T> targetList, String parentCode);/*** 将简单列表code,parentCode转换为嵌套列表形式code,childList[code,childList[...]]<br>* 举例如下:<br>*    List<T> targetList = Lists.newArrayList();<br>*    List<T> sourcelist = service.findList(category);<br>*  service.execChildListBulid(targetList, sourcelist, "0");<br>* @param sourceList 源数据列表* @param targetList 目标数据列表* @param parentCode 目标数据列表的顶级节点*/
public void execChildListBulid(List<T> sourceList, List<T> targetList, String parentCode);

数据权限调用

本次对数据权限这块进行了全面的升级,让数据权限颗粒度细化到人员身上,支持人员与权限和角色与权限。

调用示例:

/*** 添加数据权限过滤条件*/
public void addDataScopeFilter(T entity){// 举例1:公司数据权限过滤,实体类@Table注解extWhereKeys="dsf"company.getSqlMap().getDataScope().addFilter("dsf", "Company", "a.company_code");// 举例2:部门数据权限过滤,实体类@Table注解extWhereKeys="dsf"office.getSqlMap().getDataScope().addFilter("dsf", "Office", "a.office_code");// 举例3:角色数据权限过滤,实体类@Table注解extWhereKeys="dsf"role.getSqlMap().getDataScope().addFilter("dsf", "Role", "a.role_code");// 举例4:用户、员工数据权限根据部门过滤,实体类@Table注解extWhereKeys="dsfOffice"empUser.getSqlMap().getDataScope().addFilter("dsfOffice","Office", "e.office_code", "a.create_by");// 举例5:用户、员工数据权限根据公司过滤,实体类@Table注解extWhereKeys="dsfCompany"empUser.getSqlMap().getDataScope().addFilter("dsfCompany","Company", "e.company_code", "a.create_by");
}

API接口:

/*** 数据范围过滤* @param sqlMapKey sqlMap的键值,举例:如设置“dsf”数据范围过滤,则:<br>*         exists方式对应:sqlMap.dsf;   join方式对应:sqlMap.dsfForm 和 sqlMap.dsfWhere* @param ctrlTypes 控制类型,多个用“,”隔开,举例:<br>*        控制角色:Role<br>*     控制部门:Office<br>*       控制公司:Company<br>* @param bizCtrlDataFields 业务表对应过滤表别名或需要过滤的表别名加权限字段,多个使用“,”分隔,*         长度必须与tableTypes保持一致,举例:<br>*        业务表控制角色:a.role_code<br>*       业务表控制部门:a.office_code<br>*         业务表控制公司:a.company_code<br>* @param bizCtrlUserField 业务表对应表别名或用户字段。过滤只可以查看本人数据。*       不设置的话,如果没有范围权限,则查不到任何数据,举例:<br>*      业务表a.create_by"* @example*        1)在Service中调用如下两种方式:<br>*           // 添加数据权限过滤条件(控制角色)<br>*            entity.getSqlMap().getDataScope().addFilter("dsf", "Role", "a.role_code");<br>*             // 添加数据权限过滤条件(控制公司和部门)<br>*             entity.getSqlMap().getDataScope().addFilter("dsf",<br>*                 "Office,Company", "a.office_code,a.company_code");<br>*       2)MyBatis \@Table 注解中调用如下:<br>*            采用 EXISTS方式调用   : \@Table(extWhereKeys="dsf")*           采用JOIN方式调用  : \@Table(extFromKeys="dsf",extWhereKeys="dsf")*      3)MyBatis Mapper 中调用如下两种方式:<br>*            采用 EXISTS方式调用   : 将  ${sqlMap.dsf} 放在Where后<br>*           采用JOIN方式调用  : 将  ${sqlMap.dsfFrom} 放在Form后 ,将  ${sqlMap.dsfWhere} 放在Where后<br>*/
public QueryDataScope addFilter(String sqlMapKey, String ctrlTypes, String bizCtrlDataFields, String bizCtrlUserField);

数据库事务

事务管理对于企业应用来说是至关重要的,当出现异常情况,它也可以保证数据的一致性。

JeeSite主要使用Spring的@Transactional注解,也称声明式事务管理,是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需通过基于@Transactional注解的方式,便可以将事务规则应用到业务逻辑中。

注解属性

属性 类型 描述
传播性 枚举型 可选的传播性设置(默认值:Propagation.REQUIRED)
隔离性 枚举型 可选的隔离性级别(默认值:Isolation.ISOLATION_DEFAULT)
只读性 布尔型 读写型事务 vs. 只读型事务
超时 int型 事务超时(以秒为单位)
回滚异常类(rollbackFor) Class 类的实例,必须是Throwable 的子类 异常类,遇到时必须进行回滚。默认情况下checked exceptions不进行回滚,仅unchecked exceptions(即RuntimeException的子类)才进行事务回滚。
不回滚异常类(noRollbackFor) Class 类的实例,必须是Throwable的子类 异常类,遇到时必须不回滚。

事务传播行为

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在Propagation定义中包括了如下几个表示传播行为的常量:

  • Propagation.REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务,这是默认值。
  • Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。Isolation 接口中定义了五个表示隔离级别的常量:

  • Isolation.DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是Isolation.READ_COMMITTED。
  • Isolation.READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
  • Isolation.READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
  • Isolation.REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
  • Isolation.SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

© 著作权归作者所有

JeeSite 4.0 说说前端的那些事

引言

一个不得不说的话题,经过近几年的发展,Web前端开发已经不是一个新有的岗位了,前端技术发展非常迅速,技术更新换代也很快,对于前端工程师来说是一个很大的挑战“挣扎期”。

从统计来看,中级前端的待遇是略高于中级后端的。这对于中小企业、创业公司来说组建一个专有的前端团队还是很一件很不容易的事情,无形中增加了人力成本, 话说招前端工程师简单,但能招聘到合适的前端工程师来说,是一件非常不容易的事情。

为了解决这个事情,中小企业、创业公司都在思考一个问题,做企业应用软件,如果不去组建专门的前端团队,能有一个很好的开发平台,很好的框架,让后端工程师具备一些基本的前端知识,就可以去做出很漂亮的界面就好了。

这个想法很好,但是你会说,可能吗,会一点基础前端就能做好吗?专业的事情还是有专业的人来做,前后端分离是趋势,表现逻辑分离意义很大。没有绝对完美的事情,尽管种种诱惑,唯心自问合适自己吗?我不否认,这还要针对产品、针对项目来选择解决方案。但JeeSite的中心思想是快速快发,快速交付,控制成本,对于一个想快速交付项目来说不见得是一件好事。

重口难调,也许你不赞同这些看法,作者欢迎提问,当然你也可以将JeeSite完全作为服务端代码,快速提供数据接口,自由实现或选型一套前端UI。

好了,既然是奔着快速交付,控制成本来的,我们就依这个角度去思考方案:

  • 第一,技术选型 Spring MVC + Beetl + jQuery + Bootstrap,优势很明显,不多说,下面章节简要说下;
  • 第二,开发一些常用函数库,如:字符串工具类,集合工具类,映射转换工具类,配置工具类,权限用户工具类等;
  • 第三,封装一些常用表单控件参考Spring MVC的form标签,实现更便捷的输入框、下拉框、单选、复选等,自动进行数据绑定;
  • 第四,封装一些常用表单组件,如:多级树结构选择组件,列表选择组件,文件上传组件,验证码生成组件等等;
  • 第五,封装一些常用JS类库,如:动态加载,对话框,消息框,加载框,JS模板,Ajax,格式化,动态Tab等待;
  • 第六,封装一个JS数据表格组件DataGrid,分页,排序,多表头,分组,子表,冻结,小计,合计,编辑行,树表表格等;
  • 第七,丰富例子,如:Box盒子、表单布局、栅格布局、图表等等

模板语言界定符选择

Beetl模板语言类似JS语言和习俗,只需要将Beetl语言放入定界符号里即可,如默认的是<% %>,那JeeSite是怎样选择的呢:

设想定义:<% %>

优点:jsp标准定界符,比较容易被理解,是后端运行的语法

缺点:html后缀的模板,不能和html标签混用,否则IDE会提示语法错误

设想定义:@ 回车符

优点:简单

缺点:多行beetl语法时,比较麻烦,并且会出现多余的很多空格空行

设想定义:[ ]、[@ ]、[@ ]

有点:可以和html混用

缺点:与js的[]冲突,必须使用\转义

**设想定义:[@ @]、[# #]、# #、@ @**

优点:基本没有标示符冲突

缺点:感官、阅读、写法稍微差点

设想定义:<!--# -->、<!--: -->

优点:使用html注释,没有标示符冲突

缺点:使用IDE高亮时,没有写代码的感觉,像是在写注释

最终选择:<% %>

经过上述分析,最终还是回归默认,使用jsp标准定界符<%%>来作为模板语言的定界符,一方同比较容易被理解,明确是后端运行的语法,另一方面冲突少,边界比较好界定

写一个通用布局的页面

<% layout('/layouts/default.html', {title: '菜单管理', libs: ['validate'], bodyClass: ''}){ %>
<div class="main-content"><div class="box box-main"><div class="box-header with-border"><div class="box-title"><i class="fa icon-book-open"></i> 菜单管理</div><div class="box-tools pull-right"><button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button></div></div><div class="box-body"></div><div class="box-footer"></div></div>
</div>
<% } %>

调用默认布局 /layouts/default.html,自动引入页面头部和尾部内容,通过参数设置要加载的css和js类库,参数如下:

title参数: 设置页面的标题名字

libs参数: 设置页面要引入的css和js类库,支持类库如下:

默认引入:layer、select2、WdatePicker

  • zTree:树结构控件
  • tabPage:动态页签插件
  • dataGrid:数据表格组件
  • validate:表单验证组件
  • inputmask:表单格式化工具
  • fileupload:文件上传插件
  • ueditor:富文本编辑器控件

bodyClass参数: 设置body的class属性值

定义常用函数库

常用工具类导入

以下工具类可通过@类型快速调用,如:${@Global.getConfig('key')}

  • Global:全局配置类,全局常量,读取属性文件参数值等
  • EncodeUtils:封装各种格式的编码解码工具类,HEX、Base64、HTML、URL、XSS过滤、SQL过滤等
  • ListUtils:List常用工具类,继承Apache的ListUtils,New工具、快速提取属性值、类型转换等
  • MapUtils:Map常用工具类,继承Apache的MapUtils,New工具、Map与Bean互转等
  • SetUtils:Set常用工具类,继承Apache的SetUtils,New工具等
  • IdGenerate:封装各种生成唯一性ID算法的工具类,生成LongUUID,StringUUID,Code生成等
  • ByteUtils:字节转换工具
  • DateUtils:日期工具类,继承Apache的DateUtils
  • NumberUtils:BigDecimal工具类,继承Apache的NumberUtils类
  • ObjectUtils:对象操作工具类,继承Apache的ObjectUtils类
  • StringUtils:字符串工具类,继承Apache的StringUtils类
  • TimeUtils:时间计算工具类,xx天xx时xx分xx秒,刚刚,xx秒,xx分钟,xx小时前、xx天前
  • WorkDayUtils:工作日计算工具类,计算日期直接的工作日等
  • BeanMapper:简单封装Dozer,对象数据映射
  • JaxbMapper:Jaxb实现XML与Java Object的转换
  • JsonMapper:简单封装Jackson,实现Json与Java Object的转换
  • ClassUtils:Class扫描工具类,根据接口查询类,根据继承查询类等
  • ReflectUtils:反射工具类,方便进行getter/setter方法, 访问私有变量, 调用私有方法
  • ModuleUtils:模块工具类,方便获取系统模块信息
  • UserUtils:用户工具类,方便获取进行用户及相关信息

以下是Beetl函数及扩展函数

  • date:返回一个java.util.Date类型的变量,如 date() 返回一个当前时间(对应java的java.util.Date); ${date( "2011-1-1" , "yyyy-MM-dd" )} 返回指定日期
  • print:打印一个对象 print(user.name);
  • println:打印一个对象以及回车换行符号,回车换号符号使用的是模板本身的,而不是本地系统的.如果仅仅打印一个换行符,则直接调用println() 即可
  • nvl:函数nvl,如果对象为null,则返回第二个参数,否则,返回自己 nvl(user,"不存在")
  • isEmpty:判断变量或者表达式是否为空,变量不存在,变量为null,变量是空字符串,变量是空集合,变量是空数组,此函数都将返回true
  • isNotEmpty:同上,判断对象是否不为空
  • has:变量名为参数,判断是否存在此全局变量,如 has(userList),类似于1.x版本的exist("userList"),但不需要输入引号了
  • assert:如果表达式为false,则抛出异常
  • trim:截取数字或者日期,返回字符,如trim(12.456,2)返回"12.45",trim(date,'yyyyy')返回"2017"
  • trunc:截取数字,保留指定的小数位,如trunc(12.456,2) 输出是12.45.不推荐使用,因为处理float有问题,兼容原因保留了
  • decode:一个简化的if else 结构,如 decode(a,1,"a=1",2,"a=2","不知道了")},如果a是1,这decode输出"a=1",如果a是2,则输出"a==2", 如果是其他值,则输出"不知道了"
  • debug:在控制台输出debug指定的对象以及所在模板文件以及模板中的行数,如debug(1),则输出1 [在3行@/org/beetl/core/lab/hello.txt],也可以输出多个,如debug("hi",a),则输出hi,a=123,[在3行@/org/beetl/core/lab/hello.txt]
  • range:接收三个参数,初始值,结束值,还有步增(可以不需要,则默认为1),返回一个Iterator,常用于循环中,如for(var i in range(1,5)) {print(i)},将依次打印1234.
  • flush:强制io输出。
  • pageCtx:仅仅在web开发中,设置一个变量,然后可以在页面渲染过程中,调用此api获取,如pageCtx("title","用户添加页面"),在其后任何地方,可以pageCtx("title") 获取该变量
  • cookie:返回指定的cookie对象 ,如var userCook = cookie("user"), allCookies = cookie();
  • isBlank:判断对象是否是一个空字符串,${isBlank('str')}
  • isNotBlank:判断对象是否不是一个空字符串,${isBlank('str')}
  • toJson:将对象转Json字符串,${toJson(Object)}
  • fromJson:将Json字符串转换为对象,${fromJson(Object)}
  • hasPermi:判断是否有改权限;单个权限验证:${hasPermi('sys:user:edit')};多个AND关系:${hasPermi('sys:user:view,sys:user:edit', 'and')}; 多个OR关系:${hasPermi('sys:user:view,sys:user:edit', 'or')}
  • cookie:获取cookie值,${cookie(name, isRemove)}

数据类型格式化

日期格式化:

Today is ${date,dateFormat="yyyy-MM-dd"}
Today is ${date,dateFormat}
如果date为日期类型可简写:
${date,“yyyy-MM-dd”}

数值格式化:

Salary is ${salary,numberFormat="##.##"}

基本控件封装(类似Spring MVC表单标签)

form 表单标签

生成一个form标签,支持指定model属性,类似SpringMVC的<form:form modelAttribute=""/>标签的属性,自动给表单内的控件绑定属性值
<#form:form id="inputForm" model="${user}" action="${ctx}/sys/user" method="POST" class="form-horizontal">表单内容
</#form:form>
支持上传文件:
<#form:form id="inputForm" model="${user}" action="${ctx}/sys/user" method="POST" enctype="multipart/form-data" class="form-horizontal">表单内容
</#form:form>

控件属性:

var p = {// 标签参数id: id!,                    // 表单IDmodel: model!,               // 绑定Model对象,例如:${user!}action: action!,          // 表单请求地址method: method!,           // 请求方法,默认 postenctype: enctype!,            // 发送之前进行数据编码,上传文件时指定:multipart/form-data};

input 输入框标签

自动绑定form:form上指定的model下的userName属性,类似SpringMVC的<form:input path=""/>标签的属性
<#form:input path="userName" maxlength="100" class="form-control required "/>
日期格式化:
<#form:input path="userName" maxlength="100" dataFormat="date" class="form-control required "/>
数值格式化:
<#form:input path="userName" maxlength="100" dataFormat="number" class="form-control required "/>
不自动绑定,把path改为name就可以:
<#form:input name="userName" value="${user.userName}" maxlength="100" class="form-control required "/>

控件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值type: type!'text',          // 元素的类型,默认textdataFormat: dataFormat!'',  // 数据格式化,支持如下值:// date: 日期;// datetime: 日期时间;// number: 数值类型,保留2位小数};

select 下拉框标签

根据字典类型设置下拉数据:
<#form:select path="userType" dictType="sys_user_type" class="form-control required " />
增加一个空白选项:
<#form:select path="roleType" dictType="sys_role_type" blankOption="true" class="form-control " />
多选下拉列表:
<#form:select path="roleType" dictType="sys_role_type" multiple="true" class="form-control " />
手动设置下拉框值,类似SrpingMVC的<form:options items="" itemLabel="" itemValue=""/>标签的属性:
<#form:select path="moduleCodes" items="${moduleList}" itemLabel="moduleName" itemValue="moduleCode" class="form-control required" />

控件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值dictType: dictType!,      // 字典类型,从字典里获取,自动设置items、itemLabel、itemValueitems: items![],          // 列表数据,可接受对象集合,如:List<DictData>itemLabel: itemLabel!'',   // 指定列表数据中的什么属性名作为option的标签名itemValue: itemValue!'',  // 指定列表数据中的什么属性名作为option的value值multiple: multiple!'false', // 是否为多选框blankOption: @ObjectUtils.toBoolean(blankOption!false), // 是否默认有个空白选择项目};

radio 输入框标签

类似<#form:select/>标签的使用方法
<#form:radio path="menuType" dictType="sys_menu_type" class="form-control required" />

控件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值dictType: dictType!,      // 字典类型,从字典里获取,自动设置items、itemLabel、itemValueitems: items!([]),            // 列表数据,可接受对象集合,如:List<DictData>itemLabel: itemLabel!'',   // 指定列表数据中的什么属性名作为option的标签名itemValue: itemValue!'',  // 指定列表数据中的什么属性名作为option的value值};

checkbox 复选框标签

类似<#form:select/>标签的使用方法,后台接受moduleCodes为字符串,选择多个自动使用“,”分隔,相比SpringMVC必须是List方便的多
<#form:checkbox path="moduleCodes" items="${moduleList}" itemLabel="moduleName" itemValue="moduleCode" class="form-control required" />
生成一个复选框按钮,后台接受replaceFile为Global.YES或Global.NO值:
<#form:checkbox path="replaceFile" label="是否替换现有文件" class="form-control"/>

控件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值dictType: dictType!'',      // 字典类型,从字典里获取,自动设置items、itemLabel、itemValueitems: items!([]),            // 列表数据,可接受对象集合,如:List<DictData>itemLabel: itemLabel!'',   // 指定列表数据中的什么属性名作为option的标签名itemValue: itemValue!'',  // 指定列表数据中的什么属性名作为option的value值label: label!,               // 只有一个复选按钮的情况下设置};

textarea 文本域标签

<#form:textarea path="remarks" rows="3" maxlength="200" class="form-control"/>

控件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值};

hidden 隐藏域标签

<#form:hidden path="menuCode" />

控件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值type: type!'hidden',        // 元素的类型,默认hidden};

表单组件封装

treeselect 树结构选择

封装layer+zTree实现树结构选择组件,使用场景如:部门选择,行政区划选择,栏目列表选择等

<#form:treeselect id="parent" title="上级菜单"name="parent.id" value="${menu.parent.id!}"labelName="parent.menuName" labelValue="${menu.parent.menuName!}"url="${ctx}/sys/menu/treeData?excludeCode=${menu.menuCode}&sysCode=${menu.sysCode}&isShowCode=2"class="" allowClear="true" canSelectRoot="true" canSelectParent="true"/>

组件属性:

var p = {// 标签参数id: id!,                    // 元素IDname: name!,             // 隐藏域名称value: value!,              // 隐藏域值labelName: labelName!,       // 标签框名称labelValue: labelValue!,    // 标签框值class: class!'',           // 标签框的CSS类名placeholder: placeholder!,  // 标签框的预期值的提示信息dataMsgRequired: thisTag.attrs['data-msg-required'],   // 必填错误提示信息btnClass: btnClass!,     // 标签框后面的按钮CSS类名title: title!'选项',            // 对话框标题boxWidth: boxWidth!300,         // 对话框宽度,默认300像素boxHeight: boxHeight!400,        // 对话框高度,默认400像素url: url!,                   // 树结构,数据源地址 [{id, pid, name}]readonly: @ObjectUtils.toBoolean(readonly!false),     // 是否只读模式allowInput: @ObjectUtils.toBoolean(allowInput!false), // 是否允许label框输入allowClear: @ObjectUtils.toBoolean(allowClear!true),    // 是否允许清空选择内容checkbox: @ObjectUtils.toBoolean(checkbox!false), // 是否显示复选框,是否支持多选,如果设置canSelectParent=true则返回父节点数据expandLevel: @ObjectUtils.toInteger(expandLevel!(-1)),        // 默认展开层次级别(默认:如果有1个根节点,则展开一级节点,否则不展开)canSelectRoot: @ObjectUtils.toBoolean(canSelectRoot!false),      // 可以选择跟节点canSelectParent: @ObjectUtils.toBoolean(canSelectParent!false),  // 可以选择父级节点returnFullName: @ObjectUtils.toBoolean(returnFullName!false),   // 是否返回全路径,包含所有上级信息,以 returnFullNameSplit 参数分隔returnFullNameSplit: returnFullNameSplit!'/',                 // 是否返回全路径,的分隔符,默认“/”};

iconselect 图标选择

<#form:iconselect path="menuIcon" class=""/>

组件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值class: class!'',            // 隐藏域和标签框的CSS类名};

validcode 验证码

<#form:validcode name="validCode" isRequired="true" isRemote="true" />

组件属性:

var p = {id: id!name,               // 验证码输入框IDname: name!,             // 验证码输入框名称(必填)isRequired: @ObjectUtils.toBoolean(isRequired!true),  // 是否必填,默认必填dataMsgRequired: thisTag.attrs['data-msg-required'],   // 必填错误提示信息isRemote: @ObjectUtils.toBoolean(isRemote!true),        // 是否支持实时远程验证dataMsgRemote: thisTag.attrs['data-msg-remote'],     // 必填错误提示信息isLazy: @ObjectUtils.toBoolean(isLazy!false),           // 是否懒加载验证码图片,原noRefresh参数};

listselect 列表选择

<#form:listselect id="userSelect" title="用户"url="${ctx}/sys/user/userSelect?userType=${role.userType}" allowClear="false" checkbox="true" itemCode="userCode" itemName="userName"/>

组件属性:

var p = {// 标签参数id: id!,                    // 元素IDpath: path!,             // 绑定form上model中属性的值name: name!,                // 隐藏域名称value: value!,              // 隐藏域值labelPath: labelPath!,       // 绑定form上model中属性的值labelName: labelName!,      // 标签框名称labelValue: labelValue!,    // 标签框值class: class!'',           // 标签框的CSS类名placeholder: placeholder!,  // 标签框的预期值的提示信息dataMsgRequired: thisTag.attrs['data-msg-required'],   // 必填错误提示信息btnClass: btnClass!,     // 标签框后面的按钮CSS类名title: title!'选项',            // 对话框标题boxWidth: boxWidth!'$(top.window).width() - 100',         // 对话框宽度boxHeight: boxHeight!'$(top.window).height() - 100',  // 对话框高度 url: url!,                 // 树结构,数据源地址 [{id, pid, name}]readonly: @ObjectUtils.toBoolean(readonly!false),     // 是否只读模式allowInput: @ObjectUtils.toBoolean(allowInput!false), // 是否允许label框输入allowClear: @ObjectUtils.toBoolean(allowClear!true),    // 是否允许清空选择内容checkbox: @ObjectUtils.toBoolean(checkbox!false), // 是否显示复选框,是否支持多选,如果设置canSelectParent=true则返回父节点数据itemCode: itemCode!,   // 选择后结果集中的Code属性名,返回到隐藏域的值itemName: itemName!,  // 选择后结果集中的Name属性名,返回到输入框的值};

fileupload 文件上传

1、文件上传:
<#form:fileupload id="upload1" bizKey="${user.id}" bizType="user_upload1"uploadType="all" class="required" readonly="false"/>
后台代码:FileUploadUtils.saveFileUpload(user.getId(), "user_upload1");2、图片上传:
<#form:fileupload id="upload2" bizKey="${user.id}" bizType="user_upload2"uploadType="image" class="required" readonly="false"/>
后台代码:FileUploadUtils.saveFileUpload(user.getId(), "user_upload2");3、返回路径:
<#form:fileupload id="upload3" returnPath="true"filePathInputId="upload3Path" fileNameInputId="upload3Name"uploadType="image" readonly="false" maxUploadNum="3" isMini="false"/>
<#form:input name="upload3Path" class="form-control"/>
<#form:input name="upload3Name" class="form-control"/>

组件属性:

var p = {// 标签参数id: id!,                    // 元素IDbizKey: bizKey!,         // 业务表的主键值(与附件关联的业务数据)bizType: bizType!,          // 业务表的上传类型(全网唯一,推荐格式:实体名_上传类型,例如,文章图片:article_photo)returnPath: @ObjectUtils.toBoolean(returnPath!false),    // 是否是返回文件路径到输入框(默认false),可将路径直接保存到某个字段里filePathInputId: filePathInputId!, // 设置文件URL存放的输入框的ID,当returnPath为true的时候,返回文件URL到这个输入框fileNameInputId: fileNameInputId!,   // 设置文件名称存放的输入框的ID,当returnPath为true的时候,返回文件名称到这个输入框uploadType: uploadType!'',           // 上传文件类型:all、file、image、media,若不设置,则自动根据上传文件后缀获取class: class!'',                    // 标签框的CSS类名,设置 required 加入必填验证readonly: @ObjectUtils.toBoolean(readonly!false),        // 是否只读模式,只读模式下为查看模式,只允许下载allowSuffixes: allowSuffixes!'',  // 允许上传的后缀,前台的限制,不能超越file.*AllowSuffixes的设置,例如:.jpg,.png,maxUploadNum: @ObjectUtils.toInteger(maxUploadNum!300),       // 多文件下允许最多上传几个,默认300个,设置-1代表不限制imageMaxWidth: @ObjectUtils.toInteger(imageMaxWidth!1024),   // 图片压缩,最大宽度(uploadType为image生效),设置-1代表不做任何处理imageMaxHeight: @ObjectUtils.toInteger(imageMaxHeight!768),   // 图片压缩,最大宽度(uploadType为image生效),设置-1代表不做任何处理isLazy: @ObjectUtils.toBoolean(isLazy!false),             // 设置为ture需要点击上传按钮才上传文件,否则选择后就直接上传isMini: @ObjectUtils.toBoolean(isMini!false),             // 是否是精简上传窗口,无边距,无边框preview: preview!'',                                        // 是否显示预览按钮,接受参数:weboffice};

imageclip 图片裁剪

<img id="avatarImg" class="profile-user-img img-responsive img-circle"src="${@user.getAvatarUrl().replaceFirst('/ctxPath', ctxPath)}">
<#form:imageclip name="imageBase64" btnText="修改头像" btnClass="btn-block"imageId="avatarImg" imageDefaultSrc="${ctxStatic+'/images/user1.jpg'}"circle="true"/>
后台代码:
// 如果设置了头像,则保存头像
if (StringUtils.isNotBlank(imageBase64)){if ("EMPTY".equals(imageBase64)){user.setAvatar(StringUtils.EMPTY);}else{String imageUrl = "avatar/"+user.getUserCode()+"."+FileUtils.getFileExtensionByImageBase64(imageBase64);String fileName = Global.getUserfilesBaseDir(imageUrl);FileUtils.writeToFileByImageBase64(fileName, imageBase64);user.setAvatar(Global.USERFILES_BASE_URL + imageUrl);}
}

组件属性:

var p = {// 标签参数id: id!,                    // 元素ID,如果不填写,则与name相同path: path!,                // 绑定form上model中属性的值name: name!,                // 元素名称,不填写value: value!,                // 元素值class: class!'',            // 隐藏域的CSS类名btnText: btnText!'选择图片',  // 按钮的名字btnClass: btnClass!'',        // 按钮的CSS类名imageId: imageId!'',                   // 裁剪后base64返回到img的idimageDefaultSrc: imageDefaultSrc!'', // 图片默认地址,清除后使用地址circle: circle!'false',   // 是否圆形图片};

ueditor 富文本在线编辑器

<#form:ueditor name="text" maxlength="10000" height="200" class="required"simpleToolbars="false" readonly="false" outline="false"/>

组件属性:

var p = {// 标签参数id: id!,                // 元素ID,如果不填写,则与name相同path: path!,            // 绑定form上model中属性的值name: name!,            // 元素名称,不填写value: value!,            // 元素值class: class!'',        // 标签框的CSS类名,设置 required 加入必填验证maxlength: maxlength!'',    // 编辑器最大输入字数,为空代表无限制height: height!'200',      // 编辑器的高度,默认200simpleToolbars: @ObjectUtils.toBoolean(simpleToolbars!false), // 是否是简单的工具条readonly: @ObjectUtils.toBoolean(readonly!false),     // 是否只读模式outline: @ObjectUtils.toBoolean(outline!false),   // 大纲视图options: options!'',   // UE附加选项,逗号隔开。};

封装通用JavaScript方法


/*** 输出日志*/
log(msg);/*** 输出错误日志*/
error(msg);/*** URL 编码*/
js.encodeUrl(url);/*** URL 解码*/
js.decodeUrl(url);/*** 得到 IE 版本,如果是IE返回:IE版本号,否则返回:false* if (js.ie && js.ie <= 8){ alert('浏览器版本过低') }*/
js.ie;/*** 安全取值,复杂类型或嵌套类型时,取不到属性中的属性时不抛出异常* js.val(jsonObj, 'user.office.name');*/
js.val(jsonObj, attrName);/*** 返回HashCode唯一值(默认忽略大小写)* @param str 要获取的字符串HashCode值* @param caseSensitive 是否大小写敏感(默认false)* @usage js.hashCode(str);*/
js.hashCode(str, caseSensitive);/*** 异步加载文件,loadFile v1.0* js.loadFile(file文件路径, callback成功回调, error失败回调)* js.loadFile('js/test.js',function(){},function(data){});* js.loadFile(['js/test.js','css/test.css'],function(){},function(data){});*/
js.loadFile(file, callback, error);/*** 打开一个Window窗体*/
js.windowOpen(url, name, width, height);/*** 关闭当前Window窗体*/
js.windowClose();/*** 给URL地址添加参数,如果原来有参数则用&前缀,如果没有则用?前缀*/
js.addParam(url, params);/*** 获取URL地址的参数*/
js.getParam(paramName, url);/*** 查看Object的内容,手机调试用* @param obj*/
js.alertObj(obj);/*** 获取字典标签* js.getDictLabel(${@DictUtils.getDictListJson('sys_menu_type')}, val, '未知', true)*/
js.getDictLabel(dictListJson, value, defaultValue, inCss);/// message dialog/*** 显示加载框* @param message 加载框提示信息* @param ignoreMessageIfExists 如果已经有加载框现在,则忽略message信息的设置* @usage js.loading('正在保存...');*/
js.loading(message, ignoreMessageIfExists);/*** 关闭加载框* @param timeout 关闭延迟时间* @param forceClose 是否强制关闭* @usage js.closeLoading(1000, true);*/
js.closeLoading(timeout, forceClose);/*** 得到layer对话框对象* js.layer.msg();*/
js.layer;/*** 显示提示框* @param message 提示消息* @param title 提示标题* @param type 提示类型(success、error、warning、info)* @param timeout 自动关闭毫秒(默认4000毫秒)*/
js.showMessage(message, title, type, timeout);/*** 显示错误提示框*/
js.showErrorMessage(responseText);/*** 关闭提示框*/
js.closeMessage();/*** 提示对话框* @param message 提示消息* @param options 对话框选项* @param closed 对话框关闭回调方法* @usage js.alert('你好!', function(){})* @usage js.alert('你好!', {icon: 2}, function(){})*/
js.alert(message, options, closed);/*** 确认对话框* @param message 确认信息* @param urlOrFun 确认后的跳转的地址,或调用的方法* @param data 如果urlOrFun是地址,该参数是调用地址的参数信息* @param callback 执行ajax的回调方法,如果为空,则直接通过location=urlOrFun跳转。* @param dataType 返回数据类型(默认json)* @param async 是否异步(默认true)* @param loadingMessage 调用loading(loadingMessage)的提示信息。* @usage js.confirm('确认删除吗?', '$ctx/biz/delete?id=123', function(data){alert('删除成功')}, 'json', true, '正在删除...');* @usage js.confirm('确认删除吗?', '$ctx/biz/delete', {id: '123'}, function(data){alert('删除成功')}, 'json', true, '正在删除...');* @usage js.confirm('确认删除吗?', function(data){alert('删除成功')});*/
js.confirm(message, urlOrFun, data, callback, dataType, async, loadingMessage);/// js template/*** 根据js模板生成代码,使用laytpl引擎* @param id 模板ID* @param data 模板数据(可选)* @param callback 如果填写,则为异步渲染* @usage *       模板格式: <sc ript id="dempTpl" type="text/template">//<!--*                   这里写模块内容...*                //--></sc ript>*       调用方法: js.template('dempTpl', data);*       模版语法:*           输出一个普通字段,不转义html:   {{ d.field }}*            输出一个普通字段,并转义html:   {{= d.field }}*          JavaScript脚本: {{# JavaScript statement }}*/
js.template(id, data, callback);/// ajax form/*** AJAX 提交* js.ajaxSubmit('/sys/user/save', {param: 1}, function(data){})*/
js.ajaxSubmit(url, data, callback, dataType, async, message);/*** AJAX 提交表单(支持文件上传)* js.ajaxSubmitForm($(form), function(data){})*/
js.ajaxSubmitForm(formJqueryObj, callback, dataType, async, message);/// string/*** String两边去空格*/
js.trim(str);/*** String的startWith*/
js.startWith(str, start);/*** String的endWith*/
js.endWith(str, end);/*** 截取字符串,区别汉字和英文*/
js.abbr(name, maxLength);/// number/*** 格式化数值* @param num 待格式化的树* @param cent 保留小数位数* @param isThousand 是否进行千分位格式化*/
js.formatNumber(num, cent, isThousand);/*** 金额格式化(千位符,小数四舍五入)金额每隔三位加一个逗号* @param s 要格式化的数值* @param n 小数位数*/
js.formatMoney(s, n);/*** 数值前补零*/
js.numberPad(num, n);/// date/*** 日期格式化* @param date 日期 new Date()* @param f 格式化字符串 yyyy-MM-dd HH:mm:ss*/
js.formatDate(date, f);/*** 字符串转为日期* @param date*/
js.parseDate(date);/*** 日期加减* @param date* @param dadd 天数*/
js.addDate(date, dadd);/*** 快速选择日期方法* @param type 1今日,2本周,3本月,4本季度,5上月* @param beginDateId 开始日期控件的ID* @param endDateId    结束日期控件的ID*/
js.quickSelectDate(type, beginDateId, endDateId);/// cookie/*** cookie 操作* @param name Cookie名称* @param value Cookie值,填写表示设置,不填写表示获取* @parma options:{expires:7} 如果是数字,则expires单位为天。*/
js.cookie(name, value, options);/// tabPage/*** 得到TabPage对象*/
js.tabPage;/*** 初始化TAB页面* @param id*/
js.initTabPage(id, options);/*** 添加TAB页面* @param $this         点击的对象* @param title        提示标题* @param url           访问的路径* @param closeable        是否有关闭按钮* @param refresh        打开后是否刷新重新加载*/
js.addTabPage($this, title, url, closeable, refresh);/*** 获取当前TAB页面* @param currentTabCallback(contents, contentWindow) 当前页面回调方法,传入当前tab页面的contents和contentWindow*/
js.getCurrentTabPage(currentTabCallback);/*** 获取当前页面的上一个TAB页面,并激活上级页面* @param preTabCallback(contents, contentWindow) 传入上一个tab页面的contents和contentWindow* @param isCloseCurrentTab 是否关闭当前页签*/
js.getPrevTabPage(preTabCallback, isCloseCurrentTab);/*** 关闭当前TAB页面,并激活上级页面* @param preTabCallback(contents, contentWindow) 关闭前的回调方法,传入上一个tab页面的contents和contentWindow*/
js.closeCurrentTabPage(preTabCallback);

封装数据表格组件DataGrid

数据表格是一个必不可少的元素,在选择这个选型的时候尝试了很多开源组件,最终选择jqGrid,只是因为它接近经典思维,用着还算顺手,最主要的是遇见什么问题都可以自行解决和修复问题,有人说jqGrid不好看,这没关系这完全而已自行编写CSS改造它,下面看看一个简单的例子:

<#form:form id="searchForm" model="${config}" action="${ctx}/sys/config/listData" method="post" class="form-inline "data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">参数名称:<#form:input path="configName" maxlength="100" class="form-control" />参数键名:<#form:input path="configKey_like" maxlength="100" class="form-control" /><button type="submit" class="btn btn-primary btn-sm">查询</button><button type="reset" class="btn btn-default btn-sm">重置</button>
</#form:form>
<table id="dataGrid"></table>
<div id="dataGridPage"></div>
// 初始化DataGrid对象
$('#dataGrid').dataGrid({// 查询数据表单searchForm: $('#searchForm'),// 设置数据表格列columnModel: [ {header:'参数名称', name:'configName', index:'a.config_name', width:200, formatter: function(val, obj, row, act){return '<a href="${ctx}/sys/config/form?id='+row.id+'" class="btnList" data-title="编辑参数">'+val+'</a>';}},{header:'参数键名', name:'configKey', index:'a.config_key', width:200},{header:'参数键值', name:'configValue', sortable:false, width:260, classes:"nowrap"},{header:'操作', name:'actions', width:100, sortable:false, title:false, formatter: function(val, obj, row, act){var actions = [];<% if(hasPermi('sys:config:edit')){ %>actions.push('<a href="${ctx}/sys/config/form?id='+row.id+'" class="btnList" title="编辑参数"><i class="fa fa-pencil"></i></a>&nbsp;');<% } %><% if(hasPermi('sys:config:delete')){ %>actions.push('<a href="${ctx}/sys/config/delete?id='+row.id+'" class="btnList" title="删除参数" data-confirm="确认要删除该参数吗?"><i class="fa fa-trash-o"></i></a>&nbsp;');<% } %>return actions.join('');}}],// 加载成功后执行事件ajaxSuccess: function(data){}
});

是不是比你使用foreach方便的多,封装后名字叫dataGrid,这只是展示了冰山一角,它支持所有jqGrid参数,即简化了代码编写,又不失功能

提供丰富的演示例子

  • AdminLTE 2.4
  • Bootstrap 3.3
  • Layer 3.0
  • My97DatePicker 4.8
  • jQurey Select2 4.0
  • jQurey Validation 1.16
  • jQurey zTree API 3.5
  • jQurey zTree Demo 3.5
  • jQuery jqGrid 4.7

JeeSite 4.0 内置功能模块规划

【点击图片看大图】   小灯泡图标:待完成功能

JeeSite4.0学习相关推荐

  1. mysql5.0镜像_Mysql5.0学习笔记(一)

    Mysql5.0学习笔记(一) -基本sql语句与支持字符集 1.登录 mysql -h localhost -u root 2.创建用户firstdb(密码firstdb)和数据库,并赋予权限于fi ...

  2. asp.net2.0学习历程 菜鸟到中级程序员的飞跃【月儿原创】

    asp.net2.0学习历程 菜鸟到中级程序员的飞跃 --30本好书点评 作者:清清月儿 主页:http://blog.csdn.net/21aspnet/           时间:2007.5.1 ...

  3. [EntLib]微软企业库5.0 学习之路——第五步、介绍EntLib.Validation模块信息、验证器的实现层级及内置的各种验证器的使用方法——上篇...

    本文是为后面的学习之路做铺垫,简单介绍下企业库中的Validation模块的一些相关知识,包括Validation模块的简介.用途.使用方法.默认提供的多种验证器的介绍等. 一.简介及用途 在实际的项 ...

  4. tensorflow 1.0 学习:参数和特征的提取

    tensorflow 1.0 学习:参数和特征的提取 在tf中,参与训练的参数可用 tf.trainable_variables()提取出来,如: #取出所有参与训练的参数 params=tf.tra ...

  5. ASP.NET MVC 3.0学习系列文章—Model in ASP.NET MVC 3.0

    系列文章 ASP.NET MVC 3.0学习系列文章-序 ASP.NET MVC 3.0学习系列文章--Razor and ASP.NET MVC 3.0 ASP.NET MVC 3.0学习系列文章- ...

  6. Microsoft .NET Pet Shop 4.0 学习之旅(三) - 项目的引用关系2

    Microsoft .NET Pet Shop 4.0 学习之旅(三) 项目的引用关系2 <?xml:namespace prefix = o ns = "urn:schemas-mi ...

  7. [EntLib]微软企业库5.0 学习之路——第七步、Cryptographer加密模块简单分析、自定义加密接口及使用—上篇...

    在完成了后,今天开始介绍企业库中的新模块:Cryptographer(加密模块),这个模块在日常的大多数项目的作用非常重要,例如:网站会员密码.身份证号.网站配置等,通过对信息进行加密可以保证项目数据 ...

  8. ExtJs2.0学习系列--Ext.Panel

    ExtJs2.0学习系列--Ext.Panel 2008-06-29 12:59 上一篇文章ExtJs2.0学习系列(1)--Ext.MessageBox ,受到了大家的褒贬不一,还是有的朋友提出好的 ...

  9. (转)[EntLib]微软企业库5.0 学习之路——第十步、使用Unity解耦你的系统—PART2——了解Unity的使用方法(1)...

    原文地址:http://www.cnblogs.com/kyo-yo/archive/2010/11/01/Learning-EntLib-Tenth-Decoupling-Your-System-U ...

最新文章

  1. 适合写python的电脑_这篇写给想选计算机专业的学弟学妹们
  2. 单据体新增界面,单价与金额字段自动进行合计
  3. linux ubuntu文件浏览器如何显示隐藏文件?(快捷键ctrl+h)
  4. hadoop伪分布式配置修改配置文件的时候无法保存(没有权限保存)
  5. 带你读论文丨异常检测算法及发展趋势分析
  6. idea生成有参构造的快捷键,idea生成有参构造的快捷键_Idea_学习_09_Idea 方法自动生成参数默认名...
  7. Spring+springMvc+Mybatis
  8. mac flutter 开发环境配置 从0到1 流程
  9. 强悍的 ubuntu —— 窗口界面管理与设置
  10. 使用data attributes
  11. linux如何使用eth0网卡,Linux运维学习之如何取出网卡eth0的ip地址?
  12. 编译出错:self-encoder.context-me_method = ME_UMH;
  13. ROS专题----导航功能包navigation基础汇总
  14. OpenG 编程指南英文整理
  15. 偏差-方差分解bias-variance decomposition
  16. SpringCloud版本Hoxton SR5 --- 第三讲:Ribbon 、Ribbon与Feign配合使用
  17. 人体运动检测与跟踪(转)
  18. 深度学习 如何查看 GPU使用情况
  19. Web3.0世界知识体系分享-Web3.0基本载体(NFT)
  20. 全天50个亮星排行表

热门文章

  1. cs230 deeplearning.ai 1: Standard NN
  2. 快速创建微信小程序,注册即认证,无需300元认证费
  3. 杨辉三角 [USACO06FEB]数字三角形Backward Digit Su…
  4. P1118 [USACO06FEB]数字三角形`Backward Digit Su`…(C++_DFS)
  5. 【原创】解决AD账户被莫名其妙的锁定问题
  6. 去掉迅雷导致桌面的右下角不时弹出广告
  7. 苹果官方鼠标移动速度慢问题解决(Magic Mouse)2021-07-29
  8. php 关键词设置,关键词库URL设置
  9. php签入html出来的影响seo吗_黑帽seo盈利-黑帽seo转型-seo优化黑帽
  10. Google Earth Engine(GEE)——NOAA_PERSIANN-CDR全球人工神经网络-气候数据集(降水量)