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

Posted 一盏烛火 孤灯独伴我

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于后台部分业务重构的思考及实践相关的知识,希望对你有一定的参考价值。

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

作者: 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;
        }
    }


}

五、成绩

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

以上是关于关于后台部分业务重构的思考及实践的主要内容,如果未能解决你的问题,请参考以下文章

关于后台系统自动生成的一点思考

关于后台系统自动生成的一点思考

关于重构工作的一点思考

关于数据库“状态”字段设计的思考与实践

腾讯技术工程 |QQ相册后台存储架构重构与跨IDC容灾实践

微信后台异步消息队列的优化升级实践分享