美文网首页
通用规则树引擎解决方案

通用规则树引擎解决方案

作者: StonyBlue | 来源:发表于2024-09-19 16:08 被阅读0次

    背景

    因业务需求要做一个条件可视化组件, 这个组件我设计了草图(图1)需要支持逻辑运算(OR, AND), 关系运算(大于,小于,等于,包含,区间等).
    截屏2024-09-20 13.55.28.png 截屏2024-09-20 13.55.45.png

    规则树组件

    简单调研了下蚂蚁的RuleTree, 当前内部已有的RuleView. 根据当前的需求已经现有工程的依赖, 还有部分定制功能来衡量选择.
    组件 逻辑AND 逻辑OR 右值跟随左值类型变化 关系运算 功能完整 antd4支持 定制难度
    RuleTree 中等
    RuleView 简单

    简单

    从定制难度上来说RuleView简单容易上手, 而且内部有多个场景在使用, RuleTree很老都是使用的是antd3, 和现有的技术栈也不搭.

    改造后的RuleView, 支持逻辑切换OR, AND

    import React, { useEffect } from 'react';
    import classnames from 'classnames';
    
    const OPERATOR_AND = 'AND';
    const OPERATOR_OR = 'OR';
    const OPERATORS = [OPERATOR_AND, OPERATOR_OR];
    
    export const RuleView = ({
      value,
      onChange,
      children,
      isSubTree,
    }: {
      value?: string;
      // defaultValue: 'AND' | 'OR';
      onChange?: (v: string) => void;
      children: React.ReactNode;
      isSubTree?: boolean;
    }) => {
      const [op, setOp] = React.useState(value || 'OR');
      useEffect(() => {
        if (value) {
          setOp(value);
        }
      }, [value]);
      const toggleOperator = () => {
        const nextIndex = (OPERATORS.indexOf(op) + 1) % OPERATORS.length;
        const newOp = OPERATORS[nextIndex];
        // setOp(newOp);
        onChange?.(newOp);
      };
      return (
        <div className="flex items-center w-full">
          <div
            className={classnames(
              'relative after:content-[""] after:block after:absolute after:w-7 after:top-1/2 after:bottom-0 after:border-t-2 after:left-full',
              { 'pl-7': isSubTree },
            )}
          >
            <div
              onDoubleClick={toggleOperator}
              className={classnames(
                'flex pl-2 pr-2 items-center rounded text-white h-8',
                {
                  'bg-sky-400': op === OPERATOR_AND,
                  'bg-green-400': op === OPERATOR_OR,
                },
              )}
            >
              {op}
            </div>
          </div>
          <div className="ml-7 flex">
            <div>{children}</div>
          </div>
        </div>
      );
    };
    export const RuleViewItem = ({
      children,
      isFirst,
      isLast,
    }: {
      children: React.ReactNode;
      isFirst?: boolean;
      isLast?: boolean;
    }) => {
      return (
        <div
          className={classnames(
            'relative before:block before:content-[""] before:absolute before:w-0.5 before:border-l-2',
            {
              'before:top-1/2 before:bottom-0': !isLast && isFirst,
              'before:top-0 before:bottom-1/2': !isFirst && isLast,
              'before:top-0 before:bottom-0': !isFirst && !isLast,
            },
            'after:block after:content-[""] after:absolute after:top-1/2 after:bottom-0 after:w-7 after:border-t-2',
          )}
        >
          {children}
        </div>
      );
    };
    

    样式依赖Tailwind CSS

    npm install tailwindcss postcss autoprefixer css-loader style-loader --save-dev
    npx tailwindcss init -p

    这样会得到一个tailwind.config.js, postcss.config.js文件
    检查文件postcss.config.js内容:
    module.exports = {
      plugins: {
        tailwindcss: {},
        autoprefixer: {},
      },
    };
    
    在全局index.css中添加
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    

    规则树的数据结构定义

    export interface ConditionValue {
      operation: string;
      children?: ConditionValue[];
      left?: DataValue;
      right?: DataValue;
    }
    export interface DataValue {
      kind?: string;
      type?: string;
      value?: any;
      format?: string;
    }
    

    规则组件定义

    const ConditionView = ({disabled}:{disabled?:boolean}) => {
        return <div className="flex justify-center overflow-x-auto">
            <Form.Item label="条件" name={['condition','operation']} noStyle>
                <RuleView >
                    <Form.List
                        name={['condition','children']}
                        rules={[
                            {
                                validator: (r, v) => {
                                    console.log('root condition >>>> ', r, v);
                                    if (!v || !v?.length || v?.length === 0) {
                                        return Promise.reject(new Error('条件组不能为空'));
                                    }
                                    const errors = v.map((child: ConditionValue) =>
                                        validatorRuleTreeItem(child),
                                    );
                                    return Promise.all(errors)
                                        .then(() => Promise.resolve()) // 如果所有子项验证通过
                                        .catch(err => Promise.reject(err)); // 如果有任何一个子项验证失败
                                },
                            },
                        ]}
                    >
                        {(fields, {add, remove}, {errors}) => (
                            <>
                                {fields.map((field, index) => {
                                    return (<div key={field.key}>
                                            <Form.Item label="节点" name={[field.name]} noStyle>
                                                <ConditionNodeView
                                                    key={field.key}
                                                    field={field}
                                                    fields={fields}
                                                    add={add}
                                                    remove={remove}
                                                    index={index}
                                                />
                                            </Form.Item>
                                        </div>
                                    );
                                })}
                                <RuleViewItem isLast>
                                    <div className="h-10 relative flex items-center pl-7">
                                        <Tooltip title="添加一组条件">
                                            <Button
                                                icon={<PlusOutlined />}
                                                disabled={disabled}
                                                onClick={() => add(initialFieldValue)}
                                            />
                                        </Tooltip>
                                    </div>
                                </RuleViewItem>
                                <Form.ErrorList errors={errors} />
                            </>
                        )}
                    </Form.List>
                </RuleView>
            </Form.Item>
        </div>;
    };
    
    export const ConditionNodeView = ({
                                   value,
                                   onChange,
                                   field,
                                   fields,
                                   add,
                                   remove,
                                   index,
                                   disabled,
                               }: {
        value?: ConditionValue;
        onChange?: (v: ConditionValue) => void;
        field: FormListFieldData;
        fields: FormListFieldData[];
        add: (defaultValue?: any, insertIndex?: number | undefined) => void;
        remove: (index: number | number[]) => void;
        index: number;
        disabled?: boolean;
    }) => {
        // console.log('ConditionNodeView >>>> ', value);
        if(value?.operation === 'OR' || value?.operation === 'AND') {
            return (
                <>
                    <RuleViewItem key={field.key} isFirst={index === 0}>
                        <Form.Item label="条件" name={[field.name, 'operation']} noStyle>
                            <RuleView isSubTree>
                                <Form.List name={[field.name, 'children']}>
                                    {(subFields, { add: addSubField, remove: removeSubField }, {errors:subErrors}) =>
                                        <>
                                            {subFields.map((subField, subIndex) => {
                                                return (<div key={subField.key}>
                                                        <Form.Item label="节点" name={[subField.name]} noStyle>
                                                            <ConditionNodeView
                                                                key={subField.key}
                                                                field={subField}
                                                                fields={subFields}
                                                                add={addSubField}
                                                                remove={removeSubField}
                                                                index={subIndex}
                                                            />
                                                        </Form.Item>
                                                    </div>
                                                );
                                            })}
                                            <RuleViewItem isLast>
                                                <div className="h-10 relative flex items-center pl-7">
                                                    <Tooltip title="添加一组条件">
                                                        <Button
                                                            type="dashed"
                                                            icon={<PlusOutlined />}
                                                            disabled={disabled}
                                                            onClick={() => addSubField(initialFieldValue)}
                                                        />
                                                    </Tooltip>
                                                    <Popconfirm
                                                        title="确认删除该组条件?"
                                                        disabled={fields?.length <= 1 || disabled}
                                                        onConfirm={() => { remove(field.name); }}
                                                    >
                                                        <Tooltip
                                                            title={
                                                                fields?.length !== 1 ? '删除该组条件' : undefined
                                                            }
                                                        >
                                                            <Button
                                                                type="dashed"
                                                                icon={
                                                                    <DeleteOutlined
                                                                        style={{
                                                                            color: fields?.length <= 1 || disabled ? 'gray' : '#f50',
                                                                        }}
                                                                    />
                                                                }
                                                                disabled={fields.length <= 1 || disabled}
                                                            />
                                                        </Tooltip>
                                                    </Popconfirm>
                                                    <Divider type="vertical" />
                                                    <Tooltip title="添加一个条件">
                                                        <Button shape="circle"
                                                                type="dashed"
                                                                icon={<PlusOutlined />}
                                                                style={{
                                                                    color: !disabled ? '#f50' : 'gray',
                                                                }}
                                                                onClick={() => !disabled && addSubField(initialSubFieldValue)}/>
                                                    </Tooltip>
                                                </div>
                                            </RuleViewItem>
                                            <Form.ErrorList errors={subErrors} />
                                        </>
                                    }
                                </Form.List>
                            </RuleView>
                        </Form.Item>
                    </RuleViewItem>
                </>
            );
        }
        return (
            <>
                <RuleViewItem key={field.key} isFirst={index === 0}>
                    <div className="flex items-center ml-7 pb-1">
                        <div className="flex pl-3 pr-3 pt-3 bg-slate-100">
                            <Form.Item
                                key={field.key}
                                name={[field.name]}
                                validateFirst
                                rules={[
                                    { required: true },
                                    {
                                        validator: (r, v) => {
                                            console.log('RuleViewItem condition >>>> ', r, v);
                                            if (!v) {
                                                return Promise.reject(new Error('条件不能为空'));
                                            }
                                            return validatorRuleTreeItem(v);
                                        },
                                    },
                                ]}
                            >
                                <RuleTreeItem
                                    disabled={disabled} />
                            </Form.Item>
                            <div className="mt-1.5 ml-3" data-desc="添加单个条件">
                                <CopyOutlined
                                    style={{
                                        color: !disabled ? '#f50' : 'gray',
                                    }}
                                    onClick={() => !disabled && add(value || initialSubFieldValue)}
                                />
                            </div>
                            <div className="mt-1.5 ml-3" data-desc="删除单个条件">
                                <DeleteOutlined
                                    style={{
                                        color: fields.length !== 1 && !disabled ? '#f50' : 'gray',
                                    }}
                                    onClick={() => fields.length !== 1 && !disabled && remove(field.name)}
                                />
                            </div>
                        </div>
                    </div>
                </RuleViewItem>
            </>
        );
    };
    // 初始字段值
    export const initialFieldValue = {
        operation: 'AND',
        children: [
            {
                operation: OPERATOR.EQ,
                left: { kind: 'REF', type: 'STRING', operation: '' },
                right: { kind: 'CONST', type: 'STRING', operation: '' },
            },
        ],
    };
    export const initialSubFieldValue = {
        operation: OPERATOR.EQ,
        left: { kind: 'REF', type: 'STRING', operation: '' },
        right: { kind: 'CONST', type: 'STRING', operation: '' },
    }
    

    规则树左右值选中组件

    interface RuleTreeItemProps {
        value?: ConditionValue;
        onChange?: (v?: ConditionValue) => void;
        disabled?: boolean;
        mapping?: ModelMapping[];
    }
    
    export const RuleTreeItem: React.FC<RuleTreeItemProps> = ({
                                                                         value,
                                                                         onChange,
                                                                         disabled = false,
                                                                     }) => {
        const modelMappingContext = useContext(ModelMappingContext);
    
        return <>
            <RuleTreeItemWarp value={value} onChange={onChange} mapping={modelMappingContext?.mapping} disabled={disabled}/>
        </>;
    }
    
    // 自定义 组件
    const RuleTreeItemWarp: React.FC<RuleTreeItemProps> = ({
                                                           value,
                                                           onChange,
                                                           disabled = false,
                                                           mapping,
                                                       }) => {
     
        const handleLeftChange = useCallback(
            (v?: string) => {
                const map = mapping?.find(it => it.identifier === v);
                const v1 = {
                    operation: value?.operation || '',
                    left: {
                        value: v,
                        type: map?.type,
                        kind: map?.kind,
                        format: map?.format,
                    },
                    right: {
                        value: null,
                        type: value?.right?.type,
                        kind: value?.right?.kind,
                        format: value?.right?.format,
                    },
                };
                onChange && onChange(v1);
            },
            [value],
        );
    
        const handleOperatorChange = useCallback(
            (v?: string) => {
                const v1 = {
                    operation: v || '',
                    left: value?.left,
                    right: {
                        value: null,
                        type: value?.right?.type,
                        kind: value?.right?.kind,
                        format: value?.right?.format,
                    },
                };
                onChange && onChange(v1);
            },
            [value],
        );
    
        const handleRightKindChange = useCallback(
            (v?: string) => {
                const v1 = {
                    operation: value?.operation || '',
                    left: value?.left,
                    right: {
                        value: null,
                        type: value?.right?.type,
                        kind: v,
                        format: value?.right?.format,
                    },
                };
    
                onChange && onChange(v1);
            },
            [value],
        );
    
        const handleRightSelectChange = useCallback(
            (v?: string) => {
                const map = mapping?.find(it => it.identifier === v);
                const v1 = {
                    operation: value?.operation || '',
                    left: value?.left,
                    right: {
                        value: v,
                        type: map?.type,
                        kind: value?.right?.kind,
                        format: map?.format,
                    },
                };
                onChange && onChange(v1);
            },
            [value],
        );
    
        const handleRightChange = useCallback(
            (v?: string) => {
                const v1 = {
                    operation: value?.operation || '',
                    left: value?.left,
                    right: {
                        value: v,
                        type: value?.left?.type,
                        kind: value?.right?.kind,
                        format: value?.left?.format,
                    },
                };
                onChange && onChange(v1);
            },
            [value],
        );
    
        const mappingOptions = useMemo(() => {
            if (!mapping) return [];
            const ops =
                (mapping
                    ?.filter(it => {
                        if (it.invisible) {
                            return !it.invisible;
                        }
                        return true;
                    })
                    .map(it => ({
                        label: it.name,
                        value: it.identifier,
                    })) as Option[]) ?? [];
            // console.log('mappingOptions', ops);
            return ops;
        }, [value]);
    
        return (
            <>
                <Space>
                    <Select
                        showSearch
                        filterOption={(input, option) =>
                            (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
                            (option?.value ?? '').toString().toLowerCase().includes(input.toLowerCase())
                        }
                        placeholder="模型数据"
                        defaultValue={value?.left?.value}
                        options={mappingOptions}
                        onChange={handleLeftChange}
                        style={{ minWidth: '120px' }}
                        disabled={disabled}
                    />
                    <Select
                        value={value?.operation}
                        style={{ minWidth: '120px' }}
                        onChange={handleOperatorChange}
                        options={
                            value?.left?.type === DATA_TYPE.BOOLEAN
                                ? [OPERATOR.EQ].map(it => ({
                                label: OPERATOR_MAP.get(it)?.label ?? it,
                                value: it,
                            })) ?? []
                                : [
                                OPERATOR.EQ,
                                OPERATOR.NEQ,
                                OPERATOR.GT,
                                OPERATOR.GTE,
                                OPERATOR.LT,
                                OPERATOR.LTE,
                                OPERATOR.BETWEEN,
                                OPERATOR.RANGE,
                                OPERATOR.IN,
                                OPERATOR.NOT_IN,
                            ].map(it => ({
                                label: OPERATOR_MAP.get(it)?.label ?? it,
                                value: it,
                            })) ?? []
                        }
                    />
    
                    <Select
                        placeholder="种类"
                        value={value?.right?.kind || DATA_KIND.CONST}
                        options={[
                            { label: '引用', value: DATA_KIND.REF },
                            { label: '输入', value: DATA_KIND.CONST },
                        ]}
                        onChange={handleRightKindChange}
                        style={{ width: '100px' }}
                        disabled={disabled}
                    />
                    {DATA_KIND.REF === value?.right?.kind ? (
                        <Select
                            showSearch
                            filterOption={(input, option) =>
                                (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) ||
                                (option?.value ?? '')
                                    .toString()
                                    .toLowerCase()
                                    .includes(input.toLowerCase())
                            }
                            placeholder="模型数据"
                            defaultValue={value?.right?.value}
                            options={mappingOptions}
                            onChange={handleRightSelectChange}
                            style={{ minWidth: '120px' }}
                            disabled={disabled}
                        />
                    ) : (
                        <RuleTreeItemRight
                            value={value}
                            onChange={handleRightChange}
                            disabled={disabled}
                        />
                    )}
                </Space>
            </>
        );
    };
    
    const RuleTreeItemRight = ({
                                   value,
                                   onChange,
                                   disabled,
                               }: {
        disabled?: boolean;
        value?: ConditionValue;
        onChange?: (v?: any) => void;
    }): React.ReactElement => {
        
        if (
            value?.left?.type === DATA_TYPE.DATE ||
            value?.left?.type === DATA_TYPE.SECOND ||
            value?.left?.type === DATA_TYPE.MILLIS
        ) {
            if (
                value?.operation === OPERATOR.RANGE ||
                value?.operation === OPERATOR.BETWEEN
            ) {
                return (
                    <DateTimeRangePicker
                        value={value?.right?.value}
                        onChange={v => {
                            onChange?.(v);
                        }}
                    />
                );
            } else if (
                value?.operation === OPERATOR.IN ||
                value?.operation === OPERATOR.NOT_IN
            ) {
                return (
                    <DateTimeMultiPicker
                        value={value?.right?.value}
                        onChange={v => {
                            onChange?.(v);
                        }}
                    />
                );
            } else {
                return (
                    <DateTimePicker
                        value={value?.right?.value}
                        onChange={v => {
                            onChange?.(v);
                        }}
                    />
                );
            }
        } else if (value?.left?.type === DATA_TYPE.NUMBER) {
            if (
                value?.operation === OPERATOR.RANGE ||
                value?.operation === OPERATOR.BETWEEN
            ) {
                return (
                    <NumberRangePicker
                        value={value?.right?.value}
                        onChange={v => {
                            onChange?.(v);
                        }}
                    />
                );
            } else if (
                value?.operation === OPERATOR.IN ||
                value?.operation === OPERATOR.NOT_IN
            ) {
                return (
                    <Select
                        mode="tags"
                        allowClear
                        style={{ minWidth: '280px' }}
                        placeholder="Please select"
                        defaultValue={value?.right?.value || []}
                        onChange={v => {
                            onChange?.(v);
                        }}
                    />
                );
            } else {
                return (
                    <InputNumber
                        style={{ minWidth: '240px' }}
                        defaultValue={value?.right?.value}
                        onChange={v => {
                            onChange?.(v);
                        }}
                    />
                );
            }
        } else if (value?.left?.type === DATA_TYPE.BOOLEAN) {
            return (
                <Select
                    allowClear
                    style={{ minWidth: '160px' }}
                    placeholder="Please select true or false"
                    defaultValue={value?.right?.value}
                    options={[
                        { label: '真', value: true },
                        { label: '假', value: false },
                    ]}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        }
        if (
            value?.operation === OPERATOR.IN ||
            value?.operation === OPERATOR.NOT_IN
        ) {
            return (
                <Select
                    mode="tags"
                    allowClear
                    style={{ minWidth: '240px' }}
                    placeholder="Please select"
                    defaultValue={value?.right?.value || []}
                    onChange={v => {
                        onChange?.(v);
                    }}
                />
            );
        }
        return (
            <Input
                style={{ minWidth: '160px' }}
                disabled={disabled}
                defaultValue={value?.right?.value}
                onChange={e => {
                    onChange?.(e.target.value);
                }}
            />
        );
    };
    
    export const validatorRuleTreeItem = (v: ConditionValue): Promise<any> => {
        if (v.operation === 'OR' || v.operation === 'AND') {
            if (!v.children || v.children.length === 0) {
                return Promise.reject(new Error('条件不能为空'));
            }
            const errors = v.children.map(child => validatorRuleTreeItem(child));
            return Promise.all(errors)
                .then(() => Promise.resolve()) // 如果所有子项验证通过
                .catch(err => Promise.reject(err)); // 如果有任何一个子项验证失败
        } else {
            if (!v.left || !v.right) {
                return Promise.reject(new Error('条件不能为空'));
            }
            if (!v.left.value || v.left.value === '') {
                return Promise.reject(new Error('条件左值不能为空'));
            }
            if (!v.right.value || v.right.value === '') {
                if (DATA_TYPE.BOOLEAN === v.right?.type) {
                    if (v.right?.value === null || undefined === v.right?.value) {
                        console.log('条件右值不能为空:', v);
                        return Promise.reject(new Error(`条件右值不能为空`));
                    } else {
                        return Promise.resolve();
                    }
                }
                console.log('条件右值不能为空:', v);
                return Promise.reject(new Error(`条件右值不能为空`));
            }
        }
        return Promise.resolve();
    };
    

    规则树效果预览

    截屏2024-09-20 14.47.54.png

    规则树数据预览

    {
        "operation": "OR",
        "children": [
            {
                "children": [
                    {
                        "left": {
                            "type": "NUMBER",
                            "value": "orderStatus",
                            "kind": "REF"
                        },
                        "operation": "EQ",
                        "right": {
                            "kind": "CONST",
                            "type": "NUMBER",
                            "value": 5300
                        }
                    },
                    {
                        "children": [
                            {
                                "children": [
                                    {
                                        "left": {
                                            "value": "bizType",
                                            "kind": "REF",
                                            "type": "NUMBER"
                                        },
                                        "operation": "EQ",
                                        "right": {
                                            "kind": "CONST",
                                            "type": "NUMBER",
                                            "value": 24
                                        }
                                    },
                                    {
                                        "right": {
                                            "kind": "CONST",
                                            "type": "STRING",
                                            "value": [
                                                "24001",
                                                "2486"
                                            ]
                                        },
                                        "left": {
                                            "type": "STRING",
                                            "value": "cpCode",
                                            "kind": "REF"
                                        },
                                        "operation": "IN"
                                    }
                                ],
                                "operation": "AND"
                            },
                            {
                                "children": [
                                    {
                                        "left": {
                                            "type": "NUMBER",
                                            "value": "bizType",
                                            "kind": "REF"
                                        },
                                        "operation": "EQ",
                                        "right": {
                                            "kind": "CONST",
                                            "type": "NUMBER",
                                            "value": 56
                                        }
                                    },
                                    {
                                        "left": {
                                            "type": "STRING",
                                            "value": "cpCode",
                                            "kind": "REF"
                                        },
                                        "operation": "IN",
                                        "right": {
                                            "kind": "CONST",
                                            "type": "STRING",
                                            "value": [
                                                "5644",
                                                "5625"
                                            ]
                                        }
                                    }
                                ],
                                "operation": "AND"
                            }
                        ],
                        "operation": "OR"
                    },
                    {
                        "left": {
                            "format": "yyyy-MM-dd HH:mm:ss",
                            "kind": "REF",
                            "type": "MILLIS",
                            "value": "expireTime"
                        },
                        "operation": "BETWEEN",
                        "right": {
                            "type": "MILLIS",
                            "value": [
                                "2024-09-01 00:00:00",
                                "2024-10-31 00:00:00"
                            ],
                            "format": "yyyy-MM-dd HH:mm:ss",
                            "kind": "CONST"
                        }
                    },
                    {
                        "left": {
                            "kind": "REF",
                            "type": "NUMBER",
                            "value": "orderStatus"
                        },
                        "operation": "NEQ",
                        "right": {
                            "kind": "REF",
                            "type": "NUMBER",
                            "value": "beforeOrderStatus"
                        }
                    }
                ],
                "operation": "AND"
            }
        ]
    }
    

    规则引擎

    规则树转换
    规则树需要将数据转换成, 扁平的逻辑表达式, 例如:

    (cpCode in [5644, 5625] and bizType == 56) or (cpCode in [2433, 2401] and bizType == 24)

    定义实体:

    使用递归将规则树转换成逻辑表达式

    public class ConditionValue  {
        private String operation;
        private List<ConditionValue> children;
        // 左右值, 比较类型, 非OR,AND时候有值
        private DataValue left;
        private DataValue right;
         public String expression() {
            if (Operation.isLogical(operation) || operation == null) {
                return children.stream().map(it -> it.expression()).collect(Collectors.joining(Operation.ofSymbol(operation), " ( ", " ) "));
            }
            if(Operation.NOT_IN.name().equals(operation)) {
                return " (!(" + left.getValue() +  " in " + right.getValue() + ")) ";
            } else if (Operation.IN.name().equals(operation)) {
                return " (" + left.getValue() +  " in " + right.getValue() + ") ";
            } else if (Operation.RANGE.name().equals(operation)) {
                return " (" + left.getValue() + " >= " + right.getValue() + "[0] && " + left.getValue() + " <= " + right.getValue() + "[1]" + ") ";
            } else if (Operation.BETWEEN.name().equals(operation)) {
                return " (" + left.getValue() + " > " + right.getValue() + "[0] && " + left.getValue() + " < " + right.getValue() + "[1]" + ") ";
            }
            return " (" + left.getValue() + " " + Operation.ofSymbol(operation) + " " + right.getValue() + ") ";
        }
    }
    

    规则引擎设计

    引擎使用antlr4定义一个简单脚本, 弱类型语法, 使用groovy、golang、javascript的一些常见语法组成, antlr4天然支持多语言, 表达式执行就天然支持多语言, 不受受限于语言.
    a. 支持逻辑运行OR、AND
    b. 支持弱类型关系比较自动类型转换, 100 == "100"
    c. 支持in关系, uid in ["4042344", "512009"]
    d. 使用antlr4的visitor模式实现
    parser grammar 表达式 一部分定义:

    expression
        :
        FUNC  LPAREN formalParameterList? RPAREN functionBody                                   # FunctionDeclExpression
        | arrowFunctionParameters ARROW arrowFunctionBody                                       # ArrowFunctionDeclExpression
        | member=expression LBRACK min=expression? COLON max=expression? RBRACK                 # MemberSubsetExpression
        | member=expression opt=(OPTIONAL_CHAINING | QUESTION)? LBRACK index=expression RBRACK  # MemberIndexExpression
        | member=expression opt=QUESTION? DOT method=identifier                                 # MemberCallExpression
        | expression   argumentList                                                             # ArgumentsExpression
        | expression op=(INC | DEC)                                                             # PostIncrementExpression //后置递增运算符
        | op=(INC | DEC) expression                                                             # PreIncrementExpression  //前置递增运算符
        | op=(ADD | SUB) expression                                                             # UnaryExpression
        | op=(TILDE | BANG) expression                                                          # NotExpression
        | lhs=expression op=(MUL | DIV | MOD) rhs=expression                                    # MultiplicativeExpression
        | lhs=expression op=(ADD | SUB) rhs=expression                                          # AdditiveExpression
        | lhs=expression op=(LE | GE | GT | LT | EQUAL | NOTEQUAL | IN) rhs=expression          # RelationalExpression
        | lhs=expression op=(AND | OR) rhs=expression                                           # LogicalExpression
        | cond=expression '?' lhs=expression ':' rhs=expression                                 # TernaryExpression
        | lhs=expression '?:' rhs=expression                                                    # ElvisExpression
        | <assoc = right> lhs=expression '=' rhs=expression                                     # AssignmentExpression
        | identifier                                                                            # IdentifierExpression
        | literal                                                                               # LiteralExpression
        | arrayLiteral                                                                          # ArrayLiteralExpression
        | objectLiteral                                                                         # ObjectLiteralExpression
        | LPAREN expression RPAREN                                                              # ParenthesizedExpression
        ;
    

    关系运算实现

    func (this *MainVisitor) VisitRelationalExpression(ctx *RelationalExpressionContext) interface{} {
        logrus.Debugln("VisitRelationalExpression:", ctx.GetText())
        left, right := this.Visit(ctx.GetLhs()), this.Visit(ctx.GetRhs())
        //检查
        left = ExpressionCallIfNecessary(left, []interface{}{})
        right = ExpressionCallIfNecessary(right, []interface{}{})
        logrus.Debugln("VisitRelationalExpression ctx.GetOp(): ", ctx.GetOp().GetText())
    
        switch ctx.GetOp().GetTokenType() {
        case FlyParserEQUAL:
            return func_eq(left, right)
        case FlyParserNOTEQUAL:
            switch left.(type) {
            case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
                switch left.(type) {
                case float64, float32:
                    return cast.ToFloat64(left) != cast.ToFloat64(right)
                }
                return cast.ToInt64(left) != cast.ToInt64(right)
            case float64, float32:
                return cast.ToFloat64(left) != cast.ToFloat64(right)
            }
            return left != right
        case FlyParserGT:
            switch left.(type) {
            case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
                switch left.(type) {
                case float64, float32:
                    return cast.ToFloat64(left) > cast.ToFloat64(right)
                }
                return cast.ToInt64(left) > cast.ToInt64(right)
            case float64, float32:
                return cast.ToFloat64(left) > cast.ToFloat64(right)
            }
            panic("not support Relational left type")
        case FlyParserGE:
            switch left.(type) {
            case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
                switch left.(type) {
                case float64, float32:
                    return cast.ToFloat64(left) >= cast.ToFloat64(right)
                }
                return cast.ToInt64(left) >= cast.ToInt64(right)
            case float64, float32:
                return cast.ToFloat64(left) >= cast.ToFloat64(right)
            }
            panic("not support Relational left type")
        case FlyParserLT:
            switch left.(type) {
            case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
                switch left.(type) {
                case float64, float32:
                    return cast.ToFloat64(left) < cast.ToFloat64(right)
                }
                return cast.ToInt64(left) < cast.ToInt64(right)
            case float64, float32:
                return cast.ToFloat64(left) < cast.ToFloat64(right)
            }
            panic("not support Relational left type")
        case FlyParserLE:
            switch left.(type) {
            case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
                switch left.(type) {
                case float64, float32:
                    return cast.ToFloat64(left) <= cast.ToFloat64(right)
                }
                return cast.ToInt64(left) <= cast.ToInt64(right)
            case float64, float32:
                return cast.ToFloat64(left) <= cast.ToFloat64(right)
            }
            panic("not support Relational left type")
        case FlyParserIN:
            if isNil(right) {
                return false
            }
            list, ok := right.([]interface{})
            if !ok {
                panic("not support Relational right type")
            }
            for _, v := range list {
                if func_eq(left, v) {
                    return true
                }
            }
            return false
        }
        panic("not support Relational expression")
    }
    func func_eq(left interface{}, right interface{}) bool {
        switch left.(type) {
        case int64, int, int32, uint, uint32, uint64, int8, int16, uint16:
            switch left.(type) {
            case float64, float32:
                return cast.ToFloat64(left) == cast.ToFloat64(right)
            }
            return cast.ToInt64(left) == cast.ToInt64(right)
        case float64, float32:
            return cast.ToFloat64(left) == cast.ToFloat64(right)
        case string:
            if rightStr, ok := right.(string); ok {
                return left.(string) == rightStr
            }
            return left.(string) == cast.ToString(right)
        }
        return left == right
    }
    

    规则表达式执行测试

    func TestExpression(t *testing.T) {
        type tt struct {
            expression string
            value      interface{}
            recover    bool
            scope      map[string]interface{}
        }
        caseList := [...]tt{
            {
                scope: map[string]interface{}{
                    "orderEnter":  1693875200,
                    "orderTime":   1693875200,
                    "orderStatus": 5300,
                    "uid":         40488888,
                },
                expression: "( ( (orderEnter >= orderTime)&&(orderStatus == 5300)&&(orderStatus in [5300,5400]) )||( (orderEnter < orderTime)&&( (orderEnter > orderTime) ) ) && uid in [\"40488888\", \"2075114\"] )",
                value:      true,
            },
            {
                scope: map[string]interface{}{},
                expression: "var x = 6;" +
                    "y,z := 2,4;" +
                    "x == y+z",
                value: true,
            },
            {
                scope: map[string]interface{}{},
                expression: "var x = [6,7,8];" +
                    "y := \"7\";" +
                    "y in x",
                value: true,
            },
        }
        checkCase := func(t *testing.T, c tt) (ex error) {
            defer func() {
                if r := recover(); r != nil {
                    ex = fmt.Errorf("panic: %v", r)
                }
            }()
            val := calc(c.expression, c.scope)
            if val != nil && reflect.TypeOf(val).Kind() == reflect.Map {
                str, err := jsoniter.MarshalToString(val)
                fmt.Println("result str: ", str)
                assert.Nil(t, err)
                assert.JSONEq(t, c.value.(string), str)
                return
            }
            assert.Equal(t, val, c.value)
            return ex
        }
        for _, c := range caseList {
            err := checkCase(t, c)
            if c.recover {
                fmt.Println(err)
                assert.NotNil(t, err)
            } else {
                assert.Nil(t, err)
            }
        }
    }
    
    func calc(expression string, scope map[string]interface{}) interface{} {
        input := antlr.NewInputStream(expression)
        lexer := NewFlyLexer(input)
    
        stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)
    
        parser := NewFlyParser(stream)
        parser.BuildParseTrees = true
        parser.RemoveErrorListeners()
        parser.AddErrorListener(&VerboseListener{})
        tree := parser.Program()
    
        visitor := NewMainVisitor(nil, scope)
    
        var result = visitor.Execute(tree)
        return result
    }
    

    相关文章

      网友评论

          本文标题:通用规则树引擎解决方案

          本文链接:https://www.haomeiwen.com/subject/fzgoljtx.html