Loading

实习总结

实习日志

20211229213941

task 1:后台管理nyjk-admin项目搭建,引入aks加解密接口进行页面展示

  1. nyjk-admin项目引入nyjk-common的jar包。
  2. 调用common中得 AksDeviceCryptoGateway 接口注入对象。
  3. 编写相关controller接口
  4. 使用ajax 请求数据并进行页面渲染

task 2:区块链存证、四级地址、级联地址、文件上传于下载(OSS)、iot摄像头sdk对接获取视频流等内部jsf服务

  • 项目调用总体流程

    graph BT A[nyjk-app:nyjk-platform-domain<基础服务:面向数据库操作<entity,BaseService>>] --对象注入--> B[nyjk-app:nyjk-platform-app<业务处理:domain依赖注入<BO,BizService>>]; B--对象注入-->C[nyjk-app:nyjk-platform-adapter<适配层:贯通上下层,轻量业务:Impl+对象转换+参数校验 >]; C--implement-->D[nyjk-api:nyjk-platform-adapter-api:service<面向内部接口暴露DTO>/web<面向前端接口暴露 VO>];
    • 项目结构

      1. nyjk-common:主要书写各种sdk的相关工具类以及一些通用方法,不涉及平台业务逻辑。
      2. nyjk-api:主要定义nyjk-common接口(异常、枚举、网关、校验、常量)和nyjk-app对外接口(service对内部系统和web对外部前端)
        20220203114532
        service主要定义平台内部使用的外部接口(注册到jsf中心,然后通过jsf以rpc方式调用,譬如nyjk-admin去调用一些服务),使用DTO(Data Transfer Object)命名实体对象,且使用NyjkRequest和NyjkResponse泛型类进行统一封装。
        20220203115001
        web主要定义与前端交互的外部接口(将接口上传到网关,建立网关到接口的映射,并且注册到jsf中心,当前端发送请求到网关,网关找到相关接口映射,并调用jsf接口,最后通过rpc方式调用项目的接口接口实现类),使用VO(View Object)命名实体对象,且使用WebBaseRequest(实体类extend方式)和 WebResponse(泛型)。
        20220203120303
      3. nyjk-app:平台主要业务,具体逻辑看上述流程图即可。
        • Adapter 主要实现api中定义的外部接口,做DTO/VO到BO的转换以及一些校验逻辑,业务逻辑不重。
        • App 为主要逻辑的实现,由于Domain中都是单表的基础服务(CURD),因此需要通过代码的方式组合多表操作,实体类以BO(Business Object)命名。
        • Domain 为数据库的操作模块,主要通过mybaties-plus框架实现,实体类以entity命名。
        • attention:由于nyjk-app下有多个子模块,因此注意模块之间得解耦,譬如nyjk-platform和nyjk-asset-mgt之间不能再pom中相互引入,需要使用infrastructure 防腐层进行rpc调用才行。
    • 项目运行 profiles

              <!-- 定义不同的环境配置、本地、测试、预发、正式,它们的配置都是不同的,所以为了手动去修改数据库、日志等配置,采用profiles可以一键切换 -->
              <profiles>
                  <profile>
                      <id>local</id>
                      <properties>
                          <profile.active>local</profile.active>
                          <profile.dir>${profiles.dir}/local</profile.dir>
                      </properties>
                  </profile>
      
                  <profile>
                      <id>test</id>
                      <properties>
                          <profile.active>test</profile.active>
                          <profile.dir>${profiles.dir}/test</profile.dir>
                      </properties>
                  </profile>
                  <profile>
                      <id>pre-prod</id>
                      <properties>
                          <profile.active>pre-prod</profile.active>
                          <profile.dir>${profiles.dir}/pre-prod</profile.dir>
                      </properties>
                  </profile>
                  <profile>
                      <id>prod</id>
                      <properties>
                          <profile.active>prod</profile.active>
                          <profile.dir>${profiles.dir}/prod</profile.dir>
                      </properties>
                  </profile>
              </profiles>
      
              <resources>
                  <!--引入directory所有配置文件-->
                  <resource>
                      <directory>src/main/java</directory>
                      <includes>
                          <include>**/*.xml</include>
                      </includes>
                      <filtering>true</filtering>
                  </resource>
                  <resource>
                      <directory>src/main/resources</directory>
                      <!--先排除所有的与application相关配置文件-->
                      <excludes>
                          <exclude>application*.yml</exclude>
                      </excludes>
                  </resource>
                  <resource>
                      <directory>src/main/resources</directory>
                      <!--引入所需环境的配置文件-->
                      <filtering>true</filtering>
                      <includes>
                          <include>application.yml</include>
                          <include>application-${profile.active}.yml</include>
                          <include>logback/logback-${profile.active}.xml</include>
                      </includes>
                  </resource>
                  <resource>
                      <directory>${profile.dir}</directory>
                      <filtering>true</filtering>
                  </resource>
              </resources>
      

      20220203133336
      20220203133404
      filtering:开启 directory 下的过滤,使得yml文件中引用pom中变量时,通过@value_of_pom@进行引用,能够避免maven的$引用方式的冲突
      ${profile.active}:根据profile选择,动态切换yml文件.

  • mybatis-plus

    • MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
      20220203134007

    • 通过内置代码生成器,生成entity、mapper、service层代码,提高了开发效率,在Service接口中定义相关方法,在ServiceImpl中具体实现即可。
      20220203134559

    • 使用代码封装好了简单sql,不需要在xml中书写大量sql语句,直接方法调用即可,解决了数据库移植性差的问题。

          public List<BlockchainEvidenceEntity> queryList(BlockchainEvidenceQueryParam queryParam) {
              return blockchainEvidenceMapper.selectList(setQueryWrapper(queryParam));
          }
      
           public QueryWrapper<BlockchainEvidenceEntity> setQueryWrapper(BlockchainEvidenceQueryParam queryParam) {
      
              //使用QueryWrapper进行封装,只有不为空时追加查询条件,.eq(不为空追加,字段名,字段值)
              QueryWrapper<BlockchainEvidenceEntity> queryWrapper = new QueryWrapper<>();
              queryWrapper.lambda().eq(StringUtils.isNotEmpty(queryParam.getEvidenceNo()), BlockchainEvidenceEntity::getEvidenceNo, queryParam.getEvidenceNo())
                      .eq(queryParam.getCustType() != null, BlockchainEvidenceEntity::getCustType, queryParam.getCustType())//
                      .eq(StringUtils.isNotEmpty(queryParam.getCustNo()), BlockchainEvidenceEntity::getCustNo, queryParam.getCustNo())
                      .eq(StringUtils.isNotEmpty(queryParam.getCustName()), BlockchainEvidenceEntity::getCustName, queryParam.getCustName())
                      .eq(queryParam.getBizType() != null, BlockchainEvidenceEntity::getBizType, queryParam.getBizType())
                      .eq(StringUtils.isNotEmpty(queryParam.getBizNo()), BlockchainEvidenceEntity::getBizNo, queryParam.getBizNo())
                      .eq(queryParam.getSourceDataType() != null, BlockchainEvidenceEntity::getSourceDataType, queryParam.getSourceDataType())
                      .eq(StringUtils.isNotEmpty(queryParam.getSourceData()), BlockchainEvidenceEntity::getSourceData, queryParam.getSourceData())
                      .eq(StringUtils.isNotEmpty(queryParam.getEvidenceId()), BlockchainEvidenceEntity::getEvidenceId, queryParam.getEvidenceId())
                      .eq(StringUtils.isNotEmpty(queryParam.getEvidenceType()), BlockchainEvidenceEntity::getEvidenceType, queryParam.getEvidenceType())
                      .eq(StringUtils.isNotEmpty(queryParam.getEvidenceName()), BlockchainEvidenceEntity::getEvidenceName, queryParam.getEvidenceName())
                      .eq(StringUtils.isNotEmpty(queryParam.getEvidenceSize()), BlockchainEvidenceEntity::getEvidenceSize, queryParam.getEvidenceSize());
      
              if (queryParam.getCreatedDateRange() != null) {
                  if (queryParam.getCreatedDateRange().getStart() != null) {
                      queryWrapper.lambda().ge(BlockchainEvidenceEntity::getCreatedDate, queryParam.getCreatedDateRange().getStart());
                  }
                  if (queryParam.getCreatedDateRange().getEnd() != null) {
                      queryWrapper.lambda().lt(BlockchainEvidenceEntity::getCreatedDate, queryParam.getCreatedDateRange().getEnd());
                  }
              }
              if (queryParam.getEvidenceDateRange() != null) {
                  if (queryParam.getEvidenceDateRange().getStart() != null) {
                      queryWrapper.lambda().ge(BlockchainEvidenceEntity::getEvidenceDate, queryParam.getEvidenceDateRange().getStart());
                  }
                  if (queryParam.getEvidenceDateRange().getEnd() != null) {
                      queryWrapper.lambda().lt(BlockchainEvidenceEntity::getEvidenceDate, queryParam.getEvidenceDateRange().getEnd());
                  }
              }
              return queryWrapper;
          }
      
  • 对象转换:mapstruct

    • Spring和Apach提供了BeanUtils工具包 copyProperties(Ojbect source, Object target) 是通过反射机制(运行时)进行实现,导致拷贝属性的花费时间长,性能低。并且是一种浅拷贝。

    • mapstruct 方法定义以及字节码文件。
      能够转换单一对象,还能转换List对象;
      由于接口,可以定义default/static方法
      如果使用@Mapping指定转换映射,可以传入多个参数或者对象
      如果不使用@Mapping,必须保证target和source对象的属性名相同
      attention: mapstruct无法进行向上(继承父类属性)的转换。

          import org.mapstruct.Mapper;
          import org.mapstruct.Mapping;
          import org.mapstruct.Mappings;
          import org.mapstruct.factory.Mappers;
      
          /**
          * @Mapper 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
          *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
          */
          @Mapper
          public interface AddressVoConverter {
      
              /**
              * 获取该类自动生成的实现类的实例
              * 接口中的属性都是 public static final 的 方法都是public abstract的
              */
              AddressVoConverter INSTANCE = Mappers.getMapper(AddressVoConverter.class);
      
              /**
              * 这个方法就是用于实现对象属性复制的方法
              *
              * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
              *
              * @param user 这个参数就是源对象,也就是需要被复制的对象
              * @return 返回的是目标对象,就是最终的结果对象
              *
              * @Mappings({
              *        @Mapping(source = "id", target = "userId")
              * })
              **/
              List<AreaResVO> convert (List<AreaDTO> areaDTO);
      
              Level4AddressResVO convert (Level4AddressDTO level4AddressDTO);
          }
      
          //字节码文件
          public class AddressVoConverterImpl implements AddressVoConverter {
              public AddressVoConverterImpl() {
              }
      
              public List<AreaResVO> convert(List<AreaDTO> areaDTO) {
                  if (areaDTO == null) {
                      return null;
                  } else {
                      List<AreaResVO> list = new ArrayList(areaDTO.size());
                      Iterator var3 = areaDTO.iterator();
      
                      while(var3.hasNext()) {
                          AreaDTO areaDTO1 = (AreaDTO)var3.next();
                          list.add(this.areaDTOToAreaResVO(areaDTO1));
                      }
      
                      return list;
                  }
              }
      
              public Level4AddressResVO convert(Level4AddressDTO level4AddressDTO) {
                  if (level4AddressDTO == null) {
                      return null;
                  } else {
                      Level4AddressResVO level4AddressResVO = new Level4AddressResVO();
                      level4AddressResVO.setProvinceId(level4AddressDTO.getProvinceId());
                      level4AddressResVO.setProvinceName(level4AddressDTO.getProvinceName());
                      level4AddressResVO.setCityId(level4AddressDTO.getCityId());
                      level4AddressResVO.setCityName(level4AddressDTO.getCityName());
                      level4AddressResVO.setDistrictId(level4AddressDTO.getDistrictId());
                      level4AddressResVO.setDistrictName(level4AddressDTO.getDistrictName());
                      level4AddressResVO.setTownId(level4AddressDTO.getTownId());
                      level4AddressResVO.setTownName(level4AddressDTO.getTownName());
                      level4AddressResVO.setMessage(level4AddressDTO.getMessage());
                      return level4AddressResVO;
                  }
              }
      
              protected AreaResVO areaDTOToAreaResVO(AreaDTO areaDTO) {
                  if (areaDTO == null) {
                      return null;
                  } else {
                      AreaResVO areaResVO = new AreaResVO();
                      areaResVO.setAreaId(areaDTO.getAreaId());
                      areaResVO.setAreaName(areaDTO.getAreaName());
                      areaResVO.setAreaLevel(areaDTO.getAreaLevel());
                      areaResVO.setNameCode(areaDTO.getNameCode());
                      areaResVO.setAreaCode(areaDTO.getAreaCode());
                      areaResVO.setPostCode(areaDTO.getPostCode());
                      return areaResVO;
                  }
              }
          }
      
  • OSS文件上传,关于Base64 编码上传

    • 本质上还是流式上传,但是多了个解码的过程,然后再转成 InputStream
    • 不使用MultipartFile,可以使用inputStream 流式上传,但需要指定文件名,以获取后缀类型(其它的类型String,Byte[]都可转换为inputStream形式进行上传)
        public OssUploadResDTO upload(String base64, String fileType) {
    
            byte[] decode = BaseEncoding.base64().decode(base64);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(decode);
            return upload(inputStream, fileType);
        }
    
  • JSF 申请步骤

    1. 将需要暴露的接口,copy其全包名,到JSF服务管理平台进行接口申请(配置本地host)
    2. 在该项目(nyjk-app)模块下的JSF注册中心(spring-platform-provider.xml)进行注册
    <!-- 区块链服务 -->
    <jsf:provider id="blockchainEvidenceServiceJsf" interface="com.jd.jdt.nyjk.platform.adapter.service.evidence.BlockchainEvidenceService" alias="${jsf.nyjk.alias}" ref="blockchainEvidenceImpl"/>
    <!-- id 唯一,分组别名要正确-->
    
    1. 在 nyjk-admin 的pom中引入 nyjk-api:nyjk-platform-adapter-api:service 接口暴露模块(jar)
      并且在 spring-jsf-consumer.xml中引入 区块链服务 进行消费
    <!-- 区块链存证服务 -->
    <jsf:consumer id="blockchainEvidenceService" interface="com.jd.jdt.nyjk.platform.adapter.service.evidence.BlockchainEvidenceService" alias="${jsf.nyjk.alias}" protocol="jsf" timeout="10000" retries="0"/>
    
    1. 启动 nyjk-app 项目
    2. 在 nyjk-admin 中以 依赖注入 的方式引用该服务进行消费
  • 内部sdk使用流程

    1. 阅读相关文档,申请使用权限(测试、线上)
    2. 引入相关jar包
    3. 在 spring-jsf-comsumer.xml 中配置该sdk的jsf调用
    4. 在 nyjk-common 中书写封装代码
  • 接口的单元测试(由于单元测试和正常项目启动的环境是相互独立的,因此在进行单元测试时,也需要将对配置文件的修改同步到单元测试的 resource 的相关文件中)

        @Slf4j
        @RunWith(SpringRunner.class)
        @SpringBootTest(classes = AdapterApplicationTest.class)
        public class JdOssWebFacadeTest {
    
            @Autowired
            JdOssWebFacade jdOssWebFacade;
    
            @Test
            public void test(){
                BaseEncoding baseEncoding = BaseEncoding.base64();
                try {
                    ClassPathResource classPathResource = new ClassPathResource("test.png");
                    InputStream inputStreamImg = classPathResource.getInputStream();
                    byte[] bytes = IOUtils.toByteArray(inputStreamImg);
                    String encode = baseEncoding.encode(bytes);
                    FileUploadReqVO reqVO = new FileUploadReqVO();
                    reqVO.setFile(encode);
                    reqVO.setType("jpeg");
                    WebResponse<FileUploadResVO> webResponse = jdOssWebFacade.fileUpload(reqVO);
                    System.out.println(webResponse.getData().getAttachmentNo());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
    • @RunWith(SpringRunner.class):表明该测试类要使用注入的类,比如@Autowire,如此才能注入成功,否则会报空指针异常。并且指定一个特定的Runner,告诉该类通过用什么运行环境运行。
    • @SpringBootTest(classes = AdapterApplicationTest.class):启动spring容器、加载spring上下文。
    • @SpringBootTest与@RunWith 这两个是配合使用的,runwith是junit的注解,而springboottest是spring的注解,一般单元测试会把spring与junit结合测试。

task 3:nyjk-admin 项目服务器中打包部署到测试环境

  • 在jci平台上绑定对应git的branch,然后构建部署即可,值得注意的是,nyjk-common和nyjk-api需要maven deploy/jci也构建部署,否则nyjk-app会构建失败。

task 4: 流程引擎EasyFlow以及京东擎天OA

  • 流程实现

    1. easyflow 基本内容熟悉

      • easyflow执行过程中变量

        name type description
        context FlowContext 流程上下文
        param FlowParam 流程参数
        bizParam 依赖业务传入 FlowParam中的param
        result FlowResult 流程结果
        bizResult 依赖业务传入 FlowResult中的result
        nodeContext NodeContext 节点上下文
        actionResult 依赖具体接口实现 节点动作执行结果
      • 监听器事件常量 Listener —— 横向切面

             public class FlowEventTypes {
                 /**
                 * Common events.
                 */
                 /**FlowEngineImpl method——FlowResult invokeFlowEngine(FlowParam param)*/
                 //流程引擎启动 method —— start
                 public static final String FLOW_ENGINE_START = "FLOW_ENGINE_START";
                 //流程引擎结束 method try —— end
                 public static final String FLOW_ENGINE_END = "FLOW_ENGINE_END";
                 //流程引擎完成 mehtod finally
                 public static final String FLOW_ENGINE_COMPLETE = "FLOW_ENGINE_COMPLETE";
        
                 /**FlowEngineImpl method——FlowResult invokeFlow(FlowContext context)*/
                 //流程开始 method try —— start
                 public static final String FLOW_START = "FLOW_START";
                 //流程结束 method try —— end
                 public static final String FLOW_END = "FLOW_END";
                 //流程结束 method finally
                 public static final String FLOW_COMPLETE = "FLOW_COMPLETE";
                 
                 /**FlowEngineImpl method——void init(FlowContext context)*/
                 //初始化流上下文开始 method —— start
                 public static final String INIT_START = "INIT_START";
                 //初始化流上下文结束 method —— end
                 public static final String INIT_END = "INIT_END";
                 
                 /**BaseFlowRunner method——void run(FlowContext context)*/
                 //运行开始 method —— start
                 public static final String RUN_START = "RUN_START";
                 //运行结束 method —— end
                 public static final String RUN_END = "RUN_END";
                 
                 /**BaseFlowRunner method——NodeContext invokeNode(FlowNode node, NodeContext currentNode, FlowContext context, Flow flow)*/
                 //节点开始 method try —— start
                 public static final String NODE_START = "NODE_START";
                 //节点结束 method end —— end
                 public static final String NODE_END = "NODE_END";
                 //节点完成 method finally
                 public static final String NODE_COMPLETE = "NODE_COMPLETE";
                 
                 /**
                 * Extension events.
                 */
                 /**NodeImpl method——NodeContext execute(NodeContext nodeContext, FlowContext context)*/
                 //节点执行前预处理开始 preHandler != null
                 public static final String NODE_PRE_START = "NODE_PRE_START";
                 //节点执行前预处理结束 preHandler != null
                 public static final String NODE_PRE_END = "NODE_PRE_END";
                 /**NodeImpl method——Object invokeAction(NodeContext nodeContext, FlowContext context)*/
                 //节点执行开始 method —— start
                 public static final String NODE_ACTION_START = "NODE_ACTION_START";
                 //节点执行结束 method —— end
                 public static final String NODE_ACTION_END = "NODE_ACTION_END";
                 //节点执行后处理开始 method postHandler != null
                 public static final String NODE_POST_START = "NODE_POST_START";
                 // 节点执行后执行结束 method postHandler != null
                 public static final String NODE_POST_END = "NODE_POST_END";
             }
        
    2. 流程引擎结合具体业务实现过程

      1. 配置流程JSON模板,并保存到数据库中

        {
            "id":"ASSET_REVIEW_001",
            "name":"资产盘点审核流程",
            "nodes":[
                {
                    "id":"01",
                    "name":"流程开始",
                    "properties":{
                        "order":1
                    },
                    "action":{
                        "exp":"@flowEngineNodeAction.startNode(param.param)"
                    },
                    "post":{
                        "when":"nodeContext.actionResult==true",
                        "to":"02"
                    }
                },
                {
                    "id":"02",
                    "name":"盘点提交",
                    "properties":{
                        "order":2,
                        "nodeAction":"@wxAppReviewAction.execute(bizParam)"
        
                    },
                    "pre":null,
                    "action":{
                        "exp":"@flowEngineNodeAction.approvalNode(param.param)"
                    },
                    "post":{
                        "conditions":[{"when":"nodeContext.actionResult==true", "to":"03"}]
                    }
                },
                {
                    "id":"03",
                    "name":"盘点复合",
                    "properties":{
                        "order":3,
                        "nodeAction":"@checkProcessAction.execute(bizParam)"
                    },
                    "pre":null,
                    "action":{
                        "exp":"@flowEngineNodeAction.approvalNode(param.param)"
                    },
                    "post":{
                        "conditions":[{"when":"nodeContext.actionResult==true", "to":"04"}]
                    }
                },
                {
                    "id":"04",
                    "name":"流程结束",
                    "properties":{
                        "order":4
                    },
                    "action":{
                        "exp":"@flowEngineNodeAction.endNode(param.param)"
                    }
                }
            ],
            "listeners":[
                {
                    "createExp":"@flowEngineNodeListener"
                }
            ]
        }
        

        properties: 设置自定义属性。
        pre: preHandler。
        post: postHandler,其中Conditions进行条件判断,是否流转到下一节点。
        action: 每个节点需要执行的动作,根据业务需要自定义实现。exp/createExp,使用SPEL进行解析,在SpringBoot项目中,以@为前缀在ApplicationContext获取对应bean实体。
        Listeners:使用自定义监听器,监听流程事件。

      2. extends FlowEngineImpl,重写getFlow方法:从数据库获取JSON文件
        原本方式, 本地路径读取

            /**
            * 原本执行方式
            */
            public class FlowIndexTest {
                
                public static final Logger logger = LoggerFactory.getLogger(FlowIndexTest.class);
        
                @Test
                public void testFlow001() {
                    FlowEngineImpl flowEngine = new FlowEngineImpl();
                    //设置json文件路径
                    flowEngine.setFlowPath("classpath:flow/index/flow_index001.json");
                    //进行流程引擎初始化
                    flowEngine.init();
                    
                    //设置自定义参数
                    Map<String, Object> paramData = new HashMap<>();
                    paramData.put("amount", new BigDecimal(80));
                    //设置流ID, 节点ID, 自定义参数对象
                    FlowParam param = new FlowParam("flow_index001", "LIMIT_JUDGE", paramData);
                    //执行流程引擎,并返回结果
                    FlowResult result = flowEngine.execute(param);
                    logger.info("Result:" + result);
                    assertEquals("DO_LOAN", result.getContext().getEndNodes().get(0).getNodeId());
                }
            }
        
                /**
                * 流程引擎初始化
                */
                public void init() {
                    if (inited) {
                        return;
                    }
                    if (applicationContext != null) {
                        //设置SpelHelper的ApplicationContext
                        SpelHelper.setApplicationContext(applicationContext);
                    }
                    //加载流
                    loadFlow();
                    if (listeners != null) {
                        //设置监听器,可多个
                        listeners.forEach(listener -> eventTrigger.addListener(listener));
                    }
                    inited = true;
                }
        
               /**
               * 通过文件路径,加载json文件流
               */
               protected void loadFlow() {
                   PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
                   Resource[] resources;
                   //获取多个流路径
                   String[] flowPaths = flowPath.split(",");
                   //遍历每个路径
                   for (String path : flowPaths) {
                       try {
                           //加载当前类加载器以及父类加载器所在路径的资源文件
                           resources = resolver.getResources(path.trim());
                           for (Resource resource : resources) {
                               if (logger.isInfoEnabled()) {
                                   logger.info("Start parsing definition files:" + resource.getURI());
                               }
                               try (InputStream is = resource.getInputStream()) {
                                   String flowDefinition = IOUtils.toString(is);
                                   //将inputstream解析成List<Flow>, 可能该文件下有多个flow
                                   List<Flow> flowList = flowParser.parse(flowDefinition);
                                   //建立第一个flow的id到流字符串的映射关系
                                   flowDefinitionMap.put(flowList.get(0).getId(), flowDefinition);
                                   //将流id和流放进map集合,建立映射关系
                                   flowList.forEach(flow -> {
                                       if (flowMap.containsKey(flow.getId())) {
                                           throw new FlowException("Flow " + flow.getId() + " exists");
                                       }
                                       flowMap.put(flow.getId(), flow);
                                   });
                               }
                           }
                       } catch (IOException e) {
                           throw new RuntimeException("Flow definition file parse exception", e);
                       }
                   }
               }
        
                protected FlowResult executeFlow(FlowParam param) {
                    // init flow context
                    FlowContext context = initContext(param);
                    // find flow definition
                    Flow flow = findFlow(context);
                    if (flow.getFilters() == null || flow.getFilters().size() == 0) {
                        return invokeFlow(context);
                    } else {
                        FilterChain<FlowContext, FlowResult> chain = new FilterChain<FlowContext, FlowResult>(flow.getFilters(),
                                p -> invokeFlow(p));
                        return chain.doFilter(context);
                    }
                }
        
                /**
                * Find flow definition.
                */
               protected Flow findFlow(FlowContext context) {
                   Flow flow = getFlow(context.getFlowId());
                   context.setFlow(flow);
                   // Exists scenario changing flow id
                   context.setFlowId(flow.getId());
                   return flow;
               }
        
                // 流程引擎 invokeFlow 之前,先找根据flowId找到找到Flow对象
                public Flow getFlow(String flowId) {
                    return flowMap.get(flowId);
                }
        
                // 实现ApplicationListener<ContextRefreshedEvent>接口
                // 当ApplicationContext被初始化或刷新时,会触发ContextRefreshedEvent事件
                // 当执行到 String[] flowPaths = flowPath.split(","); 会报空指针异常,因此flowPath还未设置值
                public void onApplicationEvent(ContextRefreshedEvent event) {
                    if (inited) {
                        return;
                    }
                    this.applicationContext = event.getApplicationContext();
                    init();
                }
        

        现有需求,要从数据库读取,因此不使用init方法和loadFlow,需要手动设置ApplicationContext并且重写getFlow方法,

            private void unifiedEntrance(FlowEngineBO flowEngineBO) {
                // 设置SPEL到Context上下文,
                // nyjk.SpelHelper和eaysflow.SpelHelper两个,但是由于eaysflow.SpelHelper没有设置context
                SpelHelper.setApplicationContext(applicationContext);
                FlowParam flowParam= new FlowParam(flowEngineBO.getFlowId(), flowEngineBO.getCurNode(), flowEngineBO);
                FlowResult result = flowEngine.execute(flowParam);
                log.info("流程引擎启动结果:{}", JSONObject.toJSONString(result));
            }
        
            /**
            * 流程引擎初始化
            * @author huangxiang11
            * @date 2022/1/7 20:25
            **/
           @Slf4j
           @Service
           public class NyjkFlowEngineImpl extends FlowEngineImpl {
        
               @Autowired
               private ConfigBaseService configBaseService;
        
               /**
                * 每次execute执行,调用该方法, 用来加载数据库流程
                * 1. 装载 json
                * 2. 根据用户id获取审批流程节点状态(在execute之前,设置流程节点id,以及相关流程节点参数),并在流程引擎中同步该状态
                */
               @Override
               public Flow getFlow(String flowId) {
                   // 如果已经包含flowId,返回该 Map<flowId, Flow>对应的flow对象
                   if (flowMap.containsKey(flowId)) {
                       return flowMap.get(flowId);
                   }
                   // 不包含则去数据库根据flowId + 配置类型 查找流程json
                   ConfigQueryParam queryParam = new ConfigQueryParam();
                   queryParam.setCfgType(ConfigTypeConstants.FLOW_ENGINE_CFG);
                   queryParam.setCfgKey(flowId);
                   ConfigEntity configEntity = configBaseService.querySingle(queryParam);
                   // 同步当前对象
                   synchronized (this) {
                       FlowParser flowParser = super.getFlowParser();
                       // 解析 json 文件,获取多个流(一般只有一个流程)
                       List<Flow> flowList = flowParser.parse(configEntity.getCfgValue());
                       // 建立第一个流的id 与 json 的映射关系, 保存到 flowDefinitionMap
                       flowDefinitionMap.put(flowList.get(0).getId(), configEntity.getCfgValue());
                       // 将flowId 与 flow对象映射 保存到 flowMap
                       flowList.forEach(flow -> flowMap.put(flow.getId(), flow));
                       return flowMap.get(flowId);
                   }
               }
        
               // 进行重写,防止空指针异常
               @Override
               public void onApplicationEvent(ContextRefreshedEvent event) {
               }
           }
        
      3. 根据JSON文件中的SPEL表达式(EXP, Listener)书写相关逻辑。

        • 在FlowEngineNodeAction中维护流程引擎的业务逻辑(更新流程执行表和流程实例表,以及该节点是否可重复执行,是否发送待办任务到OA平台、接收OA的MQ消息同步结果到数据库)

          • 表设计

            CREATE TABLE `approval_flow_instance` (
                `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
                `flow_instance_no` varchar(32) NOT NULL COMMENT '流程编号',
                `flow_type` varchar(32) NOT NULL COMMENT '流程编号类型',
                `cust_type` varchar(32) DEFAULT NULL COMMENT '客户类型,PERSON:个人 COMPANY:企业',
                `cust_no` varchar(32) DEFAULT NULL COMMENT '内部客户号',
                `cust_name` varchar(512) DEFAULT NULL COMMENT '客户名称',
                `biz_type` varchar(32) DEFAULT NULL COMMENT '业务类型',
                `biz_no` varchar(32) DEFAULT NULL COMMENT '业务编号',
                `flow_name` varchar(128) DEFAULT NULL COMMENT '流程名称',
                `flow_id` varchar(32) DEFAULT NULL COMMENT '流程定义的ID',
                `postscript` varchar (512) DEFAULT NULL COMMENT '附言',
                `cur_node` varchar(32) DEFAULT NULL COMMENT '流程当前状态',
                `creator_no_type` varchar(32) DEFAULT NULL COMMENT '流程创建者用户编号类型',
                `creator_no` varchar(32) DEFAULT NULL COMMENT '流程创建者用户编号',
                `executor_type` varchar(32) DEFAULT NULL COMMENT '当前可执行人类型',
                `executor` varchar(1024) DEFAULT NULL COMMENT '当前可执行人',
                `executor_data` varchar(1024) DEFAULT NULL COMMENT '当前执行人数据',
                `ext_data` json DEFAULT NULL COMMENT '扩展字段',
                `instance_status` varchar(32) DEFAULT NULL COMMENT '状态',
                `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
                `modified_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
                `deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除标识',
                PRIMARY KEY (`id`),
                KEY `idx_flow_no` (`flow_instance_no`),
                KEY `idx_cust_no` (`cust_no`)
            ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='审批流程实例表';
            CREATE TABLE `approval_flow_execution` (
                `id`bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
                `flow_instance_no` varchar(32) DEFAULT NULL COMMENT '流程实例编号',
                `from_node` varchar(32) DEFAULT NULL COMMENT '执行前节点',
                `event` varchar(64) DEFAULT NULL COMMENT '执行的事件',
                `to_node` varchar(32) DEFAULT NULL COMMENT '执行后的节点',
                `executor_type` varchar(32) DEFAULT NULL COMMENT '可执行者类型',
                `executor` varchar(1024) DEFAULT NULL COMMENT '可执行者编号,多个时以英文逗号分隔',
                `actual_executor` varchar(32) DEFAULT NULL COMMENT '实际执行者用户编号',
                `remark` varchar(1024) DEFAULT NULL COMMENT '审批备注',
                `ext_data` json DEFAULT NULL COMMENT '扩展字段',
                `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
                `modified_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
                `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标识',
                PRIMARY KEY (`id`),
                KEY `idx_flow_no` (`flow_instance_no`)
            ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='审批流程执行记录表';
            
          • 入参, 每次执行调用流程引擎,都需要指定流程ID(执行那个流程)、节点ID(流程某个节点的执行情况)、流程引擎需要的一些参数(Object bizParam,用于其他人实现业务逻辑时的所需的自定义参数)。

                    // 设置SPEL到Context上下文,
                    // nyjk.SpelHelper和eaysflow.SpelHelper两个,但是由于eaysflow.SpelHelper没有设置context
                    SpelHelper.setApplicationContext(applicationContext);
                    FlowParam flowParam= new FlowParam(flowEngineBO.getFlowId(), flowEngineBO.getCurNode(), flowEngineBO);
                    FlowResult result = flowEngine.execute(flowParam);
            
          • startNode(param.param)、endNode(param.param)
            维护开始和结束节点的通用方法,值得注意的是,这两个节点是自动通过的。举例说明,当发起流程时,会自动从开始节点流转到中间审批节点,同理,如果倒数第二个中间节点审批同意时,也会自动流转到结束节点。并且也需要更改数据库相关表数据。

                    /**
                    * 开始节点,自动通过到下一节点,不存在从擎天过来的情况
                    * @param flowEngineBO
                    * @return boolean
                    */
                   public boolean startNode(FlowEngineBO flowEngineBO) {
                       // 判断是否为上一节点流转(第一个节点,没有上一节点流转,没啥意义,但以防万一)
                       if (flowEngineBO.getIsPreNode()) {
                           return false;
                       }
                       // 如果下一个节点需要接入擎天OA, 准备模板信息进行发布
                       List<ApprovalCfg> list = configBizService.getApprovalCfg(flowEngineBO.getCustNo());
                       // 获取下一个节点的配置信息(发送OA、可重复执行、审批人等信息)
                       ApprovalNodeCfg nodeCfg = ApprovalCfgUtil.getApprovalNodeCfg(list, flowEngineBO.getFlowType(), flowEngineBO.getNextNodeId());
                       // 更新数据库数据,不需要审批,因此result直接为True,
                       updateLocalProcessInfo(flowEngineBO, Boolean.TRUE);
                       // 是否发送MQ消息到OA平台
                       if (nodeCfg.getSyncQingtian()) {
                           // 发布下一个节点的待办事项及申请表单,第一个节点自动通过
                           sendMqProcessInfo(flowEngineBO, nodeCfg);
                       }
                       //设置为流转
                       flowEngineBO.setIsPreNode(Boolean.TRUE);
                       // actionResult
                       return true;
                   }
            
          • approvalNode(param.param)
            维护中间节点的通用方法,根据审批结果来更新数据库数据并且是否发送下一个节点的MQ审批消息到内部OA平台。

                /**
                * 节点审批-需要SPEL
                * @param flowEngineBO
                * @return boolean
                */
               public boolean approvalNode(FlowEngineBO flowEngineBO) {
            
                   //节点流转下来,直接停止
                   if (flowEngineBO.getIsPreNode()) {
                       return Boolean.FALSE;
                   }
                   //获取spel表达式
                   String spelStr = flowEngineBO.getSpelMethode();
                   boolean result = false;
                   if (StringUtils.isNotEmpty(spelStr)) {
                       // 执行该节点的nodeAction属性中的方法(其他人实现),获取审批结果
                       result = (boolean) SpelHelper.evalWithDefaultContext(spelStr, flowEngineBO, true);
                       // 更新数据库,发送下一个节点待办任务到OA
                        middleNodeProcess(flowEngineBO, result);
                   } else {
                       log.info("配置文件异常,找不到spel表达式!!!!!!!!!!!");
                   }
                   return result;
               }
            
          • 由于easyFlow本身是工作流(一次性执行完所有流程)的形式,因此需要改成审批流(节点之间能够停顿,等待审批人的同意或失败)的形式.
            分析:由于每个节点的执行既可以是上一个流程节点(同意)流转过来的,也可以是通过FlowParam指定执行节点,而第一种情况是导致easyFlow为工作流的原因。
            办法:设置一个标志位 isPreNode,如果是从execute(FlowParam param)过来,该标志位为0;如果是从上一个节点流转过来的,该标志为1,并进行一个简单的逻辑判断,为0则放行,为1则停止。

        • 自定义nodeAction属性,提供通用方法给其他人,让他们书写与之相关的业务逻辑代码。

                  @Override
                  public boolean execute(Object nodeParam) {
                      //取值
                      Map<String, Object> paramMap = (Map<String, Object>) nodeParam;
                      // 获取审批结果
                      FlowNodeStatusEnum statusEnum = (FlowNodeStatusEnum) paramMap.get("verifyResult");
                      // 流程实例No
                      String flowInstanceNo = (String) paramMap.get("flowInstanceNo");
                      //查询custNo
                      ApprovalFlowInstanceEntity entity = flowInstanceBaseService.queryEntityByNo(flowInstanceNo);
                      String custNo = entity.getCustNo();
                      //审查结果
                      boolean result = StringUtils.equals(statusEnum.getStatusCode(), "1");
                      if (result) {
                          //更新数据库客户表数据
                          updateCustomerStatus(CustStatusEnum.SUCCESS_CONFIRM, custNo);
                      } else {
                          //更新数据库客户表数据
                          updateCustomerStatus(CustStatusEnum.FAIL_CONFIRM, custNo);
                      }
                      return result;
                  }
          
      4. 总结

        • 第一次接触流程引擎,由于EasyFlow进行很好的解耦,因此刚开始上手比较难,需要阅读它本身的一些逻辑,并进行相关适配性修改(数据库读取、工作流转审批流),在项目周期比较短的情况下,组长给了较为足够的时间让我琢磨,同时也给了我很多建议,才勉强实现出来。不过由于自身代码水平较为欠缺,实现的较为冗余,因此这只是一个开始,写代码应该是一个提前规划框架的过程,考虑各种情况,而不应该是走一步看一步,导致代码需要不断修改增添,最后逻辑混乱。
        • 看了EasyFlow的表层的实现逻辑,发现较为简单,希望后面能把它的源码都看完。
  • Exception

    • onApplicationEvent多次执行
      这是由于流程引擎的内部implement ApplicationListener接口,因此实现了onApplicationEventf方法,导致启动项目多次执行,解决办法需要继承实现类,并对该方法进行重写。
    • EL1057E: No bean resolver registered in the context to resolve access to bean 'contractFlowListener'
      一般情况是el表达式书写错误,导致无法在applicationContext取到相应bean,因此需要在json流程文件中,使用@进行标识,且保证无语法错误。

task 5 资产盘点功能的业务实现

  • 资产盘点伪流程图
    20220211160725

  • 资产盘点表结构

          CREATE TABLE `asset_review` (
              `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
              `review_no` varchar(32) DEFAULT NULL COMMENT '资产盘点编号',
              `review_type` varchar(32) DEFAULT NULL COMMENT '资产盘点类型,如牛只盘点等',
              `cust_type` varchar(32) DEFAULT NULL COMMENT '发起盘点任务的客户类型,PERSON:个人 COMPANY:企业',
              `cust_no` varchar(32) DEFAULT NULL COMMENT '发起盘点任务的内部客户号',
              `cust_name` varchar(512) DEFAULT NULL COMMENT '发起盘点任务的客户名称',
              `user_no` varchar(32) DEFAULT NULL COMMENT '发起盘点任务的用户编号',
              `expected_num` int(11) DEFAULT NULL COMMENT '盘点期望值',
              `actual_num` int(11) DEFAULT NULL COMMENT '盘点实际值',
              `review_explain` varchar(1024) DEFAULT NULL COMMENT '盘点说明',
              `review_start_date` datetime DEFAULT NULL COMMENT '盘点开始时间',
              `review_end_date` datetime DEFAULT NULL COMMENT '盘点结束时间',
              `approval_flow_instance_no` varchar(32) DEFAULT NULL COMMENT '审批流程编号',
              `review_status` varchar(32)  COMMENT '盘点状态,未开始、盘点中、已盘点',
              `submitter_user_no` varchar(32) COMMIT '盘点提交人用户编号',
              `submitter_name` varchar(64) COMMIT '盘点提交人名字',
              `reviewer_user_no` varchar(32) COMMIT '盘点复核人用户编号',
              `reviewer_name` varchar(64) COMMIT '盘点复核人名字',
              `status` varchar(32) DEFAULT 'VALID' COMMENT '状态',
              `ext_data` json DEFAULT NULL COMMENT '扩展数据',
              `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
              `modified_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
              `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
              PRIMARY KEY (`id`),
              KEY `idx_custno` (`cust_no`)
          )ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='资产盘点表';
    
          CREATE TABLE `asset_review_detail` (
              `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
              `review_detail_no` varchar(32) DEFAULT NULL COMMENT '资产盘点明细编号',
              `review_no` varchar(32) DEFAULT NULL COMMENT '资产盘点编号',
              `review_type` varchar(32) DEFAULT NULL COMMENT '资产盘点类型,如牛只盘点等',
              `review_cust_type` varchar(32) DEFAULT NULL COMMENT '盘点人的客户类型,PERSON:个人 COMPANY:企业',
              `review_cust_no` varchar(32) DEFAULT NULL COMMENT '盘点人的内部客户号',
              `review_cust_name` varchar(512) DEFAULT NULL COMMENT '盘点人的客户名称',
              `review_user_no` varchar(32) DEFAULT NULL COMMENT '盘点人用户编号',
              `review_user_name` varchar(32) DEFAULT NULL COMMENT '盘点人用户名字',
              `review_asset_type` varchar(32) DEFAULT NULL COMMENT '被盘点对象类型,如登记的生物资产(牛只)、某些设备等',
              `review_asset_no` varchar(32) DEFAULT NULL COMMENT '被盘点对象类编号,如牛只的资产登记编号等',
              `asset_review_idx` int(11) DEFAULT NULL COMMENT '当前资产是被第几次盘点',
              `asset_cust_type` varchar(32) DEFAULT NULL COMMENT '盘点资产对应的客户类型,PERSON:个人 COMPANY:企业',
              `asset_cust_no` varchar(32) DEFAULT NULL COMMENT '盘点资产对应的的内部客户号',
              `asset_cust_name` varchar(512) DEFAULT NULL COMMENT '盘点资产对应的客户名称',
              `asset_base_no` varchar(32) DEFAULT NULL COMMENT '基地编号',
              `asset_base_name` varchar(32) DEFAULT NULL COMMENT '基地名称',
              `asset_houses_no` varchar(32) DEFAULT NULL COMMENT '圈舍编号',
              `asset_houses_name` varchar(32) DEFAULT NULL COMMENT '圈舍名称',
              `expected_num` int(11) DEFAULT NULL COMMENT '盘点期望值',
              `actual_num` int(11) DEFAULT NULL COMMENT '盘点实际值',
              `review_explain` varchar(1024) DEFAULT NULL COMMENT '盘点说明',
              `review_start_date` datetime DEFAULT NULL COMMENT '盘点开始时间',
              `review_end_date` datetime DEFAULT NULL COMMENT '盘点结束时间',
              `review_detail_status` varchar(32)  COMMENT '盘点状态,未盘点、已盘点、未知',
              `ext_data` json DEFAULT NULL COMMENT '扩展数据',
              `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
              `modified_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
              `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识',
              PRIMARY KEY (`id`),
              KEY `idx_reviewAssetNo` (`review_asset_no`)
          )ENGINE=InnoDB AUTO_INCREMENT= 1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='资产盘点明细表';
    
  • 开始盘点:只有金融机构能够去进行盘点,因为金融机构提供资金给核心企业或养殖户购买牛只(后续有鸡、羊等生物资产),所以金融机构需要核实这些资产的情况。

    1. 查询该金融机构进行关系绑定的所有核心企业或养殖户,获取他们的所有养殖资产(递归实现)
    2. 在资产盘点中新增一条数据,而在养殖资产明细表中,如果该个资产已经盘点过,则新增一条盘点明细数据,盘点次数为上次盘点次数的基础上加一;如果这个资产上次为未盘点过,则盘点次数和上次盘点次数保持一致;如果这个资产为首次盘点,则为一
    3. 发起盘点流程审批
    4. 进入流程小程序盘点节点,该节点只有两种情况才能通过,一种是小程序端进行盘点扫描到牛只数量(通过牛耳标)达到期望值;一种是金融机构提前结束盘点。值得注意的是该节点属于可重复提交节点,小程序端既可以一次性扫描到全部牛只进行提交,也可以每次扫描到若干头牛进行提交。
  • 结束盘点:即金融机构未等到所有资产都被盘点到,就提前结束盘点,流程从小程序盘点节点流转到下一审批节点。

  • 总结:由于这个是接手其他离职同事的代码,组长要我将这个功能完整流程测试一次,后面发现存在一些问题,和离职同事交流过后进行了修改。但第二天,组长发现这里的代码逻辑全部有问题,需要重写,就在表关系不是很明确的情况,被组长催促着赶快完成,后面虽然写完了,但是测试的时候,由于表结构不清晰,和前端联调一直有问题,最后发现表里的假数据造的有问题。给我的教训是,虽然组长没有明说,但是自己也一定要主动去问表关系,不能会耽误很多时间,以为会是代码层面问题。明确需求很重要!!!!

引用

posted @ 2022-02-14 21:21  Rookie丶flying  阅读(50)  评论(0编辑  收藏  举报