背景:最近接手了一个资金流向监测的项目,其中有一个模块功能是需要实现对机构银行动账明细的监测,如交易对方的户名是否在黑名单、交易备注里面是否有非法的关键字如烟酒、交易时间是否在非法的时间段如凌晨一点到凌晨五点等。
当前实现的部分的伪代码如下:
java
private void checkTransactionAlarm(TransactionHistoryEntity transactionHistoryEntity) {
// 查询机构信息
WebInstitutionInfoEntity monitorInstitution = institutionInfoService.getOne(transactionHistoryEntity);
if (monitorInstitution == null) {
return;
}
List<Integer> warnList = new ArrayList<>();
List<String> warnInfoList = new ArrayList<>();
// 白名单告警,判断对方账户是否在白名单中
boolean inWhiteList = checkWhiteList(monitorInstitution, transactionHistoryEntity, warnList, warnInfoList);
if (inWhiteList) {
return;
}
// 黑名单告警,判断对方账户是否在黑名单中
checkBlackList(monitorInstitution, transactionHistoryEntity, warnList, warnInfoList);
// 单次交易金额告警,查询账户单次交易的策略配置,可能为空,为空则没有策略
checkSingleMoney(monitorInstitution, transactionHistoryEntity, warnList, warnInfoList);
// 交易时间告警
checkDealTime(monitorInstitution, transactionHistoryEntity, warnList, warnInfoList);
// 4.敏感词告警
checkSensetiveWords(monitorInstitution, transactionHistoryEntity, warnList, warnInfoList);
// 5.单次交易历史基线偏离
checkSingleDealBaseline(monitorInstitution, transactionHistoryEntity, warnList, warnInfoList);
if (warnList.isEmpty()) {
// 不存在告警
return;
}
// 开始存告警和发通知
WebTransactionAlarmEntity alarmEntity = buildSingeWarn(monitorInstitution,
transactionHistoryEntity,
warnList, warnInfoList);
alarmService.save(alarmEntity);
webInstitutionInfoMapper.updateAlarmStatus(monitorInstitution.getId());
// 发短信通知
sendMsgWithInstitution(monitorInstitution);
}
当前方式实现的痛点与糟点:
- 由于开发并不规范、所有规则的校验都在一个方法里面。几百行啊几百行 db操作和业务混合校验在一起。
- 当前实现无法改变校验规则的校验顺序(每一次修改都需要动一大片代码),都需要重启项目
- 一旦增加新的校验规则、都需要改变此方法,不利于测试也不方便调试。
为了避免后续相关需求的发生变化时、再来修改这一部分的代码,如更改校验顺序,不得不优化这一部分代码,仔细分析这一部分逻辑,所有的单个规则都遵循这三个步骤:
- 加载规则(数据库或者缓存里面)
- 动账校验
- 返回校验结果
因此聪明的您肯定会想到使用模版模式,将动账校验交由子类实现、其余各步骤在父类中实现,到这里解决了一个问题即所有规则的加载和业务校验都混合在一起的情况。此时还剩下另一个问题需要解决: 即当需要修改校验顺序时,如何在不重启的情况下,满足此需求;由于校验顺序是一个接着一个、那么我们肯定会想到过滤器链这个东西,毕竟没有那个 javaer 没学过这个!如果碰巧还了解过设计模式那么就会自然而然的想到责任链模式。实际上问题在这里已经得到解决了。
在有足够的理论支撑以及优化方向下(模版模式 + 责任链模式),以下代码为演示代码、并不是实际项目里面,毕竟我不想被认出来。
校验规则的建模:
java
@Data
public class RuleEntity {
private Long ruleId;
// 执行顺序
private Integer ruleOrder;
// 字符类[ 黑名单 1| 白名单 2 ] 、 时间类3 24小时制、金额类4 自定义扩展
private Integer ruleType;
// 如果是时间类,则有值,否则给个默认值就好了
private LocalTime startTime;
private LocalTime endTime;
// 方便查看
private String ruleName;
// 如果是金额类、则为对应的金额的字符串
private String value;
// 1> 2>= 3< 4<= 5=
private Integer operateType;
}
动账交易明细的建模(省略其他内容):
java
@Data
public class TransactionEntity {
// 金额
private long amount;
// 交易日期
private String transactionDate;
// 交易时间
private String transactionTime;
// 被监管的户名
private String fromUsername;
// 对方户名
private String toUsername;
}
其他实体类:
java
// 动账交易上下文,
@Data
public class TransactionContext {
private TransactionEntity transaction;
private ValidationResult validationResult;
public TransactionContext(TransactionEntity transaction) {
this.transaction = transaction;
this.validationResult = new ValidationResult();
}
}
// 校验结果,
public class ValidationResult {
private boolean passed = true;
private List<String> errors = new ArrayList<>();
private Map<String, Object> attributes = new HashMap<>();
}
首先使用模板模式定义单个规则的执行流程
java
public interface RuleFilter {
RuleEntity getRule();
void validate(TransactionContext context);
}
public abstract class AbstractFilter implements RuleFilter {
@Override
public RuleEntity getRule() {
return loadRule();
}
protected abstract RuleEntity loadRule();
public void check(TransactionContext context){
RuleEntity rule = loadRule();
validate(context);
afterValidate(context);
}
// 后置钩子函数,可以做记录、打印校验结果等操作、取决于自己的业务、甚至可以做是否进行后续继续校验的操作
protected abstract void afterValidate(TransactionContext context);
@Override
public void validate(TransactionContext context) {
throw new UnsupportedOperationException("Unimplemented method 'validate'");
}
}
以金额校验和交易时间规则举例:
java
public class AmountFilter extends AbstractFilter {
// 模拟从数据库加载,实际上可以在父类上注入对应的mapper,直接查就好了
@Override
protected RuleEntity loadRule() {
RuleEntity rule = new RuleEntity();
rule.setRuleId(1L);
rule.setRuleOrder(1);
rule.setRuleType(4);
rule.setRuleName("金额过滤");
rule.setValue("1000");
rule.setOperateType(2);
return rule;
}
@Override
public void validate(TransactionContext context) {
TransactionEntity transaction = context.getTransaction();
if (calculate(transaction.getAmount(), loadRule().getValue(),
loadRule().getOperateType())) {
context.getValidationResult().addError("金额过滤", "金额 " + transaction.getAmount() + " " + getOperateTypeName(loadRule().getOperateType()) + " " + loadRule().getValue());
}
}
private boolean calculate(Long amount, String value, Integer operateType) {
return switch (operateType) {
case 1 -> amount > Long.parseLong(value);
case 2 -> amount >= Long.parseLong(value);
case 3 -> amount < Long.parseLong(value);
case 4 -> amount <= Long.parseLong(value);
case 5 -> amount == Long.parseLong(value);
default -> false;
};
}
private String getOperateTypeName(Integer operateType) {
return switch (operateType) {
case 1 -> "大于";
case 2 -> "大于等于";
case 3 -> "小于";
case 4 -> "小于等于";
case 5 -> "等于";
default -> "未知";
};
}
@Override
protected void afterValidate(TransactionContext context) {
}
}
时间校验规则如下:
java
public class TransactionTimeFilter extends AbstractFilter {
@Override
protected RuleEntity loadRule() {
RuleEntity rule = new RuleEntity();
rule.setRuleName("时间过滤");
rule.setRuleOrder(2);
rule.setStartTime(LocalTime.of(20, 0));
rule.setEndTime(LocalTime.of(10, 0));
return rule;
}
@Override
public void validate(TransactionContext context) {
RuleEntity rule = loadRule();
TransactionEntity transaction = context.getTransaction();
ValidationResult validationResult = context.getValidationResult();
String transactionTime = transaction.getTransactionTime();
LocalTime transactionTimeLocalTime = LocalTime.parse(transactionTime);
if (isInExceptionTimeRange(transactionTimeLocalTime, rule.getStartTime(), rule.getEndTime())){
validationResult.addError("时间过滤", "异常时间交易");
}
}
private boolean isInExceptionTimeRange(LocalTime time, LocalTime startTime, LocalTime endTime) {
// 时间规则是跨天 如 20:00~10:00 交易时间在23:00~01:00 23:00~01:00 异常时间
if (startTime.isAfter(endTime)) {
// 跨天:在 startTime~23:59 或 00:00~endTime 范围内
return time.isAfter(startTime) || time.isBefore(endTime);
}
// 同一天:在 startTime ~ endTime 范围内(包含起点,不包含终点)10:00~20:00
return time.isAfter(startTime) && time.isBefore(endTime);
}
@Override
protected void afterValidate(TransactionContext context) {
}
}
上述已经实现了两个校验规则、现在只需要把他们组装成一个链表结果就好了
java
public class DynamicFilterChain {
// 直接借助SpringBoot的能力,可以全部自动注入
private List<RuleFilter> filters;
public void init(){
// 模拟加载各个过滤器
List<RuleFilter> filters = Arrays.asList(new AmountFilter(), new TransactionTimeFilter());
// 排序校验规则、这一步就已经实现了顺序调整
filters.sort(Comparator.comparingInt(o -> o.getRule().getRuleOrder()));
this.filters = new ArrayList<>(filters);
}
public void doFilter(TransactionContext transactionContext) {
for (RuleFilter filter : filters) {
filter.validate(transactionContext);
// 实际上可以在这里判断是否还需要往下执行,如对方户名在白名单中,就不需要进行后续的校验了。
// 更加推荐用某一个字段来控制,这样每一条规则都可以进行判断是否后续执行了,而不用在这里写死了。
if (filter.getRule().getRuleOrder() == 1 && transactionContext.getValidationResult().isPassed()){
return;
}
}
}
}
由于代码的注释已经很详细了,也就没有过多解释的必要了,到这里优化也已经结束了,只需要把init()方法暴露给controller, 当我们修改对应的规则之后,重新刷新一下就好了。
ps: 由于被监管的账户是第二天凌晨才会拉取其动账信息,因此在这里不存在当前代码还在检验动账对应的规则却发生变化的情况,如果是实时的,则需要增加额外的控制逻辑。