实现解析数据源、生成SQL、读取数据源、解析word模板、识别占位符与约定标识符、识别模板样式(字体尺寸颜色等)、填充数据。
用来保存前端配置的表结构大概这样
前端提交的配置数据类似这样
{ template:1 //模板 datasources:[{ //数据源 name:'数据源名称' tables:[{ table:'hr_department' //数据源-关联表名 join:'left' //连接方式(inner:内连接, left:左连接 ,right:右连接) relations:[{ //关联条件 join:'and' //关联方式(可选and,or,ors) column:'id' //列名 compare:10 //比较运算符 value:null //常量值或变量(二选一) relattion_table:'hr_employee' //比较表名(二选一) relattion_column:'department_id' //比较列名 }] //end-relattions }] //end-tables ,conditions:[{ //过滤条件 join:'and' table:'hr_employee' column:'type_id' compare:10 value:100 //常量值或变量 }]//end-conditions ,havings:[{ //分组过滤条件 join:'and' method:'sum' table:'hr_employee' column:'type_id' compare:10 value:100 //常量值或变量 }]//end-havings ,orders:[{ //排序 table:'hr_employee' column:'type_id' type:'ASC' }] ,groups:[{ //分组 table:'hr_employee' column:'type_id' }] }] //end-datasources }把以上参数保存到各自的表中。
关于于常量值或变量
常量就不用说了配置时直接提供值
变量分内置变量和配置变量
内置变量应该提交约定好
一般有两类
1.后台约定或数据库约定,如{id}或{pk}表示主表的主键
2.前端约定,一般固定在url参数中附带,如打印时url中要带一个id=123参数来定位要打印哪行数据, {param.id}就表示那个123
配置变量比较灵活,与前端约定的区别是,这里的变量名一般由用户指定
在填充模板时,前端会提交一个参数用来定位数据,如id=123
接下来根据参数123和配置项从数据库中提取数据。
1.组装SQL
2.执行SQL获取结果集(如果只有一个数据库源(库),这一步就执行一个SQL就完事了)
3.解析word中的占位符,根据占位符从结果集中提取数据
4.替换占位答生成新的word
组装SQL的细节比较多,主要是关联条件与过滤条件的组合,这里我们通过一个ConfigStore来实现,参考【关于ConfigStore】
下面的示例代码与我们的约定略有不同,示例中默认有一个主表,而按我们的约定不需要区分主次都在数据源下的表中并列(所以15-47行是多余的)
Map<String, DataSet> values = new HashMap<>(); //业务库 AnylineService biz = ServiceProxy.service("biz"); //主表 values.put("master", biz.querys(template.getString("table_name"), "++id:"+pv)); //多数据源 DataSet ds = service.querys("t_ds_datasource", "template_id:"+template.getId()); if(ds.size() > 0) { //数据源关联的全部数据 //所有的关联条件和过渡条件一次查出 避免重复查询数据库 DataSet all_tables = service.querys("t_ds_datasource_table", new DefaultConfigStore().in("DATASOURCE_ID", ds.getStrings("ID"))); DataSet all_relations = service.querys("t_ds_datasource_table_relation", new DefaultConfigStore().in("DATASOURCE_ID", ds.getStrings("ID"))); DataSet all_filters = service.querys("t_ds_datasource_filter", new DefaultConfigStore().in("DATASOURCE_ID", ds.getStrings("ID"))); for(DataRow datasource:ds){ //每个 数据组组装一条SQL 返回一个结果集 ConfigStore configs = new DefaultConfigStore(); String masterTableName = datasource.getString("TABLE_NAME"); String masterTableAlias = datasource.getString( "TABLE_ALIAS"); if(BasicUtil.isEmpty(masterTableAlias)){ masterTableAlias = masterTableName; } //TableBuilder实现多表关联 org.anyline.data.param.TableBuilder builder = org.anyline.data.param.TableBuilder.init(masterTableName +" AS "+ masterTableAlias); //需要查询的列 List<String> columns = new ArrayList<>(); DataSet tables = all_tables.getRows("DATASOURCE_ID", datasource.getId()); if(tables.size() == 0){ //如果只有一个表直接查*省了查询表结构的过程 columns.add("*"); }else{ //如果有多个表,一般会出现列重名的情况,所以需要一个前缀来区分(这里用表名或别名__) List<String> cols = service.columns(masterTableName); for(String col:cols){ columns.add(masterTableName+"."+col + " AS " + masterTableAlias + "__" + col); } for(DataRow table:tables){ String tableName = table.getString( "NAME"); //有别名的用别名,没有别名的用原表名 String tableAlias = table.getString( "ALIAS", "NAME"); cols = biz.columns(tableName); for(String col:cols){ //添加前缀 避免列重名 columns.add(tableName+"."+col + " AS " + tableAlias + "__" + col); } } } //每个表参考主表重复上面的过程 for(DataRow table:tables){ String condition = null; String tableName = table.getString("NAME"); String tableAlias = table.getString( "ALIAS"); if(BasicUtil.isEmpty(tableAlias)){ tableAlias = tableName; } //最关键的ConfigStore需要熟悉 ConfigStore conditions = new DefaultConfigStore(); //与数据源和表相关的 关联条件 DataSet relations = all_relations.getRows("DATASOURCE_ID", datasource.getId(), "TABLE_ID", table.getId()); //关联条件 for(DataRow relation:relations){ String columnName = relation.getString("COLUMN_NAME"); String value = relation.getString("VAL"); String join = relation.getString("JOIN_CODE"); Compare compare = compare(relation.getInt("COMPARE_CODE", 10)); String relationTableName = relation.getString("RELATION_TABLE_NAME"); String relationTableAlias = relation.getString("RELATION_TABLE_ALIAS", "RELATION_TABLE_NAME"); String relationColumnNmae = relation.getString("RELATION_COLUMN_NAME"); if(BasicUtil.isEmpty(value)) { //两个表之间关联 conditions.condition(join, compare, concat(tableAlias, columnName) , "${"+concat(relationTableAlias, relationColumnNmae)+"}"); }else{ //常量值 conditions.condition(join, compare, concat(tableAlias, columnName), value); } } //生成ON关联条件 ConditionChain chain = conditions.getConfigChain().createAutoConditionChain(); condition = chain.getRunText(false, null, RuntimeHolder.getRuntime()); //把占位值添加到SQL主体中 List<RunValue> vals = chain.getRunValues(); for(RunValue runValue:vals){ configs.addStaticValue(runValue.getValue()); } //left join/right join Join.TYPE join = Join.TYPE.valueOf(table.getString("JOIN_CODE").toUpperCase()); builder.join(join, tableName, condition); } //过滤条件 DataSet filters = all_filters.getRows("DATASOURCE_ID", datasource.getId()); for(DataRow filter:filters){ String value = filter.getString("VAL"); String join = filter.getString("JOIN_CODE"); String tableName = filter.getString("TABLE_NAME"); String columnName = filter.getString("COLUMN_NAME"); Compare compare = compare(filter.getInt("COMPARE_CODE", 10)); configs.condition(join, compare, concat(tableName, columnName), value); } configs.columns(columns); configs.and(masterTableAlias+".id", pv); DataSet set = biz.querys(builder.build(), configs); values.put(datasource.getCode(), set); } }
现在所有的结果集已经放在了values中,
下一步解析word中的占位符,
主要通过WDocument类来实现提取占位符,操作表格等
如果没有循环输出的情况直接通过WDocument替换全局占位符即可,
因为这里涉及到样表循环输出所以需要定位需要循环的结构(行)在循环结构的上级(表)中插入行,
插入行时注意复制原表的字体格式,注意一般不是在最后追加,注意合并单元格,注意多层遍历
这里我们只演示简单的一层循环
//模板文件 WDocument doc = new WDocument(tmp_file); List<WTable> tables = doc.tables(); for(WTable table:tables){ List<WTr> trs = table.getTrs(); for(int i=0; i<trs.size(); i++){ WTr tr = trs.get(i); List<WTc> tcs = tr.getTcs(); int static_qty = 0; int placeholder_qty = 0; List<String> tr_placeholders = new ArrayList<>(); //当前行的占位 int template_index = 0; for(WTc tc:tcs){ String txt = tc.getTexts(); if(BasicUtil.isNotEmpty(txt)){ List<String> td_placeholders = placeholders(txt); if(!td_placeholders.isEmpty()){ placeholder_qty ++; tr_placeholders.addAll(td_placeholders); }else{ static_qty ++; } } } if(placeholder_qty > static_qty){ //多行 WTr template_tr = tr; //以当前行为模板 template_index = trs.indexOf(template_tr); String tr_ds = null; //数据源 for(String placeholder:tr_placeholders){ if(!placeholder.startsWith("master")){ tr_ds = placeholder.split(":")[0]; if(values.containsKey(tr_ds)) { break; } } } DataSet for_datas = values.get(tr_ds); //数据集 if(null != for_datas) { Map<String,String> keys = new HashMap<>(); //占位符与列之前对应 placeholder <> data.key List<String> placeholders = template_tr.placeholders(); for (String placeholder : placeholders) { String[] ks = placeholder.split(":"); String p_ds = ks[0]; String p_tab = ks[1]; String p_col = ks[2]; String k = p_tab + "__" + p_col; if ("master".equalsIgnoreCase(p_ds)) { k = p_col; } keys.put(placeholder, k); } //根据数据集添加行 for (DataRow row : for_datas) { Map<String,String> replaces = new HashMap<>(); for(String placeholder:placeholders){ String key = keys.get(placeholder); String value = row.getString(key); if(null != value){ //检测复选框 String check = "□"; String checked= "☑"; value = value.replace(check, "<br/>"+check); value = value.replace(checked, "<br/>"+checked); if(value.startsWith("<br/>")){ value = value.substring(6); } } replaces.put(placeholder, value); } WTr append_tr = template_tr.clone(true); append_tr.replace(replaces); table.insert(template_index++, append_tr); } table.remove(template_tr);//删除模板行 } } } } List<String> placeholders = doc.placeholders(); //替换主表占位 for(String p:placeholders){ //master:t_entrust:entrust_unitName String[] ks = p.split(":"); String p_ds = ks[0]; String p_tab = ks[1]; String p_col = ks[2]; DataSet set = values.get(p_ds); String v = ""; String k = p_tab + "__" + p_col; if("master".equalsIgnoreCase(p_ds)){ k = p_col; } if(null != set && !set.isEmpty()){ DataRow row = set.getRow(0); v = row.getString(k); } System.out.println("[placeholder]"+p+"/"+k+"="+v); doc.replace(p, v); } doc.save();doc.save()后就把最终的格式保存到了相关的word文件,下一步把文件提交给minio服务器,再把新的url交给前端预览就可以了 更复杂的场景就需要更复杂的约定规则,但是对于后台来说主要还是用到CofigStore,WDocument两个来类就可以了。