關於後臺部分業務重構的思考及實踐

關於後臺部分業務重構的思考及實踐

做者: 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

具體代碼參見後臺對帳業務實現。

九、幾點約定

  1. 前端: startTime 、endTime、pageNo、pageSize、
  2. 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、提問

一、在改造過程當中,使用哪些設計模式? 二、面向接口編程在何處體現的比較明顯? 三、試試說出做者進行重構代碼的心情?

相關文章
相關標籤/搜索