zoukankan      html  css  js  c++  java
  • 关于后台部分业务重构的思考及实践

    关于后台部分业务重构的思考及实践

    作者: ljmatlight
    时间: 2017-09-25

    积极主动,想事谋事,敢作敢为,能做能为。


    当职以来,随着对公司业务和项目的不断深入,不断梳理业务和公司技术栈。
    保证在完成分配开发任务情况下,积极思考优化方案并付诸实践。

    一、想法由来

    由于当前我司主要针对各大银行信用卡平台展开相关业务,
    故不难看出,各银行信用卡平台虽然有各自的特性,
    但其业务相似程度仍然很高,除必要的重复性工作外,仍有很大提升优化空间。
    例如: 各个银行平台都需要对账工作、都要安排人力去开发重复类似的功能,
    且不能很好地适应新的需求变化,修改耗时费力,可维护性较差。

    二、业务分析

    依托具体业务场景进行分析,每个平台都具有对账功能。
    对账业务:
    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
    @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;
    
    }
    

    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
     */
    @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 
    

    具体代码参见后台对账业务实现。

    9、几点约定

    1. 前端: startTime 、endTime、pageNo、pageSize、
    2. md – sql命名空间后缀 : list、count、total、export

    四、交流提高

    不足之处,还请各位同事多多指教,谢谢。


    同时经过调整最终形成以下基础业务层代码。

    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;
            }
        }
    
    
    }
    
    
    

    五、成绩

    在后续业务开展过程中,此基础业务层代码封装发挥了较好的作用,
    大大缩短了开发时间,提高了工作效率,同时也提高了程序的易维护性。

    六、提问

    1、在改造过程中,使用哪些设计模式?
    2、面向接口编程在何处体现的比较明显?
    3、试试说出作者进行重构代码的心情?

  • 相关阅读:
    rocketmq学习(一) rocketmq介绍与安装
    基于redis的分布式锁实现
    SSTI(服务器模板注入)学习
    PHP文件包含漏洞(利用phpinfo)复现
    ubuntu搭建vulhub漏洞环境
    sqli-labs通关教程----51~65关
    sqli-labs通关教程----41~50关
    sqli-labs通关教程----31~40关
    sqli-labs通关教程----21~30关
    sqli-labs通关教程----11~20关
  • 原文地址:https://www.cnblogs.com/ljmatlight/p/9050747.html
Copyright © 2011-2022 走看看