客户信息管理系统是基于客户(王总)的超前的设计思想和简化二次开发客观需求,因此客户系统必须具有高度的可扩展性和适应性,并且是具有可配置的柔性系统,包括可定制的菜单,可定制的列表,可定制的表单;同时自动生成表单JS校验,自动生成CRUD(增,删,改查)SQL语句和变更更正审核SQL语句,并自动完成数据库操作。为了方便二次开发,适应新的业务需求和校验需求,我们还提供了柔性接口供实现。
客户信息管理系统设计分为数据库设计、软件概要设计及详细设计,其中数据库设计主要集中在模版组、模版信息的存储设计,动态菜单、数据字典的存储设计。软件设计包括模版及模版组信息的CRUD操作,基于模版信息的动态列表,动态表单,动态JS函数,定制JS函数,动态SQL,动态缺省数据,业务校验接口设计。
从上图可以看出,模版组(TempletGroup)和模版组用户(TempletGroupUser)理论上是一对一关系,从而每个能管理客户信息的用户就拥有一个模版组,每个模版组包含若干个模版(Templet),每个模版对应与一个客户信息的业务表(TempletTable),每个模版对应多个模版字段(TempletField),模版字段中包括表单元素的显示属性,包括是否显示,是否必填,是否跨列显示,显示表单元素的类型(输入框,下拉框等),列表显示属性(显示的列名),及业务上的修改或变更是否需要审核等。因为客户关系管理系统的业务表对应多个模版,因此肯定有共有的属性,同样字段也有共有的属性,所以设计了TempletTable和TempletTableColumn两个表,同时这个TempletTable表还是定制菜单的基础数据。在理论上,一个用户拥有一个模版组,一个模版组拥有n个用户。
名称
Templet
注释
模版组模版项目明细
名称
注释
数据类型
文本说明
强制
templetId
模版项目ID
INTEGER
TRUE
templetTableId
模版表ID
INTEGER
对应templetTable
FALSE
templetGroupId
模版组ID
INTEGER
FALSE
templetCategoryId
模版类型ID
INTEGER
不使用
FALSE
templetDisplayName
模版显示名称
VARCHAR2(100)
FALSE
templetListJspFile
列表显示模版文件(暂不使用)
VARCHAR2(200)
不使用
FALSE
templetJspFile
对应的JSP文件名称和路径(暂不使用)
VARCHAR2(200)
不使用
FALSE
templetPageName
对应的页面名称(暂不使用)
VARCHAR2(200)
不使用
FALSE
templetDelegateClass
模版对应的代理类(暂不使用)
VARCHAR2(200)
不使用
FALSE
templetRanking
显示顺序
INTEGER
FALSE
templetExtend
模版扩展信息
VARCHAR2(200)
未使用
FALSE
templetTablePeriod
报表周期,年,季度,月,周,日
VARCHAR2(10)
未使用
FALSE
templetTablePromptPeriod
报表提示周期,年,季度,月,周,日
VARCHAR2(10)
未使用
FALSE
templetState
状态,1可用,0不可用
CHAR(1)
FALSE
名称
TempletField
注释
模版表字段明细
名称
注释
数据类型
文本说明
强制
fieldId
字段id
INTEGER
TRUE
templetTableColumnId
列流水号
INTEGER
对应templetTableColumn表
FALSE
templetId
模版项目ID
INTEGER
FALSE
columnName
数据库表列名(暂不使用)
VARCHAR2(100)
不使用
FALSE
fieldName
列名称(暂不使用)
VARCHAR2(100)
不使用
FALSE
fieldDisplayName
显示名称
CHAR(100)
FALSE
formTypyId
表单元素类型ID
INTEGER
FALSE
formSelectId
下拉表单对应的编码表ID
INTEGER
FALSE
formDefaultValue
表单元素默认值
VARCHAR2(200)
FALSE
formVerifyRuleId
表单元素校验规则ID
INTEGER
FALSE
formMaxlength
表单输入数据长度
INTEGER
FALSE
formWidth
表单元素宽度
INTEGER
FALSE
formHeight
表单元素高度
INTEGER
FALSE
formIsSpan
表单是否跨列显示,1是,0否
CHAR(1)
FALSE
isRequired
是否为必填项,1必填,0可不填
CHAR(1)
FALSE
isDisplay
是否可以显示,1可以,0不可
CHAR(1)
FALSE
isModify
是否可编辑,1 可以,0 不可以
CHAR(1)
FALSE
isAlterCheck
是否要变更复核,1是,0否
CHAR(1)
FALSE
isModifyCheck
是否需要修改复核
CHAR(1)
FALSE
isListDisplay
是否在列表中显示,1是,0 否
CHAR(1)
FALSE
isUserDefined
是否允许用户自定义编辑,1是 0否
CHAR(1)
FALSE
fieldOrder
是否对字段排序,默认为空,填写desc 或 asc
VARCHAR2(10)
FALSE
fieldExtend
表单后缀信息
VARCHAR2(200)
FALSE
fieldRanking
显示顺序
INTEGER
FALSE
fieldAlign
显示对齐方式,left 居左 ,center 居中 ,right 居右
VARCHAR2(20)
FALSE
fieldDisplayStyle
字段显示样式
VARCHAR2(20)
FALSE
名称
TempletTable
注释
业务树菜单项目表
名称
注释
数据类型
文本说明
强制
templetTableId
模版表ID
INTEGER
TRUE
templetTableBaseType
类型-法人表,自然人表
VARCHAR2(100)
TRUE
templetTableType
表类型,0主表,1普通,2财务报表,3外部表,9公共基本表。如果该属性为空,表示该项是个节点,不是具体的业务
CHAR(1)
FALSE
templetTableParentId
父表Id,如果是跟为0
INTEGER
TRUE
templetTableName
物理表名称,如果是一级,二级节点为空
VARCHAR2(100)
FALSE
templetTableKey
关键字,外部系统调用时指定该Key
VARCHAR2(100)
暂时没有使用
FALSE
templetTableDisplay
表显示名称
VARCHAR2(100)
FALSE
templetTableExtend
表扩展信息
VARCHAR2(200)
记录了主表的加载类的名称
FALSE
templetTableJspForm
表对应的Form JSP文件
VARCHAR2(200)
不使用
FALSE
templetTableJspList
对应列表显示Jsp文件
VARCHAR2(200)
不使用
FALSE
templetTableJspControl
表业务对应的 控制器
VARCHAR2(200)
菜单树对应得JSP文件
FALSE
templetTableRanking
显示顺序号
INTEGER
FALSE
templetTableState
状态,1可用,0不可用
CHAR(1)
FALSE
名称
TempletTableColumn
注释
数据库表物理字段,对应数据字典
1.1 表TempletTableColumn列
名称
注释
数据类型
文本说明
强制
templetTableColumnId
列流水号
INTEGER
TRUE
templetTableId
模版表ID
INTEGER
FALSE
templetTableColumnName
列名称
VARCHAR2(100)
TRUE
templetTableColumnComment
列说明
VARCHAR2(500)
FALSE
templetTableColumnType
列类型
VARCHAR2(100)
TRUE
templetTableColumnSize
列长度
INTEGER
FALSE
templetTableColumnNull
是否可以Null,Y是,N否
CHAR(1)
TRUE
templetTableColumnRanking
列序号
INTEGER
FALSE
如图所示,从类及接口的命名方面就可以很清楚地知道设计思想,DataMap是接口,DefaultDataMap是缺省实现类,GeneralListMap是具体实现类,ColDatamgr和ExtendColDataMgr是管理DataMap接口的类。listTemplet.jsp是jsp列表模版,模板使用ColMetaInfo来展现列表,普通的Jsp只需要include列表模板即可。
根据需求分析,列表页面是基于模版设置,显示相关表头,根据来自数据库的查询数据和模板设置,按一定的格式显示表体。因此我们要构架一个数据和显示的桥梁,显示属性是基于模版设置(封装到ColMetaInfo对象),数据是取之客户信息业务表(每行数据是放到HashMap,key是列名,value值是字段的值,把HashMap加入到ArrayList中),它们之间的桥梁就是列名。
ColDataMgr的主要功能是,获取相关的数据和显示信息。
public ArrayList getColMetaInfos()
{
。。。 。。。
}
public ArrayList getData()
{
。。。 。。。
}
使用jsp模版可以把显示格式和显示逻辑分离,同时我们可以看出ColDataMgr其实就是处理显示的逻辑和数据,而ListTemplet.jsp处理显示的格式。推而广之,我们可以根据不同的显示需求,产生多个类似 ListTemplet.jsp, 如适应打印格式的模版,适应导出Excel格式的模版,细心的读者在看源代码的过程中会发现,DataMap接口中都有类似的方法,如getExeclColMetaInfos(),getPrintColMetaInfos(),在缺省实现类中都有缺省实现,如果需要,直接在子类中覆写(override)此类方法即可。
<%
//获取后台传来的列表显示属性和显示数据
ArrayList colMetaInfos = (ArrayList) request.getAttribute("colMetaInfos");
ArrayList datas = (ArrayList) request.getAttribute("datas");
。。。 。。。
%>
//根据colMetaInfos显示表头
。。。 。。。
<TR>
//循环显示列名称
<TD class=ItemTitle height=20 nowrap>
<%=colMeta.getDisplayName()%>
</TD>
</TR>
//通过嵌套循环,根据colMetaInfos和Datas显示表体
。。。 。。。
//没有记录显示空白行
。。。。。。
</table>
列表设计的范围是对单表的显示,使用ColDataMgr就可以了,为了达到多个关联表(一对一关系)显示,使用ExtendColDataMgr,。ExtendColDataMgr代码如下:
public class ExtendColDataMgr
{
。。。 。。。
//初始化类的相关属性
public ExtendColDataMgr(ArrayList oldColMetaInfos, ArrayList oldDatas, ArrayList newColMetaInfos, HashMap newDatas, String linkKey)
{
。。。 。。。
}
public ArrayList getDatas()
{
。。。 。。。
}
}
从代码上可以明显地看出,ExtendColDataMgr是对ColDataMgr的进一步扩展,其中ArrayList
(oldDatas)中 的每个元素都是HashMap,每个HashMap中都有linkKey,该键为外键的值(如110),而在新的数据是HashMap,其中的key就是linkKey(如110), value值是HashMap。从而可以把新的值逐行加到旧的值内部。下面是数据结构示意图:
OldDatas
newDatas
name
password
adress
linkKey
linkKey
age
sex
张三
122
长春
110
112
23
1
李四
123
北京
111
111
25
0
王五
124
广州
112
110
24
1
如上图所示,FormDisplayMgr是管理接口FormMap的管理类,AbstractFormMap类是缺省实现类,GeneralFormDisplayMap是通用的表单影射类,ExtendFormMap类是FormMap接口的代理封装类,ExtendFormMap类实现了FormMap接口,又封装了FormMap。同样showInfoTemplet.jsp和formTemplet.jsp是jsp模版文件,普通的表单jsp只要include模版jsp就行了。showInfoTemplet.jsp和formTemplet.jsp是使用DisplayAttribute来显示页面,其中ShowInfTemplet.jsp用于显示详细信息页面,formTemplet.jsp用于新增、修改页面。
根据需求,显示页面要基于模版配置,配置的属性经过实现接口FormMap的封装,产生DisplayAtttribute的ArrayList,然后再在页面上显示出来。当然还有自动校验Js语句,
ExtendFormMap同样是对GeneralDisplayMap的再次封装,主要适用于多个表数据放入一个表单中:
public ArrayList getDisplayAttributes(){
ArrayList list = this.formMap.getDisplayAttributes();
list.addAll(new GeneralFormDisplayMap(colDisplayMetaDatas, selectValuesMap, colDefaultData).getDisplayAttributes());
return list;
}
在最初的设计中,并没有FormSelectAttribute类,是后来提出下拉菜单要求有级联关系,如省,市, 县之间有级联关系,原来的设计是在DisplayAttribute类中有二维数组,存储下拉菜单的value,text。为了满足新的需求,加上了FormSelectAttribute类,加上了子下拉菜单的名称,代码如下:
public class FormSelectAttribute {
private String selectName;
private String subSelectName;
private String[][] allOptions;
。。。。。。
}
下拉菜单的级联是通过父菜单发生改变,子菜单也发生变化,即JS的onChange来触发:
οnchange="chageDiv_<%=formElementsKey%>(this, '<%=display.getSelectValues().getSubSelectName()%>')"
下拉菜单封装在DIV中,因此改变DIV中的内容即可实现,使用JS的innnerHTML即可实现:
document.getElementById("div_" + targetId).innerHTML = str;
为了实现多级(n级)菜单的联动,使用了递归算法:
function nestFlushSubSelect(targetId)
{
。。。。。。
<%
if(selectMap.get(display.getSelectValues().getSubSelectName()) != null)
{
%>
nestFlushSubSelect(<%=display.getSelectValues().getSubSelectName()%>);
<%}%>
。。。。。。
}
public interface JsFunctionGenerator {
public String getFunctionText();
}
public ValidateFunctionGenerator(long templetId)
{
。。。 。。。
}
public ValidateFunctionGenerator(long templetId, String accessFunctions)
{
。。。 。。。
}
public ValidateFunctionGenerator(long templetId, String accessFunctions, String functionNo)
{
。。。 。。。
}
表单元素间的互相校验关系可以写在表单提交函数的后面。
其实从接口上考虑,并没什么稀奇之处,代码如下:
public interface BizSqlGenerator {
public String getSql();
public String[] getSqls();
}
即要么得到一个sql语句,如Select语句,要么得到Insert语句,要么得到一组语句,如审核Sql语句,还有是既要一条语句,又要一组语句,如撤销。
为此,我们设计了缺省实现类,如下:
public abstract class DefaultBizSqlGenerator implements BizSqlGenerator {
public String getSql()
{
return null;
}
public String[] getSqls()
{
return null;
}
}
其下的子类,都各有各的构造方法,从而初始化类的相关属性。从这个方面来说,面向对象编程其实是面向接口编程,接口内只定义外界调用的方法,至于怎么实现,实现的条件等问题由实际实现类来考虑,下面以审核SQL语句生成器例子说明,其他几个子类都类似。
//构造方法要完成初始化modifyInfos,修改的内容
private String[] ids;
private ArrayList modifyInfos = new ArrayList();
private String userId;
public BizAuditSqlGenerator(String[] ids, String userId)
{
this.ids = ids;
this.userId = userId;
init();
}
public String[] getSqls()
{
//使用modifyInfos生成审核SQL语句数组,这样就达到了既实现了接口的方法,又可以不增加新的接口方法的目的。
。。。 。。。
}
//初始化modifyInfos
private void init(){
。。。 。。。
}
缺省数据是由模版定义的,在新增页面上,显示的就是模版定义的缺省数据,但有些缺省数据则直接和运行时间有关,而不是死的缺省数据。
相当于一个工厂方法,根据defaultKey来创建类并调用方法获取缺省值。从而为下一步二次开发提供了基础。
public class DefaultValueMgr {
public static String getValue(String defaultKey)
{
String ret = "";
if(defaultKey.equals(ClientConstant.DefaultValue.CURRENT_YEAR))
{
ret = (new Integer(DefaultDate.getCurrentYear())).toString();
}
。。。 。。。
return ret;
}
}
public class DefaultDate {
public static int getCurrentYear()
{
return Calendar.getInstance().get(Calendar.YEAR);
}
public static int getCurrentMonth()
。。。 。。。
public static int getCurrentDay()
。。。 。。。
}
在ClientConstant的内部类ClientConstant,在模版中也是存储这样的缺省值,当程序获知这样的缺省值时,就会产生相关的缺省值,同样如果以后有新的需求也可以修改这样三个类的代码。
该值同样保存在模版的缺省值字段中,当程序读到这样的值,就会自动调用相关的方法来获取缺省值
public static class ClientConstant {
public static final String CURRENT_YEAR = "$default_current_year";
public static final String CURRENT_MONTH = "$default_current_month";
public static final String CURRENT_DAY = "$default_current_day";
}
通过上图可知,FiltetFactory工厂类根据模版Id(templetId),调用静态常量类ClientConstant,获取相应的实现FilterOperator接口的实现类的全名(含包名FullName)字符串,FiltetFactory工厂类使用Class.forName(类的全名),创建FilterOperator接口的实现类,调用FilterOperator接口的实现类的接口方法即可。
public class FilterFactory {
/**
* 根据templetTableId,从ClientConstant中得到过滤类的全名(含包名),
* 根据类名称创建过滤类
* @param templetTableId
* @return
*/
public static FilterOperator getFilterOperator(String templetTableId)
{
。。。 。。。
filterOperator = (FilterOperator)(Class.forName(filterOperatorClassName).newInstance();
。。。 。。。
}
工厂类依赖静态常量类的配置,同样你可以自己写配置文件,或properties,或xml,当使用bean.xml来配置,恭喜你,你在使用Spring,事实上Spring的底层也没什么高招,也就是用java的反射而已。
public static class FilterProperty
{
private static HashMap filterClassProperties = new HashMap();
//初始化filterClassProperties, templetId,和类FullName初始化
private static void initFilter()
{
。。。 。。。
filterClassProperties.put("100681", "com.iss.itreasury.clientmanage.customer.bizlogic.valid.CorpComeMarkInfo");
。。。 。。。
}
//根据模版Id(templetId),获取相应的实现FilterOperator接口的实现类的全名(含包名FullName)字符串
public static String getFilterClassName(String key)
{
initFilter();
return (String)filterClassProperties.get(key);
}
}
public abstract class AbstractFilterOperator implements FilterOperator {
protected String msg;
public boolean isPass(HashMap hm) {
String act = (String) hm.get("operation");
boolean bool = false;
if (ClientConstant.BizWebOperation.BIZ_INSERT.equalsIgnoreCase(act)) {
bool = validInsert(hm);
} else if (ClientConstant.BizWebOperation.BIZ_MODIFY
.equalsIgnoreCase(act)
|| ClientConstant.BizWebOperation.BIZ_CORRECT
.equalsIgnoreCase(act)) {
bool = validModify(hm);
} else if (ClientConstant.BizWebOperation.BIZ_DELETE
.equalsIgnoreCase(act)) {
bool = validDelete(hm);
} else {
bool = false;
this.msg = "无法识别的操作类型";
}
return bool;
}
public String getAlterString() {
return this.msg;
}
。。。 。。。
}
有些不适用于模版处理的情况,例如模版的定制、报表等模块开发,可以直接使用类似于其他模块那样,自行实现,与模版无关。
客户信息管理系统是基于模版定制的软件架构,是在数据库中定义的模版的基础之上,展示动态列表、动态表单、动态JS校验、动态SQL、自动执行的功能,完全实现了配置化管理。并且留有大量的可扩展的接口,简化和优化了二次开发,是非常有益的设计尝试和超前的设计理念。
努力,在于我热爱我的事业,与中国的软件一起走向成熟,走向世界。 联系作者: lijj_72@hotmail.com