关于后台部分业务重构的思考及实践
关于后台部分业务重构的思考及实践
作者: 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、几点约定
- 前端: startTime 、endTime、pageNo、pageSize、
- 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、试试说出作者进行重构代码的心情?