6.2 集成平台(改)

企业在数字化转型过程中内外部集成是一个必然需求、也是趋势

集成的诉求主要来自两个方面:1.企业的数字化改造是由外而内逐步进行的(内部异构集成)、2.企业数字化方向是朝越来越开放的方向发展(外部平台、工具集成)。总的来说企业在数字化转型过程中内外部集成是一个必然需求、也是趋势。所以我们不能简单地去理解做个API对接就结束了,而是要统一规划构建成企业的集成门户对API定义,安全、控制、记录等做全方位管理。oinone在下个版本规则中也纳入了基于集成平台之上做产品化配置的需求

概述

pamirs-eip为平台提供企业集成门户的相关功能,如请求外部接口使用的【集成接口】和对外开放被其他系统请求调用的【开放接口】功能。在请求外部接口时,还支持了多个接口调用(路由定义)、分页控制(paging)、增量控制(incremental)等功能。

准备工作

Step1 POM与模块依赖

pamirs-demo-api 和 pamirs-second-api 的pom文件中引入pamirs-eip2-api包依赖

<dependency>
    <groupId>pro.shushi.pamirs.core</groupId>
    <artifactId>pamirs-eip2-api</artifactId>
</dependency>

DemoModule和SecondModule 增加对EipModule的依赖

@Module(dependencies = {EipModule.MODULE_MODULE})

pamirs-demo-boot和pamirs-second-boot工程的pom文件中引入pamirs-eip2-core包依赖

<dependency>
    <groupId>pro.shushi.pamirs.core</groupId>
    <artifactId>pamirs-eip2-core</artifactId>
</dependency>

Step2 yaml配置文件参考

pamirs-demo-boot和pamirs-second-boot工程的application-dev.yml文件中增加配置pamirs.boot.modules增加eip,即在启动模块中增加eip模块

pamirs:
    boot:
    modules:
        - eip

pamirs-demo-boot和pamirs-second-boot工程的application-dev.yml文件中增加eip模块的数据源与路由配置

pamirs:
    framework:
    data:
        ds-map:
        eip: eip
    datasource:
    eip:
      driverClassName: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://127.0.0.1:3306/eip_v3?useSSL=false&allowPublicKeyRetrieval=true&useServerPrepStmts=true&cachePrepStmts=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
      username: root
      password: oinone
      initialSize: 5
      maxActive: 200
      minIdle: 5
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      asyncInit: true

pamirs-demo-boot工程的application-dev.yml文件中修改eip的配置

pamirs:
    eip:
    open-api:
      enabled: false

pamirs-second-boot工程的application-dev.yml文件中修改eip的配置

pamirs:
  eip:
    enabled: true
    open-api:
      enabled: true
      route:
        host: 127.0.0.1
        port: 8094
        aes-key: Nj5Thnxz4rV8Yy1FLGA2hUym3RepB8MKgafEaYC4GKo=

注:

hosts配置在远程调用时不能使用127.0.0.1,可配置为0.0.0.0进行自动识别。若自动识别仍无法访问,请准确配置其他已知的可访问IP地址。

aes-key:用下面代码生成

附录:AES Key生成

pro.shushi.pamirs.core.common.EncryptHelper加解密帮助类,默认支持AES、RSA类型的数据加解密方法,也可自定义其他类型的加解密方法。

System.out.println(EncryptHelper.getKey(EncryptHelper.getAESKey()));

Step3 在pamirs-second-api新建一个SessionTenantApi实现类

只要在我们公共的jar包中构建类似DemoSessionTenant类就可以了,之所以要构建SessionTenantApi实现类是因为EIP是以租户信息做路由的。所以这里我们写死返回一个“pamirs”租户就好了。

记得要重新mvn install second工程,再刷新demo工程

package pro.shushi.pamirs.second.api.tenant;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.framework.session.tenant.api.SessionTenantApi;
import pro.shushi.pamirs.meta.api.core.session.SessionClearApi;
import pro.shushi.pamirs.meta.common.spi.SPI;

@Order(99)
@Component
@SPI.Service
public class DemoSessionTenant implements SessionTenantApi, SessionClearApi {
    public String getTenant() {
        return "pamirs";
    }

    public void setTenant(String tenant) {
    }

    public void clear() {
    }
}

开放接口(举例)

Step1 用于演示的模型定义

package pro.shushi.pamirs.second.api.model;

import pro.shushi.pamirs.meta.annotation.Field;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.base.IdModel;

@Model.model(TestOpenApiModel.MODEL_MODEL)
@Model(displayName = "演示开放接口模型")
public class TestOpenApiModel extends IdModel {

    public static final String MODEL_MODEL = "demo.second.TestOpenApiModel";

    @Field.String
    @Field(displayName = "名称")
    private String name;

    @Field.Integer
    @Field(displayName = "年龄")
    private Integer age;
}
package pro.shushi.pamirs.second.api.tmodel;

import pro.shushi.pamirs.meta.annotation.fun.Data;

import java.io.Serializable;

@Data
public class TestOpenApiResponse implements Serializable {

    private Long id;
    private String name;
    private Integer age;

}

Step2 用于演示的接口定义

  1. TestOpenApiModelService接口定义了内部访问函数queryById

  2. TestOpenApiModelServiceImpl 实现TestOpenApiModelService接口,并定义了两个开放接口

a. queryById4Open函数用@Open进行注解,请求路径是:openapi/pamirs/方法名?tenant=pamirs,返回对象结构为函数返回值类型的JSON格式

b. queryById4OpenError函数用@Open(config = TestEipConfig.class,path = "error")进行注解,请求路径为:openapi/pamirs/error?tenant=pamirs,返回对象结构为函数返回值类型的JSON格式

c. config = TestEipConfig.class 设置通用配置类,可以在TestEipConfig增加@Open.Advanced。优先级低于方法上的注解。TestEipConfig见集成接口举例的集成配置模型定义。配置模型在开放接口定义中非必须的,在集成接口定义中时是必须。

d更多注解配置见Eip注解说明

package pro.shushi.pamirs.second.api.service;

import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
public interface TestOpenApiModelService {

    String FUN_NAMESPACE ="demo.second.TestOpenApiModelService";
    @Function
    TestOpenApiModel queryById(Long id);

}
package pro.shushi.pamirs.second.core.service;

import org.apache.camel.ExtendedExchange;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.annotation.Open;
import pro.shushi.pamirs.eip.api.enmu.EipExpEnumerate;
import pro.shushi.pamirs.eip.api.entity.openapi.OpenEipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.common.exception.PamirsException;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;
import pro.shushi.pamirs.second.api.service.TestOpenApiModelService;
import pro.shushi.pamirs.second.api.tmodel.TestOpenApiResponse;

import java.util.Optional;

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {

    @Override
    @Function
    public TestOpenApiModel queryById(Long id) {
        return new TestOpenApiModel().queryById(id);
    }

    @Function
    @Open
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        String id = Optional.ofNullable(String.valueOf(context.getInterfaceContext().getIteration("id"))).orElse("");
        TestOpenApiModel temp  = queryById(Long.valueOf(id));
        TestOpenApiResponse response = new TestOpenApiResponse();
        if(temp != null ) {
            response.setAge(temp.getAge());
            response.setId(temp.getId());
            response.setName(temp.getName());
        }else{
            response.setAge(1);
            response.setId(1L);
            response.setName("oinone eip test");
        }
        OpenEipResult<TestOpenApiResponse> result = new OpenEipResult<TestOpenApiResponse>(response);
        return result;
    }

    @Function
    // @Open(config = TestEipConfig.class,path = "error")
    @Open(path = "error")
    @Open.Advanced(
            httpMethod = "post"
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}

Step3 用于演示的请求协议

请求方法:POST

{
  "id":111
}

请求参数格式:

{
  "success":true,
  "errorCode":0,
  "result":{
    "age":1,
    "id":1,
    "name":"oinone eip test"
  }
}

成功响应格式:

{
  "success":true,
  "errorCode":0,
  "result":{
    "age":1,
    "id":1,
    "name":"oinone eip test"
  }
}

异常响应格式:

{
  "success":false,
    "errorCode":"20140000",
  "errorMsg":"系统异常, 测试异常"
}

Step4 初始化EIP

构建SecondModuleBizInit 实现InstallDataInit, UpgradeDataInit, ReloadDataInit接口,调用Eip注解模式的初始化方法:EipResolver.resolver(SecondModule.MODULE_MODULE,null)

package pro.shushi.pamirs.second.core.init;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.boot.common.api.command.AppLifecycleCommand;
import pro.shushi.pamirs.boot.common.api.init.InstallDataInit;
import pro.shushi.pamirs.boot.common.api.init.ReloadDataInit;
import pro.shushi.pamirs.boot.common.api.init.UpgradeDataInit;
import pro.shushi.pamirs.eip.api.annotation.EipResolver;
import pro.shushi.pamirs.second.api.SecondModule;

import java.util.Collections;
import java.util.List;

@Component
public class SecondModuleBizInit implements InstallDataInit, UpgradeDataInit, ReloadDataInit {

    @Override
    public boolean init(AppLifecycleCommand command, String version) {
        initEip();
        return Boolean.TRUE;
    }

    @Override
    public boolean reload(AppLifecycleCommand command, String version) {
        initEip();
        return Boolean.TRUE;
    }

    @Override
    public boolean upgrade(AppLifecycleCommand command, String version, String existVersion) {
        initEip();
        return Boolean.TRUE;
    }

    @Override
    public List<String> modules() {
        return Collections.singletonList(SecondModule.MODULE_MODULE);
    }

    @Override
    public int priority() {
        return 0;
    }

    private void initEip() {
        EipResolver.resolver(SecondModule.MODULE_MODULE,null);
    }
}

Step5 重启看效果

Second模块重新 mvn install

重启pamris-second-boot工程

请求地址一:http://localhost:8094/openapi/pamirs/queryById4Open?tenant=pamirs

curl --location --request POST 'http://localhost:8094/openapi/pamirs/queryById4Open?tenant=pamirs' \
--header 'Content-Type: application/json' \
--data-raw '{
    "id": "111",
}'

返回结果:

{"errorCode":0,"result":{"age":1,"id":1,"name":"oinone eip test"},"success":true}

请求地址二:http://localhost:8094/openapi/pamirs/error?tenant=pamirs

curl --location --request POST 'http://localhost:8094/openapi/pamirs/error?tenant=pamirs' \
--header 'Content-Type: application/json' \
--data-raw '{
    "id": "111",
}'

返回结果:

{"success":false,"errorCode":"20140000","errorMsg":"系统异常, 测试异常"}

集成接口(举例)

下面的例子我们将用集成接口调用开放接口,集成接口配置在Demo模块

Step1 用于演示的集成配置模型定义

  1. 配置模型必须继承IEipAnnotationSingletonConfig接口

  2. 自定义construct函数,达到修改配置时可以自动刷新对应的集成接口和开放接口

  3. host为服务端:域名+端口,如www.oinone.top:80

  4. shcema为请求协议:http或https

  5. 类上可以增加@Open.Advanced 和 @Integrate.Advanced 配置达到通用配置的作用,优先级低于方法上的注解

package pro.shushi.pamirs.demo.api.model;

import pro.shushi.pamirs.eip.api.annotation.IEipAnnotationSingletonConfig;
import pro.shushi.pamirs.meta.annotation.Field;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.base.IdModel;
import pro.shushi.pamirs.meta.enmu.FunctionOpenEnum;
import pro.shushi.pamirs.meta.enmu.FunctionTypeEnum;

@Model.model(TestEipConfig.MODEL_MODEL)
@Model(displayName = "演示集成接口配置模型")
public class TestEipConfig extends IdModel implements IEipAnnotationSingletonConfig<TestEipConfig> {

    public static final String MODEL_MODEL = "demo.TestEipConfig";

    @Field(displayName = "服务端域名")
    private String host;
    @Field(displayName = "请求协议Http或Https")
    private String schema;

    @Function(openLevel = FunctionOpenEnum.API,summary = "演示集成接口配置模型")
    @Function.Advanced(type = FunctionTypeEnum.QUERY)
    public TestEipConfig construct(TestEipConfig config){
        TestEipConfig config1 = config.singletonModel();
        if(config1!=null){
            return config1;
        }
        return config.construct();
    }
}

Step2 用于演示的接口定义

  1. TestIntegrateService接口定义了内部访问函数callQueryById、callQueryByData、callQueryByIdError

  2. TestIntegrateServiceImpl 实现TestIntegrateService接口,并把方法定义为集成接口

a. config = TestEipConfig.class 设置通用配置类,可以在TestEipConfig增加@Integrate.Advanced。优先级低于方法上的注解。它在集成接口定义中时是必须。

b. @Integrate.Advanced(path=""),集成接口最终请求地址:TestEipConfig.schema+"://"+TestEipConfig.host+path

c. 方法体返回空就可以,实际执行时候该方法被拦截并不会执行

d. @Integrate.ConvertParam

ⅰ. 请求从方法入参数会以参数名为key放在上下文中,@Integrate.ConvertParam(inParam="data.id",outParam="id")代表把参数名为data的id属性值,放到上下文key为id上。

ⅱ. url和head的参数转化参见paramConverter的特殊转换

e. finalResultKey是指最终的请求参数key,会从上下文中找对应的key值作为请求参数

f. 更多注解配置见Eip注解说明

package pro.shushi.pamirs.demo.api.service;

import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Fun(TestIntegrateService.FUN_NAMESPACE)
public interface TestIntegrateService {
    String FUN_NAMESPACE ="demo.TestIntegrateService";
    @Function
    EipResult<SuperMap> callQueryById(String id);
    @Function
    EipResult<SuperMap> callQueryByData(TestOpenApiModel data);
    @Function
    EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data);

}
package pro.shushi.pamirs.demo.core.service;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.api.service.TestIntegrateService;
import pro.shushi.pamirs.eip.api.annotation.Integrate;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Fun(TestIntegrateService.FUN_NAMESPACE)
@Component
public class TestIntegrateServiceImpl implements TestIntegrateService {

    @Override
    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
    public EipResult<SuperMap> callQueryById(String id) {
        return null;
    }

    @Override
    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
    @Integrate.RequestProcessor(
            convertParams={
                    @Integrate.ConvertParam(inParam="data.id",outParam="id")
            }
    )
    public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
        return null;
    }

    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
    @Integrate.RequestProcessor(
            finalResultKey = "out",
            convertParams={
                    @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
            }
    )
    public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
        return null;
    }

}

Step3 新建TestEipAction

package pro.shushi.pamirs.demo.core.action;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.api.service.TestIntegrateService;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Action;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.enmu.ActionContextTypeEnum;
import pro.shushi.pamirs.meta.util.JsonUtils;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Model.model(TestEipConfig.MODEL_MODEL)
@Component
@Slf4j
public class TestEipAction {

    @Autowired
    private TestIntegrateService testIntegrateService;

    @Action(displayName = "调用集成接口callQueryById",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryById(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryById("111");
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByIdError",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdError(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByIdError((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByData",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdSuccess(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByData((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }
}

Step4 构建测试入口

@UxMenu("集成测试")@UxRoute(TestEipConfig.MODEL_MODEL) class TestEipConfigMenu{}

Step5 初始化Eip

在DemoModuleBizInit 类中增加私有方法initEip,并在init 、reload 、upgrade 中调用该私有化

private void initEip() {
    EipResolver.resolver(DemoModule.MODULE_MODULE,null);
}

Step6 重启看效果

  1. 点击【集成测试】菜单,点击【新增】按钮,配置集成接口信息

image.png

  1. 点击【集成测试】菜单,依次点击三个方法

image.png

  1. 模块切换进入【集成接口】,点击【日志】菜单。比对请求不同点

a. 设置finalResultKey的表达从接口上下文中取最终对应的value作为请求参数,不然会传全部接口上下文过去

b. callQueryByIdError,集成接口和对应的开放接口对结果认知差别

ⅰ. 开放接口返回异常信息

ⅱ. 集成接口没有对返回信息作判定,直接当成功。集成接口如何判定对方返还的是错误,请见自定义异常判定

自定义异常判定(举例)

Step1 新建异常判定函数

新建异常判定处理类TestExceptionPredictFunction,实现IEipExceptionPredict接口。从context.getInterfaceContextValue(DEFAULT_ERROR_CODE_KEY)中获取成功与否的值,并进行判断

package pro.shushi.pamirs.demo.core.service;

import pro.shushi.pamirs.core.common.StringHelper;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.IEipExceptionPredict;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;

import static pro.shushi.pamirs.eip.api.IEipContext.DEFAULT_ERROR_CODE_KEY;

@Fun(TestExceptionPredictFunction.FUN_NAMESPACE)
public class TestExceptionPredictFunction implements IEipExceptionPredict<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.TestExceptionPredictFunction";
    public static final String FUN ="testFunction";

    @Override
    public boolean test(IEipContext<SuperMap> context) {
        return !Boolean.TRUE.toString().equals(StringHelper.valueOf(context.getExecutorContextValue(DEFAULT_ERROR_CODE_KEY)));
    }

    @Function
    @Function.fun(FUN)
    public Boolean testFunction(IEipContext<SuperMap> context) {
        return test(context);
    }
}

Step2 修改TestIntegrateServiceImpl类

修改TestIntegrateServiceImpl的callQueryByIdError函数的注解,增加@Integrate.ExceptionProcessor注解

  1. errorCode ="success", 从返回结果中拿键值为“success”的值,作为接口上下文对应键值为IEipContext.DEFAULT_ERROR_CODE_KEY的值

  2. exceptionPredictFun 指定函数名

  3. exceptionPredictNamespace 指定函数命名空间

    
    …… 依赖类引用

@Fun(TestIntegrateService.FUN_NAMESPACE)
@Component
public class TestIntegrateServiceImpl implements TestIntegrateService {

……其他代码

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        }
)
@Integrate.ExceptionProcessor(
        errorCode ="success",
        exceptionPredictFun = TestExceptionPredictFunction.FUN,
        exceptionPredictNamespace = TestExceptionPredictFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

}

### Step3 重启看效果

1. 点击【集成测试】菜单,点击【调用集成接口callQueryByIdError 】

2. 模块切换进入【集成接口】,点击【日志】菜单。集成接口是否成功调用判定为false

![image.png](https://doc.oinone.top/wp-content/uploads/2024/05/1665041696328-76259be4-72ed-47c2-b3ea-4f7f3c9e6417.png)

## 自定义安全策略(AccessToken)

开发接口提供默认三种实现方式,例子采用DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN

 认证模式,最简单的安全模式:是有accessToken

1. DEFAULT_AUTHENTICATION_PROCESSOR_FUN:根据EipApplication配置的加密类型进行加解密处理的认证

2. DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN:不进行加解密处理的认证。

3. DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN:使用MD5简单验签的认证。

### Step1 给开放接口加上认证处理配置
```java
……依赖类引用

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {

    @Override
    @Function
    public TestOpenApiModel queryById(Long id) {
        return new TestOpenApiModel().queryById(id);
    }

    @Function
    @Open
    @Open.Advanced(
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        String id = Optional.ofNullable(String.valueOf(context.getInterfaceContext().getIteration("id"))).orElse("");
        TestOpenApiModel temp  = queryById(Long.valueOf(id));
        TestOpenApiResponse response = new TestOpenApiResponse();
        if(temp != null ) {
            response.setAge(temp.getAge());
            response.setId(temp.getId());
            response.setName(temp.getName());
        }else{
            response.setAge(1);
            response.setId(1L);
            response.setName("oinone eip test");
        }
        OpenEipResult<TestOpenApiResponse> result = new OpenEipResult<TestOpenApiResponse>(response);
        return result;
    }

    @Function
    @Open(path = "error")
    @Open.Advanced(
        httpMethod = "post",
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}
</code></pre>
<h3>Step2 修改TestEipAction,增加返回结果判断</h3>
<pre><code class="language-java">@Model.model(TestEipConfig.MODEL_MODEL)
@Component
@Slf4j
public class TestEipAction {

    @Autowired
    private TestIntegrateService testIntegrateService;

    @Action(displayName = "调用集成接口callQueryById",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryById(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryById("111");
        if(!eipResult.getSuccess()){
            throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryById 失败").errThrow();
        }
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));-
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByIdError",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdError(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByIdError((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        if(!eipResult.getSuccess()){
            throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryByIdError 失败").errThrow();
        }
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByData",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdSuccess(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByData((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        if(!eipResult.getSuccess()){
            throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryByData 失败").errThrow();
        }
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }
}</code></pre>
<h3>Step3 重启看效果</h3>
<p>期待效果,因为开放接口增加了安全相关校验,集成接口这边并没有调整,所以都会抛错</p>
<p><img src="https://doc.oinone.top/wp-content/uploads/2024/05/1665028192254-4f96bda0-d2c2-4b23-a7be-268c84a920f1.png" alt="image.png" /></p>
<h3>Step4 为集成接口定义auth处理类</h3>
<h3>Step4_1 修改 TestEipConfig</h3>
<p>增加了appkey和appSecret两个字段,用于存储开放接口的appkey和appSecret。大家注意我们是用集成接口去访问开放接口,大家可以要想象成两个应用。</p>
<p>站在开放接口角度,但凡需要访问开放接口,就需要先申请appkey和appSecret,并通过appkey和appSecret来换取accessToken,再用accessToken来请求。</p>
<p>站在集成接口角度,我们需要访问对方的接口,必须遵循对方接口安全规范。</p>
<pre><code class="language-java">package pro.shushi.pamirs.demo.api.model;

import pro.shushi.pamirs.eip.api.annotation.IEipAnnotationSingletonConfig;
import pro.shushi.pamirs.eip.api.annotation.Integrate;
import pro.shushi.pamirs.meta.annotation.Field;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.base.IdModel;
import pro.shushi.pamirs.meta.enmu.FunctionOpenEnum;
import pro.shushi.pamirs.meta.enmu.FunctionTypeEnum;

@Model.model(TestEipConfig.MODEL_MODEL)
@Model(displayName = "演示集成接口配置模型")
@Integrate.ExceptionProcessor(
        errorCode ="success",
        exceptionPredictFun = "testFunction",
        exceptionPredictNamespace = "demo.TestExceptionPredictFunction"
)
public class TestEipConfig extends IdModel implements IEipAnnotationSingletonConfig<TestEipConfig> {

    public static final String MODEL_MODEL = "demo.TestEipConfig";

    @Field(displayName = "服务端域名")
    private String host;
    @Field(displayName = "请求协议Http或Https")
    private String schema;

    @Field(displayName = "appkey")
    private String appKey;

    @Field(displayName = "appSecret")
    @Field.String(size = 4096)
    private String appSecret;

    @Function(openLevel = FunctionOpenEnum.API,summary = "演示集成接口配置模型")
    @Function.Advanced(type = FunctionTypeEnum.QUERY)
    public TestEipConfig construct(TestEipConfig config){
        TestEipConfig config1 = config.singletonModel();
        if(config1!=null){
            return config1;
        }
        return config.construct();
    }
}
</code></pre>
<h3>Step4_2 修改TestIntegrateService和TestIntegrateServiceImpl</h3>
<ol>
<li>
<p>定义和实现fetchOinoneAccessToken</p>
</li>
<li>
<p>修改集成注解增加auth处理器</p>
</li>
</ol>
<h3>Step4_3 新建TestAuthFunction</h3>
<ol>
<li>
<p>获取TestEipConfig的配置信息</p>
</li>
<li>
<p>先从缓存中拿accessToken</p>
<pre><code class="language-java">
@Function
EipResult<SuperMap> fetchOinoneAccessToken(String appkey,String appSecret) ;</code></pre>
</li>
</ol>
<pre><code>
```java

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN
)
public EipResult<SuperMap> callQueryById(String id) {
    return null;
}

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN
)
public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN

)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/get/access-token?tenant=pamirs")
@Integrate.RequestProcessor(
        convertParams={
                @Integrate.ConvertParam(inParam="appkey",outParam= IEipContext.HEADER_PARAMS_KEY+".appkey"),
                @Integrate.ConvertParam(inParam="appSecret",outParam=IEipContext.HEADER_PARAMS_KEY+".appSecret")
        }
)
@Integrate.ResponseProcessor(
        convertParams={
                @Integrate.ConvertParam(inParam="access_token",outParam="accessToken"),
        }
)
public EipResult<SuperMap> fetchOinoneAccessToken(String appkey,String appSecret) {
    return null;
}

a. 拿到直接放到请求头中

b. 拿不到调用对方接口获取accessToken(这里对方接口也就是我们自己的开放平台的接口)获取accessToken

package pro.shushi.pamirs.demo.core.service;

import org.apache.camel.ExtendedExchange;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.api.service.TestIntegrateService;
import pro.shushi.pamirs.eip.api.IEipAuthenticationProcessor;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.common.spring.BeanDefinitionUtils;

import java.util.concurrent.TimeUnit;

@Fun(TestAuthFunction.FUN_NAMESPACE)
@Component
public class TestAuthFunction implements IEipAuthenticationProcessor<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.TestAuthFunction";
    public static final String FUN ="testAuthentication";
    @Function
    @Function.fun(FUN)
    public Boolean testAuthentication(IEipContext<SuperMap> context,ExtendedExchange exchange) {
        return authentication(context,exchange);
    }

    @Override
    public boolean authentication(IEipContext<SuperMap> context, ExtendedExchange exchange) {
        //获取第三方配置
        TestEipConfig testEipConfig = new TestEipConfig().singletonModel();
        TestIntegrateService testIntegrateService =  BeanDefinitionUtils.getBean(TestIntegrateService.class);
        RedisTemplate redisTemplate = (RedisTemplate)BeanDefinitionUtils.getBean("redisTemplate");
        String accessToken = (String)redisTemplate.opsForValue().get(testEipConfig.getAppKey());
        if(StringUtils.isNotBlank(accessToken)){
            context.putInterfaceContextValue(IEipContext.HEADER_PARAMS_KEY+".accessToken",accessToken);
        }else {
            //调用获取accessToken接口
            EipResult<SuperMap> eipResult = testIntegrateService.fetchOinoneAccessToken(testEipConfig.getAppKey(), testEipConfig.getAppSecret());
            if (eipResult.getSuccess()) {
                accessToken = String.valueOf(eipResult.getContext().getInterfaceContextValue("accessToken"));
                if (StringUtils.isBlank(accessToken)) {
                    return Boolean.FALSE;
                }
                context.putInterfaceContextValue(IEipContext.HEADER_PARAMS_KEY + ".accessToken", accessToken);
                //oinone的accessToken默认失效时间为15分钟
                redisTemplate.opsForValue().set(testEipConfig.getAppKey(), accessToken,13*60, TimeUnit.SECONDS);
            } else {
                return Boolean.FALSE;
            }
        }
        return Boolean.TRUE;
    }
}

Step4_4 修改TestEipAction

借用TestEipConfig 模型的列表页提供一个新增EipApplication的入口

……引用依赖类
@Model.model(TestEipConfig.MODEL_MODEL)
@Component
@Slf4j
public class TestEipAction {
    ……其他代码

    @Action(displayName = "创建一个集成应用",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig createEipApplication(TestEipConfig data){

        EipApplication eipApplication = new EipApplication().setName("测试的集成应用").queryOne();
        if(eipApplication ==null){
            eipApplication = new EipApplication().setName("测试的集成应用");
            eipApplication.create();
        }else {
            eipApplication.updateById();
        }
        return data;
    }
}

Step5 重启看效果

  1. 点击【集成测试】菜单,点击【创建一个集成应用】按钮,数据库查看创建出来的EipApplication记录和EipAuthentication记录

image.png

image.png

  1. 点击【集成测试】菜单,编辑记录,填写appkey和appSecret

image.png

image.png

  1. 点击【集成测试】菜单,依次点击恢复原有的效果,只有【调用集成接口callQueryByIdError】报错其他不再报错了,也可以进入集成接口模块的日志菜单查看请求日志

image.png

自定义安全策略(AccessToken+加密)

例子采用DEFAULT_AUTHENTICATION_PROCESSOR_FUN安全策略,根据EipApplication配置的加密类型进行加解密处理的认证。安全模式:accessToken+加密

当开放接口采用DEFAULT_AUTHENTICATION_PROCESSOR_FUN的策略,需要外部调用方请求参数为{result:"加密后的请求体内容"}的结构,result的值为加密后的请求体内容。

Step1 更改开放接口认证方式

认证方式换成DEFAULT_AUTHENTICATION_PROCESSOR_FUN,@Open.Advanced(authenticationProcessorFun= EipFunctionConstant.DEFAULT_AUTHENTICATION_PROCESSOR_FUN)

小伙伴们可以把@Open.Advanced(authenticationProcessorFun)的配置挪到TestEipConfig中去,这样就不用为每个接口配置了,自己动手吧。

//…… 包引用
@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {

//…… 其他代码

    @Function
    @Open
    @Open.Advanced(
            authenticationProcessorFun= EipFunctionConstant.DEFAULT_AUTHENTICATION_PROCESSOR_FUN,
            authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
       //…… 其他代码
    }

    @Function
    @Open(path = "error")
    @Open.Advanced(
            httpMethod = "post",
            authenticationProcessorFun= EipFunctionConstant.DEFAULT_AUTHENTICATION_PROCESSOR_FUN,
            authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}

Step2 重启看效果

期待效果,因为开放接口修改了安全相关校验,集成接口这边并没有调整,所以都会抛错

image.png

Step3 为集成接口定义新的auth处理类

Step3_1 修改 TestEipConfig

增加两个字段encryptType和publicKey,这里加密类型为RSA,我们为集成接口申请的EipApplication没有指定加密类型,默认RSA。oinone的开放接口总共支持两种:RSA、AES


    @Field(displayName = "encryptType")
    private EncryptTypeEnum encryptType;

    @Field(displayName = "publicKey")
    @Field.String(size = 4096)
    private String publicKey;

Step3_2 新建TestInOutConvertFunction

新建TestInOutConvertFunction实现IEipInOutConverter接口

package pro.shushi.pamirs.demo.core.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import org.apache.camel.ExtendedExchange;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.EncryptHelper;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.eip.api.IEipInOutConverter;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.util.JsonUtils;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

@Fun(TestInOutConvertFunction.FUN_NAMESPACE)
@Component
public class TestInOutConvertFunction implements IEipInOutConverter {

    public static final String FUN_NAMESPACE ="demo.third.TestInOutConvertFunction";
    public static final String FUN ="exchangeObject";

    @Function
    @Function.fun(FUN)
    @Override
    public Object exchangeObject(ExtendedExchange exchange, Object inObject) throws Exception {
        //获取第三方配置
        TestEipConfig testEipConfig = new TestEipConfig().singletonModel();
        //数据加密
        String data;
        if (inObject instanceof InputStream) {
            data = (String) JSON.parseObject((InputStream)inObject, String.class, new Feature[0]);
        } else if (inObject instanceof String) {
            data = (String)inObject;
        } else {
            data = JsonUtils.toJSONString(inObject);
        }
        switch (testEipConfig.getEncryptType()) {
            case RSA:
                try {
                    data = EncryptHelper.encryptByKey(EncryptHelper.getPublicKey(testEipConfig.getEncryptType().value(), testEipConfig.getPublicKey()), data);
                } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) {
                    e.printStackTrace();
                    data = null;
                }
                break;
            case AES:
                try {
                    data = EncryptHelper.encryptByKey(EncryptHelper.getSecretKeySpec(testEipConfig.getEncryptType().value(), testEipConfig.getPublicKey()), data);
                } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
                    e.printStackTrace();
                    data = null;
                }
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + testEipConfig.getEncryptType());
        }
        //放到指定的路径中,因为集成接口的finalResultKey为"out",这样刚好构建{result:""}的请求结构,根据不同公司不一样
        return "{result:\""+data+"\"}";
    }
}

Step3_3 修改TestIntegrateServiceImpl

为集成方法增加inOutConverter的注解

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestInOutConvertFunction.FUN,
        inOutConverterNamespace = TestInOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryById(String id) {
    return null;
}

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestInOutConvertFunction.FUN,
        inOutConverterNamespace = TestInOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestInOutConvertFunction.FUN,
        inOutConverterNamespace = TestInOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

Step4 重启看效果

  1. 点击【集成测试】菜单,点击【创建一个集成应用】按钮,数据库查看创建出来的EipApplication记录和EipAuthentication记录,为记录填写publicKey

image.png

image.png

  1. 再次点击【集成测试】菜单,依次点击恢复原有的效果,只有【调用集成接口callQueryByIdError】报错其他不再报错了,也可以进入集成接口模块的日志菜单查看请求日志

image.png

自定义安全策略(AccessToken+签名)

例子采用DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN

安全策略,安全模式:accessToken+签名

签名支持:md5和hmac

签名方式和签名摘要:请求方指定,在请求体中设置signatureMethod、signature

Step1 更改开放接口认证方式

认证方式换成DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,@Open.Advanced(authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN)

//…… 包引用

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {
//…… 其他代码

    @Function
    @Open
    @Open.Advanced(
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        //…… 其他代码
    }

    @Function
    @Open(config = TestEipConfig.class,path = "error")
    @Open.Advanced(
        httpMethod = "post",
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}

Step2 重启看效果

期待效果,因为开放接口修改了安全相关校验,集成接口这边并没有调整,所以都会抛错

image.png

Step3 为集成接口定义新的auth处理类

Step3_1 新建SignUtil

新建SignUtil签名工具类

package pro.shushi.pamirs.demo.core.util;

import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

public class SignUtil {

    public static String signTopRequest(Map<String, String> params, String secret, String signMethod) throws IOException {
        // 第一步:检查参数是否已经排序
        String[] keys = params.keySet().toArray(new String[0]);
        Arrays.sort(keys);

        // 第二步:把所有参数名和参数值串在一起
        StringBuilder query = new StringBuilder();
        if ("md5".equals(signMethod)) { //签名的摘要算法,可选值为:hmac,md5,hmac-sha256
            query.append(secret);
        }
        for (String key : keys) {
            String value = params.get(key);
            if (StringUtils.isAllBlank(new CharSequence[]{key, value})) {
                query.append(key).append(value);
            }
        }

        // 第三步:使用MD5/HMAC加密
        byte[] bytes;
        if ("hmac".equals(signMethod)) {
            bytes = encryptHMAC(query.toString(), secret);
        } else {
            query.append(secret);
            bytes = encryptMD5(query.toString());
        }

        // 第四步:把二进制转化为大写的十六进制(正确签名应该为32大写字符串,此方法需要时使用)
        return byte2hex(bytes);
    }

    public static byte[] encryptHMAC(String data, String secret) throws IOException {
        byte[] bytes = null;
        try {
            SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacMD5");
            Mac mac = Mac.getInstance(secretKey.getAlgorithm());
            mac.init(secretKey);
            bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        } catch (GeneralSecurityException gse) {
            throw new IOException(gse.toString());
        }
        return bytes;
    }

    private static byte[] encryptMD5(String data) throws IOException {
        return encryptMD5(data.getBytes(StandardCharsets.UTF_8));
    }

    private static byte[] encryptMD5(byte[] data) throws IOException {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            return md.digest(data);
        } catch (NoSuchAlgorithmException var3) {
            throw new IOException(var3.toString(), var3);
        }
    }

    public static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }
}

Step3_2 新建TestMd5InOutConvertFunction

  1. 跟加密一样,必须在所有内容都处理完成后进行处理,要不然参数个数对不上的。

  2. 例子中新增一种设置Header的方法,exchange.getMessage().setHeader前面,在前面我们学习AccessToken的时候用的方法是:context.putInterfaceContextValue(IEipContext.HEADER_PARAMS_KEY+".accessToken"

    
    package pro.shushi.pamirs.demo.core.service;

import org.apache.camel.ExtendedExchange;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.core.util.SignUtil;
import pro.shushi.pamirs.eip.api.IEipInOutConverter;
import pro.shushi.pamirs.eip.api.converter.DefaultInOutConverter;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Fun(TestMd5InOutConvertFunction.FUN_NAMESPACE)
@Component
public class TestMd5InOutConvertFunction extends DefaultInOutConverter implements IEipInOutConverter {

public static final String FUN_NAMESPACE ="demo.third.TestMd5InOutConvertFunction";
public static final String FUN ="exchangeObject";

@Function
@Function.fun(FUN)
public Object exchangeObject(ExtendedExchange exchange, Object inObject) throws Exception {
    if (inObject instanceof Map) {
        Map<String, Object> map = (Map)inObject;
        if (MapUtils.isNotEmpty(map)) {

            TestEipConfig testEipConfig = new TestEipConfig().singletonModel();
            String signatureMethod ="md5";
            Map<String, String> params = new HashMap(map.size());
            for(Map.Entry<String ,Object> entry: map.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                params.put(key, String.valueOf(value));
            }
            try {
                String signature = SignUtil.signTopRequest(params, testEipConfig.getAppKey(), signatureMethod);
                exchange.getMessage().setHeader("signatureMethod",signatureMethod);
                exchange.getMessage().setHeader("signature",signature);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return super.exchangeObject(exchange, inObject);
}

}


### Step3_3 修改TestIntegrateServiceImpl

更换inOutConverter的注解为TestMd5InOutConvertFunction
```JAVA
@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestMd5InOutConvertFunction.FUN,
        inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryById(String id) {
    return null;
}

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestMd5InOutConvertFunction.FUN,
        inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestMd5InOutConvertFunction.FUN,
        inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE

)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

Step4 重启看效果

点击【集成测试】菜单,依次点击恢复原有的效果,只有【调用集成接口callQueryByIdError】报错其他不再报错了,也可以进入集成接口模块的日志菜单查看请求日志

image.png

自定义序列化方式

Step1 新增一个xml的开放接口

为TestOpenApiModelServiceImpl新增一个方法queryById4ReturnXml,使用@Open注解,路径配置为xml。同时记得重启second应用,发布这个openApi

@Function
@Open(path = "xml")
@Open.Advanced(
        httpMethod = "post",
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
)
public String queryById4ReturnXml(IEipContext<SuperMap> context , ExtendedExchange exchange) {
    return "<result><id>12</id><success>true</success></result>";
}

Step2 Demo工程引入dom4j的依赖包

在总工程中修改pom.xml增加对dom4j的依赖管理

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.1</version>
</dependency>

在pamirs-demo-core工程中修改pom.xml增加对dom4j的依赖

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
</dependency>

Step3 自定义序列化函数TestSerializableFunction

新建TestSerializableFunction实现 IEipSerializable<SuperMap>接口

package pro.shushi.pamirs.demo.core.service;

import com.alibaba.fastjson.JSONArray;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipSerializable;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;

@Fun(TestSerializableFunction.FUN_NAMESPACE)
@Component
public class TestSerializableFunction implements IEipSerializable<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.third.TestSerializableFunction";
    public static final String FUN ="serializable";

    @Override
    @Function.Advanced(displayName = "自定义xml序列化方式")
    @Function.fun(FUN)
    public SuperMap serializable(Object inObject) {
        if (inObject == null) {
            return new SuperMap();
        } else {
            SuperMap result;
            if (inObject instanceof String) {
                String inObjectString = (String)inObject;
                if (StringUtils.isNotBlank(inObjectString)) {
                    result = this.stringToSuperMap(inObjectString);
                } else {
                    result = new SuperMap();
                }
            } else if (inObject instanceof InputStream) {
                result = this.inputStreamToString((InputStream)inObject);
            } else if (inObject instanceof SuperMap) {
                result = (SuperMap) inObject;
            }
            else{
                result = new SuperMap();
            }
            return result;
        }
    }

    protected SuperMap inputStreamToString(InputStream inputStream) {
        StringBuilder sb = new StringBuilder();
        String line;
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
            while ((line = bufferedReader.readLine()) != null) {
                sb.append(line);
            }
            return serializable(sb.toString());
        } catch (IOException e) {
            return new SuperMap();
        }
    }

    protected SuperMap stringToSuperMap(String s) {
        SuperMap result = new SuperMap();
        try {
            Document document = DocumentHelper.parseText(s);
            Element root = document.getRootElement();
            iterateNodes(root, result);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public static void iterateNodes(Element node, SuperMap superMap){
        //获取当前元素的名称
        String nodeName = node.getName();
        if(superMap.containsKey(nodeName)){
            //该元素在同级下有多个
            Object object = superMap.getIteration(nodeName);
            List<Object> list = Lists.newArrayList();
            if(object instanceof JSONArray){
                list = (List) object;
            }else {
                list = Lists.newArrayList();
                list.add(object);
            }
            //获取该元素下所有子元素
            List<Element> listElement = node.elements();
            if(listElement.isEmpty()){
                //该元素无子元素,获取元素的值
                String nodeValue = node.getTextTrim();
                list.add(nodeValue);
                superMap.putIteration(nodeName, list);
                return;
            }
            //有子元素
            SuperMap subMap = new SuperMap();
            //遍历所有子元素
            for(Element e:listElement){
                //递归
                iterateNodes(e, subMap);
            }
            list.add(subMap);
            subMap.putIteration(nodeName, list);
            return;
        }
        List<Element> listElement = node.elements();
        if(listElement.isEmpty()){
            //该元素无子元素,获取元素的值
            String nodeValue = node.getTextTrim();
            superMap.putIteration(nodeName, nodeValue);
            return;
        }
        //有子节点,新建一个JSONObject来存储该节点下子节点的值
        SuperMap subMap = new SuperMap();
        //遍历所有一级子节点
        for(Element e:listElement){
            //递归
            iterateNodes(e, subMap);
        }
        superMap.putIteration(nodeName, subMap);
    }

}

Step4 新增一个异常判定类

新增TestXmlExceptionPredictFunction异常判定类,因为原来的异常判断类配置在了TestEipConfig,所以所有方法都会默认使用TestEipConfig类上定义的异常判断类。

package pro.shushi.pamirs.demo.core.service;

import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.IEipExceptionPredict;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;

@Fun(TestXmlExceptionPredictFunction.FUN_NAMESPACE)
public class TestXmlExceptionPredictFunction implements IEipExceptionPredict<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.TestXmlExceptionPredictFunction";
    public static final String FUN ="testFunction";

    @Override
    public boolean test(IEipContext<SuperMap> context) {
        return Boolean.FALSE;
    }

    @Function
    @Function.fun(FUN)
    public Boolean testFunction(IEipContext<SuperMap> context) {
        return test(context);
    }
}

Step5 修改TestIntegrateService和TestIntegrateServiceImpl

TestIntegrateService增加方法定义

@Function
EipResult<SuperMap> callQueryReturnXml(TestOpenApiModel data) ;

TestIntegrateServiceImpl实现callQueryReturnXml 并定义为集成接口,同时配置自定义序列化函数

……引用依赖类

@Fun(TestIntegrateService.FUN_NAMESPACE)
@Component
public class TestIntegrateServiceImpl implements TestIntegrateService {
        ……其他代码

    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(
            path = "/openapi/pamirs/xml?tenant=pamirs",
            httpMethod = "post"
    )
    @Integrate.RequestProcessor(
            finalResultKey = "out",
            convertParams={
                    @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
            },
            authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
            authenticationProcessorFun =  TestAuthFunction.FUN,
            inOutConverterFun = TestMd5InOutConvertFunction.FUN,
            inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE

    )
    @Integrate.ResponseProcessor(
            finalResultKey = "result",
            serializableFun = TestSerializableFunction.FUN,
            serializableNamespace = TestSerializableFunction.FUN_NAMESPACE
    )
    @Integrate.ExceptionProcessor(
            errorCode ="success",
            exceptionPredictFun = TestXmlExceptionPredictFunction.FUN,
            exceptionPredictNamespace = TestXmlExceptionPredictFunction.FUN_NAMESPACE
    )
    public EipResult<SuperMap> callQueryReturnXml(TestOpenApiModel data) {
        return null;
    }

}

Step6 增加测试入口

修改TestEipAction 类增加一个callQueryByXml的Action定义,调用testIntegrateService集成接口的callQueryReturnXml方法

@Action(displayName = "调用集成接口callQueryReturnXml",contextType = ActionContextTypeEnum.CONTEXT_FREE)
public TestEipConfig callQueryByXml(TestEipConfig data){
    EipResult<SuperMap> eipResult = testIntegrateService.callQueryReturnXml((TestOpenApiModel)new TestOpenApiModel().setName("cpc").setId(111L));
    if(!eipResult.getSuccess()){
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryReturnXml 失败").errThrow();
    }
    PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
    return data;
}

Step7 重启看效果

image.png

Eip注解说明

开放接口注解

Open
├── name 显示名称
├── config 配置类
├── path 路径
├── Advanced 更多配置
│ ├── httpMethod 请求方法,默认:post
│ ├── inOutConverterFun 输入输出转换器函数名称
│ ├── inOutConverterNamespace 输入输出转换器函数命名空间
│ ├── authenticationProcessorFun 认证处理器函数名称
│ ├── authenticationProcessorNamespace 认证处理器函数命名空间
│ ├── serializableFun 序列化函数名称
│ ├── serializableNamespace 序列化函数命名空间
│ ├── deserializationFun 反序列化函数名称
│ └── deserializationNamespace 反序列化函数命名空间

集成接口注解

Integrate
├── name 显示名称
├── config 配置类
├── Advanced 更多配置
│ ├── host 请求域名+端口
│ ├── path 请求路径 以“/”开头
│ ├── schema 请求协议 http或者https
│ └── httpMethod 请求方法,默认post
├── ExceptionProcessor 异常配置
│ ├── exceptionPredictFun 异常判定函数名
│ ├── exceptionPredictNamespace 异常判定函数命名空间
│ ├── errorMsg 异常判定Msg的键值
│ └── errorCode 异常判定errorCode的键值
├── RequestProcessor 请求处理配置
│ ├── finalResultKey 请求的最终结果键值
│ ├── inOutConverterFun 输入输出转换器函数名称
│ ├── inOutConverterNamespace 输入输出转换器函数命名空间
│ ├── paramConverterCallbackFun 参数转换回调函数名称
│ ├── paramConverterCallbackNamespace 参数转换回调函数命名空间
│ ├── authenticationProcessorFun 认证处理器函数名称
│ ├── authenticationProcessorNamespace 认证处理器函数命名空间
│ ├── serializableFun 序列化函数名称
│ ├── serializableNamespace 序列化函数命名空间
│ ├── deserializationFun 反序列化函数名称
│ ├── deserializationNamespace 反序列化函数命名空间
│ └── convertParams 参数转化集合
│ └── ConvertParam 参数转化
│ ├── inParam 输入参数的键值
│ └── outParam输出参数的键值
├── ResponseProcessor 请求处理配置
│ ├── finalResultKey 响应的最终结果键值
│ ├── inOutConverterFun 输入输出转换器函数名称
│ ├── inOutConverterNamespace 输入输出转换器函数命名空间
│ ├── paramConverterCallbackFun 参数转换回调函数名称
│ ├── paramConverterCallbackNamespace 参数转换回调函数命名空间
│ ├── authenticationProcessorFun 认证处理器函数名称
│ ├── authenticationProcessorNamespace 认证处理器函数命名空间
│ ├── serializableFun 序列化函数名称
│ ├── serializableNamespace 序列化函数命名空间
│ ├── deserializationFun 反序列化函数名称
│ ├── deserializationNamespace 反序列化函数命名空间
│ └── convertParams 参数转化集合
│ └── ConvertParam 参数转化
│ ├── inParam 输入参数的键值
│ └── outParam 输出参数的键值

Eip处理函数说明

Eip处理函数 作用 需实现接口 说明
inOutConverter 输入输出转换器函数 IEipInOutConverter inOutConverter 核心处理请求体的构造或响应体的构造
paramConverter 参数转换回调函数 IEipParamConverter 如果自定义paramConverter,会让默认ConvertParam配置的(inParam,outParam)转化失效了,需要自行处理
authenticationProcessor 认证处理器函数 IEipAuthenticationProcessor<SuperMap>
serializable 序列化函数 IEipSerializable<SuperMap> 序列化方法将任意对象转换为上下文承载对象
deserialization 反序列化函数 IEipDeserialization<SuperMap> 目前这个意义不大,因为如果设置了finalResultKey,就会不走反序列化。现在都是通过inoutconverter处理的。
exceptionProcessor 异常判定函数 IEipExceptionPredict<SuperMap>

paramConverter的特殊转换

paramConverter的特殊转换
URL_QUERY_PARAMS_KEY IEipContext.URL_QUERY_PARAMS_KEY.xxx是请求参数,会拼接在url的问号后面 convertParams={@Integrate.ConvertParam(inParam="id",outParam=IEipContext.URL_QUERY_PARAMS_KEY+".id") }
HEADER_PARAMS_KEY IEipContext.HEADER_PARAMS_KEY.xxx是http请求头参数 convertParams={@Integrate.ConvertParam(inParam="id",outParam=IEipContext.HEADER_PARAMS_KEY+".id") }

调用流程

请求调用流程

6.2 集成平台(改)

响应调用流程

6.2 集成平台(改)

Oinone社区 作者:史, 昂原创文章,如若转载,请注明出处:https://doc.oinone.top/oio4/9326.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

Like (0)
史, 昂's avatar史, 昂数式管理员
Previous 2024年5月23日 am8:12
Next 2024年5月23日 am8:14

相关推荐

  • 3.3.7 字段之序列化方式

    本文核心是带大家全面了解oinone的序列方式,包括支持的序列化类型、注意点、如果新增客户化序列化方式以及字段默认值的反序列化。 一、数据存储的序列化 (举例) 使用@Field注解的serialize属性来配置非字符串类型属性的序列化与反序列化方式,最终会以序列化后的字符串持久化到存储中。 ### Step1 新建PetItemDetail模型、并为PetItem添加两个字段 PetItemDetail继承TransientModel,增加两个字段,分别为备注和备注人 package pro.shushi.pamirs.demo.api.tmodel; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.base.TransientModel; import pro.shushi.pamirs.user.api.model.PamirsUser; @Model.model(PetItemDetail.MODEL_MODEL) @Model(displayName = "商品详情",summary = "商品详情",labelFields = {"remark"}) public class PetItemDetail extends TransientModel { public static final String MODEL_MODEL="demo.PetItemDetail"; @Field.String(min = "2",max = "20") @Field(displayName = "备注",required = true) private String remark; @Field(displayName = "备注人",required = true) private PamirsUser user; } 图3-3-7-1 PetItemDetail继承TransientModel 修改PetItem,增加两个字段petItemDetails类型为List和tags类型为List,并设置为不同的序列化方式,petItemDetails为JSON(缺省就是JSON,可不配),tags为COMMA。同时设置 @Field.Advanced(columnDefinition = varchar(1024)),防止序列化后存储过长 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.demo.api.tmodel.PetItemDetail; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.enmu.NullableBoolEnum; import java.math.BigDecimal; import java.util.List; @Model.model(PetItem.MODEL_MODEL) @Model(displayName = "宠物商品",summary="宠物商品",labelFields = {"itemName"}) public class PetItem extends AbstractDemoCodeModel{ public static final String MODEL_MODEL="demo.PetItem"; @Field(displayName = "商品名称",required = true) private String itemName; @Field(displayName = "商品价格",required = true) private BigDecimal price; @Field(displayName = "店铺",required = true) @Field.Relation(relationFields = {"shopId"},referenceFields = {"id"}) private PetShop shop; @Field(displayName = "店铺Id",invisible = true) private Long shopId; @Field(displayName = "品种") @Field.many2one @Field.Relation(relationFields = {"typeId"},referenceFields = {"id"}) private PetType type; @Field(displayName = "品种类型",invisible = true) private Long typeId; @Field(displayName = "详情", serialize = Field.serialize.JSON, store = NullableBoolEnum.TRUE) @Field.Advanced(columnDefinition = "varchar(1024)") private List<PetItemDetail> petItemDetails; @Field(displayName = "商品标签",serialize = Field.serialize.COMMA,store = NullableBoolEnum.TRUE,multi = true) @Field.Advanced(columnDefinition = "varchar(1024)") private…

    2024年5月23日
    1.6K00
  • 3.5.6.4 动作的配置

    在3.5.3【Action的类型】一文中,我们介绍Action的几种类型,以及组合动作。 通用配置 配置项 可选值 默认值 作用 name 动作名称 label 显示名称 icon 图标 type primary defaultlink primary 按钮类型样式,支持主要样式、次要样式以及链接样式。 bizStyle defaultsuccesswarningdangerinfo default 按钮业务样式,支持成功(green)、警告(yellow)、危险(red)、信息(grey)四种样式。 invisible truefalse condition false 展示规则,有简单的true/false显隐,也支持复杂的表达式 disabled truefalse condition 根据动作上下文类型进行自动推断 是否禁用自动推断规则:当上下文类型为【单行】时,相当于使用表达式LIST_COUNT(context.activeRecords) != 1当上下文类型为【多行】时,相当于使用表达式LIST_COUNT(context.activeRecords) <= 1当上下文类型为【单行或多行】时,相当于使用表达式LIST_COUNT(context.activeRecords) == 0 disabledTitle string 根据动作上下文类型进行自动推断 禁用悬浮提示 表3-5-6-12 动作通用配置 二次确认配置 二次确认框默认支持两种模式,对话框和气泡框; 对话框 图3-5-6-51 对话框提示 气泡框 图3-5-6-52 气泡框警告 配置项 配置项 可选值 默认值 作用 备注 confirm string 二次确认提示文字 配置后开启二次确认 confirmType POPPER(气泡提示框) MODAL(对话框) POPPER 确认框类型 confirmPosition TM(按钮上方) BM(按钮下方) LM(按钮左侧) RM(按钮右侧) BM 确认框位置 气泡框该配置生效 enterText 确定 确定按钮文字 cancelText 取消 取消按钮文字 表3-5-6-13 配置项 弹出层动作配置(窗口动作ViewAction) 目前平台对于弹出层支持了两种展示形式。弹窗(modal/dialog)和抽屉(drawer) 支持两种配置方式【内嵌视图配置】和【引用已有页面】,内嵌视图配置优先于引用已有页面。 内嵌视图配置 该配置对于弹窗和抽屉均适用。 <action name="窗口动作名称" label="创建"> <view model="模型编码" type="form"> <template slot="form" widget="form"> <field data="id" invisible="true" /> <field data="code" label="编码" widget="Input" /> <field data="name" label="名称" widget="Input" /> </template> <template slot="footer"> <action name="$$internal_DialogCancel" label="关闭" type="default" /> <action name="create" label="确定" /> </template> </view> </action> 图3-5-6-53 内嵌视图配置 引用已有页面配置 该配置对于弹窗和抽屉均适用。 <view model="模型编码" type="form"> <template slot="form" widget="form"> <field data="id" invisible="true" /> <field data="code" label="编码" widget="Input" /> <field data="name" label="名称" widget="Input" /> </template> <template slot="footer"> <action name="$$internal_DialogCancel" label="关闭" type="default" /> <action name="create" label="确定" /> </template> </view> 图3-5-6-54 引用已有页面示例 <action name="窗口动作名称" label="创建" resViewName="$viewName$" /> 图3-5-6-55 引用已有页面 弹窗 当窗口动作的路由方式(target)为dialog时,内嵌视图/引用页面将以弹窗形式展示在页面上。 配置项 配置项 可选值 默认值 作用 title…

    2024年5月23日
    1.4K00
  • 3.3.10 字段类型之关系描述的特殊场景

    在3.3.9【字段类型之关系与引用】一文中已经描述了各种关系字段的常规写法,还有一些特殊场景如:关系映射中存在常量,或者M2M中间表是大于两个字段构成。 举例说明关系字段-高级用法 场景描述 PetTalent模型增加talentType字段,PetItem与PetTalent的多对多关系增加talentType(达人类型),PetItemRelPetTalent 中间表维护petItemId、petTalentId以及talentType,PetDogItem和PetCatItem分别重写petTalents 字段,关系中增加常量描述。示意图如下 实际操作步骤: Step1 新增 TalentTypeEnum package pro.shushi.pamirs.demo.api.enumeration; import pro.shushi.pamirs.meta.annotation.Dict; import pro.shushi.pamirs.meta.common.enmu.BaseEnum; @Dict(dictionary = TalentTypeEnum.DICTIONARY,displayName = "达人类型") public class TalentTypeEnum extends BaseEnum<TalentTypeEnum,Integer> { public static final String DICTIONARY ="demo.TalentTypeEnum"; public final static TalentTypeEnum DOG =create("DOG",1,"狗达人","狗达人"); public final static TalentTypeEnum CAT =create("CAT",2,"猫达人","猫达人"); } Step2 PetTalent模型增加talentType字段 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.demo.api.enumeration.TalentTypeEnum; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; @Model.model(PetTalent.MODEL_MODEL) @Model(displayName = "宠物达人",summary="宠物达人",labelFields ={"name"}) public class PetTalent extends AbstractDemoIdModel{ public static final String MODEL_MODEL="demo.PetTalent"; @Field(displayName = "达人") private String name; @Field(displayName = "达人类型") private TalentTypeEnum talentType; } Step3 修改PetItem的petTalents字段,在关系描述中增加talentType(达人类型) @Field.many2many(relationFields = {"petItemId"},referenceFields = {"petTalentId","talentType"},through = "PetItemRelPetTalent") @Field.Relation(relationFields = {"id"}, referenceFields = {"id","talentType"}) @Field(displayName = "推荐达人",summary = "推荐该商品的达人们") private List<PetTalent> petTalents; Step4 PetDogItem增加petTalents字段,重写父类PetItem的关系描述 talentType配置为常量,填入枚举的值 增加domain描述用户页面选择的时候自动过滤出特定类型的达人,RSQL用枚举的name @Field.many2many(relationFields = {"petItemId"},referenceFields = {"petTalentId","talentType"},through = "PetItemRelPetTalent") @Field.Relation(relationFields = {"id"}, referenceFields = {"id","talentType"}) @Field(displayName = "推荐达人",summary = "推荐该商品的达人们") private List<PetTalent> petTalents; Step5 PetCatItem增加petTalents字段,重写父类PetItem的关系描述 talentType配置为常量,填入枚举的值 增加domain描述用户页面选择的时候自动过滤出特定类型的达人,RSQL用枚举的name @Field(displayName = "推荐达人") @Field.many2many( through = "PetItemRelPetTalent", relationFields = {"petItemId"}, referenceFields = {"petTalentId","talentType"} ) @Field.Relation(relationFields = {"id"}, referenceFields = {"id", "#2#"}, domain = " talentType == CAT") private List<PetTalent> petTalents; Step6 清除中间表demo_core_pet_item_rel_pet_talent的数据记录 清除PetItem与PetTalent的多对多中间表demo_core_pet_item_rel_pet_talent的数据记录 Step7 重启看效果 修改达人记录,选择不同达人类型 PetItem、PetCatItem、PetDogItem不同的交互页面

    2024年5月23日
    1.9K00
  • 3.5.7.6 自定义字段

    字段是什么 字段的基本概念 定义:字段通常指的是数据的一个单独项,它可以是一个文本框、下拉菜单、复选框等,用于在用户界面上收集或展示数据。 用途:在表单中,字段用于收集用户输入;在表格或列表中,字段用于显示数据。 类型:字段可以有不同的类型,如文本、数字、日期等,这些类型通常由数据模型定义。 Oinone框架中的字段 在Oinone框架中,字段的设计和实现遵循以下原则: 后端模型驱动:前端的字段直接由后端的数据模型决定。这意味着后端定义了哪些数据应该展示,以及如何展示。 减少前后端联调:由于字段的定义和行为是由后端控制的,前后端的联调需求大大减少。前端开发者主要关注于如何呈现这些字段,而后端则负责数据的逻辑和结构。 灵活性与规范性:虽然Oinone推荐所有场景都遵循后端模型驱动字段的原则,以保持前后端的一致性和减少沟通成本,但它也为高度定制化的前端页面提供了灵活性。 元数据使用:Oinone可能还使用元数据来进一步定义字段的行为,例如它们是否可见、如何验证用户输入等。 结合前后端 在使用Oinone时,理解前后端如何合作来定义和展示字段是很重要的。这种方法不仅提高了开发效率,而且有助于确保数据的一致性和应用程序的可维护性。同时,对于那些需要特定定制或特殊处理的场景,开发团队能够灵活地适应这些需求,在遵守总体架构原则的同时进行一些特定的调整和优化。 作用场景 在Oinone框架中,字段扮演着连接后端数据模型和前端用户界面的重要角色。其作用场景包括但不限于以下几点: 业务组件的核心: Oinone集成了AntdDesignVue的全部UI组件,将它们转化为业务组件。这些业务组件以字段的形式存在,使得前端开发变得简单高效。开发人员可以直接使用这些现成的业务组件来构建用户界面,大大减少了开发工作量。 无代码开发支持: 字段的设计使得Oinone支持无代码开发。开发者可以通过拖拉拽的方式在前端快速构建界面,而后端模型的定义直接决定了这些界面的生成。这种模式简化了传统的前端开发流程,提升了开发效率。 个性化定制: 虽然标准的UI组件可以满足大部分需求,但复杂多变的业务场景往往需要更多个性化的处理。在Oinone中,开发者可以根据具体业务需求和公司的UI指南,定义专门针对特定行业或客户的定制化字段和组件。 与无代码平台的结合: Oinone允许将个性化的字段和组件与无代码平台相结合。这意味着即使在进行个性化定制时,也能保持使用无代码工具的便利性,实现更灵活、更高效的前端开发。 适应多维度业务需求: 由于字段在Oinone中的灵活性和可定制性,它们能够适应多维度的业务需求,无论是从UI设计、用户体验还是业务逻辑的角度,字段都能提供合适的解决方案 自定义字段 示例工程目录 以下是需关注的工程目录示例,main.ts更新导入./field: 图3-5-7-24 自定义字段工程目录示例 示例代码 创建自定义字段组件: 使用Vue框架创建一个新的组件(例如 CustomStringFieldVue),并定义其模板、脚本和样式。 在模板中定义字段的HTML结构。 在脚本中使用 defineComponent 来定义Vue组件。 字段类的定义: 导入必要的模块,如 FormFieldWidget, ModelFieldType, SPI, ViewType 等。 使用 @SPI.ClassFactory 装饰器来注册自定义字段。 在类内部初始化并设置组件。 SPI注册参数解释: viewType: 指定视图类型,如表单视图或搜索视图。 widget: 可以指定组件名称。 ttype: 字段的业务类型,例如字符串、数字等。 multi: 指明字段是否支持多值。 model: 定义字段所属的模型。 viewName: 指定视图名称。 name: 定义所属字段的名称。 import {FormFieldWidget, ModelFieldType, SPI, ViewType} from '@kunlun/dependencies'; import CustomStringFieldVue from './CustomStringField.vue'; @SPI.ClassFactory( FormFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String }) ) export class CustomStringField extends FormFieldWidget { public initialize(props) { super.initialize(props); this.setComponent(CustomStringFieldVue); return this; } } 图3-5-7-24 自定义字段组件(TS)示例 <template> <div class="custom-string-filed-wrapper"> 字段组件 </div> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ inheritAttrs: false, name: 'CustomStringFieldVue' }) </script> <style lang="scss"> .custom-string-filed-wrapper { } </style> 图3-5-7-24 自定义字段组件(Vue)示例 效果 图3-5-7-24 自定义字段效果示例

    2024年5月23日
    2.4K00
  • 梅丛银

    认识陈鹏程及数式核心团队同学已经有一段时间了,在我们多次的交流讨论中时常会谈及:未来中国哪家软件企业能在互联网云原生时代走出来超越传统软件企业?史昂说这是他的梦想,也是他们团队这么多年坚持技术和产品研发与应用优先思考之路。史昂及数式核心团队面向企业应用市场历经三年的潜心研发和实战交付,推出Oinone产品及配套的低代码平台工具:对比国内外应用软件平台在开放生态和云原生均有它的继承性和独特性,特别是将技术平台赋予企业各种业务领域属性,便于企业客户和开发伙伴的二次开发并能快速搭建各类企业核心应用场景是Oinone的最大亮点。Oinone的内在特点之一是参考了全球最大开源ERP Odoo的元数据模型设计,同时基于业务中台架构和云原生技术,形成了自己一套国际化的快速开发平台、建模规范和应用产品,通过自己进场落地很多品牌企业的应用中台化不断迭代升级,走出了一条具有显著特色的新应用软件之路。史昂及团队特点谦卑、善于思考,善于吸收他山之精华,这是创业团队难能可贵之点,由此能善于与生态伙伴合作也是能够走的更远更长的基础基因。最后希望和祝愿Oinone能为中国企业在云时代数字化实践做出更多的贡献,为软件产业构建强大的应用生态和开发社区,真正树立起Oinone自己的软件品牌形象。 资深IT咨询专家&浩鲸云智能专家学院院长:梅丛银

    Oinone 7天入门到精通 2024年5月23日
    1.6K00

Leave a Reply

Please Login to Comment