- 一. Springboot介绍
- 二. 开发平台
- 二. 开发环境搭建
- 三. 工程结构
- 开发流程步骤
- maven依赖解决
- 开发注意事项
- 批量规范
- 子系统间数据交互码值翻译方案
- ESB交易方案
- 流水号及业务编号
- 删除,修改
- 取上上级机构
- 取柜面虚拟用户
- Web前端
一. Springboot介绍
1. SpringBoot是什么?
在Spring框架这个大家族中,产生了很多衍生框架,比如 Spring、SpringMvc框架等,SpringBoot是一种全新的编程规范,是一个服务于框架的框架,他的产生简化了框架的使用,简化了Spring众多框架中所需的大量且繁琐的配置文件。
2. SpringBoot可以做什么?
最明显的特点是,让文件配置变的相当简单、让应用部署变的简单(SpringBoot内置服务器,并装备启动类代码),可以快速开启一个Web容器进行开发。
3. SpringBoot的特点

①良好的基因
因为SpringBoot是伴随着Spring 4.0而生的,boot是引导的意思,也就是它的作用其实就是在于帮助开发者快速的搭建Spring框架
②简化编码
比如我们要创建一个 web 项目,在使用 Spring 的时候,需要在 pom 文件中添加多个依赖,而 Spring Boot 则会帮助开发着快速启动一个 web 容器,在 Spring Boot 中,我们只需要在 pom 文件中添加如下一个 starter-web 依赖即可。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
我们点击进入该依赖后可以看到,Spring Boot 这个 starter-web 已经包含了多个依赖,包括之前在 Spring 工程中需要导入的依赖,我们看一下其中的一部分,如下:
<!-- .....省略其他依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId><version>5.0.7.RELEASE</version><scope>compile</scope></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.0.7.RELEASE</version><scope>compile</scope></dependency>
由此可以看出,Spring Boot 大大简化了我们的编码,我们不用一个个导入依赖,直接一个依赖即可。
③简化配置
Spring 虽然使Java EE轻量级框架,但由于其繁琐的配置,一度被人认为是“配置地狱”。各种XML、Annotation配置会让人眼花缭乱,而且配置多的话,如果出错了也很难找出原因。Spring Boot更多的是采用 Java Config 的方式,对 Spring 进行配置。举个例子:
我新建一个类,但是我不用@Service注解,也就是说,它是个普通的类,那么我们如何使它也成为一个 Bean 让 Spring 去管理呢?只需要@Configuration和@Bean两个注解即可,如下:
public class TestService {public String sayHello () {return "Hello Spring Boot!";}}
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JavaConfig { @Bean public TestService getTestService() { return new TestService(); } }
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class JavaConfig {@Beanpublic TestService getTestService() {return new TestService();}}
@Configuration表示该类是个配置类,@Bean表示该方法返回一个 Bean。这样就把TestService作为 Bean 让 Spring 去管理了,在其他地方,我们如果需要使用该 Bean,和原来一样,直接使用@Resource注解注入进来即可使用,非常方便。
@Resource private TestService testService;
另外,部署配置方面,原来 Spring 有多个 xml 和 properties配置,在 Spring Boot 中只需要个 application.yml即可。
④简化部署
在使用 Spring 时,项目部署时需要我们在服务器上部署 tomcat,然后把项目打成 war 包扔到 tomcat里,在使用 Spring Boot 后,我们不需要在服务器上去部署 tomcat,因为 Spring Boot 内嵌了 tomcat,我们只需要将项目打成 jar 包,使用java -jar xxx.jar一键式启动项目。
另外,也降低对运行环境的基本要求,环境变量中有JDK即可。
⑤简化监控
我们可以引入 spring-boot-start-actuator 依赖,直接使用 REST 方式来获取进程的运行期性能参数,从而达到监控的目的,比较方便。但是 Spring Boot 只是个微框架,没有提供相应的服务发现与注册的配套功能,没有外围监控集成方案,没有外围安全管理方案,所以在微服务架构中,还需要 Spring Cloud 来配合一起使用。
4. Springboot例子
下载springboot的一个工程
其中pom.xml
文件内容
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>myproject</artifactId><version>0.0.1-SNAPSHOT</version><!-- Inherit defaults from Spring Boot --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.9.RELEASE</version></parent><!-- Add typical dependencies for a web application --><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies><!-- Package as an executable jar --><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
写一个启动类
import org.springframework.boot.\*;import org.springframework.boot.autoconfigure.\*;import org.springframework.web.bind.annotation.\*;@RestController@EnableAutoConfigurationpublic class Example {@RequestMapping("/")String home() {return "Hello World!";}public static void main(String\[\] args) throws Exception {SpringApplication.run(Example.class, args);}}
Mvn环境下
$ mvn spring-boot:run
. \_\_\_\_ \_ \_\_ \_ \_/\\\\ / \_\_\_'\_ \_\_ \_ \_(\_)\_ \_\_ \_\_ \_ \\ \\ \\ \\( ( )\\\_\_\_ | '\_ | '\_| | '\_ \\/ \_\` | \\ \\ \\ \\\\\\/ \_\_\_)| |\_)| | | | | || (\_| | ) ) ) )' |\_\_\_\_| .\_\_|\_| |\_|\_| |\_\\\_\_, | / / / /\=========|\_|==============|\_\_\_/=/\_/\_/\_/:: Spring Boot :: (v2.0.9.RELEASE)....... . . ........ . . . (log output here)....... . . ......... Started Example in 2.222 seconds (JVM running for 6.514)Hello World!
因为有了
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
$ mvn package
执行打包程序打成myproject-0.0.1-SNAPSHOT.jar
Linux下执行$ java -jar target/myproject-0.0.1-SNAPSHOT.jar
. \_\_\_\_ \_ \_\_ \_ \_/\\\\ / \_\_\_'\_ \_\_ \_ \_(\_)\_ \_\_ \_\_ \_ \\ \\ \\ \\( ( )\\\_\_\_ | '\_ | '\_| | '\_ \\/ \_\` | \\ \\ \\ \\\\\\/ \_\_\_)| |\_)| | | | | || (\_| | ) ) ) )' |\_\_\_\_| .\_\_|\_| |\_|\_| |\_\\\_\_, | / / / /\=========|\_|==============|\_\_\_/=/\_/\_/\_/:: Spring Boot :: (v2.0.9.RELEASE)....... . . ........ . . . (log output here)....... . . ......... Started Example in 2.536 seconds (JVM running for 2.864)
5. Springboot 注释
@EnableAutoConfiguration
spring通常建议我们将main方法所在的类放到一个root包下,@EnableAutoConfiguration(开启自动配置)注解通常都放到main所在类的上面,这样@EnableAutoConfiguration可以从逐层的往下搜索各个加注解的类。
@SpringBootApplication
一个@SpringbootApplication相当于@Configuration,
@EnableAutoConfiguration和@ComponentScan并具有他们的默认属性值。
二. 开发平台
高伟达信贷系统所用开发平台为自主研发的EasyDDA平台,EasyDDA开发平台全称“高伟达分布式架构信贷应用开发平台(Global Info TechEasy**DistributedDevelopment ArchitectureA**ll-IN-ONE)”。支持分布式架构的信贷应用开发平台的建设,能有效地平衡软件目标(用户需求目标或者市场定位目标)与技术特性,具备高性能、高度抽象以及高可扩展性等特性,同时还具备高可靠、高安全等特性。EasyDDA平台针对高性能、高可用性、高可靠、高安全等特性进行了设计,主要实现以下几方面特点:
高性能:架构采用前后台分离的模式进行设计,前后页面通过控制层调用后台服务进行业务处理。前后台之间通过JSON数据进行数据传递,减少数据传输压力。
高可用性:专门针对系统的操作界面进行了UI设计,并增加用户个性化设置的功能,可以根据个人的喜好设置系统的操作风格和常用功能。可以通过简单的系统配置来实现新产品的开发,提高产品开发效率,满足业务需求。
高可靠、高安全性:系统内部有专门的内部服务总线,供各个子系统之间进行服务调用,并设计专门的安全组件来对系统的访问权限进行管理。可以在前台对未授权登录进行访问控制,也可以在数据库访问层控制访问权限。
1. 平台体系结构
EasyDDA平台包含开发体系、安全体系、运行维护体系三大体系,支持PC、PAD、手机等多终端访问的一站式(ALL-IN-ONE)开发平台。目前金融行业IT架构多数存在分布式微服务架构的系统和传统SOA架构的单体系统并存的情况,EasyDDA开发平台的前置组件(IFDS)能够有效的支持双生态环境下的系统开发和运行。平台体系架构图如下图所示:

图表 1体系架构图
- 开发体系:EasyDDA的开发体系包括开发工具、代码管理工具、协同开发工具、缺陷管理工具、知识库管理工具、DevOps工具等工具类组件。
- 安全体系:对于信贷类系统来说,安全体系是保障系统平稳运行的重中之重。EasyDDA平台的安全体系包括安全策略管理组件、API网关组件、用户安全认证组件等安全组件。
- 运行维护体系:EasyDDA开发平台封装了缓存管理、异常处理、日志处理、上传/导出、权限管理、异步调度、流程引擎、规则引擎、模型引擎等基础技术组件,能够支撑信贷类应用系统的正常运转。平台在维护方面还提供了监控预警平台、日志管理平台、自动化部署平台等日常运维使用的工具。
2. 平台技术架构
EasyDDA平台是前后端分离的分布式架构平台,基于领域建模,支持跨平台、多渠道。前台主要使用的是VUE,基于渐进式框架构建用户界面,同时使用了H5、Element-UI、BootStrap、EChars等开源的前端组件。后台使用的是跨平台的JVM引擎,以公司EasySADP基础技术框架为基础,封装SpringCloud的相关技术组件,作为后台的基础平台。技术架构如下图所示:

图表 2技术架构图
关键技术介绍:
VUE
Vue.js是一个构建数据驱动的Web界面的渐进式框架。Vue.js的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件。与其他重量级框架不同的是,Vue采用自底向上增量开发的设计。Vue的核心库只关注视图层,并且非常容易学习,非常容易与其它库或已有项目整合。另一方面,Vue完全有能力驱动采用单文件组件和Vue生态系统支持的库开发的复杂单页应用。
SpringFramework
作为应用的IoCContainer,负责管理Bean之间的引用依赖关系,同时利用AOP机制,提供企业级服务,如事务控制、安全控制、数据访问等;简单来说,Spring是一个分层的JavaSE/EEfull-stack(一站式)轻量级开源框架。
SpringBoot
Spring Boot是所有基于 Spring Framework 5.0 开发的项目的起点。Spring Boot 的设计是为了让你尽可能快的跑起来,Spring 应用程序并且尽可能减少你的配置文件。其设计目的是用来简化新Spring应用的初始搭建以及开发过程。他有两种非常重要的策略:开箱即用和约定优于配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
SpringCloud
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
ELK
ELK是Elasticsearch、Logstash、Kibana三大开源框架首字母大写简称,我们主要用来做日志收集、管理和分析。
Elasticsearch是一个基于Lucene、分布式、通过Restful方式进行交互的近实时搜索平台框架。
Logstash是ELK的中央数据流引擎,用于从不同目标(文件/数据存储/MQ)收集的不同格式数据,经过过滤后支持输出到不同目的地(文件/MQ/redis/elasticsearch/kafka等)。
Kibana可以将elasticsearch的数据通过友好的页面展示出来,提供实时分析的功能。
Nginx
Nginx是一个高性能的HTTP和反向代理web服务器,其特点是占有内存少,并发能力强,也可以实现负载均衡服务。主要用来承载新信贷系统的Web界面功能。
Druid
Druid为监控而生的数据库连接池,它是阿里巴巴开源平台上的一个项目。Druid是Java语言中最好的数据库连接池,Druid能够提供强大的监控和扩展功能.它可以替换DBCP和C3P0连接池。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
SpringMVC
SpringMVC属于SpringFrameWork的后续产品,已经融合在SpringWebFlow里面。Spring框架提供了构建Web应用程序的全功能MVC模块。SpringMVC作为表现层框架,负责接收请求,分发请求和界面表示及响应等;
MyBatis
作为数据访问层主要的技术框架,提供较好的JDBCSQL编写兼容性,并可以基于Ehcache、Redis实现对于查询结果集的二级缓存。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(PlainOrdinaryJavaObjects,普通的Java对象)映射成数据库中的记录。
Redis
Redis是一个开源的使用ANSIC语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。Redis是一个key-value存储系统,和Memcached类似。Redis是一个高性能的key-value数据库,Redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。
EhCache
EhCache是一个纯Java的进程内缓存框架,具有快速、精干等特点,Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用存,JavaEE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
JSON
JSON(JavaScriptObjectNotation,JS对象标记)是一种轻量级的数据交换格式。它基于ECMAScript(w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得JSON成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。
二. 开发环境搭建
1. 软件安装
后端开发所用软件jdk1.8 ,idea(开发工具),git客户端,maven(3.5.4),git(git_v2.20.1.1),idea安装过程在此略过。
Idea工具配置
Maven配置如下图:

在点击Configure=>setting,后弹出一个工具导航栏,在头顶的输入区,输入“maven”,右边填入maven的配置。
Maven home directory :maven安装地址目录User setting file: maven 配置文件Local repository: maven 本地仓库 (非常重要,要设置好)
快捷键+pluin
在setting操作界面搜索Keymap,在右边可根据自己的使用习惯选择eclipse或是别的软件快捷键设置。如下图

Pluin:
同样在setting界面,输入pluin,需要安装.ignore,Alibaba Java Coding Guidelines 两个软件。
应为idea自带git,所以不需要安装。

;为.ignore,Alibaba Java Coding Guidelines 两个软件
2. Clone代码

如上图所示,选择Check out from Version Control=》git

弹出如下界面

URL:就是项目在gitlab版本管理上的地址
Dirctory:为本地项目存放文件夹。
Log in GitHub:输入分配给每个人的gitlab用户名和密码,
点击Test 测试一下,返回ok后,可直接点击Clone 下载代码。
输入本项目地址:XXXXXXXXXXXXXXX,
3. Java规范
命名规范
(一)命名风格:
a)代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:_name / __name / $name / name_ / name$ /
b) 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。
正例:ali / alibaba / taobao / cainiao/ aliyun/ youku / hangzhou 等国际通用的名称,可视同英文。
反例:DaZhePromotion [打折] / getPingfenByName() [评分] / String fw[福娃] / int 某变量 = 3
(二)常量命名:
a) 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:SYSTEM_CODE_NUM / INDEXEXECTYPE_RULE
反例:System_Code / INDEXEXECTYPE
b) 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。
正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD
(三)类命名:
a) 类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等
正例:ForceCode / UserDO / HtmlDTO / TbCmnApproveJurisdictionService/ TcpUdpDeal /
BusiCodeController
反例:forcecode / UserDo / HTMLDto / TBCMNApproveJurisdictionService/ TCPUDPDeal / BUSICodeController
(四)方法、参数、成员变量、局部变量命名:
方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格。
正例: localValue / getBusiCode() /userId
接口:
- 类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的
Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。
正例:/*
/*删除已关联的岗位* @param pd* @throws Exception*/void delUserPost(PageData pd)throws Exception;//接口基础常量String COMPANY = "alibaba";
反例:接口方法定义 public void f();
说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现
- 接口和实现类的命名有两套规则:
对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用Impl 的后缀与接口区别。
正例:WorkFlowServiceImpl实现 WorkFlowService接口。
如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词)。
正例:AbstractTranslator 实现 Translatable 接口。
- 枚举:
枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。
正例:枚举名字为 SystemEnum
- 项目各层规范 :
Controller:功能模块名称+Controller,例如:PositionController
Service:功能模块名称+Service,例如:PositionService
Service.Impl:Service名称+Impl,例如:PositionServiceImpl
Core :功能模块名称+Core,例如:PostitionCore
Core.Impl:Core名称+Impl,例如:PostitionCoreImpl
所有类名采用驼峰格式
异常日志
错误码
- 【强制】错误码的制定原则:快速溯源、沟通标准化。
说明: 错误码想得过于完美和复杂,就像康熙字典中的生僻字一样,用词似乎精准,但是字典不容易随身携带并且简单易懂。
正例:错误码回答的问题是谁的错?错在哪?
1)错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
- 错误码必须能够进行清晰地比对(代码中容易 equals)。
- 错误码有利于团队快速对错误原因达到一致认知。
例如:CSM_ERR_0001(“CSM-ERR-0001”, “财报平衡校验失败!”, “”),SYS_ERR_0004(“SYS-ERR-0004”, “返回值不能为空,请联系管理员!”, “”)
- 【强制】错误码不体现版本号和错误等级信息。
说明:错误码以不断追加的方式进行兼容。错误等级由日志和错误码本身的释义来决定。
【强制】全部正常,但不得不填充错误码时返回五个零:00000。
【强制】错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。
说明:错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100
【强制】编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上进行,审批生效,编号即被永久固定。
【强制】错误码使用者避免随意定义新的错误码。
说明:尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。
- 【强制】错误码不能直接输出给用户作为提示信息使用。
说明:堆栈(stack_trace)、错误信息(error_message)、错误码(error_code)、提示信息(user_tip)是一个有效关联并互相转义的和谐整体,但是请勿互相越俎代庖。
【推荐】错误码之外的业务独特信息由 error_message 来承载,而不是让错误码本身涵盖过多具体业务属性。
【推荐】在获取第三方服务错误码时,向上抛出允许本系统转义,由 C 转为 B,并且在错误信息上带上原有的第三方错误码。
异常处理
- 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
正例:if (obj != null) {…}
反例:try { obj.method(); } catch (NullPointerException e) {…}
- 【强制】异常捕获后不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
- 【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。
对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
- 【强制】不要在 finally 块中使用 return。
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
反例:
private int x = 0;public int checkReturn() {try {// x 等于 1,此处不返回return ++x;} finally {// 返回的结果是 2return ++x;}}
- 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
- 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
- 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE(NullPointerException)。
反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException、ServiceException 等。 本项目BussinessException、ControllException等
项目中的异常BussinessException,EasyloanException为业务异常。调用方法
throw error(MessageEnum.SYS\_ERR\_0001.getCode(), MessageEnum.SYS\_ERR\_0001.getMessage());throw new BussinessException("CLS-ERR-0001","未获取到当前登录用户信息!");
日志规约
- 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架(SLF4J、JCL—Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
说明:日志框架(SLF4J、JCL—Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)
使用 SLF4J:
import org.slf4j.log;import org.slf4j.logFactory;private static final log log = logFactory.getlog(Test.class);
使用 JCL:
import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;private static final Log log = LogFactory.getLog(Test.class);
本項目中使用框架封裝的log,controller层,service层,core层,无需自己写直接使用logger对象即可。
其他如果确实需要,则使用如下:
【强制】
Log logger = LogFactory.get();//javacommon.easytools.log【强制】所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,过往日志格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd
正例:以 aap 应用为例,日志保存在/home/admin/aapserver/logs/aap.log,历史日志名称为
aap.log.2016-08-01
- 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log。logType:日志类型,如 stats/monitor/access 等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
正例:mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log
- 【项目强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:log.debug(“Processing trade with id: {} and symbol: {}”, id, symbol); 注意一个变量一个占位符。
- 【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。
说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT)为真时(Slf4j 的常见实现Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())这种参数内有 getName()方法调用,无谓浪费方法调用的开销。
正例:
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志if (log.isDebugEnabled()) {log.debug("Current ID is: {} and name is: {}", id, getName());}
- 【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false。
正例:<log name="com.taobao.dubbo.config" additivity="false">
- 【项目强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用e.printStackTrace()打印异常堆栈。
说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
- 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。
正例:log.error(“inputParams:{} and errorMessage:{}”, 各类参数或者对象 toString(), e.getMessage(), e);
- 【项目强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。
说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。 JsonUtil.toString
正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
- 【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
33/59Java 开发手册
- 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
- 【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。
说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。
- 【项目强制】所有应用日志均使用javacommon.easytools.log.Log 类,不允许使用log4j以及其他日志类,批量任务中使用EasyJobLogger.log()和Log类配合使用。EasyJobLogger负责输出批量调度控制台,Log负责写入应用日志。特别是异常信息,一定都要输出。
3. 单元测试
单元测试规约
- 好的单元测试必须遵守 AIR 原则。
说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
⚫ A:Automatic(自动化)
⚫ I:Independent(独立性)
⚫ R:Repeatable(可重复)
- 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。 assert示例:
package com.test.assertTest;/*** Created by IntelliJ IDEA** @author manzuo* @date 2019/7/2 19:52*/public class hello {public static void main(String[] args){double x=-10; //可以手动改变x的值,重复运行查看不同的运行结果assert x>0:"x小于0";// 这里使用了断言,规定x必须大于0,否则会抛出异常,并把“x小于0”作为报错信息(必须要开启断言机制,否则类加载器会跳过这行代码)double y = Math.sqrt(x);System.out.println(y);}}
断言的开启
Intellij IEDA开启断言:
- 保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入。
- 单元测试是可以重复执行的,不能受到外界环境的影响。
说明:单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。如果单测对外
部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
正例:为了不受外界环境影响,在测试时用 spring 这样的 DI框架注入一个本地(内存)实现或者 Mock 实现。
- 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。
说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,
那是集成测试的领域。
- 核心业务、核心应用、核心模块的增量代码确保单元测试通过。
说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。
- 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。本项目是统一写在
xxx-springboot/src/test/java 目录下 如图:

说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。
以上规约为必须准守规约。
- 单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%
说明:在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。
- 编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
C:Correct,正确的输入,并得到预期的结果。
D:Design,与设计文档相结合,来编写单元测试。
E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。
- 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。
反例:删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数据并不符合业务插入规则,导致测试结果异常。
- 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识。
正例:在阿里巴巴企业智能事业部的内部单元测试中,使用 ENTERPRISEINTELLIGENCE_UNIT_TEST的前缀来标识单元测试相关代码。
对于不可测的代码在适当的时机做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码。
在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例(UC)。
单元测试作为一种质量保障手段,在项目提测前完成单元测试,不建议项目发布后补充单元测试用例。
为了更方便地进行单元测试,业务代码应避免以下情况:
构造方法中做的事情过多。
存在过多的全局变量和静态方法。
存在过多的外部依赖。
存在过多的条件语句。
说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。
16.不要对单元测试存在如下误解:
那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。
单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的。
单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。
单元测试编写
在我们的工程目录中,会有XXX-springboot目录,我们所有的单元测试类就写在此目录下如下图:
Test代码
如下代码所示:
/*** Test:** @author: yangshenghua* @date: 2020/9/6 10:28* @version: 1.0* Description:*/@RunWith(SpringRunner.class)@SpringBootTest(classes={Application.class,ApplicationContextHolder.class, com.git.easyloan.commons.util.SecurityContextHolder.class})@TestConfiguration(value = "classpath:rules/ruleEngine_beans.xml")@ComponentScan(basePackages = {"com.git.easyloan", "javacommon.coreframe"})@ActiveProfiles(value="dev")public class ControllerTest {private log log = logFactory.getlog(this.getClass());@Autowiredprivate TbPrcPricingModelRuleService tbPrcPricingModelRuleService;@Before/*** @Before 注解的public void方法将会在每个测试方法执行之前执行一次。* @Beforelass 注解的public void方法将会在所有方法执行完毕之前执行,此修饰的方法必须是静态方法*/public void setUp() throws Exception {JwtUser jwtUser = RedisManager.getUserById("040040");UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(jwtUser, null, jwtUser.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(new HttpServletRequest() {@Overridepublic String getAuthType() {return null;}@Overridepublic Cookie[] getCookies() {return new Cookie[0];}@Overridepublic long getDateHeader(String s) {return 0;}@Overridepublic String getHeader(String s) {return null;}@Overridepublic Enumeration<String> getHeaders(String s) {return null;}@Overridepublic Enumeration<String> getHeaderNames() {return null;}@Overridepublic int getIntHeader(String s) {return 0;}@Overridepublic String getMethod() {return null;}@Overridepublic String getPathInfo() {return null;}@Overridepublic String getPathTranslated() {return null;}@Overridepublic String getContextPath() {return null;}@Overridepublic String getQueryString() {return null;}@Overridepublic String getRemoteUser() {return null;}@Overridepublic boolean isUserInRole(String s) {return false;}@Overridepublic Principal getUserPrincipal() {return null;}@Overridepublic String getRequestedSessionId() {return null;}@Overridepublic String getRequestURI() {return null;}@Overridepublic StringBuffer getRequestURL() {return null;}@Overridepublic String getServletPath() {return null;}@Overridepublic HttpSession getSession(boolean b) {return null;}@Overridepublic HttpSession getSession() {return null;}@Overridepublic String changeSessionId() {return null;}@Overridepublic boolean isRequestedSessionIdValid() {return false;}@Overridepublic boolean isRequestedSessionIdFromCookie() {return false;}@Overridepublic boolean isRequestedSessionIdFromURL() {return false;}@Overridepublic boolean isRequestedSessionIdFromUrl() {return false;}@Overridepublic boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException {return false;}@Overridepublic void login(String s, String s1) throws ServletException {}@Overridepublic void logout() throws ServletException {}@Overridepublic Collection<Part> getParts() throws IOException, ServletException {return null;}@Overridepublic Part getPart(String s) throws IOException, ServletException {return null;}@Overridepublic <T extends HttpUpgradeHandler> T upgrade(Class<T> aClass) throws IOException, ServletException {return null;}@Overridepublic Object getAttribute(String s) {return null;}@Overridepublic Enumeration<String> getAttributeNames() {return null;}@Overridepublic String getCharacterEncoding() {return null;}@Overridepublic void setCharacterEncoding(String s) throws UnsupportedEncodingException {}@Overridepublic int getContentLength() {return 0;}@Overridepublic long getContentLengthLong() {return 0;}@Overridepublic String getContentType() {return null;}@Overridepublic ServletInputStream getInputStream() throws IOException {return null;}@Overridepublic String getParameter(String s) {return null;}@Overridepublic Enumeration<String> getParameterNames() {return null;}@Overridepublic String[] getParameterValues(String s) {return new String[0];}@Overridepublic Map<String, String[]> getParameterMap() {return null;}@Overridepublic String getProtocol() {return null;}@Overridepublic String getScheme() {return null;}@Overridepublic String getServerName() {return null;}@Overridepublic int getServerPort() {return 0;}@Overridepublic BufferedReader getReader() throws IOException {return null;}@Overridepublic String getRemoteAddr() {return null;}@Overridepublic String getRemoteHost() {return null;}@Overridepublic void setAttribute(String s, Object o) {}@Overridepublic void removeAttribute(String s) {}@Overridepublic Locale getLocale() {return null;}@Overridepublic Enumeration<Locale> getLocales() {return null;}@Overridepublic boolean isSecure() {return false;}@Overridepublic RequestDispatcher getRequestDispatcher(String s) {return null;}@Overridepublic String getRealPath(String s) {return null;}@Overridepublic int getRemotePort() {return 0;}@Overridepublic String getLocalName() {return null;}@Overridepublic String getLocalAddr() {return null;}@Overridepublic int getLocalPort() {return 0;}@Overridepublic ServletContext getServletContext() {return null;}@Overridepublic AsyncContext startAsync() throws IllegalStateException {return null;}@Overridepublic AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {return null;}@Overridepublic boolean isAsyncStarted() {return false;}@Overridepublic boolean isAsyncSupported() {return false;}@Overridepublic AsyncContext getAsyncContext() {return null;}@Overridepublic DispatcherType getDispatcherType() {return null;}}));SecurityContextHolder.getContext().setAuthentication(authentication);}@Test/*** @Ignore 对包含测试类的类或@Test注解方法使用@Ignore注解将使被注解的类或方法不会被当做测试执行*/public void TestIndex() throws Exception {try {Responder responder = new Responder();PageData pd = new PageData();DTDMap dicMap = new DTDMap();//反显列值为字典 begindicMap.put("PRICIING_TYPE","PRICING_PRICE_TYPE"); //反显列时翻译定价模式为字典值dicMap.put("EFFECTIVE_MARK","PUB_EFFECT_STATUS"); //反显列时翻译生效标识为字典值dicMap.put("PRODUCT_TYPE","productCode"); //反显列时翻译产品类型名称为字典值pd.put("dicMap",dicMap);// 反显列值为字典 endPage page = new Page();page.setCurrentPage(1);page.setShowCount(10);page.setPd(pd);responder = tbPrcPricingModelRuleService.list(page);log.debug("2222222222="+JSONUtil.toJsonStr(responder.getRetObj()));} catch (EasyLoanException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}@Ignore("这个方法很傲娇,不想被测试")@Testpublic void TestIndex2() throws Exception {}}
代码说明
标签说明
@RunWith(SpringRunner.class) //sping运行标签@SpringBootTest(classes = {Application.class, ApplicationContextHolder.class, com.git.easyloan.commons.util.SecurityContextHolder.class} //spirng加载启动类 本项目需要加载@TestConfiguration(value = "classpath:rules/ruleEngine_beans.xml") //加载规则xml 本项目需要加载@ComponentScan(basePackages = {"com.git.easyloan", "javacommon.coreframe"}) //本项目需要加载@ActiveProfiles(value="dev") 加载环境配置文件,同sping.profile.active=dev //本项目需要加载@Before //注解的public void方法将会在每个测试方法执行之前执行一次。@BeforeClass //注解的public void方法将会在所有方法执行完毕之前执行,此修饰的方法必须是静态方法@Test //注解的方法,为需要测试的具体方法。@Ignore //对包含测试类的类或@Test注解方法使用@Ignore注解将使被注解的类或方法不会被当做测试执行如上例子中TestIndex2()会跳过,不会被执行
方法说明
本项目中,有些场景需要获取当前登录用户信息,因此如下方法一定要有,具体内容实现见2.2.1Test代码即可,
注意别忘加@Before标签。如果不需要获取登录用户信息,则不需要实现。
@Beforepublic void setUp() throws Exception;
idea test类自动生成
比如要生成某个类的test类,则在idea,如下图操作



如上图,选择需要测试的方法即可,注意如果选择了多个方法,不加@Ignore 的话test类会顺序执行。
设计规约
设计遵循规约
(一)存储方案和底层数据结构的设计获得评审一致通过,并沉淀成为文档。
说明:有缺陷的底层数据结构容易导致系统风险上升,可扩展性下降,重构成本也会因历史数据迁移和系统平滑过渡而陡然增加,所以,存储方案和数据结构需要认真地进行设计和评审,生产环境提交执行后,需要进行 double check。
正例:评审内容包括存储介质选型、表结构设计能否满足技术方案、存取性能和存储空间能否满足业务发展、表或字段之间的辩证关系、字段名称、字段类型、索引等;数据结构变更(如在原有表中新增字段)也需要进行评审通过后上线。
(二) 在需求分析阶段,如果与系统交互的 User 超过一类并且相关的 User Case 超过 5 个,使用用例图来表达更加清晰的结构化需求。
(三)如果某个业务对象的状态超过 3 个,使用状态图来表达并且明确状态变化的各个触发条件。
说明:状态图的核心是对象状态,首先明确对象有多少种状态,然后明确两两状态之间是否存在直接转换关系,再明确触发状态转换的条件是什么。
正例:借据状态有生效、逾期、结清、已删除、未放款、已核销等。比如逾期与结清这两种状态之间是不可能有直接转换关系的。
(四)如果系统中某个功能的调用链路上的涉及对象超过 3 个,使用时序图来表达并且明确各调用环节的输入与输出。
说明:时序图反映了一系列对象间的交互与协作关系,清晰立体地反映系统的调用纵深链路。
(五)如果系统中模型类超过 5 个,并且存在复杂的依赖关系,使用类图来表达并且明确类之间的关系。
说明:类图像建筑领域的施工图,如果搭平房,可能不需要,但如果建造蚂蚁 Z 空间大楼,肯定需要详细的施工图。
(六)如果系统中超过 2 个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活动图来表示。
说明:活动图是流程图的扩展,增加了能够体现协作关系的对象泳道,支持表示并发等。
以上规约为必须准守
(七)系统架构设计时明确以下目标:
确定系统边界。确定系统在技术层面上的做与不做。
确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。
确定指导后续设计与演化的原则。使后续的子系统或模块设计在一个既定的框架内和技术方向上继续演化。
确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等。
(八)需求分析与系统设计在考虑主干功能的同时,需要充分评估异常流程与业务边界。
反例:用户在淘宝付款过程中,银行扣款成功,发送给用户扣款成功短信,但是支付宝入款时由于断网演练产生异常,淘宝订单页面依然显示未付款,导致用户投诉。
(九)类在设计与实现时要符合单一原则。
说明:单一原则最易理解却是最难实现的一条规则,随着系统演进,很多时候,忘记了类设计的初衷。
(十)谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现。
说明:不得已使用继承的话,必须符合里氏代换原则,此原则说父类能够出现的地方子类一定能够出现,比如,“把钱交出来”,钱的子类美元、欧元、人民币等都可以出现。
(十一)系统设计阶段,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护。
说明:低层次模块依赖于高层次模块的抽象,方便系统间的解耦。
(十二)系统设计阶段,注意对扩展开放,对修改闭合。
说明:极端情况下,交付的代码是不可修改的,同一业务域内的需求变化,通过模块或类的扩展来实现。
(十三)系统设计阶段,共性业务或公共行为抽取出来公共模块、公共配置、公共类、公共方法等,在系统中不出现重复代码的情况,即 DRY 原则(Don’t Repeat Yourself)。
说明:随着代码的重复次数不断增加,维护成本指数级上升。随意复制和粘贴代码,必然会导致代码的重复,在维护代码时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {...}
(十四)避免如下误解:敏捷开发 = 讲故事 + 编码 + 发布。
说明:敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摒弃传统的审批流程,但核心关键点上的必要设计和文档沉淀是需要的。
反例:某团队为了业务快速发展,敏捷成了产品经理催进度的借口,系统中均是勉强能运行但像面条一样的代码,可维护性和可扩展性极差,一年之后,不得不进行大规模重构,得不偿失。
(十五)设计文档的作用是明确需求、理顺逻辑、后期维护,次要目的用于指导编码。
说明:避免为了设计而设计,系统设计文档有助于后期的系统维护和重构,所以设计结果需要进行分类归档保存。
(十六) 可扩展性的本质是找到系统的变化点,并隔离变化点。
说明:世间众多设计模式其实就是一种设计模式即隔离变化点的模式。
正例:极致扩展性的标志,就是需求的新增,不会在原有代码交付物上进行任何形式的修改。
(十七)设计的本质就是识别和表达系统难点。
说明:识别和表达完全是两回事,很多人错误地认为识别到系统难点在哪里,表达只是自然而然的事情,
但是大家在设计评审中经常出现语言不详,甚至是词不达意的情况。准确地表达系统难点需要具备如下能力: 表达规则和表达工具的熟练性。抽象思维和总结能力的局限性。基础知识体系的完备性。深入浅出的生动表达力。
(十八)代码即文档的观点是错误的,清晰的代码只是文档的某个片断,而不是全部。
说明:代码的深度调用,模块层面上的依赖关系网,业务场景逻辑,非功能性需求等问题是需要相应的文档来完整地呈现的。
(十九)在做无障碍产品设计时,需要考虑到:
所有可交互的控件元素必须能被 tab 键聚焦,并且焦点顺序需符合自然操作逻辑。
用于登录校验和请求拦截的验证码均需提供图形验证以外的其它方式。
自定义的控件类型需明确交互方式。
正例:用户登录场景中,输入框的按钮都需要考虑 tab 键聚焦,符合自然逻辑的操作顺序如下,“输入用户名,输入密码,输入验证码,点击登录”,其中验证码实现语音验证方式。如果有自定义标签实现的控件,则需要明确交互方式。
4. 前后端调用 +restful规范
4.1 RESTful开发规范
本项目使用vue+springboot 系统开发模式,因此,前后台交互使用RESTFUL接口规范,传输的数据采用json格式。
4.1.1 REST接口规范
Restful=有意义的URL+合适的HTTP动词
REST(英文:Representational State Transfer ,简称 REST),RESTful是一种基于HTTP的设计风格,只是提供了一组设计原则和约束条件,而不是一种标准。
4.1.2 RESTful API
RESTful API应准寻以下规则:
- 访问路径:每一个API对应一个路径,表示API具体的请求地址,如:/api/v1/system/tbMyWorkitem
- 代表一种资源,只能为名词,推荐使用复数,不能为动词,请求方法已经表达动作意义。
- URL路径不能使用大写,单词如果需要分隔,统一使用下划线。
- 路径禁止携带表示请求内容类型的后缀,比如“.json”,“.xml”
- 请求方法:对具体操作的定义,常见请求方法如下:
GET:从服务器取出资源
POST:在服务器新建一个资源
PUT:在服务器更新一个资源
DELETE:从服务器删除一个资源
- 请求内容:URL带的参数必须无敏感信息或符合安全要求;body里带参数时必须设置Content-Type
- 响应体:响应体body可以放置多种数据类型,由Content-Type头来确定。
本系统RESTful API
本系统RESTful API由:/api/版本号/子系统名称/表名(驼峰格式)组成
| 功能 | URL | HTTP Method | 后台方法 |
|---|---|---|---|
| 获取一组数据列表 | /api/v1/system/tbPubNoticeInfo/ | GET | 列表查询 |
| 根据ID获取某个数据 | /api/v1/system/tbPubNoticeInfo/{id} | GET | 获取单笔记录 |
| 新建数据 | /api/v1/system/tbPubNoticeInfo | POST | 新建记录 |
| 完整的更新数据 | /api/v1/system/tbPubNoticeInfo | PUT | 更新记录 |
| 部分更新数据 | /api/v1/system/tbPubNoticeInfo | PATCH | 部分更新 |
| 删除 | /api/v1/system/tbPubNoticeInfo/{id} | DELETE | 删除单笔数据 |
如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。 下面是一些常见的参数:
?pagNum=1&size=100:指定第几页,以及每页的记录数。
例如:
在项目demo中
GET /api/v1/system/positions?pagNum=1&size=10
注意:/api/v1/system/tbPubNoticeInfo/{id}是被允许的
/api/v1/system/tbPubNoticeInfo/{id}/xxx/{id1}是不被允许的。
可以改为/api/v1/system/tbPubNoticeInfo/xxx id和id1作为参数在body中传输
4.1.3状态码范围
本系统状态码分为:http请求状态码(status)和交易返回状态码(code)
http请求状态码:
1xx:信息,请求收到,继续处理。范围保留用于底层HTTP的东西,你很可能永远也用不到。
2xx:成功,行为被成功地接受、理解和采纳
3xx:重定向,为了完成请求,必须进一步执行的动作
4xx:客户端错误,请求包含语法错误或者请求无法实现。范围保留用于响应客户端做出的错误,例如。他们提供不良数据或要求不存在的东西。这些请求应该是幂等的,而不是更改服务器的状态。
5xx:范围的状态码是保留给服务器端错误用的。这些错误常常是从底层的函数抛出来的,甚至开发人员也通常没法处理,发送这类状态码的目的以确保客户端获得某种响应。
当收到5xx响应时,客户端不可能知道服务器的状态,所以这类状态码是要尽可能的避免。
服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
502 网关错误
503 Service Unavailable
504 网关超时
交易返回状态码
200:交易成功
非200:交易失败,具体交易码具体定义
注意:
服务标准化开发:
000000:代表成功
000001-099999:为警告
100000-899999:业务异常{
100001:业务校验失败,100002:参数校验失败,100003:业务查询异常,100004:权限校验失败,
100005:业务操作异常
}
900000-999999:系统异常{
999999:交易失败,999998:交易失败,空指针异常,999997:交易失败,通信超时,999996:不支持的类型,
999995:交易失败,运行时异常,999994:交易失败,事务回滚,999993:交易失败,通信异常。
}
4.1.4 RESTful的注意点
URL只是表达被操作的资源位置,因此不应该使用动词,且注意单复数区分。
除了POST和DELETE之外,其他的操作需要冥等的,例如对数据多次更新应该返回同样的内容。
RESTful和语言、传输格式无关。
无状态,HTTP设计本来就是没有状态的,之所以看起来有状态因为我们的浏览器使用了Cookies,每次请求都会把Session ID(可以看做身份标识)传递到headers中。
RESTful没有定义body中内容传输的格式。
4.2 前后台调用规范
4.2.1 JSON API
因为RESTful 风格仅仅规定了URL和HTTP Method的使用,并没有定义body中数据格式的,本系统使用JSON 来传递前后台数据:
- MIME 类型
JSON API数据格式已经被IANA机构接受了注册,因此必须使用application/json类型,客户端请求头中Content-Type应该为application/json。
- JSON 文档结构
在顶级节点使用data、errors,来标书数据、错误信息,注意data和errors应该是互斥的,不能在一个文档中同时存在。
{"data":[{"type":"articles","id":"1",.....}]}
例子1:
如前端vue请求:GET /api/v1/system/positions?pageNum=1&size=10 查询列表
那么后台controller代码如下:
@RestController@RequestMapping(value = "/positions")@Api(value = "岗位Controller", tags = {"岗位增删改查操作"})public class PositionController extend BaseRestController{/*** 参数维度池查询列表** @param data 查询条件* @param pageNum 页码* @return*/@Override@GetMapping@ApiOperation(value = "分页获取参数维度池信息", notes = "分页获取参数维度池信息:查询条件,页面")public ResponseEntity index(@RequestParam@ApiParam(name = "data", value = "查询条件")Map<String, String> data,@RequestParam(defaultValue = "1", required = false)@Min(value = 1, message = "页码只能为正整数")@ApiParam(name = "pageNum", value = "页码")int pageNum,@RequestParam(defaultValue = "10", required = false)@Min(value = 10, message = "条数只能为正整数")@ApiParam(name = "size", value = "条数")int size) {Responder responder = new Responder();try {log.info("参数维度池查询");PageData pd = new PageData();DTDMap dicMap = new DTDMap(); //反显列值为字典 begindicMap.put("字段","字典编码"); //反显列时翻译某列为字典值pd.put(DICMAP,dicMap); // 反显列值为字典 endpd.putAll(data);Page page = new Page();page.setCurrentPage(pageNum);page.setShowCount(size);page.setPd(pd);responder = tbPrcColumnParamService.list(page);if (isError(responder)) {responder = bussError(responder.getCode(), responder.getErrorMessage(), responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder = bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(),e.getMessage(), MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}}
/api/v1/system网关会截获解析,根据请求类型(GET、POST)转到相应的controller的方法上。
例子2:
如前端vue请求:PUT /api/v1/system/positions/{id} 修改信息
那么后台controller代码如下:
/*** 参数维度池 待修改* @param id* @return*/@Override@GetMapping("/{id}")@ApiOperation(value = "待修改参数维度池", notes = "根据参数维度池编号,待修改参数维度池数据")public ResponseEntity update(@PathVariable@NotBlank(message = "参数维度池编号不能为空")@ApiParam(name = "id", value = "参数维度池编号", required = true)String id) {Responder responder = new Responder();try {log.info("修改参数维度池");responder= tbPrcColumnParamService.findById(id);if (isError(responder)) {responder = bussError(responder.getCode(), responder.getErrorMessage(), responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder = bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(),e.getMessage(), MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}
三. 工程结构
1.前台工程结构
2.后台工程结构
本系统为springboot工程,其结构分为2部分,项目逻辑实现和项目启动
项目逻辑实现
项目逻辑实现如上图所示 pricing目录下
分为4层,即controller、applilcation、core、entity。
Controller层
负责接收前台交易请求,并对其响应返回。返回格式统一为JSON。
在定义一个Rest接口时,我们通常会使用GET,POST,PUT,DELETE几种方式来完成我们所需要进行CRUD的一些操作,
GET:一般用于查询数据,不能对数据进行更新以及插入操作。
POST:一般用于数据的插入操作,也是使用最多的传输方式,
PUT:我们使用PUT方式来对数据进行更新操作。
DELETE:用于数据删除。
与之对应的就会有@GetMapping,@PostMapping,@PathMapping,@DeleteMapping
Controller使用@RestController注释,具体代码如下
/*** 说明:岗位管理* 创建人:* 创建时间:2019-06-22*/@RestController@RequestMapping(value = "/positions")@Api(value = "岗位Controller", tags = {"岗位增删改查操作"})public class PositionController extends BaseRestController{@Autowiredprivate PositionService ompositionService;/*** 保存岗位** @param data 岗位数据* @throws Exception*/@Override@PostMapping@ApiOperation(value = "新增岗位", notes = "新增岗位")public ResponseEntity create(@NotEmpty(message = "岗位信息不能为空")@ApiParam(name = "data", value = "岗位信息", required = true)@RequestBody PageData data) throws Exception {Responder responder = new Responder();try {log.info("增加岗位");responder=ompositionService.save(pd);if (isError(responder)) {responder=bussError(responder.getCode(),responder.getErrorMessage(),responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder = bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(),e.getMessage(), MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}/*** 删除岗位** @param id 岗位编号* @throws Exception*/@Override@DeleteMapping("/{id}")@ApiOperation(value = "删除岗位", notes = "根据岗位编号,删除岗位")public ResponseEntity delete(@PathVariable@NotBlank(message = "岗位编号不能为空")@ApiParam(name = "id", value = "岗位编号", required = true) String id{Responder responder = new Responder();try {log.info("删除岗位");responder = ompositionService.delete(pd);if (isError(responder)) {responder = bussError(responder.getCode(),responder.getErrorMessage(),responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder=bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(),e.getMessage(),MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}/*** 修改岗位** @param data 岗位数据* @throws Exception*/@Override@PatchMapping("/{id}")@ApiOperation(value = "修改岗位", notes = "根据岗位编号,修改岗位")public ResponseEntity editPath(@NotBlank(message = "岗位编号不能为空")@RequestBody@NotEmpty(message = "待修改岗位信息不能为空")@ApiParam(name = "data", value = "待修改岗位信息", required = true)PageData data){Responder responder = new Responder();try {log.info("修改岗位");responder=ompositionService.edit(data);if (isError(responder)) {responder=bussError(responder.getCode(),responder.getErrorMessage(), responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder=bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(),e.getMessage(),MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}/*** 岗位查询** @param data 查询条件* @param pageNum 页码* @return* @throws Exception*/@Override@GetMapping@ApiOperation(value = "分页获取岗位信息", notes = "分页获取岗位信息:查询条件,页面")public ResponseEntity index(@RequestParam@ApiParam(name = "data", value = "查询条件")Map<String, String> data,@RequestParam(defaultValue = "1", required = false)@Min(value = 1, message = "页码只能为正整数")@ApiParam(name = "pageNum", value = "页码") int pageNum,@RequestParam(defaultValue = "10", required = false)@Min(value = 10, message = "条数只能为正整数")@ApiParam(name = "size", value = "条数")int size){Responder responder = new Responder();try {log.info("岗位查询");PageData pd = new PageData();pd.putAll(data);String keywords = pd.getString("keywords");//关键词检索条件if (null != keywords && !"".equals(keywords)) {pd.put("keywords", keywords.trim());}Page page = new Page();page.setCurrentPage(pageNum);page.setPd(pd);responder=ompositionService.list(page);if (isError(responder)) {responder=bussError(responder.getCode(),responder.getErrorMessage(),responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder=bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(),e.getMessage(),MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}/*** 批量删除** @param ids* @return*/@Override@DeleteMapping@ApiOperation(value = "批量删除岗位信息", notes = "批量删除岗位信息")public ResponseEntity batchDelete(@RequestParam@NotBlank(message = "岗位编号不能为空")@ApiParam(name = "ids", value = "主键列表")String ids){Responder responder = new Responder();try {log.info("批量删除岗位");if (null != ids && !"".equals(ids)) {String idArray[] = ids.split(",");responder = ompositionService.deleteAll(idArray);}if (isError(responder)) {responder=bussError(responder.getCode(),responder.getErrorMessage(),responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder=bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(),e.getMessage(), MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}}
服务标准化规范如下:
/* 说明:岗位管理* 创建人:* 创建时间:2019-06-22*/@ScmsController@RestController@RequestMapping(value = "/positions")@Api(value = "岗位Controller", tags = {"岗位增删改查操作"})public class PositionController extends BaseRestController{@Autowiredprivate PositionService ompositionService;/*** 保存岗位** @param data 岗位数据* @throws Exception*/@Override@PostMapping@ApiOperation(value = "新增岗位", notes = "新增岗位")public ResponseEntity create(@NotEmpty(message = "岗位信息不能为空")@ApiParam(name = "data", value = "岗位信息", required = true)@RequestBody PageData data) throws Exception {ScmsResponder responder = new ScmsResponder();log.info("增加岗位");responder=ompositionService.save(pd);return ResponderEntity.returnResponder(responder);}}
application层
如下:PositionService,分为接口定义和具体接口的实现
/*** 说明: 岗位管理接口* 创建人:* 创建时间:2019-04-22* @version*/public interface PositionService {/**新增* @param pd* @throws Exception*/public Responder save(PageData pd)throws Exception;/**删除* @param pd* @throws Exception*/public Responder delete(PageData pd)throws Exception;/**查询* @param pd* @throws Exception*/public Responder index(Page page)throws Exception;
标准的service接口定义,注意每个方法前都需要写注释,标明方法用途,参数用途。
如下图是service的实现Impl
/*** 说明: 岗位管理* 创建人:* 创建时间:2019-04-22*/@Service("ompositionService")public class PositionServiceImpl extends AlsBaseMessage implements PositionService {@Resource(name = "daoSupport")private DaoSupport dao;/*** 新增** @param pd* @throws Exception*/@Overridepublic Responder save(PageData pd) throws Exception {int number=(int)dao.save("OmPositionMapper.save", pd);return success(SUCCESS_CODE, SUCCESS_MESSAGE, number);}/*** 删除** @param pd* @throws Exception*/@Overridepublic Responder delete(PageData pd) throws Exception {int number=(int)dao.delete("OmPositionMapper.delete", pd);return success(SUCCESS_CODE, SUCCESS_MESSAGE, number);}/*** 修改** @param pd* @throws Exception*/@Overridepublic Responder edit(PageData pd) throws Exception {int number=(int)dao.update("OmPositionMapper.edit", pd);return success(SUCCESS_CODE, SUCCESS_MESSAGE, number);}}
@Service("xxxService")//为定义暴露给外部调用的service名称。@Resource(name = "daoSupport")private DaoSupport dao;//为引入的数据库操作类。单表操作可以直接在类里通过dao封装的方法直接操作数据库。
如果是多表操作,则需要调用core层的方法。
在service的实现类中会有如下类似代码,去调用core层方法。
@Resource(name="csmGuarGroupCore")private CsmGuarGroupCore csmGuarGroupCore;/*** 修改之后查询最新信息返回页面* @param pd* @return* @throws Exception*/@Overridepublic Responder editReturnPageData(PageData pd) throws Exception {Responder responder=csmGuarGroupCore.editReturnPageData(pd)if(isError(responder)){throw error(responder.getCode(),responder.getMessage());}return responder;}
Core层
public interface CsmGuarGroupCore {/*** 保存联保小组* @param pageData* @throws Exception*/public Responder saveCsmGuarGroup(PageData pageData) throws Exception;/***删除联保小组* @param partyId* @throws Exception*/public Responder deleteCsmGuarGroup(String partyId) throws Exception;/*** 查询联保小组基本信息* @return* @throws Exception*/public Responder getGuarGroupBasicInfo(String partyId) throws Exception;/*** 修改之后查询最新信息返回页面** @param pageData* @return* @throws Exception*/public Responder editReturnPageData(PageData pageData) throws Exception;}
如上图所示,core也相当于一个service,是一个原子级的service,它也有它的具体实现
如下CsmGuarGroupCoreImpl
@Component(“csmGuarGroupCore”):托管给spring管理装载
@Component("csmGuarGroupCore")public class CsmGuarGroupCoreImpl extends AlsMessage implements CsmGuarGroupCore {@Resource(name = "daoSupport")private DaoSupport dao;/*** 保存联保小组* @param pageData* @throws Exception*/@Overridepublic void saveCsmGuarGroup(PageData pageData) throws Exception {//保存参与人表dao.save("CsmPartyMapper.save",pageData);//保存联保小组客户表dao.save("CsmGuarGroupMapper.save",pageData);//保存客户管理团队pageData.put("PARTY_ID",pageData.get("UUID"));pageData.put("UUID", KeyGenerator.get32UUID());dao.save("CsmManagementTeamMapper.save",pageData);}/***删除联保小组* @param partyId* @throws Exception*/@Overridepublic void deleteCsmGuarGroup(String partyId) throws Exception {PageData pageData = new PageData();pageData.put("UUID",partyId);//删除参与人表dao.delete("CsmPartyMapper.delete",pageData);//删除联保小组表dao.delete("CsmGuarGroupMapper.delete",pageData);//删除管理团队表pageData.put("PARTY_ID",partyId);dao.delete("CsmManagementTeamMapper.deleteByPartyId",pageData);}/*** 查询联保小组基本信息* @return* @throws Exception*/@Overridepublic Responder getGuarGroupBasicInfo(String partyId) throws Exception {PageData pageData = new PageData();pageData.put("PARTY_ID",partyId);pageData=(PageData)dao.findForObject("CsmGuarGroupMapper.getGuarGroupBasicInfo",pageData);return success(SUCCESS_CODE,SUCCESS_MESSAGE,pageData);}/*** 修改之后查询最新信息返回页面** @param pageData* @return* @throws Exception*/@Overridepublic Responder editReturnPageData(PageData pageData) throws Exception {// 更新dao.update("CsmGuarGroupMapper.edit", pageData);// 取得更新后的信息pageData.put("PARTY_ID", pageData.getString("UUID"));PageData dbpd= (PageData)dao.findForObject("CsmGuarGroupMapper.getGuarGroupBasicInfo",pageData);return success(SUCCESS_CODE,SUCCESS_MESSAGE,dbpd);}}
Entity层
存放一些固定的对象。
其他说明
- 所有类前加注释
/*** 说明:对本类的说明。* 创建人:yangshenghua* 创建时间:2019年7月20日*/
- Controller
对类增加如下注释:
@RestController,@RequestMapping(value = "/positions"),@Api(value = "岗位Controller", tags = {"岗位增删改查操作"})
说明:
@RequestMapping:为前台请求的路径,
@Api:类说明
每个方法之前也要有说明如下
/*** 保存岗位** @param data 岗位数据* @throws Exception*/@PostMapping@ApiOperation(value = "新增岗位", notes = "新增岗位")public ResponseEntity create(@NotEmpty(message = "岗位信息不能为空")@ApiParam(name = "data", value = "岗位信息", required = true)PageData data) {Responder responder = new Responder();try {log.info("新增岗位");responder= ompositionService.save(pd);if (isError(responder)) {responder=bussError(responder.getCode(),responder.getErrorMessage(), responder.getMessage());}} catch (EasyLoanException e) {responder = bussError(e.errCode(), "", e.getMessage());} catch (Exception e) {responder=bussError(MessageEnum.CALL_BACK_RETCODE_ERROR.getCode(), e.getMessage(), MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage());}return ResponderEntity.returnResponder(responder);}
说明:@PostMapping:为请求的方法
@ApiOperation(value = “新增岗位”, notes = “新增岗位”):方法说明
@ApiParam(name = “data”, value = “岗位信息”, required = true):参数说明
@RequestBody PageData data:参数定义
- Service类,和方法都加上说明
Service的实现类、方法都加上说明,在类头增加@Service(“XXX”)注释,对外暴露。
Core、core实现、entity层,与上类似,在类和方法上都要有说明Core实现 在类头增加@Component(“XXXX”)注释,对外暴露,注意对外暴露的core层方法,当要调用其外的core服务可以直接调用,但是不能调用service层服务。Service可以调用多个core服务,service层不能引用别的service。Controller可以引用多个service(不能再一个事务里)。
sqlMapping 命名是模块名+Mapping,
- 子系统间接口调用
Springboot子系统调用使用feignClient,框架已经集成好,我们只要写service接口即可
1)创建interface
@FeignClient(name="${server.name.customer}",url="${gateway.url.customer}",configuration=feignCommonConfig.class)public interface FeignPostionService {/*** 列表查询岗位信息* @throws Exception*/@RequestMapping(method=RequestMethod.GET,value= "/api/v1/newparameter/positions/", consumes = MediaType.APPLICATION_JSON_VALUE)@ResponseBodypublic ResponEntity<Responder> list(@RequestParam@ApiParam(name = "page", value = "查询条件")Page page ) throws Exception;}
注意:@FeignClient: 为接口调用注释。
Name: server服务名称
Url:为网关地址,可以用变量实现
@RequestMapping:注释
Menthod:为调用目标controller方法的请求方式
Value: 为调用目标controller方法的url服务提供方的controller内部实现了list方法的实现。
- 返回
Controller层统一返回:ResponderEntity.returnResponder(responder);
返回
Responder:封装了code,message,errorMessage,retObjCode:返回交易码,200代表成功,非200代表失败,此错误码统一使用MessageEnum按子系统划分定义。 Message:返回客户端信息,用于给客户展示信息。 ErrorMessage:返回错误信息,用户给开发人员展示错误信息。 retObj:返回交易结果数据信息。 Service层:统一返回responder Core层:统一返回responder
项目启动
如上图所示,java包下的Application 为springboot启动类Resource目录下为一些配置rule、kettle、template等模板文件。
开发流程步骤
数据库建模
建模工具
数据库建模工具使用开源工具 eXERD
软件及安装使用见
建模规范
数据建模规范见svn如下目录
数据建模资料:
- http://10.5.12.36/svn/HBNX-2019-300-EDXD/项目过程区/20\_工程文档(EP)/30\_开发文档(RD)/32\_设计文档/40\_数据库模型设计/设计规范
- http://10.5.12.36/svn/HBNX-2019-300-EDXD/项目过程区/20_工程文档(EP)/30_开发文档(RD)/32_设计文档/40_数据库模型设计/数据库建模源码/ALS2020/30开发文档(RD)/32设计文档/40_数据库模型设计/数据库建模源码/ALS2020)
- http://10.5.12.36/svn/HBNX-2019-300-EDXD/项目过程区/20_工程文档(EP)/30_开发文档(RD)/32_设计文档/40_数据库模型设计/工具/数据库.xls(包含数据的用户、各数据表级的前缀、字典前缀、公共字段等)/30开发文档(RD)/32设计文档/40_数据库模型设计/工具/数据库.xls(包含数据的用户、各数据表级的前缀、字典前缀、公共字段等))
- http://10.5.12.36/svn/HBNX-2019-300-EDXD/项目过程区/20_工程文档(EP)/30_开发文档(RD)/32_设计文档/40_数据库模型设计/工具/数据库设计规范与标准.doc(数据库表、索引、序列、函数、表空间等的规范与标准)/30开发文档(RD)/32设计文档/40_数据库模型设计/工具/数据库设计规范与标准.doc(数据库表、索引、序列、函数、表空间等的规范与标准))
- http://10.5.12.36/svn/HBNX-2019-300-EDXD/项目过程区/20\_工程文档(EP)/30\_开发文档(RD)/32\_设计文档/40\_数据库模型设计/工具/ORACLE数据库开发规范20190505\_01.docx(Oracle数据库语法规范、性能优化、设计规范、书写规范、命名规范、注释规范等)
注意:
1、请务必严格按照规范执行数据建模。
2、请务必严格按照规范执行数据建模。
3、请务必严格按照规范执行数据建模。
4、为了防止由于数据库开发不规范的情况出现的返工情况,请务必在开发之前仔细阅读并遵照ORACLE数据库开发规范20190505_01.docx(路径详见3.1.2建模规范的第5点)中说明的规范。
代码生成器
如下图:在IE输入http://10.100.3.38:9083/ 地址,需要拨号公司vpn,如下图:

选择“后端代码生成”,

点击后端代码生成,注意使用代码生成器生成代码的前提,是已经在数据库中创建了表结构。
输入数据库连接地址(POC版本数据库:oracle;地址:10.100.3.15;端口:1521;用户名:XXXX;密码:XXXX;库名:XXXX,所要生成表名:XXXX),如下图:

点击连接,系统会返回所要生成表名,选择其中所需表后的反射按钮,如下图:

以SYSTEM(参数管理子系统为例),所属子系统输入system,领域名称:个人(因为有些子系统是要区分对公和对私业务,在此我们选择对私),业务模块:sys,模块类型:单/多表,其他默认即可,注意如果是多表操作,则在目标表中点放大镜,选择 多张表,并点击左侧文件夹图标,重新获取字段,如下图:

字段显示完后,点击左侧生成代码图标,如下图

如下图:选择本地路径保存代码

OK,代码生成完毕。
代码集成
打开刚下载的code.zip包,结构如下:

controller:目录对应我们开发工程里的controller层代码,只需要把里边的java类考到我们子工程controller层下,以system为例是
system/system-controller/src/man/java/com/git/easyloan/system/controller/下;如下图
service:目录对应我们开发工程里的application层代码,同理需要把里边的java类考到我们子工程application层下,以system为例是
system/system-application/src/man/java/com/git/easyloan/system/service/下;如下图:
mybatis_oracle:目录对应我们开发工程core层下的resources/mybatis/,以system为例是把mybatis_oracle目录下的文件考到
easyloan-system/system-core/src/main/resources/mybatis/system/下的相对应的模块下,multi文件夹存放多表sql,single存放单表sql(注意除了system、customer、dataApplication、afterLoan、billSystem、accounting子系统外,其他系统单表的sql文件由代码生成器统一生成,个人不可更改)。
代码调试

启动类是在子项目模块system-springboot/src/main/java/com/git/easyloan/system/Application.java
启动方式如下图:

直接点击绿色三角执行即可。如果没有Application,则如下图操作:

点击Edit Configuarions

点击 “+” 选择spring boot

给启动快捷方式取个名字,填入Name,在Main:class右侧的… ;找到启动类,保存即可。

启动过程中如出现如下图错误,无需担心,不影响测试。

启动完毕后,在浏览器输入http://localhost:8105/swagger-ui.html,注意8105是系统功能模块的端口,各个子系统模块有自己的端口,具体见下部表格

上图就是swagger的测试页面。具体swagger的标签使用见文档上部。现在我们就可以模拟前端传入JSON数据测试后台了。
子系统名称及端口
| 系统名称 | 英文名称 | 端口 |
|---|---|---|
| 客户管理子系统 | easyloan-customer | 8101 |
| 移动作业子系统 | easyloan-mobile | 8102 |
| 网贷子系统 | easyloan-netloan | 8103 |
| 客户评级子系统 | easyloan-rating | 8104 |
| 公共管理子系统 | easyloan-system | 8105 |
| 利率定价子系统 | easyloan-pricing | 8106 |
| 押品子系统 | easyloan-asset | 8107 |
| 客户授信子系统 | easyloan-credit | 8108 |
| 用信管理子系统 | easyloan-loan | 8109 |
| 票据子系统 | easyloan-billSystem | 8110 |
| 不良资产子系统 | easyloan-badassets | 8111 |
| 贷款核算子系统 | easyloan-aplus | 8120 |
| 贷后管理子系统 | easyloan-afterloan | 8113 |
| 风险预警子系统 | easyloan-riskwarn | 8114 |
| 风险分类子系统 | easyloan-classify | 8115 |
| 档案管理子系统 | easyloan-archives | 8116 |
| 批量管理子系统 | easyloan-batch | 8117 |
| 数据应用子系统 | easyloan-dataApplication | 8118 |
| 应用监控子系统 | easyloan-monitor | 8119 |
| 接口子系统 | easyloan-ifds | 8121 |
| 登录子系统 | easyloan-login | 8122 |
公共参数使用
Redis使用是通过RedisManager(com.git.easyloan.commons.redis.RedisManager)
使用方法,只需在调用类中直接引入RedisManager,调用RedisManager所提供的方法即可。 此类包含(当前登陆用户信息、登陆机构、系统参数、)的获取方法 。
目前RedisManager 提供获取字典的方法,具体如下:
A.根据字典码值获取字典对象:
(Dictionaries)RedisManager.findDictory(String dicName) ;
B.根据用户编号,获取用户信息
(PageData) RedisManager.findUserInfo(String userCode) ;
C. 根据机构编号,获取机构信息
PageData RedisManager.findOrgInfo(String orgCode);
D.获取登陆用户信息
Map RedisManager.getLoginUser();
返回map中的信息为:
| 英文字段 | 中文解释 | 存储类型 |
|---|---|---|
| userCode | 登陆用户名 | String |
| userName | 登陆用户姓名 | String |
| 登陆用户email | String | |
| enabled | 登陆用户状态 | String |
| createTime | 登陆用户创建时间 | String |
| lastPasswordResetTime | 登陆用户最后登录时间 | String |
| roles | 登陆用户角色列表 | List\ |
| loginOrg | 登陆用户登入机构信息 | Map |
| orgList | 登陆用户所属机构列表 | List\ |
| posList | 登陆用户所属岗位列表 | List\ |
E.根据产品代码获取产品信息
PageData RedisManager.findProduct(String prdname)
- 根据字典编码和字典值获取字典信息
Dictionaries RedisManager.findDictoryByVal(String dicName, String value)
G.根据法人机构码获取营业时间
PageData RedisManager.findBussiDate(String bankCode)
返回PageData 中的信息为:
| 英文字段 | 中文解释 | 存储类型 |
|---|---|---|
| legalOrgCode | 法人机构 | String |
| dataDate | 营业日期 | String |
| batchDate | 批量日期 | String |
列表字典翻译
在所有要翻译的controller类中,在查询前加入如下代码:
PageData pd = new PageData();//字典DTDMap dicMap = new DTDMap();//反显列值为字典 begindicMap.put("字段","字典编码"); //反显列时翻译某列为字典值pd.put(DICMAP,dicMap);// 反显列值为字典 end//用戶DTDMap userMap = new DTDMap();//反显列值为字典 beginuserMap .put("字段",”userCode”); //反显列时翻译某列为字典值pd.put(USRMAP,userMap );// 反显列值为字典 end//机构DTDMap orgMap = new DTDMap();//反显列值为字典 beginorgMap .put("字段",”orgCode”); //反显列时翻译某列为字典值pd.put(ORGMAP,orgMap );// 反显列值为字典 end//产品DTDMap prdMap = new DTDMap();//反显列值为字典 beginprdMap .put("字段",”productCode”); //反显列时翻译某列为字典值pd.put(PRDMAP,prdMap );// 反显列值为字典 end
在返回的查询pd中会多出一个字段为:”字段”dc的字段,其值就是翻译后的中文。
比如传入的是dicMap.put("CERTYPE","cerTypeCode");返回列表中的翻译字段为:certypedc
他就是翻译后的字段。
注意:CERTYPE 为数据库字段,大写带下划线的字段,翻译后字段为驼峰+dc
翻译前,检查一下项目core层的mybatis/config.xml 中DictoryPlugin类是否存在,如下:
<plugin interceptor="com.git.easyloan.commons.plugin.DictoryPlugin"><property name="properties" value="property-key=property-value"/></plugin>
若不存在,请替换。
业务编号生成
如果是在service层调用,请引入BusiNoCreateService,
如果是在core层调用,请引入BusiNoCreateCore
/*** 集团编号*/public String getCorporID(String InterBranch) throws Exception;/*** 联保编号** @param InterBranch* @return* @throws Exception*/public String getJoinyID(String InterBranch) throws Exception;/*** 合同、从合同编号** @param InterBranch* @param type* @return* @throws Exception*/public String getContractID(String InterBranch, String type) throws Exception;/*** 资产保全子系统业务编号*/public String getBadassetID(String InterBranch) throws Exception;/*** 出账编号*/public String getChargeOffID(String InterBranch) throws Exception;/*** 借据编号*/public String getIOUID(String InterBranch) throws Exception;/*** 档案编号*/public String getArchivesID(String InterBranch) throws Exception;/*** 放款通知书编号*/public String getLoanID(String InterBranch) throws Exception;/*** 卷号** @return* @throws java.lang.Exception*/public String getReelNumID(String InterBranch) throws Exception;/*** 客户编号** @return*/public String getCustomerId(String InterBranch, String busiType) throws Exception;/*** 经济依存客户编号* @param InterBranch* @return* @throws Exception*/public String getDependenceID(String InterBranch) throws Exception;/*** 授信批复编号** @param InterBranch* @return* @throws Exception*/public String geCreditGrantedID(String InterBranch) throws Exception;/*** 用信批复编号* @param InterBranch* @return* @throws Exception*/public String getLoanGrantedId(String InterBranch) throws Exception;/*** 获得保证金开户通知表编号** @param InterBranch* @return* @throws Exception*/public String getGuaranteeNote(String InterBranch) throws Exception;/*** 获得虚拟支票编号** @param InterBranch* @return* @throws Exception*/public String getVirtualPaperTicket(String InterBranch) throws Exception;
4.0事务调用
在service层的方法上加上@Transactional(rollbackFor = Exception.class)即可
maven依赖解决
新更新项目
每个项目都会依赖“子项目名称-config-1.0.0-SNAPSHOT.jar”,”子项目名称-dependencies-1.0.0-SNAPSHOT.jar”。在第一次下载项目的时候,本地资源库中没有这两个包所以会报错。以system项目为例子
以上两个包是在实体层引入的,如上图,依赖包就已存在,若是包名下有红线且在本地资源库中找不到以上资源包,则是未下载成功。
解决办法如下:
- 找到根项目下的pom.xml,如下图:
在pom.xml文件中找到如下代码
<parent><groupId>com.git.easyloan</groupId><artifactId>easyloan-parent</artifactId><version>2.0.0-SNAPSHOT</version><relativePath/></parent>
把上面这段代码先注释掉,也可以剪贴到别处。
- 拷贝如下代码
<properties><repository.url>http://XXXX:XXXX/nexus/repository/maven-public/</repository.url></properties><!-- 配置依赖包资源库 --><repositories><repository><id>nexus</id><name>Team Nexus Repository</name><url>${repository.url}</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository></repositories>
覆盖如下代码:
<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
保存后,会看到资源包在下载。
- 资源包下载完成后,恢复或是粘贴回如下代码
<parent><groupId>com.git.easyloan</groupId><artifactId>easyloan-parent</artifactId><version>2.0.0-SNAPSHOT</version><relativePath/></parent>
保存后,还会继续下载
- 都下在完毕后查看idea右侧maven工具中,上述的两个资源包是否依赖上,不报红色。
- 若还报红色,且本地资源库里包已存在,则关闭idea,重新打开即可。
开发注意事项
菜单
1、所有父级菜单的菜单地址不允许添加。
批量规范
目录规范:
批量的目录为batch目录,以公共管理子系统为例:
在com.git.easyloan.system.controller下面建立batch目录,根据实际需求,可以在再建立子目录,application和core同理。
代码规范
批量分为单任务处理和多任务并发处理两种
单任务处理如下:
@JobHandler(value = "interestRateJobHandler")@Componentpublic class InterestRateJobHandler extends IJobHandler {private static Log logger = LogFactory.get();/*** 执行日终任务** @param param* @return* @throws Exception*/@Overridepublic ReturnT<String> execute(String param) throws Exception {logger.info("利率日终状态批量修改......Start");EasyJobLogger.log("利率日终状态批量修改......Start");Map<String, Object> params = JSONUtil.toMap(param);InterestRateService interestRateService = (InterestRateService) ApplicationContextHolder.getServiceBean("interestRateService");PageData pd = new PageData();pd.put("bankCode", "01121");pd.put("orgCode", params.get("orgCode"));//查询需要批量处理的数据// add begin by luokai @data:2020/5/26 更改日期取值,取批量时下一营业日期pd.put("validtDate", params.get("nextDate"));// add end by luokai @data:2020/5/26 更改日期取值,取批量时下一营业日期try {interestRateService.interestRate(pd);} catch (Exception e) {String errJson = ExceptionUtil.stacktraceToString(e);logger.error("利率日终状态批量修改失败:{}",errJson);EasyJobLogger.log("利率日终状态批量修改":{}",errJson);throw new Exception(e);}logger.info("利率日终状态批量修改......End");EasyJobLogger.log("利率日终状态批量修改......End");return SUCCESS;}}
多任务并发处理
@JobHandler(value="creditBatchFintingDemoJobHandler")@Componentpublic class CreditBatchFintingDemoJobHandler extends IJobHandlerprivate static Log logger = LogFactory.get();@Autowiredprivate TbCreditLimitItemsService tbCreditLimitItemsService;/*** 授信到期失效* @param param* @return* @throws Exception*/@Overridepublic ReturnT<String> execute(String param) throws Exception {EasyJobLogger.log("creditBatchFintingDemoJobHandler-授信到期失效-begin");Map<String,Object> params = JSONUtil.toMap(param);Map map = JSONUtil.toMap(param);//批量日期String batchDate = (String) map.get("batchDate");//法人机构String bankCode = (String) map.get("bankCode");String jobId = (String) map.get("jobId");EasyJobLogger.log("creditBatchFintingDemoJobHandler-授信到期失效-batchDate:"+batchDate+",bankCode:"+bankCode);PageData pd = new PageData();pd.put("bankCode",bankCode);//add begin by qianshengli @date:20200604 修改传值pd.put("batchDate",batchDate);// add end by qianshengli 20200604PageData pp = tbCreditLimitItemsService.updateLimitFinting(pd);List<PageData> list =(List<PageData>) pp.get("list");if(null==list || list.size()==0){return SUCCESS;}//待处理数据集合List<WorkTask> dataList = new ArrayList();for (PageData pageData:list) {WorkTask task = new CreditBatchFintingWorkTaskImpl(JSONUtil.toJsonStr(pageData));//更新方法TaskBean taskBean = new TaskBean();taskBean.setTaskId("task_"+pageData.getString("orgNum")+ KeyGenerator.get32UUID());taskBean.setUuid(pageData.getString("orgNum")+ KeyGenerator.get32UUID());//放款机构taskBean.setTaskName("测试任务-"+pageData.getString("orgNum"));taskBean.setTaskNo(jobId);taskBean.setRcvDate(batchDate);taskBean.setPrvCod(bankCode);task.setTaskBean(taskBean);task.setTaskThreadKey("BATCH");dataList.add(task);}//把任务装入任务线程队列中pool.getTaskManager().getTaskQueue(dataList,"BATCH");//轮询判断任务是否正常完成while(true){int taskCount = pool.getTaskManager().getTaskCountQueueSize(jobId);int threadCount = 0;Vector<WorkThread> threadList = pool.getThredlist("BATCH");for (WorkThread workThread:threadList) {if("BATCH".equals(workThread.getThreadKey()) && WorkThread.RUNSTATE.equals(workThread.getMyState()) && jobId.equals(workThread.getInfo())){threadCount ++;}else{continue;}}EasyJobLogger.log("剩余任务数【{}】,正在执行任务线程数【{}】!",taskCount,threadCount);if(taskCount == 0 && threadCount == 0){EasyJobLogger.log("UpdateRatingCdbyBankCodeJobHandler-授信台账到期失效-end");return SUCCESS;}//等待指定时间后重新检查任务是否完Thread.sleep(Integer.parseInt(map.get("poolTime")+""));}}
public class CreditBatchFintingWorkTaskImpl implements WorkTask {private Log logger = LogFactory.get();protected String param;protected Object threadkey;protected TaskBean taskBean;public CreditBatchFintingWorkTaskImpl(String param) {this.param = param;}/*** 重算任务更新操作* @throws Exception*/@Overridepublic void execute() throws Exception {TbCreditLimitItemsService tbCreditLimitItemsService = (TbCreditLimitItemsService) ApplicationContextHolder.getServiceBean("tbCreditLimitItemsServiceImpl");logger.info("需要更新的机构:"+taskBean.getUuid());logger.info("法人机构:"+taskBean.getPrvCod());logger.info("日终日期:"+taskBean.getRcvDate());long startTime = System.currentTimeMillis();String bankCode = taskBean.getPrvCod();PageData pageData = new PageData();pageData.put("orgNum",taskBean.getUuid());pageData.put("bankCode",taskBean.getPrvCod());pageData.put("batchDate",taskBean.getRcvDate());PageData taskData = new PageData();taskData.putAll(JSONUtil.toMap(param));pageData.put("taskData", taskData);tbCreditLimitItemsService.updateCreditInfo(pageData);logger.info("授信到期更新失效批量处理完成,任务数量{},耗时{}秒",taskBean.getUuid(),((System.currentTimeMillis()-startTime)/1000));}public void setTaskThreadKey(Object key) {this.threadkey = key;}public String toString() {return this.param + "工作线程编号" + this.threadkey.toString();}public Object getTaskThreadKey() {return this.threadkey;}public TaskBean getTaskBean() {return this.taskBean;}public void setTaskBean(TaskBean taskBean) {this.taskBean = taskBean;}}
注意事项:
- 批量任务编码时,对于异常处理,如果已经有提交的数据,注意考虑提交数据的删除时机;日志打印时,注意不要在循环里打印日志,否则会出现大量价值不大的日志,增大查找问题的困难度,打印日志时,要在出现错误时,详细打印,其他情况记录关键数据即可。
- 批量任务要保持幂等性,即重复执行效果一致。
- 数据执行变更或删除前,做好备份,保留1-2天的数据,方便回滚或重复执行时回退。
- 批量处理文件时,要注意文件是否完整的接收完成,注意线程数量和事务提交的时机。
子系统间数据交互码值翻译方案
为了解决前端的客户名称、机构名称等名称的翻译显示问题。上述名称在后台数据库中均以ID形式保存,但在前端展示过程中需要显示实际的名称。这需要一个翻译的过程。下面针对不同的翻译内容,提供了不同的方案。
主表冗余方案
- 各子系统的数据模型设计中,在子系统主表中增加客户名称字段,各子系统在查询时直接展示即可。
- 客户模块中对客户名称进行增删改操作之后,将变化内容放到“待更新队列”中。
- 公共模块通过定时任务达到准实时性,将“待更新队列”中的内容通过调用服务的方式将需要更新的数据同步到所需的子系统数据库主表之中,需要各子系统对客户的信息修改提供更新服务,供公共模块进行调用。
- 日终模块通过同步任务,将日间公共模块的定时任务处理失败的任务进行处理,达到最终一致性。
Redis缓存方案
redis的使用
本平台缓存使用redis缓存
使用方式主要为两种
- 直接调用RedisManager类
例如:
根据字典码值获取字典对象:
(Dictionaries)RedisManager.findDictory(String dicName) ;
参照公共参数使用中的redis部分。
获取页面框架信息:RedisManager.findModuleMenu(String key)
获取所有机构营业日期:RedisManager.getBussiDateAll()
获取产品信息:RedisManager.findProduct(String prdname)
根据字典代码和值获取中文翻译 :RedisManager.findDictoryByVal(String dicName, String value)
获取用户信息:RedisManager.findUserInfo(String userCode)
获取机构信息:RedisManager.findOrgInfo(String orgCode)
获取法人营业日期:RedisManager.findBussiDate(String bankCode)
获取登陆JWTuser对象:RedisManager.getUserById(String id)
获取当前登陆用户信息:RedisManager.getLoginUser()
获取批量日期:RedisManager.getBatchDate(String legOrgCd)
- 使用spring支持的缓存注解@CachePut,@Cacheable,@CacheEvict
删除redis中的key,注农信版本没有
@CacheEvict(value = "sysParams", key = "'moduleTree-'.concat(#moduleCd)")public void delete(PageData pd,String moduleCd) throws Exception {dao.delete("TbPubModuleMenusMapper.delete", pd);}
修改redis中的key
@CachePut(value = "sysParams", key = "'moduleTree-'.concat(#moduleCd)", unless = "#result == null")public PageData edit(PageData pd,String moduleCd) throws Exception {dao.update("TbPubModuleMenusMapper.edit", pd);PageData list = (PageData) dao.findForObject("TbPubModuleMenusMapper.findById", pd);List<PageData> infoList = (List<PageData>) dao.findForList("TbPubModuleMenusInfoMapper.listByModuleCd", list.getString("moduleCd"));list.put("menuInfo", infoList);return list;}
新增redis中的key
@Override@CachePut(value = "sysParams", key = "'moduleTree-'.concat(#moduleCd)", unless = "#result == null")public PageData save(PageData pd,String moduleCd) throws Exception {dao.save("TbPubModuleMenusMapper.save", pd);return pd;}
查询redis中的key
@Cacheable(value = "sysParams", key = "'moduleTree-'.concat(#key)", unless = "#result == null")public PageData findModuleMenu(String key) {PageData rePd = new PageData();DTDMap quMap = new DTDMap();String url = "";if (serverName != null) {url = "http://" + serverName + "/tbPubModuleMenus/findModuleMenusList";} else if (gatewayUrl != null) {url = gatewayUrl + "/tbPubModuleMenus/findModuleMenusList";} else {log.error("初始化页面框架信息为空,初始化失败,公共子系统url未配置!");}quMap.put("moduleCode", key);ResponseEntity responseEntity = (ResponseEntity) RestTemplateManager.SendBodyByRestTemplateGet(quMap, url);rePd.putAll((Map) responseEntity.getBody());return rePd;}
@CachePut: 强制修改reids中的key值,每次都会执行调用此标签锁含方法。
@Cacheable:
@CacheEvict:默认是在所包含方法执行完成之后执行清楚redis的操作,如果想在调用方法之前就清除redis,则增加beforeInvocation=true属性
- 后台服务在Controller中,按照翻译字典的方式,将ID翻译为名称进行显示。
- 如果没有读取成功,则从数据库中直接读取,并写入Redis数据库。
- 对相关码表设计的内容进行增删改的操作成功后,删除redis数据库中的相关内容,并将新的内容写入到redis库中。
适用范围:
| 翻译内容 | 转换码值 | 备注 |
|---|---|---|
| 机构 | orgCode | |
| 柜员 | userCode | |
| 产品 | productCode |
子系统实时查询方案
- 子系统间数据交互码值翻译时,通过FeignClient的方式进行调用并翻译。
- 调用时注意不要在循环里面调用,要在循环外面调用一次,再进行组装。
子系统间调用
子系统间调用分为feignClient调用和RestTemplate调用
feignClient调用
@FeignClient(value = "${server.name.aplus}", url = "${gateway.url.aplus}", configuration = FeignCommonConfig.class, fallback = FeingHystrix.class)public interface SdpFeignClient {/*** 重置DAC** @param pd* @return PageData*/@PostMapping(value="/dac/reset", produces = MediaType.APPLICATION_JSON_VALUE)public ResponseEntity<Responder> reset(@RequestBody@NotBlank(message = "重置DAC参数不能为空")@ApiParam(name = "pd", value = "重置DAC的参数信息", required = true)PageData pd) throws Exception;}
RestTemplate调用
在代码中调用如下:
String url=“http://Ip:port/XXXXXX”(或是http://easyloan-xxx/XXXXX)ResponseEntity responseEntity=RestTemplateManager.SendBodyRestTemplatePost(param,url);
或是
ResponseEntity responseEntity=RestTemplateManager.SendBodyRestTemplateGet(param,url);
或是
ResponseEntity responseEntity=RestTemplateManager.SendBodyRestTemplatePatch(param,url);
ESB交易方案
配置说明
通过前台界面配置交易信息,说明如下:
接口配置主表信息配置:
- 前台菜单地址:参数管理->流程配置->接口配置主表
- 界面如下:


说明:
- 服务方标志:服务方系统,比如BPMS-流程引擎,CBBS-二代核心等,服务标志分为外围系统作为服务方的和二代信贷作为服务方的系统,二代信贷的服务方命名规范为:子系统简称+编号(8位)(例如CSM00001)
- 二代信贷服务方标识参考接口Excel文档。
- 请求地址前缀:服务方地址前缀,比如:二代核心:http://10.5.31.22:7800/,如果二代信贷为服务方,则配置类似服务前缀:http://EASYLOAN-SYSTEM/
接口配置服务详情配置
- 前台菜单地址:参数管理->流程配置->接口配置服务详情
- 界面如下:


说明:
- 交易代码:如果二代信贷为消费方,则按照接口清单Excel中的交易代码填写即可。如果作为服务方的,则根据接口配置主表的服务方标志对应的服务排序即可:例如:SCMS-PUB-0001
- 服务ID:如果二代信贷为消费方,则按照接口清单Excel中的服务ID填写。如果作为服务方的,则同交易代码一致即可。
- 模型版本:如果二代信贷为消费方,则按照接口清单Excel中的交易代码填写即可。如果作为服务方的,则填写1.0.
- 请求系统编号:如果二代信贷为消费方,则填写SCMS。如果作为服务方的,则为空。.
- 请求方式:POST/GET,如果二代信贷为消费方,则按照接口清单Excel中的请求方式填写即可。如果作为服务方的,则根据实际情况填写。
- URL:如果二代信贷为消费方,则按照接口清单Excel中的URL填写即可。如果作为服务方的,则填写具体的服务名称。
- 请求参数:根据实际URL抽离参数部分,比如:url为/runtime/{taskId},则请求参数为taskId。
- 返回类型:默认情况填写Object即可,返回二进制情况填写Byte
- 服务方标志:返显
- 接口说明:接口说明中文描述
本系统为服务方
- 服务编写:服务编写方式同普通的RestController一致,编写完成后配置到配置表中即可。
- 获取公共数据内容及方式:
// data为接收的参数:PageData pdPageData httpHead = (PageData)pd.get("Http_Head");//全局交易流水号httpHead.get("GLB_SEQ_NO");//服务请求者身份 操作用户的登录 ID,包括机器虚拟柜员 如果是多次交易一次提交发送的,则使用生成流水号时的操作用户 ID。httpHead.get("USER_ID");//发送方机构ID 取自统一机构管理(目前在核心管理)中的机构编号 如果是多次交易一次提交发送的,则使用生成流水号时的机构号。httpHead.get("BRANCH_ID");//授权柜员 NhttpHead.get("AUTH_TELLER");//复核柜员 NhttpHead.get("RECHECK_TELLER");//本系统编号httpHead.get("SOURCE_SYSID");//交易日期 交易发起日期字符串,YYYYMMDD,取自然日期 在整个交易流转中保持不变httpHead.get("SYS_DATE");//交易时间httpHead.get("SYS_TIMESTAMP");
- 其他数据按照自己定义的方式获取。
- 接口服务端编写注意事项:
请求方式为:POST/GET
参数定义方式有两种:
1:@PathVariable定义,通过URL传参数,例如:http://XXX/findById/123
2:@RequestBody定义,通过JSON传参数,例如:http://XXX/findUser 参数为JSON:{"userName":"aaa","orgCd":"123","userId":"200066"}
PS:这两种定义的方式可以组合使用,GET请求时,@RequestBody定义的参数,为简单类型,不要有数组和对象等嵌套。
建议大家对于参数较多(3个以上)的情况,请求方式使用:POST,接收参数为@RequestBody!
对于处理数据时,使用try{}catch{}进行处理,所有服务端都要返回报文,不管成功还是失败!参考示例如下:
PageData responseData = new PageData();try {// 处理业务逻辑// 成功后返回 responseData.put(MessageEnum.CALL_BACK_RETCODE.getCode(),MessageEnum.CALL_BACK_RETCODE_SUCCESS.getCode());responseData.put(MessageEnum.CALL_BACK_RETMSG.getCode(),MessageEnum.CALL_BACK_RETCODE_SUCCESS.getMessage());} catch (Exception e) {e.printStackTrace();//失败后返回 responseData.put(MessageEnum.CALL_BACK_RETCODE.getCode(),MessageEnum.CALL_BACK_RETCODE_ERROR.getCode());responseData.put(MessageEnum.CALL_BACK_RETMSG.getCode(),MessageEnum.CALL_BACK_RETCODE_ERROR.getMessage() + e.getMessage());}return responseData;
- 模拟ESB联调:
服务接口开发完成之后,先通过YAPI测试自己的服务,就同普通的服务测试方法一样,测试完成后,提交代码,发布到开发联调服务器进行和ESB联调测试,联调测试前,我们可以模拟ESB发送报文,测试我们发布的服务是否存在问题,按照我们在接口文档中提供的报文规范(YAPI中的测试报文也可以),在YAPI中进行模拟测试,发送报文的地址:http://10.5.102.69:8888/api/v1/system/esbService/accept/{服务编码}。
本系统为消费方
- 配置:在配置表中配置服务方的信息,见配置说明。
- 客户端编写:在application中编写FeignClient,如下:
package com.git.easyloan.system.service.feignclient;import io.swagger.annotations.ApiOperation;import io.swagger.annotations.ApiParam;import javacommon.coreframe.util.PageData;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import javax.validation.constraints.NotEmpty;@FeignClient(name ="easyloan-system", configuration = FeignCommonConfig.class)public interface EsbFeignClient {/*** 调用ESB服务* @param serviceCode* @param pd* @return PageData* @throws Exception*/@PostMapping("/esbService/{serviceCode}")@ApiOperation(value ="调用ESB服务",notes ="调用ESB服务")PageData sendToEsb(@PathVariable(value ="serviceCode")@NotEmpty(message ="服务编号不能为空")@ApiParam(name ="serviceCode", value ="服务编号", required = true)String serviceCode,@RequestBody@ApiParam(name ="pd", value ="服务报文参数", required = true)PageData pd) throws Exception;}
说明:服务编号为配置表中服务编号。
- 调用及验证方式:
PageData pd = new PageData();// pd.put(“taskId”,”1111”);// pd.put(“startTime”,””);// 如果需要在http头和自定义头中增加数据,配置方式如下:Map httpHeaderData = new HashMap();pd.put("httpHeaderData",httpHeaderData);httpHeaderData.put("**glbSeqNo**",SeqNumUtil._generatetGlbSeqNo_());httpHeaderData.put("userId",pd.getString("XXX"));httpHeaderData.put("branchId",pd.getString("XXX"));Map sysHeadData = new HashMap();pd.put("sysHeadData",sysHeadData);sysHeadData.put("reconcilDate",pd.getString("XXX"));PageData respData = esbFeignClient.sendToEsb(“服务编号”, pd);/*** 验证返回结果是否正常*/ESB.transferEsbMessage(respData, null);
说明:根据服务编号调用ESB服务,服务编号请联系各组长配置到easyloan-commons的枚举中,一个服务配置一次即可,其他组直接使用。
流水号及业务编号
~流水号(已过时)~
和ESB通信的流水号包括全局流水号和系统间流水号,规则如下:
全局流水号:日期(6 位)+ 系统编号(4 位)+ 顺序号(10 位)
系统间流水号:日期(6 位)+ 本系统的系统编号(4 位)+ 被调用系统的系统编号(4 位)+顺序号(8 位)
系统生成流水号方法如下:
全局流水号:String
glbSeqNo = SeqNumUtil._generatetGlbSeqNo_();
系统间流水号:String
sysSeqNo = SeqNumUtil._generatetSysSeqNo_();
流水号
和ESB通信的流水号包括全局流水号和系统间流水号,规则如下:
全局流水号:日期(6 位)+ 系统编号(4 位)+ 顺序号(10 位)
系统间流水号:日期(6 位)+ 本系统的系统编号(4 位)+ 被调用系统的系统编号(4 位)+顺序号(8 位)
系统生成流水号方法如下:
//在Controller、Service中注入流水号生产管理器@ResourceSeqNumManager seqNumManager;//----------------------------使用方式----------------------// 全局流水号seqNumManager.generatetGlbSeqNo();// 系统间流水号seqNumManager.generatetSysSeqNo();
删除,修改
如果系统中做删除或是修改方法的时候报:“未找到修改的数据!”或是“未找到删除的数据!”的错误。
这类错误是因为修改、删除的sql,执行返回为0条,因此需要修改sql。
查看sql,truncNo是否传值了,传的是否正确。执行成功sql后,truncNo会自增+1,所以再次修改、删除操作时,需要在查询一次获取最新的truncNo。确保执行sql返回不为0即可。
取上上级机构
调用方法:
maven更新最新的easyloan-commons.jar
使用(PageData) easyloanUtils.findUpOrg(int level, String orgCode)
参数说明:
level:为整型,代表要查询的层级,比如level为2 代表的是上上级机构。以此类推。
orgCode:为当前机构。
返回是一个机构的PageData,内容同机构表数据字段。
取柜面虚拟用户
调用方法:
maven更新最新的easyloan-commons.jar
使用String findExternalBranch(String orgCode)
参数说明:
orgCode:用户当前登陆机构
返回虚拟柜员id
注意:在子系统间调用核心的,orgCode一定要送
Web前端
开发环境
NodeJs下载 https://nodejs.org/en/ 安装步骤直接下一步即可

查看自己是否安装成功
- 打开cmd 或者 shift+鼠标右键调出PowerShell
- 输入 node -v 就可查看是否安装成功

开发工具配置
Vscode 的安装 及其 相关插件的配置

- 直接下一步安装即可





常用操作
一些基础的菜单和操作

如果想要搜索文件 需要ctrl + p调出文件搜索框进行搜索

运行终端

常用插件配置


vue项目常用的插件有如下
主要用于汉化vscode
Chinese (Simplified) Language Pack for Visual Studio Code
主要用于语法校验
ESLint
主要用于代码格式化
Prettier - Code formatter
主要用于辅助代码格式化
Vetur
主要用于vue代码高亮
VueHelper
如果有需要安装其他插件,请查看具体的相关文档自行安装和使用

有些插件需要我们自己去单独配置json文件 根据自己需要查看相关文档配置即可
我们vue项目只配置eslint插件的自动修复 配置如下


代码如下
{"eslint.enable":true,"eslint.autoFixOnSave":true,"eslint.run":"onType","eslint.options":{"extensions":[".js",".vue"]},"eslint.validate":["javascript",{"language":"vue","autoFix":true},"html","vue"],"git.ignoreMissingGitWarning":true,"editor.codeActionsOnSave":{"source.fixAll.eslint":true},"[javascript]":{"editor.defaultFormatter":"esbenp.prettier-vscode"},"eslint.codeAction.disableRuleComment":{},}
以上配置好之后 我们检查配置有没有成功 我们的项目已经设置好了代码风格校验 如果我们的编写的代码不符合我们项目设置的代码风格时 就会出现红色波浪线 如下

由于我们上面配置过了自动修复 所以我们点击ctrl + s保存之后他就会自己格式化成 符合我们项目设置的代码风格

如果没有配置成功 请重新按照配置流程在走一次
前端工程运行
- 首先 git clone 下来前端工程
- 进入工程目录 执行 npm install 安装前端开发相关依赖
- 项目工程根目录 打开 pwoershell 输入 npm run dev 启动项目
前端架构及其技术栈
前端架构图
- 主要采用MVVM模式

- 主要技术栈 Vue.js 、Vue-Router.js 、Vuex 、Echarts.js 、ElementUI 、EUUI 、Axios
前端代码开发规范
eu- 标签
- eu- 标签 在我们的el- 标签上的基础上拓展出来的
- 具体的一些组件 和 使用方法 详见文档 https://element.eleme.cn/#/zh-CN/component/installation**
说明
1.在前端工程目录里 我们一个文件夹就先当与一个页面 2.这个文件夹里包含 暂时包含六个部分 如下图所示 3.针对于增删改查 四个方向 把每个动作 都拆分成对应的文件 如 增 我们就在新增的弹框文件里完成我们的业务逻辑 改 就在我们的编辑的弹框文件里 完成我们的业务逻辑 查 就在我们的查看弹框里 完成我们的业务逻辑
页面效果展示
页面布局文件 index.vue
1.需要引入我们add,edit,view,searchForm,文件夹下的文件 2.主要用来布局 3.可以查询列表 和 展示列表 4.当点击新增 查看 编辑的功能按钮时 需要调取 引入的对应的弹框 5.详细代码见 src\renderer\views\demo\base1\index.vue
//首先需要在 script 标签下 引入我们对应的文件 并注册// 引入搜索表单 searchForm 文件夹下的index.vue<script>// 引入搜索表单 searchForm 文件夹下的index.vueimport euSearchForm from './searchForm/index.vue'// 引入搜索表单的配置项 searchForm 文件夹下的form.jsimport { formOption } from './searchform/form.js'// 获取列表数据接口import { queryList, delObj } from '@/api/demotwo/index.js'import { getAllDic } from '@/utils/setAllDic'// 引入弹框 新增 编辑 查看import euAdd from './add'import euEdit from './edit'import euView from './view'//需要把我们引入的上述文件 注册成标签形式components: {euSearchForm,euAdd,euEdit,euView},</script><!-- 注册完毕之后 我们就可以用标签的形式 在我们的template模板上以标签的形式使用 --><template><eu-card><div slot="header"><span>前端开发规范base</span></div><!--这是引入了searchForm 下的index.vue文件 主要负责搜索列表 通过组件标签的形式写入 start--><div><eu-search-form :searchform="searchform" :option="option" :span='6'><eu-button type="primary" @click="searchList" size="mini" icon="el-icon-search" :loading="searchBtnLoading">查 询</eu-button><eu-button type="primary" @click="clearSearchForm" size="mini" icon="el-icon-delete">清 空</eu-button></eu-search-form></div><!--这是引入了searchForm 下的index.vue文件 主要负责搜索列表 通过组件标签的形式写入 end--><!--这是eu-table列表组件 通过组件标签的形式写入 主要负责列表展示 start--><div><eu-button icon='el-icon-plus' type='success' size='mini' style='margin-bottom:20px;' @click='add'>新增模型</eu-button><eu-tablev-loading='tableloading'size='mini'stripe:data='tableData':header-cell-style="{'text-align':'','color':'#303133','background':'#FAFAFA'}":cell-style="{'text-align':'','color':'#303133'}"style=': 100%'><eu-table-column prop='modelCode' label='模型代码'></eu-table-column><eu-table-column prop='modelName' label='模型名称'></eu-table-column><eu-table-column prop='blCusType' label='模型类型' :formatter="typeFormatter"></eu-table-column><eu-table-column prop='enableFlag' label='生效标志' :formatter="flagFormatter"></eu-table-column><eu-table-column prop='bmversion' label='模型版本号'></eu-table-column><eu-table-column label='操作'><template slot-scope='scope'><eu-button type='text' icon='el-icon-view' size='small' @click='view(scope.index,scope.row)'>查看</eu-button><eu-button type='text' icon='el-icon-edit-outline' size='small' @click='edit(scope.index,scope.row)'>编辑</eu-button><eu-button type='text' icon='el-icon-delete' size='small' @click='del(scope.index,scope.row)'>删除</eu-button></template></eu-table-column></eu-table></div><!--这是eu-table 列表组件 通过组件标签的形式写入 主要负责列表展示 end--><!--这是eu-pagination 分页组件 通过组件标签的形式写入 主要负责分页 start--><div><eu-paginationbackgroundlayout='total, sizes, prev, pager, next, jumper':total='total'@current-change='currentPage'@size-change='sizePage':current-page.sync="page.currentPage"></eu-pagination></div><!--这是eu-pagination 分页组件 通过组件标签的形式写入 主要负责分页 end--><!--这是引入了add 下的index.vue文件 通过组件标签的形式写入 主要负责新增的弹框 start--><!--这里要绑定ref 点击新增弹框的时候 打开对应的弹框--><eu-add ref="add" @initlist="getTableList"></eu-add><!--这是引入了add 下的index.vue文件 通过组件标签的形式写入 主要负责新增的弹框 end--><!--这是引入了edit 下的index.vue文件 通过组件标签的形式写入 主要负责编辑的弹框 start--><eu-edit ref="edit" @initlist="getTableList"></eu-edit><!--这是引入了edit 下的index.vue文件 通过组件标签的形式写入 主要负责编辑的弹框 end--><!--这是引入了view 下的index.vue文件 通过组件标签的形式写入 主要负责查看的弹框 start--><eu-view ref="view"></eu-view><!--这是引入了view 下的index.vue文件 通过组件标签的形式写入 主要负责查看的弹框 end--></eu-card></template>//组件依赖的一些数据和方法<script>// 引入搜索表单 searchForm 文件夹下的index.vueimport euSearchForm from './searchForm/index.vue'// 引入搜索表单的配置项 searchForm 文件夹下的form.jsimport { formOption } from './searchform/form.js'// 获取列表数据接口import { queryList, delObj } from '@/api/demotwo/index.js'import { getAllDic } from '@/utils/setAllDic'// 引入弹框 新增 编辑 查看import euAdd from './add'import euEdit from './edit'import euView from './view'export default {//这里一定要与路由表的name一致name:'前端开发规范' ,// 需要把我们上面引入的搜索表单的文件 新增 编辑 查看的文件 注册成组件 这样可以用标签的形式 写在 template里components: {euSearchForm,euAdd,euEdit,euView},created() {this.getTableList()},data() {return {// 表单组件的搜索配置项目option: formOption(),// 要搜索的表单项目searchform: {modelCode: '',modelName: '',blCusType: '',enableFlag: ''},// 控制列表的loading状态tableloading: false,// 渲染表格所以依赖的数据tableData: [],// 分页控制total: null,page: {currentPage: 1, // 当前页数pageSize: 10 // 每页显示多少条},searchBtnLoading: false}},methods: {// 点击新增的按钮的动作 打开新增的弹框add() {this.$refs.add.dialogshow()},// 点击编辑的按钮的动作 打开编辑的弹框edit(index, row) {this.$refs.edit.dialogshow(row)},// 点击查看的按钮的动作 打开查看的弹框view(index, row) {this.$refs.view.dialogshow(row)},// 点击删除按钮的动作del(index, row) {this.$confirm('确认删除该项嘛?, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(async() => {await delObj(row.uuid)this.getTableList()return}).catch(() => {})},// 点击搜索按钮的动作async searchList() {this.searchBtnLoading = truethis.page.currentPage = 1await this.getTableList(this.searchform)this.searchBtnLoading = false},// 点击清空按钮的动作clearSearchForm() {for (const key in this.searchform) {this.searchform[key] = ''}},// 获取列表数据async getTableList(params) {this.tableloading = trueconst { list, page } = await queryList(Object.assign({ pageNum: this.page.currentPage, size: this.page.pageSize }, params))this.tableData = listthis.total = page.totalResultthis.tableloading = false},// 点击分页或者下一页时触发currentPage(val) {this.tableloading = truethis.page.currentPage = valthis.getTableList()this.tableloading = true},// 点击每页显示多少条时触发sizePage(val) {this.page.currentPage = 1this.page.pageSize = valthis.tableloading = truethis.getTableList()this.tableloading = false}}}</script>
搜索组件文件 searchForm \ index .vue
- 主要用于我们在页面布局文件里 最上面的部分 展示
- 用于搜索满足我们条件的列表
- 进行了二次封装 需要 传入配置项
//使用方法 需要在我们的布局文件用引入(不局限与布局文件 如果 有别的表单文件满足使用条件 也可以使用)
<script>// 引入搜索表单 searchForm 文件夹下的index.vueimport euSearchForm from './searchForm/index.vue'// 引入搜索表单的配置项 searchForm 文件夹下的form.jsimport { formOption } from './searchform/form.js'// 需要把我们上面引入的搜索表单的文件 这样可以用标签的形式 写在 template里components: {euSearchForm,},// 在data 函数里 定义我们的配置项目 和要搜索的表单字段option: formOption(),// 要搜索的表单项目searchform: {modelCode: '',modelName: '',blCusType: '',enableFlag: ''},</script><!--在我们当前文件的 template 标签里 以标签的形式写入我们的表单搜索组件 并且传入对应的配置项 --><template><div><!--传入我们上面定义豪的配置项--><eu-search-form :searchform="searchform" :option="option" :span='6'><!--以插槽的形式 插入我们对应的功能按钮--><eu-button type="primary" @click="searchList" size="mini" icon="el-icon-search" :loading="searchBtnLoading">查 询</eu-button><eu-button type="primary" @click="clearSearchForm" size="mini" icon="el-icon-delete">清 空</eu-button></eu-search-form></div></template>
搜索组件文件 searchForm \ form.js
主要编写我们搜索表单文件的 配置项目信息
需要同我们的搜索表单文件 一起在 页面中引入
//详细参数 需要我们写入一个数组对象 每个元素 是你的的字段 和 lable
const option = [{ label: '模型代码', //用来显示 中文名称prop: 'modelCode', //需要的字段type :'input' //文本框的类型 目前有 input select data 三种 可以在自行拓展},{ label: '模型名称',prop: 'modelName'},{ label: '模型类型',prop: 'blCusType',type: 'select',dic: 'RATING_MODEL_TYPE', //当为select的时候 需要传入对应的字典名称filter: ['6', '7'], // 希望显示的字典项 数组的元素 对应选项的value值efilter: ['6', '7'], //不希望展示字典项 数组的元素 对应选项的value值filterable: true, //是否开启搜索options:[{value:'1',text:'包子'}] //静态的字典项 跟dic只能用一个:dynamic:{value:'1',text:'包子'} //需要动态加载的选项 value 不能与当前所有选项重复},{ label: '生效标志',prop: 'enableFlag',type: 'select',dic: 'MODEL_STATUS'}]//最后导出我们的配置项信息export const formOption = () => {return option}
新增弹框文件 add \ index.vue
- 当我们点击页面布局上的新增的按钮时 打开新增弹框
- 新增弹框里面展示我们的表单 需要引入 form \ index.vue
- 点击保存的时候 发送 新增请求
//首先在script标签中引入我们对应的文件<script>// 引入 mixinsdialog 主要负责新增 编辑 查看 的公共方法import mixinsdialog from '@/components/dialogmixins/mixinsdialog.js'// 引入 我们新增的时候接口import { addObj } from '@/api/rating/model/tbRatBmConfModel_form'// 引入 我们formindex.vue 表单文件import addForm from '../form'export default {name: 'add-dialog',//混入我们刚才引入的公共的方法mixins: [mixinsdialog],//注册表单文件 用标签的形式 写入template中components: {addForm},// 外部给我们传进来的属性 可以自定义props: {titletext: {type: String,default() {return '新增模型'}}},data() {return {//用来控制点击保存按钮的时候 loading状态savebtn: false}},methods: {// 点击保存的时候动作saveForm() {//首先调取我们表单文件里的 验证方法 对我们输入的表单进行验证this.$refs.dialogForm.validForm(async(valid) => {//如果验证成功 就去发送对应的新增请求接口if (valid) {//开启搜索按钮laodingthis.savebtn = true//发送新增请求 这里由于某些接口的返回结果 不太一样 根据实际业务场景自行判断const data = await addObj(this.$refs.dialogForm.form)//如果有结果 就说明新增成功if (data) {// 关闭 按钮laodingthis.savebtn = false//关闭弹框this.dialogVisible = false//刷新列表this.$emit('initlist')}}})}}}</script><!-- 新增的弹框组件 --><template><div><eu-dialog:visible.sync="dialogVisible"@close="closeDialog":fullscreen="isfullscreen":close-on-click-modal="false"@closed="closed"destroy-on-close:><div slot="title"><span>{{titletext}}</span><div @click="dialogmax"></div></div><slot></slot><add-form ref="dialogForm"></add-form><span slot="footer"><span><eu-button size="mini" type="primary" @click="saveForm" :loading="savebtn" icon="el-icon-check">保 存</eu-button><eu-button size="mini" @click="clearForm" icon="el-icon-delete">清 空</eu-button></span></span></eu-dialog></div></template>
编辑弹框文件 edit \ index.vue
- 当我们点击页面布局上的编辑的按钮时 打开编辑弹框 需要传入我们当前行的信息
- 编辑弹框里面展示我们的表单 需要引入 form \ index.vue
- 点击保存的时候 发送 更新的请求
//首先在script标签中引入 我们对应的 文件<script>// 引入 mixinsdialog 主要负责新增 编辑 查看 的公共方法import mixinsdialog from '@/components/dialogmixins/mixinsdialog.js'// 引入 我们编辑的时候接口import { patchObj } from '@/api/rating/model/tbRatBmConfModel_form'// 引入 我们form index.vue 表单文件import editForm from '../form'export default {name: 'edit-dialog',//混入我们刚才引入的公共的方法mixins: [mixinsdialog],//注册表单文件 用标签的形式 写入template中components: {editForm},// 外部给我们传进来的属性 可以自定义props: {titletext: {type: String,default() {return '编辑模型'}}},data() {return {savebtn: false}},methods: {// 点击保存的时候动作saveForm() {//首先调取我们表单文件里的 验证方法 对我们输入的表单进行验证this.$refs.dialogForm.validForm(async(valid) => {//如果验证成功 就去发送对应的新增请求接口if (valid) {//开启搜索按钮laodingthis.savebtn = trueconst form = JSON.parse(JSON.stringify(this.$refs.dialogForm.form))form.blCusType = [form.blCusType]//发送编辑请求 这里由于某些接口的返回结果 不太一样 根据实际业务场景自行判断const data = await patchObj(form)if (!data) {// 关闭 按钮laodingthis.savebtn = false// 关闭 按钮弹框this.dialogVisible = false// 通知列表更新this.$emit('initlist')}}})}}}</script><!-- 编辑的弹框组件 --><template><div><eu-dialog:visible.sync="dialogVisible"@close="closeDialog":fullscreen="isfullscreen":close-on-click-modal="false"@closed="closed"destroy-on-close:><div slot="title"><span>{{titletext}}</span><div @click="dialogmax"></div></div><slot></slot><edit-form ref="dialogForm"></edit-form><span slot="footer"><span><eu-button size="mini" type="primary" @click="saveForm" :loading="savebtn" icon="el-icon-check">保 存</eu-button><eu-button size="mini" @click="clearForm" icon="el-icon-delete">清 空</eu-button></span></span></eu-dialog></div></template>
查看弹框文件 view \ index.vue
- 当我们点击页面布局上的编辑的按钮时 打开查看弹框 需要传入我们当前行的信息
- 查看弹框里面展示我们的表单 需要引入 form \ index.vue 并且给他传入disabled 属性 让表单文件不可编辑
//首先在script标签中引入 我们对应的 文件<script>// 引入 mixinsdialog 主要负责新增 编辑 查看 的公共方法import mixinsdialog from '@/components/dialogmixins/mixinsdialog.js'// 引入 我们formindex.vue 表单文件import viewForm from '../form'export default {//混入我们刚才引入的公共的方法name: 'view-dialog',mixins: [mixinsdialog],//注册表单文件 用标签的形式 写入template中components: {viewForm},// 外部给我们传进来的属性 可以自定义props: {titletext: {type: String,default() {return '查看模型'}}}}</script>
表单文件 form \ index.vue
- 主要用于展示 新增弹框 编辑弹框 和查看弹框的 表单详情
- 当点击编辑和查看的时候 接收当前行信息 做字段验证 验证成功之后 并去 发送请求 获取表单 详情
//在script 标签中 引入我对应需要的文件<script>//引入对应获取表单详情的接口import { getObj } from '@/api/rating/model/tbRatBmConfModel_form'export default {//外部给我们传进来的属性 这里只增加了disabled 不可编辑 可以根据实际业务场景 自定义props: {disabled: {type: Boolean,default() {return false}}},data() {return {//表单的验证规则 详细语法 参照 https://element.eleme.cn/#/zh-CN/component/layout 表单组件rules: {modelCode: [{ required: true, message: '请输入模型代码', trigger: 'blur' }],modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],blCusType: [{ required: true, message: '请选择模型类型', trigger: 'blur' }],bmversion: [{ required: true, message: '请选择模型版本号', trigger: 'blur' }],enableFlag: [{ required: true, message: '请开启生效标志', trigger: 'blur' }]},//定义表单需要的字段form: {modelCode: '',modelName: '',blCusType: '',enableFlag: '',bmversion: '',uuid: '',bankCode: '27101'},options: JSON.parse(localStorage.getItem('RATING_MODEL_TYPE'))}},methods: {// 当外层弹框点击保存的时候 触发全表单校验并通过回调函数把成功的校验传递出去validForm(callback) {this.$refs.form.validate((valid) => {if (valid) {callback(valid)} else {return false}})},// 当外层弹框点击清空时候重置表单resetForm() {this.$refs.form.resetFields()},// 查询表单async detail(row) {const data = await getObj(row.uuid)this.form = data},}}</script>

