# 高级进阶

在快速入门中，我们一起使用了简单单据监控功能，虽然平台提供了如单据、基础资料等通用的基础支持，但由于业务逻辑的复杂性，有些时候系统提供的功能可能无法满足实际业务的需求。业务需要对预警监控过程进行功能的增强或干预，为此平台提供了预警插件机制，使用方可以通过按需编写的方式，对预警功能进行组合使用。

需要指出的是**预警平台插件为单插件机制**，即如若配置开启了多个插件，那么预警平台在执行预警方案的时候只有最后一个开启的插件会执行。

预警平台整体流程图如下：

![](./assets/earlywarn_flow.png)

## 核心概念

在预警平台，使用了一些通用的名词，在此标注，以便使用者能够更好的理解后续的文档。

### 业务预警对象

业务预警对象为预警数据源承载对象，定义了预警数据的来源以及相关的操作插件。同一业务预警对象可以被多个监控方案引用。

### 预警监控方案

预警监控方案为具体执行方案，监控方案定义了所使用的业务预警对象、预警监控的数据扫描频率、数据预警条件、预警消息模板、消息发送方式、消息接收人以及消息模板。系统将根据用户配置的预警方案周期性的扫描监控对象，并按照用户配置的预警规则获取预警数据并给出相关的提示。

## 插件示例

###  自定义预警数据写出

预警平台依据监控方案对预警数据扫描后出于性能与存储的考虑，并未将需要预警的数据写出到数据库。对于某些场景而言，需要保存这些预警数据以便后续的分析。这个时候我们可以利用平台提供的业务预警对象-预警数据写出插件功能。

使用预警数据写出插件非常简单，只需两步：

1. 编写预警数据写出插件，插件实现`IEarlyWarnWriteOut`接口，框架通过上下文将预警数据传递给接口，使用者只需在接口中相关数据写出到自己想要的数据库或文件中即可。如下为将数据写出到另外一个表的例子：

```java
public class TestWriteOut implements IEarlyWarnWriteOut {
    @Override
    public void writeOut(DynamicObjectCollection list, EarlyWarnContext context) {
        if(list.isEmpty()){
            return;
        }

        DynamicObject[] saveArr = new DynamicObject[list.size()];
        for(int i = 0; i < list.size(); i++){
            DynamicObject obj = list.get(i);
            DynamicObject newObj = BusinessDataServiceHelper.newDynamicObject("test_write_out");
            newObj.set("number", obj.get("number"));
            saveArr[i] = newObj;
        }

        MainEntityType entityType = EntityMetadataCache.getDataEntityType("test_write_out");
        BusinessDataWriter.save(entityType, saveArr);
    }
}
```
2. 将插件注册到业务预警对象中。
    ![img](./assets/earlywarn_more_01.png)
3. 新增预警监控方案，引用创建的业务预警对象。
4. 当预警监控方案被触发执行成功后，预警引擎将会把相关数据写出到相关的单据或数据库。
![](./assets/earlywarn_more_writeout_01.png)

### 自定义消息解析

预警平台提供了单据、基础资料的默认消息解析插件DefaultBillMessageCompiler，但是该插件中的模板数据只能来源于指定的单据和基础资料，如若想在消息中添加一些来源于其他单据的数据，比如库存预警，某个仓库的商品产生了低库存预警，想把商品库存的批次带出到消息中，此时可以通过自定义消息解析插件来达到上述效果。使用步骤如下：

1. 编写消息解析插件，实现`IEarlyWarnMessageCompiler`接口。框架通过上下文将预警数据、模板传递给接口，用户在接口中获取当前预警的数据，从库存批次表中获取出对应批次的信息，用户构建所需要的预警信息即可。如下演示单条信息如何自定义字段：

```java 
public class TestMsgCompiler implements IEarlyWarnMessageCompiler {

    @Override
    public String getSingleMessage(String expression, List<String> fields, DynamicObject data, EarlyWarnContext context) {
        if(null == expression || null == data){
            return "";
        }

        Map<String,String> macroMap = new HashMap<>();
        for (String field: fields) {
            String value = "";
            if("customField".equals(field)){
                value = "自定义字段";
            }else{
                String[] arr = StringUtil.split(field,".");
                Object objValue = getValue(data, arr);
                value = objValue == null ? "" : objValue.toString();
            }
            macroMap.put(field, value);
        }



        StringTemplateParser parser = new StringTemplateParser();
        return parser.parse(expression,macroName->macroMap.get(macroName));
    }

    private String getValue(DynamicObject data, String[] arr){
        if(null == arr || arr.length == 0){
            return "";
        }

        Object obj = data.get(arr[0]);
        if(obj instanceof DynamicObject){
            return getValue((DynamicObject)obj, Arrays.copyOfRange(arr, 1, arr.length));
        }

        return StringUtil.toSafeString(obj);
    }

    @Override
    public String getMergeMessage(String s, List<String> list, EarlyWarnContext earlyWarnContext) {
        return null;
    }
}
```

2. 将插件注册到业务预警对象中。
![img](./assets/earlywarn_more_02.png)
3. 新增预警监控方案，引用创建的业务预警对象。
![](./assets/earlywarn_more_msg_compiler_01.png)
4. 当预警监控方案被触发执行成功后，在消息中心可以查看到预警消息。
![](./assets/earlywarn_more_msg_compiler_02.png)

### 自定义消息处理

预警平台将预警消息发送至消息中心后，默认情况下只在消息中心的消息预警中显示了一条预警消息数据。我们想对这条消息进行一些处理如在低库存预警场景下产生了一条低库存预警消息，责任人看到消息后想通过点击链接，跳转到补货页面并将预警商品自动填入补货信息中，类似场景可以通过自定义消息处理插件的方式来实现。受限于目前消息中心的处理机制，目前只提供了基于URL的跳转实现方式，通过URL将所需参数携带至相关的页面。具体使用步骤如下：

1. 编写预警消息处理插件实现`IEarlyWarnMessageHandler`接口，实现接口方法，在接口中可以通过上下文获取到相关的数据，构建自己想要的URL即可。接下来演示在点击预警消息跳转：
```java
public class TestMsgHandler implements IEarlyWarnMessageHandler {
    @Override
    public EarlyWarnMessageInfo singleMessageBuilder(DynamicObject dynamicObject, EarlyWarnContext earlyWarnContext) {
        EarlyWarnMessageInfo info = new EarlyWarnMessageInfo();
        info.setContentUrl("http://localhost:8080/ierp/?formId=pc_main_console#/dform?formId=pc_main_console");
        return info;
    }

    @Override
    public EarlyWarnMessageInfo mergeMessageBuilder(EarlyWarnContext earlyWarnContext) {
        return null;
    }
}
```
2. 将插件注册到业务预警对象中。
![img](./assets/earlywarn_more_03.png)
3. 新增预警监控方案，引用创建的业务预警对象。
![](./assets/earlywarn_more_msg_handler_01.png)
4. 当预警监控方案被触发执行成功后，在消息中心可以查看到预警消息下方有跳转超链，点击超链可以跳转到主页面。
![](./assets/earlywarn_more_msg_handler_02.png)

### 自定义预警条件

预警平台提供了默认单据、基础资料条件配置页面，该配置受限于选择的数据源，比如在低库存预警场景，不同的仓库对于商品的库存下限是不同的。这种情况通过上述默认的配置方式比较难以处理，因此平台提供了自定义预警条件的机制。自定义预警条件与默认预警条件可以共同协作。

**自定义预警条件表单需要与数据源插件配合使用。**接下来以低库存不同仓库的最低下限不同的场景为例，演示自定义预警条件如何使用的：

1. 在开发平台配置一个自定义预警条件动态表单。编写表单插件实现`IEarlyWarnConditionForm`接口，预警平台通过通过该接口获取用户在监控方案配置的自定义预警条件数据。

![img](./assets/earlywarn_more_04.png)

插件代码如下：

```java
public class CustomConditionScheduleTestPlugIn extends AbstractFormPlugin implements IEarlyWarnConditionForm {

    @Override
    public Map<String, Object> getCustomParams() {
        IDataModel model = getModel();
        String name = (String)model.getValue("name");
        String number = (String)model.getValue("number");

        Map<String,Object> dataMap = new HashMap<>();
        dataMap.put("name",name);
        dataMap.put("number",number);
        return dataMap;
    }

    @Override
    public EarlyWarnConditionCheckResult checkCustomParams() {
        IDataModel model = getModel();
        String number = (String)model.getValue("number");

        if(StringUtils.isBlank(number)){
            return EarlyWarnConditionCheckResult.failure("请填写工号");
        }

        return EarlyWarnConditionCheckResult.success("");
    }
}
```
2. 在业务预警对象中选择该动态表单。
3. 编写数据源插件，可以继承至系统提供的默认单据数据源`DefaultEarlyWarnBillDataSource`或实现自定义数据源接口`IEarlyWarnDataSource`，重写`buildFilter()`方法，合并自定义过滤条件与页面配置的通用过滤条件。数据源插件代码如下：
```java
public class TestCustomConditionDataSource extends DefaultEarlyWarnBillDataSource {

    public List<QFilter> buildFilter(String dataSource, FilterCondition filterCondition, EarlyWarnContext context) {
        List<QFilter> filters = super.buildFilter(dataSource, filterCondition, context);
        Map<String, Object> customConditionMap = context.getCustomConditionDataMap();
        String number = (String)customConditionMap.get("number");
        QFilter filter = new QFilter("number", "=", number);
        filters.add(filter);
        return filters;
    }
}
```
4. 注册数据源插件到业务预警对象中。
![img](./assets/earlywarn_more_05.png)
5. 在监控方案中选择上面的业务预警对象后，系统会自动在默认监控方案配置页面上方显示自定义的条件配置页面，用户可通过该页面自行设置自定义预警条件，预警平台会通过接口获取用户设置的自定义条件并保存至预警监控元数据中。在此，我们并未设置任何过滤条件。
![](./assets/earlywarn_more_custom_condition_01.png)
6. 当预警监控方案被触发执行成功后，在消息中心可以查看到预警消息已经成功发送给用户。
![](./assets/earlywarn_more_custom_condition_02.png)

### 自定义数据源

预警平台提供了默认单据、基础资料的数据源，考虑到后续数据源的来源可能多种多样，还有可能有多数据源协同预警的场景，如实际大型企业低库存预警场景下的某企业的各个仓库中存放的各种商品低库存下限是很有可能不一样的。上述场景中可能涉及到了库存表、仓库商品下限记录表、商品表等多表数据协同过滤，为此针对这种复杂数据源预警场景，平台提供了自定义数据源机制。自定义数据源拥有者完整的功能，可以控制到整个预警数据取数过程。具体使用方式如下：

1. 编辑自定义数据源插件，可以继承至系统提供的默认单据数据源`DefaultEarlyWarnBillDataSource`或实现自定义数据源接口`IEarlyWarnDataSource`。由于后续的预警监控方案中的默认监控条件和消息模板设置页面与数据源有关，因此如有相关设置需求，自定义数据源需要实现接口中的相关的方法以提供相关页面所需数据。如若不实现相关方法，则可以通过相关的插件机制来干预。
2. 注册数据源插件到业务预警对象中。
3. 新增预警监控方案，引用创建的业务预警对象。

接下来通过一个例子来演示多数据源处理过程。假设我们需要找出年龄大于20的用户给他发送消息，年龄信息存储在`test_multi_datasource`这个单据中，需要关联`bos_user`找出用户id给满足条件的用户发送消息。`test_multi_datasource`单据中有如下数据：
![](./assets/earlywarn_more_multidata_01.png)

接下来我们通过预警平台找出这个用户并给这个用户发送消息。先自定义一个数据源：
```java
public class TestMultiDataSource implements IEarlyWarnDataSource {
    @Override
    public List<QFilter> buildFilter(String s, FilterCondition filterCondition, EarlyWarnContext earlyWarnContext) {
        return null;
    }

    @Override
    public DynamicObjectCollection getData(String s, List<QFilter> list, EarlyWarnContext earlyWarnContext) {
        QFilter[] ageFilters = new QFilter[]{new QFilter("age",">",20)};
        DynamicObjectCollection suitUsers = ORM.create().query("test_multi_datasource",ageFilters);
        if(null == suitUsers || suitUsers.isEmpty()){
            return new DynamicObjectCollection();
        }

        List<String> numbers = new ArrayList<>();
        for(DynamicObject obj : suitUsers){
            numbers.add(String.valueOf(obj.getLong("number")));
        }

        if(numbers.isEmpty()){
            return new DynamicObjectCollection();
        }

        QFilter[] filters = new QFilter[]{new QFilter("number","in",numbers)};
        return ORM.create().query("bos_user",filters);
    }

    @Override
    public List<Map<String, Object>> getCommonFilterColumns(String s) {
        return null;
    }

    @Override
    public TreeNode getSingleMessageFieldTree(String s) {
        return new TreeNode();
    }

    @Override
    public TreeNode getMergeMessageFieldTree(String s) {
        return new TreeNode();
    }
}
```

自定义数据源发送消息时需要自定义消息解析插件，在这里为简便起见我们使用了上面创建的`TestMsgCompiler`插件：

```java
public class TestMsgCompiler implements IEarlyWarnMessageCompiler {

    @Override
    public String getSingleMessage(String expression, List<String> fields, DynamicObject data, EarlyWarnContext context) {
        if(null == expression || null == data){
            return "";
        }

        Map<String,String> macroMap = new HashMap<>();
        for (String field: fields) {
            String value = "";
            if("customField".equals(field)){
                value = "自定义字段";
            }else{
                String[] arr = StringUtil.split(field,".");
                Object objValue = getValue(data, arr);
                value = objValue == null ? "" : objValue.toString();
            }
            macroMap.put(field, value);
        }



        StringTemplateParser parser = new StringTemplateParser();
        return parser.parse(expression,macroName->macroMap.get(macroName));
    }

    private String getValue(DynamicObject data, String[] arr){
        if(null == arr || arr.length == 0){
            return "";
        }

        Object obj = data.get(arr[0]);
        if(obj instanceof DynamicObject){
            return getValue((DynamicObject)obj, Arrays.copyOfRange(arr, 1, arr.length));
        }

        return StringUtil.toSafeString(obj);
    }

    @Override
    public String getMergeMessage(String s, List<String> list, EarlyWarnContext earlyWarnContext) {
        return null;
    }
}
```

创建一个业务预警对象：
![](./assets/earlywarn_more_multidata_03.png)
创建监控方案：
![](./assets/earlywarn_more_multidata_04.png)
保存并执行，在消息中心查看预警信息：
![](./assets/earlywarn_more_multidata_02.png)

### 自定义消息接收人

预警平台提供了按`人员`、`用户变量`的方式来设置消息接收人，考虑到可能存在需要动态设置消息接收人的业务场景，平台提供了自定义消息接收人的机制，用户可以根据自己的逻辑来动态定义消息接收人。

**实现自定义消息接收人接口返回的消息接收人ID必须在苍穹系统中存在。**

接下来通过一个例子来演示如何实现自定义消息接收人功能：

1、编写插件（可预设多个插件），实现`EarlyWarnCustomReceiver`接口。其中`context`为上下文，`dynamicObjects`为表单动态对象，用户可以根据需要使用

```java
public class EarlyWarnCustomReceiverTest01 implements IEarlyWarnCustomReceiver {    @Override
    public List<Long> getReceiverIds(EarlyWarnContext context, DynamicObject[] dynamicObjects) {
        List<Long> receiverIds = new ArrayList<>();
        //获取接收人ID的业务逻辑
        /*receiverIds.add(xxx)…………*/
        return receiverIds;
    }}
```

2、在业务预警对象中注册编写好的插件，设置状态为`启用`

![1610711339931](./assets/1610711339931.png)

![1610711569558](./assets/1610711569558.png)

3、在监控方案中选择`自定义消息接收人`，再选择要使用的插件（`未启用`的插件不会显示在此处）

![1610711729907](./assets/1610711729907.png)

![1610711770978](./assets/1610711770978.png)

4、配置完成后即可使用自定义消息接收人插件功能

![1610711853052](./assets/1610711853052.png)