关于后台部分业务重构的思考及实践
ljmatlight---原文地址
积极主动,想事谋事,敢作敢为,能做能为。
当职以来,随着对公司业务和项目的不断深入,不断梳理业务和公司技术栈。
保证在完成分配开发任务情况下,积极思考优化方案并付诸实践。
一、想法由来
由于当前我司主要针对各大银行信用卡平台展开相关业务,
故不难看出,各银行信用卡平台虽然有各自的特性,
但其业务相似程度仍然很高,除必要的重复性工作外,仍有很大提升优化空间。
例如: 各个银行平台都需要对账工作、都要安排人力去开发重复类似的功能,
且不能很好地适应新的需求变化,修改耗时费力,可维护性较差。
二、业务分析
依托具体业务场景进行分析,每个平台都具有对账功能。
对账业务:
1、主要包括列表分页和导出功能
2、能够按照时间范围搜索
3、列表包括分页、金额统计、状态转换等等
优化依据:
对特性业务进行差异性对待(如导出数据字段,结果转换字段等等),
充分利用面向对象的思想进行合理的抽象层次建设
三、技术优化实践
后台技术栈为Jfinal,LayUI。
关于对账优化整体思路:
1、前端页面发起请求,传递响应参数
前端传递参数形式如下图:
PH.api2('#(base)/icbc/mall/compared/pay/list', { "comparedListBean.orderId": orderId, "comparedListBean.reqNo": reqNo, "comparedListBean.startTime": startTime, "comparedListBean.endTime": endTime, "comparedListBean.pageNo": page, "comparedListBean.pageSize": 20}, function(res) {
采用bean类首写字母小写,加 ”.” 加 属性名称的形式进行书写。
2、定义dto 进行参数的bean 形式接受
由于所有列表,都包含起始搜索时间,当前页,每页显示数量,故定义基础列表dto的Bean 如下图所示:
/** * Description: 列表请求参数封装 * <br /> Author: galsang */@Data@NoArgsConstructor@AllArgsConstructorpublic class BaseListBean { private String startTime; private String endTime; private int pageNo = 1; private int pageSize = 20; private int start = (pageNo - 1) * pageSize; }
根据具体业务可以扩展基础列表dto的Bean,
例如需要添加订单号、请求流水号,可创建Bean 继承基础bean进行扩展,如图:
/** * Description: 对账 - 列表请求参数封装 * <br /> Author: galsang */@Data@NoArgsConstructor@AllArgsConstructorpublic class ComparedListBean extends BaseListBean { private String orderId; private String reqNo; }
3、后端使用getBean 进行接收,根据需要对参数进行验证,并将Bean转换为Map
/** * 将接收参数的Bean 转换成 sqlMap * * @param modelClass Bean.class * @return * @throws BeanException */public Map<String, Object> sqlMap(Class<?> modelClass) { try { return sqlMapHandler(BeanUtil.bean2map(getBean(modelClass))); } catch (BeanException e) { e.printStackTrace(); } return null; }/** * 处理sql 参数数据 * <br /> * * @param sqlMap * @return */private Map<String, Object> sqlMapHandler(Map<String, Object> sqlMap) { // 区别是导出还是列表 if(null == sqlMap.get("start")){ return sqlMap; } int pageNo = Integer.parseInt(String.valueOf(sqlMap.get("pageNo"))); int pageSize = Integer.parseInt(String.valueOf(sqlMap.get("pageSize"))); sqlMap.put("start", (pageNo - 1) * pageSize); return sqlMap; }
如果需要对参数进行验证,则可以使用jfinal 验证Bean 的方法创建相应验证Bean。
4、将sql 语句统一写在md文件中
对账业务主要用到四种形式的sql, 故定义枚举进行统一的约定。
/** * 定义使用sql命名空间后缀 */enum NameSpaceSqlSuffix { LIST("查询列表", ".list"), COUNT("查询数量", ".count"), TOTAL("查询统计", ".total"), EXPORT("导出文件", ".export"); private String name; private String value; NameSpaceSqlSuffix(String name, String value) { this.name = name; this.value = value; } }
命名统一,可以直接定位需要实现或变动的需求,方便维护
5、结果数据转换接口
结果数据的的转换主要分为列表数据的转换和单条数据的转换,由于转换数据不一定相同,只要在具体的业务层进行定义内部类实现该接口run方法即可。
/** * Description: 结果类型数据转换接口 * <br /> Author: galsang */public interface IConvertResult { /** * 执行列表结果类型转换 * * @param records */ void run(List<Record> records); /** * 执行单个结果类型转换 * * @param record */ void run(Record record); }
6、抽象公共方法
通用查询列表
/** * 查询并转换列表数据 * * @param sql 查询列表数据sql * @param iConvertResult 数据转换 * @return 转换后的列表数据 */public List<Record> doSqlAndResultConvert(String sql, IConvertResult iConvertResult) { List<Record> orders = dbPro.find(sql); iConvertResult.run(orders); return orders; }
通过md命名空间查询列表信息
/** * 通用查询列表信息 * * @param nameSpace sql 文件的命名空间 * @param sqlMap * @param iConvertResult * @return */public Map<String, Object> listByNameSpace(String nameSpace, Map<String, Object> sqlMap, IConvertResult iConvertResult) { String sqlList = dbPro.getSqlPara(nameSpace + NameSpaceSqlSuffix.LIST.getValue(), sqlMap).getSql(); String sqlCount = dbPro.getSqlPara(nameSpace + NameSpaceSqlSuffix.COUNT.getValue(), sqlMap).getSql(); String sqlTotal = dbPro.getSqlPara(nameSpace + NameSpaceSqlSuffix.TOTAL.getValue(), sqlMap).getSql(); int pageSize = Integer.parseInt(String.valueOf(sqlMap.get("pageSize"))); return this.listBySql(sqlList, sqlCount, sqlTotal, pageSize, iConvertResult); }
通过sql查询列表信息
/** * 通用查询列表信息 * * @param sql 查询数据列表sql * @param countSql 查询统计数量sql * @param totalSql 查询统计总计sql * @param pageSize 每页显示长度 * @param iConvertResult 结果类型装换实现类 * @return 处理完成的结果数据 */public Map<String, Object> listBySql(String sql, String countSql, String totalSql, int pageSize, IConvertResult iConvertResult) { // 查询数据总量 Long counts = dbPro.queryLong(countSql); // 查询统计数据 Record total = null; if (StringUtil.isNotEmpty(totalSql)) { total = dbPro.findFirst(totalSql); iConvertResult.run(total); } // 查询列表数据并执行结果转换 List<Record> orders = doSqlAndResultConvert(sql, iConvertResult); // 响应数据组织 float pages = (float) counts / pageSize; Map<String, Object> resultMap = Maps.newHashMap(); resultMap.put("errorCode", 0); resultMap.put("message", "操作成功"); resultMap.put("data", orders); resultMap.put("totalRow", counts); resultMap.put("pages", (int) Math.ceil(pages)); if (StringUtil.isNotEmpty(totalSql)) { resultMap.put("total", total); } return resultMap; }
进行数据库查询;
对查询结果数据进行转换;
响应数据的组织。
查询导出文件数据
/** * 导出文件 * @param nameSpace * @param sqlMap * @param iConvertResult * @return */public List<Record> exportByNameSpace(String nameSpace, Map<String, Object> sqlMap, IConvertResult iConvertResult) { // 要导出的数据信息(已经转换) return doSqlAndResultConvert(dbPro.getSqlPara(nameSpace + NameSpaceSqlSuffix.EXPORT.getValue(), sqlMap).getSql(), iConvertResult); }
7、具体业务层实现
支付对账业务层
/** * Description: 对账 - 支付业务层 * <br /> Author: galsang */public class ComparedPayService extends BaseService { public static final String MARKDOWN_SQL_NAMESPACE = "mall_compared_pay"; /** * 查询信息列表 * * @param sqlMap 查询条件 * @return 响应结果数据 */ public Map<String, Object> list(Map<String, Object> sqlMap) { return super.listByNameSpace(MARKDOWN_SQL_NAMESPACE, sqlMap, new ComparedPayConvertResult()); }
继承基础抽象业务BeseService;
定义具体业务层使用的sql命名空间常量;
查询信息列表。
实现 IConvertResult 接口
/** * 结果类型装换实现类 */private final class ComparedPayConvertResult extends AbstractConvertResult { }
由于支付对账和退款对账转换数据相同,故定义抽象转换类
/** * Description: * <br /> Author: galsang */public abstract class AbstractConvertResult implements IConvertResult { List<Record> goodExts = Db.use("superfilm").find(" SELECT id, color FROM mall_good_ext "); @Override public void run(List<Record> orders) { orders.forEach(o -> { o.set("companyAmt", o.getInt("amount") - o.getInt("payAmount")); RecordUtil.sqlToJavaAmount(o, "amount", "payAmount", "pointAmt", "totalDiscAmt", "companyAmt"); o.set("style", getStyle(o.getInt("goodExtId"))); o.set("statusCN", MallOrderStatus.reasonPhraseByStatusCode(o.getInt("status"))); }); } @Override public void run(Record record) { record.set("totalCompanyAmt", record.getInt("totalAmount") - record.getInt("totalPayAmount")); RecordUtil.sqlToJavaAmount(record, "totalAmount", "totalPayAmount", "totalPointAmt", "totalTotalDiscAmt"); } /** * 获取商品规格 * * @param goodExtId 商品详情id * @return 商品规格 */ public String getStyle(final int goodExtId) { Iterator<Record> iterator = goodExts.iterator(); while (iterator.hasNext()) { Record record = iterator.next(); if (record.getInt("id").intValue() == goodExtId) { return record.getStr("color"); } } return "没有对应规格或已下架"; } }
生成导出文件
/** * 生成导出文件 * * @param sqlMap 查询条件 * @param fileSuffixName 生成文件名称后缀 * @param sheetName 工作表标题名称 * @return 要导出的文件对象 * @throws IOException * @throws URISyntaxException */public File export(Map<String, Object> sqlMap, String fileSuffixName, String sheetName) throws IOException, URISyntaxException { // TODO 需要切换sql 命名空间, 和 结果转换类 List<Record> records = super.exportByNameSpace(MARKDOWN_SQL_NAMESPACE, sqlMap, new ComparedPayConvertResult()); // 执行相应的导出操作 Workbook wb = new XSSFWorkbook(); // TODO 必须定制化操作 this.doSheet(wb, records, sheetName); return ExportPoiUtil.createExportFile(wb, fileSuffixName); }
由于导出文件字段的差异性,所以必须根据具体业务对相应的字段和数据进行修改。
/** * 填充工作表数据 * * @param wb 表格对象 * @param recordList 填充列表数据信息 * @param sheetName 工作表名称 */private void doSheet(Workbook wb, List<Record> recordList, String sheetName) { // 创建工作表 - 并制定工作表名称 Sheet sheet = wb.createSheet(WorkbookUtil.createSafeSheetName(sheetName)); short rowNum = 0; // 设置初始行号 Row row = sheet.createRow(rowNum++); // 创建表格标题行 ExportPoiUtil.header(wb, row, "序号", "订单号", "请求流水号", "商品", "商品规格", "数量", "总金额", "清算", "积分抵扣", "行内优惠", "公司补贴", "支付时间", "状态"); int serNo = 1; // 填充表格数据行 for (Record order : recordList) { int columnNum = 0; JSONObject json = new JSONObject(); json.put("amount", order.getBigDecimal("amount")); json.put("payAmount", order.getBigDecimal("payAmount")); json.put("pointAmt", order.getBigDecimal("pointAmt")); json.put("totalDiscAmt", order.getBigDecimal("totalDiscAmt")); json.put("companyAmt", order.getBigDecimal("amount").subtract(order.getBigDecimal("payAmount"))); row = sheet.createRow(rowNum++); row.createCell(columnNum++).setCellValue(serNo++); row.createCell(columnNum++).setCellValue(order.getStr("orderId")); row.createCell(columnNum++).setCellValue(order.getStr("reqNo")); row.createCell(columnNum++).setCellValue(order.getStr("goodName")); row.createCell(columnNum++).setCellValue(order.getStr("style")); row.createCell(columnNum++).setCellValue(order.getStr("count")); row.createCell(columnNum++).setCellValue(json.getDouble("amount")); row.createCell(columnNum++).setCellValue(json.getDouble("payAmount")); row.createCell(columnNum++).setCellValue(json.getDouble("pointAmt")); row.createCell(columnNum++).setCellValue(json.getDouble("totalDiscAmt")); row.createCell(columnNum++).setCellValue(json.getDouble("companyAmt")); row.createCell(columnNum++).setCellValue(new JDateTime(order.getDate("createdTime")).toString("YYYY-MM-DD hh:mm:ss")); row.createCell(columnNum++).setCellValue(order.getStr("statusCN")); } }
8、工具类
由于当前系统精确到分,数据库中以int存储分,但是前端显示的时候要求显示元,故可使用此工具类进行“分”到“元”的转换处理。
/** * Description: 记录对象相关工具类 * <br /> Author: galsang */@Slf4jpublic class RecordUtil { /** * 数据库中保存的金额(分)转换为金额(元) * * @param record 记录对象 * @param key 字段索引 */ public static void sqlToJavaAmount(Record record, String... key) { if (record != null) { int keyLength = key.length;// log.info(" keyLength ================ " + keyLength); for (int i = 0; i < keyLength; i++) {// log.info(" key[" + i + "] ================ " + key[i]); if (record.getInt(key[i]) != null) { record.set(key[i], new BigDecimal(record.getInt(key[i])).divide(BigDecimal.valueOf(100))); }else{ record.set(key[i], new BigDecimal(0)); } } } } }
文件导出工具类
/** * @Description: 导出POI文件工具类 * @Author: galsang * @Date: 2017/7/7 */public class ExportPoiUtil
具体代码参见后台对账业务实现。
9、几点约定
前端: startTime 、endTime、pageNo、pageSize、
md – sql命名空间后缀 : list、count、total、export
四、交流提高
不足之处,还请各位同事多多指教,谢谢。
同时经过调整最终形成以下基础业务层代码。
BaseService 代码如下:
/** * 基础业务层封装 * * @author ljmatlight * @date 2017/10/17 */@Slf4jpublic abstract class BaseService { /** * 由子类提供具体数据源= * * @return */ protected abstract DbPro dbPro(); /** * 由子类提供具体 sql 命名空间 * * @return */ protected abstract String sqlNameSpace(); /** * 由子类提供具体结果数据转换 * * @return */ protected abstract IConvertResult iConvertResult(); /** * 通用查询列表信息 * * @param sql 查询数据列表sql * @param countSql 查询统计数量sql * @param totalSql 查询统计总计sql * @param pageSize 每页显示长度 * @param iConvertResult 结果类型装换实现类 * @return 处理完成的结果数据 */ private Map<String, Object> listBySql(String sql, String countSql, String totalSql, int pageSize, IConvertResult iConvertResult) { // 查询数据总量 Long counts = this.dbPro().queryLong(countSql); // 查询列表数据并执行结果转换 List<Record> orders = doSqlAndResultConvert(sql, iConvertResult); // 响应数据组织 float pages = (float) counts / pageSize; Map<String, Object> resultMap = Maps.newHashMap(); resultMap.put("errorCode", 0); resultMap.put("message", "操作成功"); resultMap.put("data", orders); resultMap.put("totalRow", counts); resultMap.put("pages", (int) Math.ceil(pages)); // 查询统计数据 if (StringUtil.isNotEmpty(totalSql)) { Record total = this.dbPro().findFirst(totalSql); if (iConvertResult != null) { iConvertResult.run(total); } resultMap.put("total", total); } return resultMap; } /** * 通用查询列表信息 * * @param nameSpace sql 文件的命名空间 * @param sqlMap sql参数 * @param iConvertResult * @return */ protected Map<String, Object> listByNameSpace(String nameSpace, Map<String, Object> sqlMap, IConvertResult iConvertResult) { String sqlList = this.dbPro().getSqlPara(nameSpace + NameSpaceSqlSuffix.LIST.getValue(), sqlMap).getSql(); String sqlCount = this.dbPro().getSqlPara(nameSpace + NameSpaceSqlSuffix.COUNT.getValue(), sqlMap).getSql(); String sqlTotal = null; try { sqlTotal = this.dbPro().getSqlPara(nameSpace + NameSpaceSqlSuffix.TOTAL.getValue(), sqlMap).getSql(); } catch (Exception e) { log.info("sqlTotal === 没有统计相关 sql"); } int pageSize = Integer.parseInt(String.valueOf(sqlMap.get("pageSize"))); return this.listBySql(sqlList, sqlCount, sqlTotal, pageSize, iConvertResult); } /** * 查询并转换列表数据 * * @param sql 查询列表数据sql * @param iConvertResult 数据转换 * @return 转换后的列表数据 */ private List<Record> doSqlAndResultConvert(String sql, IConvertResult iConvertResult) { List<Record> orders = this.dbPro().find(sql); if (iConvertResult != null) { iConvertResult.run(orders); } return orders; } /** * 导出文件 * * @param nameSpace * @param sqlMap * @param iConvertResult * @return */ private List<Record> exportByNameSpace(String nameSpace, Map<String, Object> sqlMap, IConvertResult iConvertResult) { // 要导出的数据信息(已经转换) return doSqlAndResultConvert(this.dbPro().getSqlPara(nameSpace + NameSpaceSqlSuffix.EXPORT.getValue(), sqlMap).getSql(), iConvertResult); } /** * 查询信息列表 * * @param sqlMap 查询条件 * @return 响应结果数据 */ public Map<String, Object> list(Map<String, Object> sqlMap) { log.info("this.sqlNameSpace() ============= " + this.sqlNameSpace()); return this.listByNameSpace(this.sqlNameSpace(), sqlMap, this.iConvertResult()); } /** * 生成导出文件 * * @param sqlMap 查询条件 * @param fileSuffixName 生成文件名称后缀 * @param sheetName 工作表标题名称 * @return 要导出的文件对象 * @throws IOException * @throws URISyntaxException */ public File export(Map<String, Object> sqlMap, String fileSuffixName, String sheetName) throws IOException, URISyntaxException { // 需要切换sql 命名空间, 和 结果转换类 List<Record> records = this.exportByNameSpace(this.sqlNameSpace(), sqlMap, this.iConvertResult()); // 执行相应的导出操作 Workbook wb = new XSSFWorkbook(); // 必须定制化操作 this.doSheet(wb, records, sheetName); return ExportPoiUtil.createExportFile(wb, fileSuffixName); } /** * 由子类提供具体处理装换的数据 * * @param wb * @param recordList * @param sheetName */ protected abstract void doSheet(Workbook wb, List<Record> recordList, String sheetName); /** * 定义使用sql命名空间后缀 */ enum NameSpaceSqlSuffix { LIST("查询列表", ".list"), COUNT("查询数量", ".count"), TOTAL("查询统计", ".total"), EXPORT("导出文件", ".export"); private String name; private String value; NameSpaceSqlSuffix(String name, String value) { this.name = name; this.value = value; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } }
五、成绩
在后续业务开展过程中,此基础业务层代码封装发挥了较好的作用,
大大缩短了开发时间,提高了工作效率,同时也提高了程序的易维护性。