關於後臺部分業務重構的思考及實踐
做者: ljmatlight 時間: 2017-09-25前端
積極主動,想事謀事,敢做敢爲,能作能爲。java
當職以來,隨着對公司業務和項目的不斷深刻,不斷梳理業務和公司技術棧。 保證在完成分配開發任務狀況下,積極思考優化方案並付諸實踐。sql
1、想法由來
因爲當前我司主要針對各大銀行信用卡平臺展開相關業務, 故不難看出,各銀行信用卡平臺雖然有各自的特性, 但其業務類似程度仍然很高,除必要的重複性工做外,仍有很大提高優化空間。 例如: 各個銀行平臺都須要對帳工做、都要安排人力去開發重複相似的功能, 且不能很好地適應新的需求變化,修改耗時費力,可維護性較差。數據庫
2、業務分析
依託具體業務場景進行分析,每一個平臺都具備對帳功能。 對帳業務: 一、主要包括列表分頁和導出功能 二、可以按照時間範圍搜索 三、列表包括分頁、金額統計、狀態轉換等等編程
優化依據:json
- 對特性業務進行差別性對待(如導出數據字段,結果轉換字段等等),
- 充分利用面向對象的思想進行合理的抽象層次建設
3、技術優化實踐
後臺技術棧爲Jfinal,LayUI。後端
關於對帳優化總體思路:設計模式
一、前端頁面發起請求,傳遞響應參數
前端傳遞參數形式以下圖:api
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類首寫字母小寫,加 」.」 加 屬性名稱的形式進行書寫。ide
二、定義dto 進行參數的bean 形式接受
因爲全部列表,都包含起始搜索時間,當前頁,每頁顯示數量,故定義基礎列表dto的Bean 以下圖所示:
/** * Description: 列表請求參數封裝 * <br /> Author: galsang */ @Data @NoArgsConstructor @AllArgsConstructor public 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 @AllArgsConstructor public class ComparedListBean extends BaseListBean { private String orderId; private String reqNo; }
三、後端使用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。
四、將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; } }
命名統一,能夠直接定位須要實現或變更的需求,方便維護
五、結果數據轉換接口
結果數據的的轉換主要分爲列表數據的轉換和單條數據的轉換,因爲轉換數據不必定相同,只要在具體的業務層進行定義內部類實現該接口run方法便可。
/** * Description: 結果類型數據轉換接口 * <br /> Author: galsang */ public interface IConvertResult { /** * 執行列表結果類型轉換 * * @param records */ void run(List<Record> records); /** * 執行單個結果類型轉換 * * @param record */ void run(Record record); }
六、抽象公共方法
通用查詢列表
/** * 查詢並轉換列表數據 * * @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); }
七、具體業務層實現
支付對帳業務層
/** * 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")); } }
八、工具類
因爲當前系統精確到分,數據庫中以int存儲分,可是前端顯示的時候要求顯示元,故可以使用此工具類進行「分」到「元」的轉換處理。
/** * Description: 記錄對象相關工具類 * <br /> Author: galsang */ @Slf4j public 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
具體代碼參見後臺對帳業務實現。
九、幾點約定
- 前端: startTime 、endTime、pageNo、pageSize、
- md – sql命名空間後綴 : list、count、total、export
4、交流提升
不足之處,還請各位同事多多指教,謝謝。
同時通過調整最終造成如下基礎業務層代碼。
BaseService 代碼以下:
/** * 基礎業務層封裝 * * @author ljmatlight * @date 2017/10/17 */ @Slf4j public 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; } } }
5、成績
在後續業務開展過程當中,此基礎業務層代碼封裝發揮了較好的做用, 大大縮短了開發時間,提升了工做效率,同時也提升了程序的易維護性。
6、提問
一、在改造過程當中,使用哪些設計模式? 二、面向接口編程在何處體現的比較明顯? 三、試試說出做者進行重構代碼的心情?