为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

 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、几点约定

  1. 前端: startTime 、endTime、pageNo、pageSize、

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


}

五、成绩

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


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消