In the auto finance business scenario, the financial plan takes the capital as the large dimension. Capital access rules further control the scene conditions of customer information, vehicle information and loan intention. Therefore, capital access rules are also essential for financial products, which constitute a part of the rule engine of financial products.

Car finance past life album serial series, follow wechat public account [Autumn night without frost]

Car finance | financial product center of incarnations

Financial | car GPS incarnations of the audit system

Car financial | basic data platform of incarnations

Car incarnations of financial center | contract system

Car financial | financial product rules engine incarnations (last)

Car financial | financial product rules engine incarnations (medium)

Car financial | financial product rules engine incarnations (next)

Car financial | I M in the two years

Warm tip: the full text is a total of more than 2500 words, which is written on the way to work in the subway. The material code is arranged over the weekend. It is not easy. Draft on 26 November 2020, final draft on 29 November 2020.

@[toc]

A, takeaway

In the last chapter, we described the development history of the rule engine from the rule engine of financial products. In this chapter, we will focus on the development history of capital access rules and technical scheme design.

Second, the “Dark” period

At first, there were no investors and access rules in financial products. Appropriately, in order to realize business logic control of investors, each business system of auto finance adopted a scheme by judging whether the name of the financial scheme contained a certain investor. Generally speaking, financial scheme names can be justified if they follow a strict code. But once the name is not in accordance with the conventional card, the business system is sure to bug, in fact, the original had also appeared because of the name of the fault, which also reflects the original design is not reasonable and eventually lead to tragedy.

At this moment, have you ever encountered such a design struggle, for a moment of simple design or poor consideration, then left a criticism, this criticism and then in the requirements iteration or staff change, inadvertently catch a bug. This also reminds us that as programmers, we need to respect the right to write code, think more, write less bugs, I believe that one day you will regret, thanks to their own wise choice.

Development period

In the second half of 2018, the APP of auto finance business line was reconstructed for the first time. This time, for the financial product system, the product proposed the requirement design of capital and capital access rules. I believe it is not too late to save the car financial “people” brought dawn.

At this time, the Financial Products Center has completed a reconstruction, separating car-Product platform and Car-Heil, which also lays a foundation for APP reconstruction and promotes the improvement of business process for audit efficiency and interactive experience.

In terms of demand design, for the financial product system, the management of the capital and the allocation of the access rules of the capital are increased. However, as the leader of the technical design of financial products, I put some thought into the design of a good technical scheme.

Demand analysis

The capital access rules are configured from the latitude of the capital, and several rules can be configured. Each rule also contains several items, such as age, household registration, salary, etc., for the principal lender. For vehicles, cars, if used, including vehicle age, mileage; For loan intention, including down payment ratio, loan amount, total loan, actual sales price, etc.

For the APP, the capital access rules need to list the copy of each matching rule, for example, the information of the main lender: age does not meet (20 ~ 60). When I analyze the requirements and design the technical scheme, I consider the following points:

  • Each item belongs to a different category, such as the main lender, vehicle and loan information. It is important to make sure that the returned item must carry the specific category, so as to remind APP users more friendly and accurately.
  • Each item can be further classified into two types: interval and range. For example, interval and down payment ratio (30% ~ 100%); Such as scope, car (LCV, passenger car).
  • These two types can be further extracted to classify numeric and character classes.
  • If these rules and conditions are maintained in a sub-table according to the attribute-attribute value relationship, it is not so simple for the household registration in the information of the principal lender. Its storage is not the same as these, which means that another sub-table needs to be divided.
  • These rule classification items, I maintain through Java enumeration maintenance, but the database store is enumeration index, for APP interface return must be enumeration value.
  • For access rule input items, the input parameter is not an enumeration index, but an index value.

5. Core design

Through sorting and analysis, I think design mode can be introduced in code implementation to better complete the code design, and at the same time improve the expansibility of the code to support the subsequent change in demand scenarios.

I divide the main lender information (proser access handler), vehicle information (CarInfoAccessHandler) and loan intent information (LoanAccessHandler) into three subclasses to deal with the configuration items of their own rules. These three subclasses complete the inspection of the subitems of their own rules. The relationships of subclasses are linked by a chain of responsibilities. At the same time, rule conditions are further abstracted (AbstractStrategy), and MapperConfig is implemented through an enumeration class, as shown in the core code below.

1. AbstractAccessHandler

The above three processors.

AbstractAccessHandler

/ * * *@description: Access handler *@Date : 2018/7/7 下午7:45
 * @Author: Shi Dongdong -Seig Heil */
public abstract class AbstractAccessHandler {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    /** * Processor name */
    protected String handlerName;
    /** * context */
    protected FundAccessContext context;
    /** * next node */
    protected AbstractAccessHandler next;

    public AbstractAccessHandler(String handlerName, AbstractAccessHandler next) {
        this.handlerName = handlerName;
        this.next = next;
    }

    /** * call method *@param context
     */
    public final void execute(FundAccessContext context){
        this.prepare(context);
        this.call();
        this.after();
    }

    /** ** process the request */
    public abstract void call(a);

    /** * initialize *@param context
     */
    public final void prepare(FundAccessContext context){
        this.context = context;
    }

    /** ** post-processing */
    public final void after(a){
        if(null! = next){ next.execute(this.context); }}/** * Whether there is a next node *@return* /
    public boolean hasNext(a) {
        return null! = next; }/** * Access processing */
    protected final void access(MapperConfig.MapperEnumInterface[] values){
        try {
            FundAccessDTO accessDTO = context.getAccessDTO();
            Map<String,Object> beanMap = BeanMapper.objectToMap(accessDTO);
            // Check the other fields of the main lender
            for(MapperConfig.MapperEnumInterface accessEnum : values){
                final boolean hasSkipCondition = null! = accessEnum.skipCondition() && !"".equals(accessEnum.skipCondition());
                if(hasSkipCondition) {
                    AviatorContext ctx = AviatorContext.builder().expression(accessEnum.skipCondition()).env(beanMap).build();
                    if (AviatorExecutor.executeBoolean(ctx)){
                        continue;
                    }
                }
                String fileName = accessEnum.filed();
                boolean emptyValue = null == beanMap.get(fileName) || "".equals(beanMap.get(fileName));
                if(accessEnum.filter() || emptyValue){
                    continue;
                }
                StrategyContext strategyContext = new StrategyContext(context);
                AbstractStrategy strategy = SimpleStrategyFactory.create(strategyContext,accessEnum);
                strategy.access();
                if(! strategyContext.isAccess()){ context.getMessages().add(strategyContext.getTips()); } } context.setAccess(context.getMessages().size()==0);
        } catch (Exception e) {
            logger.error("{} handle exception",handlerName,e); }}protected final String formatMessage2Collection(String message, Collection<String> collection){
        return MessageFormat.format(message,collection.stream().map(Object::toString).collect(Collectors.joining(","))); }}Copy the code

CarInfoAccessHandler

/ * * *@description: Vehicle information access Handler *@Date2018/7/8 10:39 am *@Author: Shi Dongdong -Seig Heil */
public class CarInfoAccessHandler extends AbstractAccessHandler {

    public CarInfoAccessHandler(String handlerName, AbstractAccessHandler next) {
        super(handlerName, next);
    }

    @Override
    public void call(a) {
        try {
            super.access(MapperConfig.CarAccessEnum.values());
        } catch (Exception e) {
            logger.error("{} handle exception",handlerName,e); }}}Copy the code

2. Access Rule Responsibility Chain (AbstractAccess sexecutor)

AbstractAccessExecutor

/ * * *@description:
 * @Date: 2018/7/7 7:16 PM *@Author: Shi Dongdong -Seig Heil([email protected]) */
public abstract class AbstractAccessExecutor implements ChainExecutor{
    /** * context */
    protected FundAccessContext context;
    /** * processor set */
    protected List<AbstractAccessHandler> handlerList;

    /** * constructor *@param context
     */
    public AbstractAccessExecutor(FundAccessContext context) {
        this.context = context;
    }

    /** * initializes */
    protected void prepare(a){
        buildChain();
    }

    @Override
    public void execute(a) {
        this.prepare();
        handlerList.forEach(current -> {
            if(current.hasNext()){ current.execute(context); }}); }}Copy the code

FundAccessExecutor

/ * * *@description: Fund access actuator *@Date: 2018/7/8 11:36 am *@Author: Shi Dongdong -Seig Heil */
public class FundAccessExecutor extends AbstractAccessExecutor {
    /** * constructor **@param context
     */
    public FundAccessExecutor(FundAccessContext context) {
        super(context);
    }

    @Override
    public void buildChain(a) {
        this.handlerList = new ArrayList<>(3);
        this.handlerList.add(
                new ProposerAccessHandler("Master lender access".new CarInfoAccessHandler("Vehicle Access".new LoanAccessHandler("Auto Loan Access".null)))); }}Copy the code

3. Rule Conditional Strategy (AbstractStrategy)

AbstractStrategy

/ * * *@description: Abstract policy class *@Date: 2018/7/11 12:01 PM *@Author: Shi Dongdong -Seig Heil */
public abstract class AbstractStrategy {
    protected final String EMPTY = "";
    protected final String ZERO_STR = "0";
    /** * rule enumeration */
    protected MapperConfig.MapperEnumInterface accessEnum;
    /** * Policy file object */
    protected StrategyContext strategyContext;
    /** * The data object to validate the Map container */
    protected Map<String,Object> beanMap;
    /** * Data rule object Map container */
    protected Map<String,Object> propMap;
    /** * Store data dictionary Map container */
    protected Map<String,Map<String, String>> dictMap;
    /** * Rule data business entity object */
    protected FundRuleDataBo dataBo;
    /** * field name */
    protected String filedName;
    /**
     * 校验消息
     */
    protected String tips;
    /** * Whether to pass */
    protected boolean access;

    public AbstractStrategy(MapperConfig.MapperEnumInterface accessEnum, StrategyContext strategyContext) {
        this.accessEnum = accessEnum;
        this.strategyContext = strategyContext;
    }

    /** * abstract method, need to subclass */
    protected abstract void accept(a);

    /** * initializes */
    protected final void init(a){
        FundAccessContext accessContext = strategyContext.getAccessContext();
        this.beanMap = BeanMapper.objectToMap(accessContext.getAccessDTO());
        this.dataBo = accessContext.getFundRuleDataBo();
        this.propMap = dataBo.getPropMap();
        this.dictMap = dataBo.getDictMap();
        filedName = accessEnum.filed();
    }

    /** * External call method */
    public final void access(a){
        init();
        accept();
        after();
    }

    /** ** post-processing */
    public final void after(a){
        this.strategyContext.setAccess(access);
        this.strategyContext.setTips(tips);
    }

    protected final String formatMessage(String message,Object... values){
        return MessageFormat.format(message,values);
    }
    protected final String formatMessage2Collection(String message, Collection<Object> collection){
        return MessageFormat.format(message,collection.stream().map(Object::toString).collect(Collectors.joining(","))); }}Copy the code

AbstractRangeStrategy

/ * * *@description: Abstract interval class Policy class *@Date: 2018/7/11 12:14 PM *@Author: Shi Dongdong -Seig Heil */
public abstract class AbstractRangeStrategy extends AbstractStrategy implements RangeValue<Map<String.Object>,Number> {

    protected final String MIN = "Min";
    protected final String MAX = "Max";

    public AbstractRangeStrategy(MapperConfig.MapperEnumInterface accessEnum, StrategyContext strategyContext) {
        super( accessEnum, strategyContext);
    }

    @Override
    protected void accept(a) {
        String message = accessEnum.message();
        BigDecimal srcValue = new BigDecimal(Objects.toString(beanMap.get(filedName),ZERO_STR));
        BigDecimal min = (BigDecimal)min(propMap);
        BigDecimal max = (BigDecimal)max(propMap);
        srcValue = new BigDecimal(Objects.toString(beanMap.get(filedName),ZERO_STR));
        access = srcValue.compareTo(min) >= 0 && max.compareTo(srcValue) >= 0;
        if(! access){ tips = formatMessage(message,min,max); }}}Copy the code

AbstractScopeStrategy

/ * * *@description: Abstract scope class Policy class *@Date: 2018/7/11 12:14 PM *@Author: Shi Dongdong -Seig Heil */
public abstract class AbstractScopeStrategy extends AbstractStrategy implements ScopeValue<Map<String.Object>,Object> {

    public AbstractScopeStrategy(MapperConfig.MapperEnumInterface accessEnum, StrategyContext strategyContext) {
        super( accessEnum, strategyContext);
    }

    @Override
    protected void accept(a) {
        MapperConfig.EnumType enumType = accessEnum.enumType();
        String message = accessEnum.message();
        EnumValue[] enumValues = accessEnum.enums();
        String srcValue = Objects.toString(beanMap.get(filedName),EMPTY);
        List<Object> desValues = scope(propMap);
        access = desValues.contains(srcValue);
        if(access){
            return;
        }
        switch (enumType){
            case INDEX:
                message = formatMessage(message, EnumConvert.convertIndex2String(enumValues,desValues));
                break;
            case NAME:
                message = formatMessage(message, EnumConvert.convertIndex2String(enumValues,desValues));
                break;
            case DESC:
                message = formatMessage(message, EnumConvert.convertIndex2String((EnumDesc[]) enumValues,desValues, EnumDesc::getDesc));
                break;
            case DB:
                message = formatMessage(message, desValues.stream().map(d -> dictMap.get(filedName).get(d.toString())).collect(Collectors.joining(",")));
                break;
            default:
                message = formatMessage2Collection(message,desValues);
                break; } tips = message; }}Copy the code

DecimalRangeStrategy

/ * * *@description: Number range class Policy class *@Date: 2018/7/11 12:14 PM *@Author: Shi Dongdong -Seig Heil */
public class DecimalRangeStrategy extends AbstractRangeStrategy {

    public DecimalRangeStrategy(MapperConfig.MapperEnumInterface accessEnum, StrategyContext strategyContext) {
        super( accessEnum, strategyContext);
    }

    @Override
    public Number max(Map<String, Object> propMap) {
        return new BigDecimal(Objects.toString(propMap.get(filedName + MAX),ZERO_STR));
    }

    @Override
    public Number min(Map<String, Object> propMap) {
        return newBigDecimal(Objects.toString(propMap.get(filedName + MIN),ZERO_STR)); }}Copy the code

StringScopeStrategy

/ * * *@description: string range class Policy class *@Date: 2018/7/11 12:14 PM *@Author: Shi Dongdong -Seig Heil */
public class StringScopeStrategy extends AbstractScopeStrategy {

    public StringScopeStrategy(MapperConfig.MapperEnumInterface accessEnum, StrategyContext strategyContext) {
        super( accessEnum, strategyContext);
    }

    @Override
    public List<Object> scope(Map<String, Object> propMap) {
        String desValue = Objects.toString(propMap.get(filedName),EMPTY);
        returnStringTools.toList(desValue,Object.class); }}Copy the code

SimpleStrategyFactory

/ * * *@description: Policy simple factory class *@Date: 2018/7/12 8:33 PM *@Author: Shi Dongdong -Seig Heil([email protected]) */
public final class SimpleStrategyFactory {
    /** * create method *@param accessEnum
     * @return* /
    public static AbstractStrategy create(StrategyContext strategyContext,MapperConfig.MapperEnumInterface accessEnum){
        AbstractStrategy strategy = null;
        MapperConfig.FiledType filedType = accessEnum.filedType();
        MapperConfig.ValueType valueType = accessEnum.valueType();
        if(MapperConfig.ValueType.STRING == valueType && MapperConfig.FiledType.SCOPE == filedType){
            strategy = new StringScopeStrategy(accessEnum,strategyContext);
        }
        if(MapperConfig.ValueType.DECIMAL == valueType && MapperConfig.FiledType.RANGE == filedType){
            strategy = new DecimalRangeStrategy(accessEnum,strategyContext);
        }
        return strategy;
    }

    private SimpleStrategyFactory(a){}}Copy the code

4. Rule field configuration (MapperConfig)

/ * * *@description: Rule field mapping enumeration class *@Date: 2018/7/8 11:04 am *@Author: Shi Dongdong -Seig Heil */
public final class MapperConfig {
    /** * Age, industry, monthly income after tax, driver's license, province and city of household registration */
    public enum ProposerAccessEnum implements MapperEnumInterface{
        age(false."age"."Primary lender access :[age] does not meet range ({0}~{1})",FiledType.RANGE,ValueType.DECIMAL,EnumType.NONE,null.null),
        nowIndustry(false."nowIndustry"."Master lender access :[industry] does not meet scope ({0})",FiledType.SCOPE,ValueType.STRING,EnumType.NONE,null.null),
        provinceName(true."provinceName"."Admission of principal loaner :[province of domicile] does not meet ({0})",FiledType.SCOPE,ValueType.STRING,EnumType.NONE,null.null),
        cityName(true."cityName"."Access of principal lender :[domicile city] does not meet ({0})",FiledType.SCOPE,ValueType.STRING,EnumType.NONE,null.null),
        // omit other...
        ;
        private boolean filter;
        private String field;
        private String message;
        private FiledType filedType;
        private ValueType valueType;
        private EnumType enumType;
        private EnumValue[] enums;
        private String skipCondition;

        ProposerAccessEnum(boolean filter,String field, String message,FiledType filedType,ValueType valueType,EnumType enumType,EnumValue[] enums,String skipCondition) {
            this.filter = filter;
            this.field = field;
            this.message = message;
            this.filedType = filedType;
            this.valueType = valueType;
            this.enumType = enumType;
            this.enums = enums;
            this.skipCondition = skipCondition;
        }
        / / omit getter/setter
    }
    /** * Vehicle information * Used car, license plate type, model, vehicle age (month), mileage */
    public enum CarAccessEnum implements MapperEnumInterface{
        carAge(false."carAge"."Vehicle access :[used car age] does not meet ({0}~{1})",FiledType.RANGE,ValueType.DECIMAL,EnumType.NONE,null."isOld==0"),
        carAgeAddLoanPeriods(false."carAgeAddLoanPeriods"."Vehicle access :[age of second-hand car + loan term] does not meet ({0}~{1})",FiledType.RANGE,ValueType.DECIMAL,EnumType.NONE,null."isOld==0"),
        carMiles(false."carMiles"."Vehicle access :[used car mileage] does not meet ({0}~{1})",FiledType.RANGE,ValueType.DECIMAL,EnumType.NONE,null."isOld==0"),
        carType(false."carType"."Vehicle access :[model] does not meet the range ({0})",FiledType.SCOPE,ValueType.STRING,EnumType.NAME, ConstEnum.CarTypeEnum.values(),null),
        carLicenseType(false."carLicenseType"."Vehicle access :[license plate type] does not meet the scope ({0})",FiledType.SCOPE,ValueType.STRING,EnumType.DB, null.null),
        // omit other...
        ;
        // omit the fileds getter/setter
    }
    /** * Auto loan information * repayment period, auto loan amount, down payment ratio */
    public enum LoanAccessEnum implements MapperEnumInterface{
        applyLoanPeriods(false."applyLoanPeriods"."Auto loan access :[loan term] does not meet the scope ({0})",FiledType.SCOPE,ValueType.STRING,EnumType.INDEX, ConstEnum.LoanPeriodsEnum.values(),null),
        applyCarLoanAmount(false."applyCarLoanAmount"."Auto loan access :[auto loan amount] does not meet ({0}~{1})",FiledType.RANGE,ValueType.DECIMAL,EnumType.NONE,null.null),
         // omit other...
        ;
        // omit the fileds getter/setter
    }

    /** * enumerates the interface to implement */
    public interface MapperEnumInterface{
        /** * Whether to filter, this field does not get * from pd_fund_ruLE_prop@return* /
        boolean filter(a);
        /** * Field attribute name *@return* /
        String filed(a);
        /** ** copywriting *@return* /
        String message(a);
        /** * Field type *@return* /
        FiledType filedType(a);
        /** * Field value type *@return* /
        ValueType valueType(a);
        /** * Enumeration value type *@return* /
        EnumType enumType(a);
        /** * corresponds to enumeration *@return* /
        EnumValue[] enums();

        /** * Jump condition */
        String skipCondition(a);
    }

    /** * Enumeration value type */
    public enum EnumType{
        INDEX, // Indicates to obtain the corresponding index attribute through enumeration
        NAME, // Means to obtain the name attribute through enumeration
        DESC, // Indicates to obtain the desc attribute through enumeration maintenance
        DB, // Means maintenance through sy_arg_control data dictionary tables, such as approval processes
        NONE // means non-enumerable fields, such as loan amount, age, etc
    }

    /** * field type enumeration */
    public enum FiledType{
        SCOPE, // Range categories, such as cars, repayment terms, etc
        RANGE // Period categories, such as age, loan amount, monthly income after tax
    }

    /** * Field value type */
    public enum ValueType{
        STRING, // A string of characters
        DECIMAL // Number types, including Integer,Long,BigDecimal, etc}}Copy the code

5. Rule Access Business Object (FundRuleDataBo)

/ * * *@description: Funder access rules data business entity object *@Date: 2018/7/7 6:34 PM *@Author: Shi Dongdong -Seig Heil */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class FundRuleDataBo {
    /** * Rule attributes entity object */
    private Map<String,Object> propMap;
    /** * Map rule structure * < data dictionary type key, data dictionary set > */
    private Map<String,Map<String, String>> dictMap;
    /** ** Map rule structure * < province, city set > */
    private Map<String,List<String>> censusMap;
}
Copy the code

6. Rule AccessContext object (FundAccessContext)

/ * * *@description: Funder rule access context *@Date: 2018/7/5 3:48 PM *@Author: Shi Dongdong -Seig Heil */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FundAccessContext {
    /** * Whether to access through */
    private boolean access;
    /** * DTO object */
    private FundAccessDTO accessDTO;
    /** * Funder rules data entity business object */
    private FundRuleDataBo fundRuleDataBo;
    /** * Check information */
    private List<String> messages = Lists.newArrayList();

    public FundAccessContext(boolean access, FundAccessDTO accessDTO, FundRuleDataBo fundRuleDataBo) {
        this.access = access;
        this.accessDTO = accessDTO;
        this.fundRuleDataBo = fundRuleDataBo; }}Copy the code

7. To summarize

  • MapperConfig: provides extensibility for adding or adjusting rule condition fields, and provides unified management configuration for modifying returned APP copywriting.
  • AbstractAccessHandler: provides extensibility for classification processing rules.

As shown above, the API interface is provided externally to the sequence diagram of the entire related class.

Six, tail

Frankly speaking, this investor access rule is a commendable module of my code design, which adopts the design pattern (chain of responsibility, template method, strategy, factory) to maintain flexible scalability and scalability, laying a foundation for subsequent requirements iteration and development and maintenance.