背景
去年底换了公司,目前在做HR招聘系统,产品要求:管理端手动配置发布招聘要求(支持多个招聘条件AND与OR关系编排);当C端用户报名该职位时,系统执行该规则,返回报名成功或失败。根据技术调研,采用db存规则json串。然后报名时,执行该规则的方法。
前端样式
招聘岗位的多个规则关系是OR关系;同1个条件点击+号,可增加多个规则,是AND条件
代码入口
比如上述单元测试的场景:招java开发岗,要求:满足本科+35岁以下或本科+P7以上即可。
执行单测:
通过日志就能清晰的分析出,岗位配了什么规则,各规则的执行结果是什么
代码设计
类图
通过工厂+策略的设计模式,实现该规则,符合开闭原则,方便后续业务规则扩充,代码扩展
代码介绍
核心接口:InternalPositionRule 定义了规则执行的统一方法。
抽象类:AbstractPositionRule 提供了通用的比较和告警逻辑。
具体规则类:如 AgeRule、SexRule 等,继承自 AbstractPositionRule。
规则包装类:RuleWrapper 用于封装规则信息并动态创建规则实例。
规则组和规则集:RuleGroup 和 RuleSet 用于组织规则的逻辑关系(AND 和 OR)。
规则上下文:PositionRuleContext 是规则引擎的统一入口。
规则工厂:PositionRuleFactory 用于动态创建规则实例。
员工类:Employee 包含员工的基本信息,用于规则匹配。
核心代码
以下是脱敏后的核心代码
InternalPositionRule 岗位规则 接口 最顶层抽象,制定模版方法
* +-----------------------------------+
* | InternalPositionRule (接口) |
* |-----------------------------------|
* | + execute(Employee emp): boolean |
* +-----------------------------------+
* ^
* |
* |
* +-----------------------------------+
* | AbstractPositionRule<T> (抽象类) |
* |-----------------------------------|
* | + compare(String, T, T): boolean |
* |-----------------------------------|
* | - ruleValue: String |
* | - forceFlag: String |
* | - compareSymbol: String |
* +-----------------------------------+
* ^
* |
* |
* +-----------------------------------+
* | AgeRule |
* |-----------------------------------|
* | + execute(Employee emp): boolean |
* +-----------------------------------+
**/
public interface InternalPositionRule {
/**
* 执行岗位条件规则
*
* @param emp 员工信息
* @return true/false
*/
boolean execute(Employee emp);
AbstractPositionRule 岗位规则 抽象类:业务上再扩展规则,继承该类即可
@Slf4j
@Component
public abstract class AbstractPositionRule<T extends Comparable<? super T>> implements InternalPositionRule {
/**
* 执行岗位条件规则
*
* @param emp 员工信息
* @return
*/
public abstract boolean execute(Employee emp);
/**
* 比较方法
*
* @param compareSymbol 比较符号
* @param actualValue 实际值
* @param thresholdValue 设定阈值
* @return
*/
public boolean compare(String compareSymbol, T actualValue, T thresholdValue) {
boolean result;
log.info("===比较开始,比较符号【{}】,配置阈值【{}】,实际值【{}】===", compareSymbol, thresholdValue, actualValue);
//等于
if (SymbolConstant.EQ.equalsIgnoreCase(compareSymbol)) {
result = CompareUtil.equals(actualValue, thresholdValue);
//大于
} else if (SymbolConstant.GT.equalsIgnoreCase(compareSymbol)) {
result = CompareUtil.gt(actualValue, thresholdValue);
//小于
} else if (SymbolConstant.LT.equalsIgnoreCase(compareSymbol)) {
result = CompareUtil.lt(actualValue, thresholdValue);
//大于等于
} else if (SymbolConstant.GE.equalsIgnoreCase(compareSymbol)) {
result = CompareUtil.ge(actualValue, thresholdValue);
//小于等于
} else if (SymbolConstant.LE.equalsIgnoreCase(compareSymbol)) {
result = CompareUtil.le(actualValue, thresholdValue);
} else {
log.error(StrUtil.join("当前类型:", compareSymbol, InternalErrorCode.TYPE_UNSUPPORTED_ERROR.getMsg()));
throw new ServiceException(InternalErrorCode.TYPE_UNSUPPORTED_ERROR);
}
log.info("==比较结束,结果【{}】===", result);
return result;
}
/**
* TODO 产品给出告警文案?怎么触达用户?是否持久化落库?
* 告警逻辑:要求如果规则配置了告警,即使匹配失败,也允许通过,并返回告警文案
*
* @param compareResult 实际匹配结果
* @param forceFlag 禁止或告警
* @param ruleTypeEnum 规则类型
* @return
*/
public Optional<String> warn(Boolean compareResult, String forceFlag, RuleTypeEnum ruleTypeEnum) {
if (BooleanUtil.isFalse(compareResult) && ForceTypeEnum.WARN.getType().equals(forceFlag)) {
StringBuilder warnMsg = new StringBuilder().append("【").append(ruleTypeEnum.getName()).append("】").append("规则不符合;");
log.warn("告警文案:::【{}】", warnMsg);
return Optional.of(warnMsg.toString());
}
return Optional.empty();
}
}
PositionRuleContext 内部招聘-职位规则上下文,执行规则统一入口
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PositionRuleContext {
/**
* 规则集
*/
private RuleSet ruleSet;
/**
* 执行规则(执行规则的统一入口)
*
* @param emp
* @return
*/
public boolean executeRule(Employee emp) {
if (Objects.isNull(ruleSet)) {
log.info("规则集是空,不走规则引擎");
return true;
}
return ruleSet.execute(emp);
}
}
RuleSet 规则集,须知:1个招聘职位对应1个规则集,db存json结构,
1个规则集里含n个规则组,规则组之间是OR关系,每个规则组内含n个规则条件,各个规则条件是AND关系
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RuleSet {
/**
* 规则组
*/
private List<RuleGroup> ruleGroup;
public boolean execute(Employee emp) {
return ruleGroup.stream().anyMatch(condition -> condition.execute(emp));
}
}
RuleGroup 规则组
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RuleSet {
/**
* 规则组
*/
private List<RuleGroup> ruleGroup;
public boolean execute(Employee emp) {
return ruleGroup.stream().anyMatch(condition -> condition.execute(emp));
}
}
RuleWrapper 规则Wrapper类 用于封装规则的配置信息
@Slf4j
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class RuleWrapper extends AbstractPositionRule {
/**
* 规则类型
*
* @see com.suning.ebrs.module.internal.enums.RuleTypeEnum
*/
private String ruleType;
/**
* 比较符号
*/
private String compareSymbol;
//强制条件(1是,0不是)
private String forceFlag;
/**
* 规则值
*/
private String ruleValue;
@Override
public boolean execute(Employee emp) {
AbstractPositionRule rule = PositionRuleFactory.getRule(ruleType, ruleValue, compareSymbol, forceFlag);
return rule.execute(emp);
}
}
AgeRule 年龄规则
@Slf4j
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class AgeRule extends AbstractPositionRule<Integer> {
/**
* 规则值(设置阈值)
*/
private String ruleValue;
/**
* 强制条件(1是,0不是)
* 不强制,也能过通过规则,并有告警提示;
* 强制,返回实际通过结果
*/
private String forceFlag;
/**
* 比较符号
*
* @see com.suning.ebrs.module.internal.constant.SymbolConstant
*/
private String compareSymbol;
@Override
public boolean execute(Employee emp) {
log.info("===【{}】开始===", this.getClass().getSimpleName());
Integer thresholdValue = Integer.valueOf(ruleValue);
//TODO 查SAP,根据员工id查员工实际年龄
Integer actualValue = emp.getAge();
boolean compareResult = compare(compareSymbol, actualValue, thresholdValue);
Optional<String> opt = warn(compareResult, forceFlag, RuleTypeEnum.AGE_RULE);
if (opt.isPresent()) {
emp.setWarnMsg(StringUtils.isNotBlank(emp.getWarnMsg()) ? emp.getWarnMsg() + opt.get() : opt.get());
compareResult = true;
}
log.info("===【{}】结束,结果【{}】===", this.getClass().getSimpleName(), compareResult);
return compareResult;
}
}
PostLevelRule 级别规则
@Slf4j
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class PostLevelRule extends AbstractPositionRule<Integer> {
//属性值(设置阈值)
private String ruleValue;
//强制条件(1是,0不是)
private String forceFlag;
//比较符号(支持:等于,大于,小于,大于等于,小于等于)
private String compareSymbol;
@Override
public boolean execute(Employee emp) {
log.info("===【{}】开始===", this.getClass().getSimpleName());
Integer thresholdValue = Integer.valueOf(ruleValue);
//TODO 根据员工id查员工实际级别
Integer actualValue = emp.getLevel();
boolean compareResult = compare(compareSymbol, actualValue, thresholdValue);
Optional<String> opt = warn(compareResult, forceFlag, RuleTypeEnum.AGE_RULE);
if (opt.isPresent()) {
emp.setWarnMsg(StringUtils.isNotBlank(emp.getWarnMsg()) ? emp.getWarnMsg() + opt.get() : opt.get());
compareResult = true;
}
log.info("===【{}】结束,结果【{}】===", this.getClass().getSimpleName(), compareResult);
return compareResult;
}
}
Employee 员工类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
private String name; //张三
private Integer level; //6,7,8
private Integer sex; //1男,0女
private Integer age; //36
private Integer edu; //4本科 5研究生 6博士
private Integer topSchool; //重点高校:1是;0不是
private Integer political; //政治面貌 3群众,4预备党员,5党员
private List<String> certificates; //系统架构师证书,高级软考证书
//执行规则后的告警信息
private String warnMsg;
}
PositionRuleContext 职位规则上下文,执行规则统一入口
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PositionRuleContext {
/**
* 规则集
*/
private RuleSet ruleSet;
/**
* 执行规则(执行规则的统一入口)
*
* @param emp
* @return
*/
public boolean executeRule(Employee emp) {
if (Objects.isNull(ruleSet)) {
log.info("规则集是空,不走规则引擎");
return true;
}
return ruleSet.execute(emp);
}
}
PositionRuleFactory 职位规则工厂
@Slf4j
@Component
public class PositionRuleFactory {
private static final ConcurrentHashMap<String, AbstractPositionRule> ruleCache = new ConcurrentHashMap<>(16, 1);
/**
* 获取规则
*
* @param ruleType
* @param ruleValue
* @param compareSymbol
* @param forceFlag
* @return
*/
public static AbstractPositionRule getRule(String ruleType, String ruleValue, String compareSymbol, String forceFlag) {
String key = ruleType + "_" + ruleValue + "_" + compareSymbol + "_" + forceFlag;
return ruleCache.computeIfAbsent(key, k -> createRule(ruleType, ruleValue, compareSymbol, forceFlag));
}
/**
* 创建规则
*
* @param ruleType
* @param ruleValue
* @param compareSymbol
* @param forceFlag
* @return
*/
public static AbstractPositionRule createRule(String ruleType, String ruleValue, String compareSymbol, String forceFlag) {
AbstractPositionRule rule;
//年龄规则
if (RuleTypeEnum.AGE_RULE.getType().equalsIgnoreCase(ruleType)) {
rule = AgeRule.builder().ruleValue(ruleValue).forceFlag(forceFlag).compareSymbol(compareSymbol).build();
//出生日期
} else if (RuleTypeEnum.DATE_OF_BIRTH_RULE.getType().equalsIgnoreCase(ruleType)) {
rule = DateOfBirthRule.builder().ruleValue(ruleValue).forceFlag(forceFlag).compareSymbol(compareSymbol).build();
//参加工作日期
} else if (RuleTypeEnum.DATE_OF_EMPLOYMENT_RULE.getType().equalsIgnoreCase(ruleType)) {
rule = EmployeeSubgroupRule.builder().ruleValue(ruleValue).forceFlag(forceFlag).compareSymbol(compareSymbol).build();
//学历形式(在职/全日制)
} else if (RuleTypeEnum.EDU_BACKGROUND_FORM_RULE.getType().equalsIgnoreCase(ruleType)) {
rule = EduBackgroundFormRule.builder().ruleValue(ruleValue).forceFlag(forceFlag).compareSymbol(compareSymbol).build();
//性别规则
} else if (RuleTypeEnum.SEX_RULE.getType().equalsIgnoreCase(ruleType)) {
rule = SexRule.builder().ruleValue(ruleValue).forceFlag(forceFlag).compareSymbol(compareSymbol).build();
} else {
log.error("【{}】参数非法!", ruleType);
throw new ServiceException(InternalErrorCode.PARAM_ERROR);
}
return rule;
}
}
InternalRuleServiceImplTest 规则的单元测试类
@Slf4j
@SpringBootTest
@ActiveProfiles("local")
public class InternalRuleServiceImplTest {
@Resource
private InternalPositionRuleService ruleService;
@Test
public void matchTest() {
//岗位要求符合:本科+35岁以下 或 本科+P7以上
//规则组1: 本科+35岁以下
RuleGroup ruleGroup1 = this.mockRuleGroup1();
//规则组2:本科+P7以上
RuleGroup ruleGroup2 = this.mockRuleGroup2();
List<RuleGroup> ruleGroups = Arrays.asList(ruleGroup1, ruleGroup2);
RuleSet ruleSet = new RuleSet();
ruleSet.setRuleGroup(ruleGroups);
Employee emp = getEmployeeByEmpCode("123456");
System.err.println("规则匹配,单测,入参 规则信息=====>" + JSON.toJSONString(ruleSet));
System.err.println("规则匹配,单测,入参 员工信息=====>" + JSON.toJSONString(emp));
boolean result = PositionRuleContext.builder().ruleSet(ruleSet).build().executeRule(emp);
System.err.println("规则匹配,单测,匹配结果=====>" + result);
System.err.println("规则匹配,单测,告警提示=====>" + emp.getWarnMsg());
}
private RuleGroup mockRuleGroup1() {
RuleGroup ruleGroup = new RuleGroup();
List<RuleWrapper> wrappers = new ArrayList<>();
RuleWrapper wrapper1 = new RuleWrapper();
wrapper1.setForceFlag("0");
wrapper1.setRuleValue("35");
wrapper1.setRuleType(RuleTypeEnum.AGE_RULE.getType());
wrapper1.setCompareSymbol(SymbolConstant.LE);
wrappers.add(wrapper1);
RuleWrapper wrapper2 = new RuleWrapper();
wrapper2.setForceFlag("1");
wrapper2.setRuleValue("本科");
wrapper2.setRuleType(RuleTypeEnum.EDU_BACKGROUND_RULE.getType());
wrapper2.setCompareSymbol(SymbolConstant.GE);
wrappers.add(wrapper2);
ruleGroup.setRules(wrappers);
return ruleGroup;
}
private RuleGroup mockRuleGroup2() {
RuleGroup ruleGroup = new RuleGroup();
List<RuleWrapper> wrappers = new ArrayList<>();
RuleWrapper wrapper1 = new RuleWrapper();
wrapper1.setForceFlag("1");
wrapper1.setRuleValue("本科");
wrapper1.setRuleType(RuleTypeEnum.EDU_BACKGROUND_RULE.getType());
wrapper1.setCompareSymbol(SymbolConstant.GE);
wrappers.add(wrapper1);
RuleWrapper wrapper2 = new RuleWrapper();
wrapper2.setForceFlag("1");
wrapper2.setRuleValue("7");
wrapper2.setRuleType(RuleTypeEnum.POST_LEVEL_RULE.getType());
wrapper2.setCompareSymbol(SymbolConstant.GE);
wrappers.add(wrapper2);
ruleGroup.setRules(wrappers);
return ruleGroup;
}
/**
* mock员工数据
*
* @param employeeCode
* @return
*/
private Employee getEmployeeByEmpCode(String employeeCode) {
List<String> list = Arrays.asList("系统架构师证书");
return Employee.builder()
.name("张三")
//党员
.political(5)
//不是重点院校
.topSchool(0)
//本科
.edu(5)
//40岁
.age(39)
//男
.sex(1)
//系统架构师证书
.certificates(list)
//岗位6级
.level(8)
.build();
}
}