实习总结
实习日志
task 1:后台管理nyjk-admin项目搭建,引入aks加解密接口进行页面展示
- nyjk-admin项目引入nyjk-common的jar包。
- 调用common中得 AksDeviceCryptoGateway 接口注入对象。
- 编写相关controller接口
- 使用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>];-
项目结构
- nyjk-common:主要书写各种sdk的相关工具类以及一些通用方法,不涉及平台业务逻辑。
- nyjk-api:主要定义nyjk-common接口(异常、枚举、网关、校验、常量)和nyjk-app对外接口(service对内部系统和web对外部前端)
service主要定义平台内部使用的外部接口(注册到jsf中心,然后通过jsf以rpc方式调用,譬如nyjk-admin去调用一些服务),使用DTO(Data Transfer Object)命名实体对象,且使用NyjkRequest和NyjkResponse泛型类进行统一封装。
web主要定义与前端交互的外部接口(将接口上传到网关,建立网关到接口的映射,并且注册到jsf中心,当前端发送请求到网关,网关找到相关接口映射,并调用jsf接口,最后通过rpc方式调用项目的接口接口实现类),使用VO(View Object)命名实体对象,且使用WebBaseRequest(实体类extend方式)和 WebResponse(泛型)。
- 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>
filtering:开启 directory 下的过滤,使得yml文件中引用pom中变量时,通过@value_of_pom@进行引用,能够避免maven的$引用方式的冲突
${profile.active}:根据profile选择,动态切换yml文件.
-
-
-
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
-
通过内置代码生成器,生成entity、mapper、service层代码,提高了开发效率,在Service接口中定义相关方法,在ServiceImpl中具体实现即可。
-
使用代码封装好了简单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 申请步骤
- 将需要暴露的接口,copy其全包名,到JSF服务管理平台进行接口申请(配置本地host)
- 在该项目(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 唯一,分组别名要正确-->
- 在 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"/>
- 启动 nyjk-app 项目
- 在 nyjk-admin 中以 依赖注入 的方式引用该服务进行消费
-
内部sdk使用流程
- 阅读相关文档,申请使用权限(测试、线上)
- 引入相关jar包
- 在 spring-jsf-comsumer.xml 中配置该sdk的jsf调用
- 在 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
-
流程实现
-
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"; }
-
-
流程引擎结合具体业务实现过程
-
配置流程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:使用自定义监听器,监听流程事件。 -
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) { } }
-
根据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; }
-
-
总结
- 第一次接触流程引擎,由于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流程文件中,使用@进行标识,且保证无语法错误。
- onApplicationEvent多次执行
task 5 资产盘点功能的业务实现
-
资产盘点伪流程图
-
资产盘点表结构
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='资产盘点明细表';
-
开始盘点:只有金融机构能够去进行盘点,因为金融机构提供资金给核心企业或养殖户购买牛只(后续有鸡、羊等生物资产),所以金融机构需要核实这些资产的情况。
- 查询该金融机构进行关系绑定的所有核心企业或养殖户,获取他们的所有养殖资产(递归实现)
- 在资产盘点中新增一条数据,而在养殖资产明细表中,如果该个资产已经盘点过,则新增一条盘点明细数据,盘点次数为上次盘点次数的基础上加一;如果这个资产上次为未盘点过,则盘点次数和上次盘点次数保持一致;如果这个资产为首次盘点,则为一
- 发起盘点流程审批
- 进入流程小程序盘点节点,该节点只有两种情况才能通过,一种是小程序端进行盘点扫描到牛只数量(通过牛耳标)达到期望值;一种是金融机构提前结束盘点。值得注意的是该节点属于可重复提交节点,小程序端既可以一次性扫描到全部牛只进行提交,也可以每次扫描到若干头牛进行提交。
-
结束盘点:即金融机构未等到所有资产都被盘点到,就提前结束盘点,流程从小程序盘点节点流转到下一审批节点。
-
总结:由于这个是接手其他离职同事的代码,组长要我将这个功能完整流程测试一次,后面发现存在一些问题,和离职同事交流过后进行了修改。但第二天,组长发现这里的代码逻辑全部有问题,需要重写,就在表关系不是很明确的情况,被组长催促着赶快完成,后面虽然写完了,但是测试的时候,由于表结构不清晰,和前端联调一直有问题,最后发现表里的假数据造的有问题。给我的教训是,虽然组长没有明说,但是自己也一定要主动去问表关系,不能会耽误很多时间,以为会是代码层面问题。明确需求很重要!!!!