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:
    open-api:
      enabled: true
      standalone:
        # 开放接口访问IP
        host: 127.0.0.1
        # 开放接口访问端口,second为8094
        port: 8094 
        # 认证Token加密的AES密钥
        aes-key: NxDZUddmvdu3QQpd5jIww2skNx6U0w0uOAXj3NUCLu8=
      routes:
        pamirs:
          # 开放接口访问IP
          host: 127.0.0.1
          # 开放接口访问端口,second为8094
          port: 8094
          # 认证Token加密的AES密钥
          aes-key: NxDZUddmvdu3QQpd5jIww2skNx6U0w0uOAXj3NUCLu8=

注:

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低代码应用平台体验

(0)
史, 昂史, 昂
上一篇 2024年5月23日 am8:12
下一篇 2024年5月23日 am8:14

相关推荐

  • 6.4 国际化之多语言

    多语言是国际化中大家最常面对的问题,我们需要对应用的页面结构元素进行翻译,也需要对系统内容进行翻译比如:菜单、数据字典等,甚至还会业务数据进行翻译。但不管什么翻译需求,我们在实现上基本可以归类为前端翻译和后端翻译。前端翻译顾名思义是在前端根据用户选择语言对内容进行翻译,反之就是后端翻译。本文会带着大家了解oinone的前端翻译与后端翻译 准备工作 pamirs-demo-boot的pom文件中引入pamirs-translate包依赖 <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-translate</artifactId> </dependency> pamirs-demo-boot的application-dev.yml文件中增加配置pamirs.boot.modules增加translate,即在启动模块中增加translate模块 pamirs: boot: modules: – translate 后端翻译(使用) 这里通过对菜单的翻译来带大家了解翻译模块 Step1 新增翻译记录 切换应用到translate模块,点击新增翻译。 选择新增翻译生效模块 选择翻译的模型为:菜单模型 源语言选择中文,目标选择English 添加翻译项目: 源术语为:商店 翻译值为:shop 状态为:激活 Step2 查看效果 应用切换到Demo模块,在右上角切换语言至英语 后端翻译(自定义模型的翻译) 在前面菜单的翻译中,似乎我们什么都没做就可以正常通过翻译模块完成多语言的切换了。是不是真如我们想象的一样,当然不是。是因为Menu模型的displayName字段加上@Field(translate = true)注解。 Step1 为PetType模型的name字段增加翻译注解 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.base.IdModel; @Model.MultiTable(typeField = "kind") @Model.model(PetType.MODEL_MODEL) @Model(displayName="品种",labelFields = {"name"}) public class PetType extends IdModel { public static final String MODEL_MODEL="demo.PetType"; @Field(displayName = "品种名" , translate = true) private String name; @Field(displayName = "宠物分类") private String kind; } Step2 重启应用查看效果 切换应用到translate模块,点击新增翻译 切换应用到Demo模块,切换中英文,查看效果 前端翻译 还记得我们前端第一个自定义动作吗?会弹出“oinone第一个自定义Action,啥也没干”,我们要对它进行翻译。 Step1 修改前端DoNothingActionWidget.ts import translateValueByKey 提示语用translateValueByKey加上翻译 const confirmRs = executeConfirm(translateValueByKey(\’oinone第一个自定义Action,啥也没干\’)||\’oinone第一个自定义Action,啥也没干\’); 前端更多翻译工具请见前端高级特性-框架之翻译工具 import { Action, ActionContextType, ActionWidget, executeConfirm, IClientAction, SPI, ViewType, Widget, translateValueByKey } from '@kunlun/dependencies'; @SPI.ClassFactory(ActionWidget.Token({ name: 'demo.doNothing' })) export class DoNothingActionWidget extends ActionWidget { @Widget.Method() public async clickAction() { const confirmRs = executeConfirm(translateValueByKey('oinone第一个自定义Action,啥也没干')||'oinone第一个自定义Action,啥也没干'); } } //定义动作元数据 Action.registerAction('*', { displayName: '啥也没干', name: 'demo.doNothing', id: 'demo.doNothing', contextType: ActionContextType.ContextFree, bindingType: [ViewType.Table] } as IClientAction); Step2 新增翻译记录 前端翻译的翻译记录对应的模型可以随意找一个放。但要注意几点: 不要找有字读配置translate = true的模型,因为会影响后端翻译性能。 最好统一到一个模型中,便于后续管理。这里大家可以自定义一个无有业务访问且本身无需要翻译的模型来挂载,避免性能损失 Step3 刷新远程资源生成前端语言文件 Step4 新增或修改.env 前端在项目根目录下新增或修改.env,可以参考.env.example文件。通过.env文件为前端配置oss文件路径,针对I18N_OSS_URL配置项。真实前端访问翻译语言文件的路径规则为:http://bucket.downloadUrl/mainDir/租户/translate/模块/语言文件。 yaml文件中oss配置的文件路径:http://pamirs.oss-cn-hangzhou.aliyuncs.com/upload/demo/ 租户/translate/模块/语言文件 前端会自动根据上下文组织 # 后端api配置 # API_BASE_URL=http://127.0.0.1:8090 # 下面是国际化语言的cdn配置,默认用当前请求链接下的路径: /pamirs/translate/${module}/i18n_${lang}.js I18N_OSS_URL=http://pamirs.oss-cn-hangzhou.aliyuncs.com/upload/demo Step5 重启前端应用看效果 对语言进行中英文切换,进入宠狗达人页面,点击【第一个自定义Action】,查看前端翻译效果

    2024年5月23日
    20
  • 登录日志

    1. 登录日志 登录日志:记录企业成员/用户登录平台的历史明细,包括登录时间、位置、IP、登录设备、登录平台等信息,登录平台包括:PC Web、小程序、H5、APP。登录日志不需要通过审计规则订阅,平台会记录每位用户的登录日志。 操作支持导出、查看详情。

    2024年6月20日
    00
  • 4.1.2 模块之启动指令

    针对不同启动指令的组合可以满足不同场景需求,下面列举了几个常规组合方式,小伙伴们务必把这几种模式都尝试一遍,会更有体感 本节为小伙伴讲解oinone模块的几种启动方式,它是为能灵活地应对企业市场的不同场景需求,为op(本地化部署)、saas和研发提供个性化支撑。也为oinone独特性之单体与分布式的灵活切换提供基础支撑 一、部署参数 参数 名称 默认值 说明 -Plifecycle 生命周期部署指令 RELOAD 可选项:无/INSTALL/PACKAGE/RELOAD/DDL安装-install为AUTO;upgrade为FORCE打包-install为AUTO;upgrade为FORCE;profile为PACKAGE重启-install、upgrade、profile为READONLY打印变更DDL-install为AUTO;upgrade为FORCE;profile为DDL 表4-1-2-1 部署参数 如果在启动命令中配置了部署参数,可不再设置服务参数和可选项参数。下图为在启动命令中添加部署参数的示例。 图4-1-2-1 在启动命令中添加部署参数的示例 二、使用场景 针对不同启动指令的组合可以满足不同场景需求,下面列举了几个常规组合方式,小伙伴们务必把这几种模式都尝试一遍,会更有体感。 场景一:DDL(1)+RELOAD(N)应对专有DBA 因为很多公司数据库是由专门的DBA来管理的,不允许应用直接变更数据库相关配置、表结构、初始化数据。而oinone是基于元数据驱动的,任何模型、行为的变化都会自动转化成对物理存储的改变与元数据变化。 oinone为了适用企业op场景,特别增加了DDL模式。把发布上线分为两个步骤。 一:用DDL模式把涉及到数据库的变更与元数据初始化的脚本进行输出,交由客户公司DBA审批,并执行 二:用RELOAD模式,进行正常的应用重启工作,不进行安装、升级、以及数据库物理变革等操作。 #应用启动关闭自动DDL配置 pamirs.boot.profile: CUSTOMIZE pamirs.boot.options.rebuildTable: false pamirs.persistence.global.auto-create-database: false pamirs.persistence.global.auto-create-table: false 图4-1-2-2 应用启动关闭自动DDL模式 场景二:PACKAGE(1)+RELOAD(N)应对提升多机器实例效率 在机器规模相对大的场景中我们会碰到以下问题: 元数据差量计算、数据库变更、元数据变化保存都非常费时,如果每台机器都来一遍是非常费时费力的 分布式下多机器如果并发进行INSTALL,会导致数据库修改表结构、元数据变化保存锁死 所以我们可以选择一台机器用PACKAGE,其他机器采用RELOAD模式,做到合理规避问题,提升应用发布效率 场景三:INSTALL应对开发模式 研发在本地开发模式下INSTALL是最有效率的,把所需依赖模块一把启动和调试。 上线如果要用INSTALL需要注意,要逐台进行。当然也可以改进成INSTALL(1)+RELOAD(N)模式 三、启动命令解读 查看启动命令 可以在启动日志中查看当前所用启动命令。 图4-1-2-3 在启动日志中查看当前所用启动命令 生命周期管理-Plifecycle 除了通过启动YAML中pamirs.boot属性来设置启动参数,你还可以在应用启动命令中使用-Plifecycle参数来快捷控制模块生命周期的管理方式。该参数的可选项为RELOAD、INSTALL、CUSTOM_INSTALL、PACKAGE、DDL。 java -jar <your jar name>.jar -Plifecycle=RELOAD 启动命令优先级高于YAML中pamirs.boot属性中的install、upgrade和profile属性。如果不使用-Plifecycle参数,则使用YAML中pamirs.boot属性中的install、upgrade和profile属性配置。若YAML中未配置,则采用默认值。 启动配置项 默认值 RELOAD INSTALL CUSTOM_INSTALL PACKAGE DDL install AUTO READONLY AUTO AUTO AUTO AUTO upgrade AUTO READONLY FORCE FORCE FORCE FORCE profile CUSTOMIZE READONLY AUTO CUSTOMIZE PACKAGE DDL 表4-1-2-2 Plifecycle可选项与启动项对应表 profile属性请参考4.1.1【服务启动可选项】一文。只有pamirs.boot.profile=CUSTOMIZE时,在pamirs.boot.options中自定义的可选项才生效。 自动建表-PbuildTable java -jar <your jar name>.jar -PbuildTable=NEVER PbuildTable参数用于设置自动构建表结构的方式。如果不使用该参数,则options属性的默认值请参考4.1.1【服务启动可选项】一文。-PbuildTable参数可选项为: NEVER – 不自动构建表结构,会将pamirs.boot.options中的diffTable和rebuildTable属性设置为false EXTEND – 增量构建表结构,会将pamirs.boot.options中的diffTable属性设置为false,rebuildTable属性设置为true DIFF – 差量构建表结构,会将pamirs.boot.options中的diffTable和rebuildTable属性设置为true 模块在线 -PmoduleOnline java -jar <your jar name>.jar -PmoduleOnline=CHECK PmoduleOnline参数用于设置模块在线的方式。如果不使用该参数,则profile属性的默认值请参考4.1.1【服务启动可选项】一文。-PmoduleOnline参数可选项为: NEVER – 不读取存储在数据库中的模块信息,会将pamirs.boot.options中的reloadModule和checkModule属性设置为false READ – 读取存储在数据库中的模块信息,会将pamirs.boot.options中的checkModule属性设置为false,reloadModule属性设置为true CHECK – 读取存储在数据库中的模块信息并校验依赖模块是否已安装,会将pamirs.boot.options中的reloadModule和checkModule属性设置为true 元数据在线-PmetaOnline java -jar <your jar name>.jar -PmetaOnline=MODULE PmetaOnline参数用于设置元数据在线的方式,如果不使用该参数,则profile属性的默认值请参考4.1.1【服务启动可选项】一文。-PmetaOnline参数可选项为: NEVER – 不持久化元数据,会将pamirs.boot.options中的updateModule、reloadMeta和updateMeta属性设置为false MODULE – 只注册模块信息,会将pamirs.boot.options中的updateModule属性设置为true,reloadMeta和updateMeta属性设置为false ALL – 注册持久化所有元数据,会将pamirs.boot.options中的updateModule、reloadMeta和updateMeta属性设置为true 开放远程服务-PenableRpc PenableRpc参数用于设置是否开启远程服务。如果不使用该参数,则profile属性的默认值请参考4.1.1【服务启动可选项】一文。-PenableRpc参数可选项为true和false。该参数会将参数值设置到pamirs.boot.options中的publishService属性。 开启API服务-PopenApi PopenApi参数用于设置是否开启HTTP API服务。如果不使用该参数,则profile属性的默认值请参考4.1.1【服务启动可选项】一文。-PopenApi参数可选项为true和false。该参数会将参数值设置到pamirs.boot.options中的rebuildHttpApi属性。 开启字段校验-PcheckField PcheckField参数用于设置是否开启字段校验。-PcheckField参数可选项为true和false。由于通常应用的字段数量非常多,会延长系统启动时长,所以默认不会开启字段校验。 启用数据初始化服务-PinitData PinitData参数用于设置是否开启数据初始化服务。如果不使用该参数,则profile属性的默认值请参考4.1.1【服务启动可选项】一文。-PinitData参数可选项为true和false。该参数会将参数值设置到pamirs.boot.options中的updateData属性。 四、不使用自动构建数据库表功能 Oinone LCDP默认提供框架的所有服务,所以会自动构建数据库表。如果不需要使用Oinone的存储构建服务,可以设置YAML文件中关于自动建表的配置。这样就不会动态构建数据库表,你可以手动搭建数据库表。 通过配置启动YAML中pamirs.boot.options.rebuildTable为false彻底关闭自动建表功能。 pamirs: boot: options: rebuildTable: false 图4-1-2-4 不使用自动构建数据库表功能 也可以按需配置启动YAML中pamirs.persistence配置来关闭部分数据源的自动建表功能。persistence配置既可以针对全局也可以分数据源进行配置。 pamirs: persistence: global: # 是否自动创建数据库的全局配置,默认为true autoCreateDatabase: true # 是否自动创建数据表的全局配置,默认为true autoCreateTable: true <your ds key>: # 是否自动创建数据库的数据源配置,默认为true autoCreateDatabase: true # 是否自动创建数据表的数据源配置,默认为true…

    2024年5月23日
    00
  • 4.4 Oinone的分布式体验进阶

    在分布式开发中,每个人基本只负责自己相关的模块开发。所以每个研发就都需要一个环境,比如一般公司会有(N个)项目环境、1个日常环境、1个预发环境、1个线上环境。在整项目环境的时候就特别麻烦,oinone的好处是在于每个研发可以通过boot工程把需要涉及的模块都启动在一个jvm中进行开发,并不依赖任何环境,在项目开发中,特别方便。但当公司系统膨胀到一定规模,大到很多人都不知道有哪些模块,或者公司出于安全策略考虑,或者因为启动速度的原因(毕竟模块多了启动的速度也会降下来)。本文就给大家介绍oinone与经典分布式组织模式的兼容性 一、模块启动的最小集 我们来改造SecondModule模块,让该模块的用户权限相关都远程走DemoModule Step1 修改SecondModule的启动工程application-dev.yml文件 除了base、second_core两个模块保留,其他模块都去除了。 pamirs: boot: init: true sync: true modules: – base – second_core 图4-4-1 SecondModule的application-dev.yml仅配置两个模块 Step2 去除boot工程的依赖 去除SecondModule启动工程的pom依赖 <!– <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-resource-core</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-user-core</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-auth-core</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-message-core</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-international</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-business-core</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-apps-core</artifactId> </dependency> –> 图4-4-2 去除boot工程多余的依赖 Step3 重启SecondModule 这【远程模型】和【远程代理】均能访问正常 图4-4-3 远程模型和远程代理菜单均能访问正常 Step4 SecondModule增加对模块依赖 我们让SecondModule增加用户和权限模块的依赖,期待效果是:SecondModule会对用户和权限的访问都会走Dome应用,因为Demo模块的启动工程中包含了user、auth模块。 修改pamirs-second-api的pom文件增加对user和auth的api包依赖 <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-user-api</artifactId> </dependency> <dependency> <groupId>pro.shushi.pamirs.core</groupId> <artifactId>pamirs-auth-api</artifactId> </dependency> 图4-4-4 修改pamirs-second-api的pom文件 修改SecondModule类,增加依赖定义 @Module( dependencies = {ModuleConstants.MODULE_BASE, AuthModule.MODULE_MODULE, UserModule.MODULE_MODULE} ) 图4-4-5 配置SecondModule的依赖 Step5 修改RemoteTestModel模型 为RemoteTestModel模型增加user字段 @Field.many2one @Field(displayName = "用户") private PamirsUser user; 图4-4-6 为RemoteTestModel模型增加user字段 Step6 重启系统看效果 mvn install pamirs-second工程,因为需要让pamirs-demo工程能依赖到最新的pamirs-second-api包 重启pamirs-second和pamirs-demo 两个页面都正常 图4-4-7 示例效果一 图4-4-8 示例效果二 二、PmetaOnline的NEVER指令(开发时环境共享) 我们在4.1.2【模块之启动指令】一文中介绍过 “-PmetaOnline指令”,该参数用于设置元数据在线的方式,如果不使用该参数,则profile属性的默认值请参考服务启动可选项。-PmetaOnline参数可选项为: NEVER – 不持久化元数据,会将pamirs.boot.options中的updateModule、reloadMeta和updateMeta属性设置为false MODULE – 只注册模块信息,会将pamirs.boot.options中的updateModule属性设置为true,reloadMeta和updateMeta属性设置为false ALL – 注册持久化所有元数据,会将pamirs.boot.options中的updateModule、reloadMeta和updateMeta属性设置为true oinone的默认模式下元数据都是注册持久化到DB的,但当我们在分布式场景下新开发模块或者对已有模块进行本地化开发时,做为开发阶段我们肯定是希望复用原有环境,但不对原有环境照成影响。那么-PmetaOnline就很有意义。让我们还没有经过开发自测的代码产生的元数据仅限于开发本地环境,而不是直接影响整个大的项目环境 PmetaOnline指令设置为NEVER(举例) Step1 为DemoCore新增一个DevModel模型 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; @Model.model(DevModel.MODEL_MODEL) @Model(displayName = "开发阶段模型",summary="开发阶段模型,当PmetaOnline指令设置为NEVER时,本地正常启动但元数据不落库",labelFields={"name"}) public class DevModel extends AbstractDemoCodeModel{ public static final String MODEL_MODEL="demo.DevModel"; @Field(displayName = "名称") private String name; } 图4-4-9 为DemoCore新增一个DevModel模型 Step2 为DevModel模型配置菜单 @UxMenu("开发模型")@UxRoute(DevModel.MODEL_MODEL) class DevModelProxyMenu{} 图4-4-10 为DevModel模型配置菜单 Step3 启动Demo应用时指定-PmetaOnline 图4-4-11 启动Demo应用时指定-PmetaOnline Step4 重启系统看效果 查看元数据 图4-4-12 DB查看元数据变化 菜单与页面能正常操作 图4-4-13 开发模型菜单可正常操作 图4-4-14 开发模型详情页面可正常操作 Step5 Never模式需注意的事项 业务库需设定为本地开发库,这样才不会影响公共环境,因为对库表结构的修改还是会正常进行的 如果不小心影响了公共环境,需要对公共环境进行重启恢复 系统新产生的元数据(如:例子中的【开发模式】菜单)不受权限管控 三、分布式开发约定 设计约定 跨模块的存储模型间继承,在部署时需要跟依赖模块配置相同数据源。这个涉及模块规划问题,比如业务上的user扩展模块,需要跟user模块一起部署。…

    2024年5月23日
    00
  • 3.3.8 字段类型之基础与复合

    模型字段是描述实体的特征属性,本文介绍重点介绍字段的基础类型与复合类型 使用@Field注解来描述模型的字段。如果未配置字段类型,系统会根据Java代码的字段声明类型来自动获取业务类型。建议配置displayName属性来描述字段在前端的显示名称。可以使用defaultValue配置字段的默认值。 一、安装与更新 使用@Field.field来配置字段的不可变更编码。字段一旦安装,无法在对该字段编码值进行修改,之后的字段配置更新会依据该编码进行查找并更新;如果仍然修改该注解的配置值,则系统会将该字段识别为新字段,存储模型会创建新的数据库表字段,而原字段将会rename为废弃字段。 二、字段类型 类型系统由基本类型、复合(组件)类型、引用类型和关系类型四种类型系统构成。通过类型系统描述应用程序、数据库和前端视觉视图如何进行交互,数据及数据间关系如何处理的协议。其中引用类型和关系类型介绍详见3.3.9【字段类型之关系与引用】一文,字段命名规范参见3.3.1【构建第一个Model】一文,这里不再赘述。 基本类型 业务类型 Java类型 数据库类型 规则说明 BINARY ByteByte[] TINYINTBLOB 二进制类型,不推荐使用 INTEGER ShortIntegerLongBigInteger smallintintbigintdecimal(size,0) 整数, 包括整数(10-11位有效数字)、长整数(19-20位有效数字)和大整数(超过19位)。【数据库规则】:默认使用int;如果size小于6则使用smallint;如果size超过6则使用int;如果size超过10位数字,即大于11(包含符号位),则使用长整数bigint;如果size超过19位数字,即大于20(包含符号位),则使用大数decimal。若未配置size,则按Java类型推测。【前端交互规则】:整数使用Number类型,长整数和大整数前后端协议使用字符串类型。 FLOAT FloatDoubleBigDecimal float(M,D)double(M,D)decimal(M,D) 浮点数,?包括单精度浮点数(7-8位有效数字)、双精度浮点数(15-16位有效数字)和大数(超过15位)。【数据库规则】:默认使用单精度浮点数float;如果size超过7位数字,即大于等于8,则使用双精度浮点数double;如果size超过15位数字,即大于等于16,则使用大数decimal。若未配置size,则按Java类型推测。【前端交互规则】:单精度浮点数float和双精度浮点数double使用Number类型(因为都使用IEEE754协议64位进行存储),大数前后端协议使用字符串类型。 BOOLEAN Boolean tinyint(1) 布尔类型,值为1,true(真)或0,false(假) ENUM Enum 与数据字典指定基本类型一致 【前端交互规则】:可选项从ModelField的options字段获取,options字段值为字段指定数据字典子集的JSON序列化字符串。前后端传递的是可选项的name,数据库存储使用可选项的value。multi属性为true,则使用多选控件;multi属性为false,则使用单元控件 STRING String varchar(size) 字符串,size为长度限制默认值参考,前端可以view中覆盖该配置 TEXT String text 多行文本,编辑态组件为多行文本框,长度限制为配置项size值 HTML String text 富文本编辑器 DATETIME java.util.Datejava.sql.Timestamp datetime(fraction)timestamp(fraction) 日期时间类型【数据库规则】:日期和时间的组合,时间格式为?YYYY-MM-DD HH:MM:SS[.fraction],默认精确到秒,在默认的秒精确度上,可以带小数,最多带6位小数,即可以精确到?microseconds (6 digits) precision。可以通过设置fraction来设置精确小数位数,最终存储在字段的decimal属性上。【前端交互规则】:前端默认使用日期时间控件,根据日期时间类型格式化格式format格式化日期时间 YEAR java.util.Date year 年份类型日期类型【数据库规则】:默认“YYYY”格式表示的日期值【前端交互规则】:前端默认使用年份控件,根据日期类型格式化格式format格式化日期 DATE java.util.Datejava.sql.Date datedate 日期类型【数据库规则】:默认“YYYY-MM-DD”格式表示的日期值【前端交互规则】:前端默认使用日期控件,根据日期类型格式化格式format格式化日期 TIME java.util.Datejava.sql.Time time(fraction)time(fraction) 时间类型【数据库规则】:默认“HH:MM:SS”格式表示的时间值【前端交互规则】:前端默认使用时间控件,根据日期类型格式化格式format格式化日期 表3-3-8-1 字段基本类型 复合类型 业务类型 Java类型 数据库类型 规则说明 MONEY BigDecimal decimal(M,D) 金额,前端使用金额控件,可以使用currency设置币种字段 表3-3-8-2 字段复合类型 不可变更字段 使用immutable属性来描述该字段前后端都无法进行更新操作,系统会忽略不可变更字段的更新操作。 自动生成编码的字段 详见3.3.5【模型编码生成器】一文。 字段的序列化与反序列化 使用@Field注解的serialize属性来配置非字符串类型属性的序列化与反序列化方式,最终会以序列化后的字符串持久化到存储中。 详见3.3.7【字段之序列化方式】一文 前端默认配置 可以使用@Field注解中的以下属性来配置前端的默认视觉与交互规则,也可以在前端设置覆盖以下配置。 @Field(required),是否必填 @Field(invisible),是否不可见 @Field(priority),字段优先级,列表的列使用该属性进行排序 更多前端默认视图配置详见:3.5.4【Ux注解详解】一文,如:readonly是否只读等。 举例 回顾我们前面学习例子 现有PetShop代码如下 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.core.common.enmu.DataStatusEnum; import pro.shushi.pamirs.demo.api.enumeration.PetShopOptionEnum; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import java.sql.Time; import java.util.List; @Model.model(PetShop.MODEL_MODEL) @Model(displayName = 宠物店铺,summary=宠物店铺,labelFields ={shopName} ) @Model.Code(sequence = DATE_ORDERLY_SEQ,prefix = P,size=6,step=1,initial = 10000,format = yyyyMMdd) public class PetShop extends AbstractDemoIdModel { public static final String MODEL_MODEL=demo.PetShop; @Field(displayName = 店铺编码) private String code; @Field(displayName = 店铺编码2) @Field.Sequence(sequence = DATE_ORDERLY_SEQ,prefix = C,size=6,step=1,initial = 10000,format = yyyyMMdd) private String codeTwo; @Field(displayName = 店铺名称,required = true) private String shopName; @Field(displayName = 开店时间,required = true) private Time openTime; @Field(displayName = 闭店时间,required = true) private Time closeTime; @Field(displayName =…

    2024年5月23日
    00

Leave a Reply

登录后才能评论