# 业务系统后端开发
后端开发需具备java、maven、git、springboot等基础知识。
# demo启动
下载模板工程(stms)、修改模板配置为所对接平台的相应配置;包括nacos、db(平台配置中已经包含redis、kafka相关配置)
# 修改配置文件
bootstrap-dev.yml
db:
url: jdbc:mysql://ip:3306/stms
username: root
password: password
driver: com.mysql.jdbc.Driver
nacos:
address: ip:8848
namespace: namespace
group: DEV_GROUP
shared-dataids: dev.yml
2
3
4
5
6
7
8
9
10
数据库关注点数据库目前支持mysql、sqlserver两种,选择自己合适的数据库
nacos关注地址、命名空间、分组都需要与平台nacos配置一致
# spingboot 启动
正常的spingboot项目启动,启动类为com.glodon.nepoch.stms.StmsApplication.java
# 模板目录结构
stms
├── Dockerfile
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── glodon
│ └── nepoch
│ └── stms
│ ├── controller
│ │ └── ProjectController.java
│ ├── entity
│ │ └── ProjectEntity.java
│ ├── mapper
│ │ └── ProjectMapper.java
│ ├── message
│ │ └── MessageReceiver.java
│ ├── model
│ │ └── ProjectModel.java
│ ├── service
│ │ ├── imple
│ │ │ └── ProjectServiceImpl.java
│ │ └── IProjectService.java
│ └── StmsApplication.java
└── resources
├── application.yml
├── bootstrap-dev.yml
├── bootstrap-prod.yml
├── bootstrap-test.yml
├── bootstrap.yml
└── logback-spring.xml
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- com.glodon.nepoch.stms: 路径中stms为模块名称,不同模块使用不同名称
- resource:springboot 配置相关,管理应用配置
- controller:控制层,为接口入口,用于入参出参处理
- entity:实体类,原则上与DB中表一一对应,维护DB与 实体类的映射
- mapper:持久层,主要暴露DB处理接口,对应Mybatis中mapper接口
- message:消息队列相关逻辑,示例为消息消费方逻辑
- model:模型层,配合controller 处理接口中的数据体,mvc中view层数据交互模型
- service:服务层,主要业务逻辑实现层;
平台nepoch-framework-parent封装了三方依赖,包括不限于spring cloud、mybatis、redis、kafka、fastdfs、内置jdk等。在mvc各层处理中简化接口操作,因此service、mapper、entity等 提供了通用DB操作sdk。entity中提供了IBaseEntity(普通实体类)、ITreeEntity(树形实体类);对应service层有INepochBaseService、INepochTreeService接口,NepochBaseServiceImpl、NepochTreeServiceImpl实现类;mapper层有NepochBaseMapper、NepochTreeMapper;正对树接口数据,集成、实现各层相应类可提供basis中基础字段填充、tree中treepath、parentId等字段自动填充。针对tree型结构处理后续章节有相应工具类介绍。
各层代码编写方式参考demo中逻辑实现、同时平台提供代码生成工具(逆向工程)详见后续章节
# 开发约定
java开发规范:点击下载 (opens new window)
db规范
-- 系统表命名以 s_ 开头 -- 流程相关表命名以 c_ 开头 -- 文件相关表命名以 b_ 开头
1
2
3
# 逆向工程
mybatis代码生成器(参考路径:https://blog.csdn.net/qq_41973208/article/details/106685861)
# 代码地址
git代码仓库:点击跳转 (opens new window)
git分支:glink-docker
# 使用步骤
1、数据库创建对应的表(注意:必须将id设置为主键)
2、按需修改字段
- author:类文件创建作者
- email:作者邮箱
- tablePrefix:表前缀,生成时将过滤掉前缀,如C: common, B: business, S: system
- tableNames:待生成的表名,请根据实际情况修改
- isTreeEntity:是否树形实体
- overrideExistFile:是否覆盖旧文件
- projectPathToRoot:代码生成的相对工程路径,如"nepoch-workflow-parent/workflow-editor/workflow-editor-service"
- packageFullName:待生成的类文件包全名,如"com.glodon.nepoch.workflow.center.extend"
3、执行MyBatisPlusCodeGenerator.main方法
/**
* 生成mybatis 相关文件
*
* @author <A href="mailto:yangmd@glodon.com">yangmd</A>
* @version 1.0
*/
public class MyBatisPlusCodeGenerator {
//作者:用于在类文件上注释谁生成的
private static final String author = "user";
//作者邮箱
private static final String email = "user@qq.com";
// 表前缀,生成时,将过滤掉前缀 C: common, B: business, S: system
private static final String[] tablePrefix = {"C", "B", "S"};
// 待生成的表名,请根据实际情况修改
private static final String[] tableNames = {"b_test_table"};
//是否树形实体
private static final boolean isTreeEntity = false;
//数据库信息
private static final String dbUrl = "jdbc:mysql://ip:3306/stms?characterEncoding=UTF-8";
//是否覆盖旧文件
private static final boolean overrideExistFile = true;
//设置代码生成相对工程路径
private static final String projectPathToRoot = "stms";
// 待生成的类文件包全名
private static final String packageFullName = "com.glodon.nepoch.stms";
public static void main(String[] args) {
CodeGeneratorConfig codeGeneratorConfig = CodeGeneratorConfig.of(
author, email, tablePrefix, packageFullName, isTreeEntity,
tableNames);
codeGeneratorConfig
.setDbUrl(dbUrl)
.setOverrideExistFile(overrideExistFile)
.setProjectPathToRoot(projectPathToRoot)
.createAutoGeneratorAndExecute();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 模板中相关SDK
# 引用其他工程SDK
所在应用的pom.xml中引入其它工程的jar包(带包的全路径),此处以File系统引入system系统为例说明,
glink-file-service服务的pom.xml文件部分配置如下:打开
<!--此处可以查看当前应用的jar包路径-->
<parent>
<artifactId>nepoch-file-parent</artifactId>
<groupId>com.glodon.nepoch.file</groupId>
<version>2.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!--此处表示此应用为文件服务应用-->
<artifactId>file-service</artifactId>
<properties>
<fastdfs-client.version>1.26.4</fastdfs-client.version>
</properties>
<dependencies>
<dependency>
<groupId>com.glodon.nepoch.file</groupId>
<artifactId>file-api</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.glodon.nepoch.file</groupId>
<artifactId>file-sdk</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<!-- 需要引入的系统管理应用的配置(此处为引用的配置) -->
<dependency>
<groupId>com.glodon.nepoch.glink</groupId>
<artifactId>system-manage-sdk</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
如下图:
# 内置SDK
# 获取租户相关信息
@Autowired
private TenantContext tenantContext;
// 获取租户id
String tenantId = tenantContext.getId();
2
3
4
5
# 获取产品相关信息
- 产品的上下文对象信息:ProductContext
public interface ProductContext {
/**
* 获取当前产品ID,数据来源 saas 库的 s_intgr_product
*/
String getProductId();
/**
* 获取当前产品code,数据来源 saas 库的 s_intgr_product
*/
String getProductCode();
/**
* 获取当前子系统id,数据来源 saas 库的 s_intgr_subsystem
*/
String getSubSystemId();
/**
* 获取当前模块id,数据来源 saas 库的 s_intgr_feature
*/
String getModeId();
/**
* 区分私有化或云平台, glink_cloud 云平台
*/
String getSaasType();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 使用方式,在类中注入该对象,直接调用该对象的属性方法
@Resource
private ProductContext productContext;
2
# 当前登录相关信息获取
- 组织的上下文对象信息:OrgContext
public interface OrgContext {
/**
* 获取当前组织ID,数据来源 saas 库的 s_sys_org
*/
String getId();
/**
* 获取当前组织编码, 数据来源 saas 库的 s_sys_org
*/
String getCode();
/**
* 组织节点类型, 数据来源 saas 库的 s_sys_org
*/
Integer getType();
/**
* 组织维度, 数据来源 saas 库的 s_sys_org
*/
Integer getDim();
/**
* 获取当前组织名称, 数据来源 saas 库的 s_sys_org
*/
String getName();
/**
* 获取组织路径, 数据来源 saas 库的 s_sys_org
*/
String getTreePath();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- 使用方法:在类中注入该对象,直接调用该对象的属性方法
@Autowired
private OrgContext orgContext;
2
- 用户的上下文对象信息:UserContext(用户登录时进行权限认证,通过后即可加载用户部分信息)
public interface UserContext {
/**
* 获取当前用户Id,数据来源 saas 库的 s_sys_user
*/
String getId();
/**
* 获取当前用户编码,数据来源 saas 库的 s_sys_user
*/
String getCode();
/**
* 用户类型,数据来源 saas 库的 s_sys_user
*/
Integer getType();
/**
* 获取当前用户姓名,数据来源 saas 库的 s_sys_user
*/
String getName();
/**
* 获取当前用户手机号,数据来源 saas 库的 s_sys_user
*/
String getMobile();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- 使用方法:在类中注入该对象,直接调用该对象的属性方法
@Autowired
private UserContext userContext;
2
- 登录的信息(包括用户及组织的信息):CurrentLoginContext
public class CurrentLoginContext {
@Autowired
private UserContext userContext;
@Autowired
private OrgContext orgContext;
@Autowired
private SystemContext systemContext;
public String orgId() {
return orgContext.getId();
}
public String orgCode() {
return orgContext.getCode();
}
public OrgNodeType orgType() {
Integer type = orgContext.getType();
if (type == null) {
throw new BusinessException("无法获取当前组织节点类型.");
}
return OrgNodeType.parse(type);
}
public String orgName() {
return orgContext.getName();
}
public String system() {
return systemContext.getId();
}
public String application() {
return systemContext.getAppId();
}
public String userId() {
return userContext.getId();
}
public String userCode() {
return userContext.getCode();
}
public String userName() {
return userContext.getName();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- 使用方法:在类中注入该对象,直接调用该对象的属性方法(建议直接使用UserContext、OrgContext等对象)
@Autowired
private CurrentLoginContext loginContext;
2
# 树形结构工具类
构造树形工具的类:TreeModelHelper
- list转树结构
public class TreeModelHelper {
public static <U extends ITreeModel, T extends ITreeModelWrapper> List<T> convert(List<U> treeModels, Class<T> clazz) {
return convert(treeModels, clazz, (node1, node2) -> {
if (node1.getOrderNum() == null) {
return node2.getOrderNum() == null ? 0 : -1;
} else {
return node2.getOrderNum() == null ? 1 : node1.getOrderNum().compareTo(node2.getOrderNum());
}
});
}
}
2
3
4
5
6
7
8
9
10
11
例:
private List<OrgUserModel> parseOrgEntityTree(List<OrgEntity> orgList) {
List<OrgUserModel> orgUserModelList = MapUtils.mapList(orgList, OrgUserModel.class);
return TreeModelHelper.convert(orgUserModelList, OrgUserModel.class);
}
2
3
4
- 树结构中节点遍历
public class TreeModelHelper {
public static <T extends ITreeModelWrapper> void visitTreeNodes(List<T> treeModes, Function<T, Boolean> vistor){
Queue<T> queue = new LinkedList<>(treeModes);
//避免存在循环嵌套的情况,导致死循环
Set<String> visited = new HashSet<>();
while (!queue.isEmpty()){
T t = queue.poll();
boolean result = vistor.apply(t);
if(!result){
break;
}
if(!visited.contains(t.getId())){
List children = t.getChildren();
if(children != null){
queue.addAll(children);
}
}
visited.add(t.getId());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
例:
@Override
public IPage<UserModel> queryUsersByPositionId(PageModel pageModel, String positionId, String userName, String account) {
//获取机构数据
HrOrgInfo hrOrgInfoData = hrSystemClient.getHrOrgInfoData(hrProviderContext.hrProviderId());
//转化为列表数据
List<HrOrgInfo> hrOrgInfos = Collections.singletonList(hrOrgInfoData);
List<HrUserInfo> users = new ArrayList<>();
//更新或者过滤列表中的数据,同时转化为树状结构
TreeModelHelper.visitTreeNodes(hrOrgInfos, hrOrgInfo -> {
// 节点users属性不为空
if (positionId.equals(hrOrgInfo.getId())){
List<HrUserInfo> users1 = hrOrgInfo.getUsers();
if(StringUtils.isNotEmpty(userName)){
users1.stream().filter(obj -> obj.getName().contains(userName)).collect(Collectors.toList());
}
users.addAll(users1);
}
return true;
});
//放入分页对象中,进行分页处理
Page<UserModel> page = new Page<>();
page.setRecords(MapUtils.mapList(users, UserModel.class)).setTotal(users.size())
.setCurrent(pageModel.getCurrentPage()).setSize(pageModel.getPageSize());
return page;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 树节点处理函数,自定义函数
public class TreeModelHelper {
/**
* 根据给定的树节点集合返回包含父节点的树形结构
*
* @param listEntities
* @param resultClazz
* @param searchByIdFunc
* @param <U>
* @param <T>
* @return
*/
public static <U extends ITreePathSupport & ITreeModel, T extends ITreeModelWrapper>
List<T> constructTreeSearchResult(List<U> listEntities,
Class<T> resultClazz,
Function<Collection<String>, Collection<U>> searchByIdFunc) {
return constructTreeSearchResult(listEntities, resultClazz, searchByIdFunc, (node1, node2) -> {
if (node1.getOrderNum() == null) {
if (node2.getOrderNum() == null) {
return 0;
}
return -1;
} else if (node2.getOrderNum() == null) {
return 1;
} else {
return node1.getOrderNum().compareTo(node2.getOrderNum());
}
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
例:
@ApiOperation("获取人事组织树形结构")
@GetMapping("/tree")
public List<OrgTreeInfo> treeOrg(@RequestParam(name = "name",required = false)
@ApiParam(value = "过滤字符串,模糊匹配") String wildName) {
//根据组织维度和组织名称查询组织列表数据
List<OrgEntity> orgEntities = orgService.queryByDimIdAndName(DimConstant.HR_DIM_ID, wildName);
if (CollectionUtils.isEmpty(orgEntities)){
return Collections.emptyList();
}
//组织列表数据更新机构全路径值
List<OrgTreeInfo> orgTreeInfoList = orgEntities.stream().map(obj -> {
OrgTreeInfo orgTreeInfo = MapUtils.map(obj, OrgTreeInfo.class);
orgTreeInfo.setOrgNamePath(obj.getNamePath());
return orgTreeInfo;
}).collect(Collectors.toList());
//查询的机构列表集构造树形结构
return TreeModelHelper.constructTreeSearchResult(orgTreeInfoList, OrgTreeInfo.class, null);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 分页
分页对象:PageModel
例:
@Override
public IPage<OrgUserInfo> selectOrgUserPage(PageModel pageModel, String orgId, String name, String account, String mobile) {
// 获取机构表的数据集
List<OrgEntity> orgEntityList = orgMapper.selectBatchIds(Arrays.asList(orgId));
// 获取机构用户关联表的数据集
List<OrgUserEntity> orgUserEntityList = listOrgUserByRelId(Arrays.asList(orgId), null);
IPage<OrgUserInfo> pageInfo = new Page<>(pageModel.getCurrentPage(),pageModel.getPageSize());
if(CollectionUtils.isEmpty(orgUserEntityList)){
return CommonMethod.getPageList(pageInfo, Collections.emptyList());
}
List<String> userIdList = orgUserEntityList.parallelStream().map(OrgUserEntity::getUserId).collect(Collectors.toList());
// 获取用户表的数据集
List<UserEntity> userEntityList = userMapper.selectBatchIds(userIdList);
// 机构表、机构用户关联表、用户表数据集关联获取结果集
List<OrgUserInfo> orgUserInfoList = CommonMethod.getOrgUserInfoByAssociate(orgUserEntityList, orgEntityList, userEntityList);
// 数据结果集进行分页操作
return getPageList(pageInfo, orgUserInfoList);
}
public static <T> IPage<T> getPageList(IPage<T> pageInfo, List<T> list){
int currPage = (int)pageInfo.getPages() == 0 ? 1 : (int)pageInfo.getPages();
int pageSize = (int)pageInfo.getSize();
// 从第几条数据开始
int firstIndex = (currPage - 1) * pageSize;
// 到第几条数据结束
int lastIndex = list.size()<currPage * pageSize ? list.size() : currPage * pageSize;
if(CollectionUtils.isEmpty(list)){
pageInfo.setTotal(0);
pageInfo.setRecords(Collections.emptyList());
}else{
List<T> recordlist = list.subList(firstIndex, lastIndex);
pageInfo.setTotal(list.size());
pageInfo.setRecords(recordlist);
}
return pageInfo;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 流程集成
# 集成流程引擎SDK
1、在业务系统工程pom.xml文件中引入流程中心SDKMaven依赖,如下所示:
<dependency>
<groupId>com.glodon.nepoch.glink</groupId>
<artifactId>process-center-sdk</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
2
3
4
5
2、导入流程SDK配置类,即在启动类上添加如下注解:
@Import({WorkFlowClientConfiguration.class})
public class StmsApplication {
public static void main(String[] args) {
SpringApplication.run(StmsApplication.class, args);
}
}
2
3
4
5
6
3、在业务代码中注入相应Feign Client
public class LeaveDetailsController {
@Resource
private WorkFlowSubmitClient wfClient;
@ApiOperation(value = "保存提交请假申请", notes = "保存提交请假申请")
@PostMapping(value = "/submit", produces = "application/json;charset=UTF-8")
public Object submit(@RequestBody WFSubmitDTO wfSubmitDTO) {
return wfClient.submitWorkFlow(wfSubmitDTO);
}
}
2
3
4
5
6
7
8
9
# 集成redis消息中心
流程中心目前使用redis作为消息中间件,用于更新流程的流转状态
state | 状态 |
---|---|
1 | 已提交 |
2 | 审批中 |
3 | 审批通过 |
1、在业务系统工程pom.xml文件中添加Maven依赖
<!-- redis消息通知依赖 -->
<dependency>
<groupId>com.glodon.nepoch.framework.notify</groupId>
<artifactId>nepoch-framework-notify-redis</artifactId>
</dependency>
2
3
4
5
2、业务系统对应的nacos中添加如下配置
nepoch:
message:
notify: redis
consumer: true
2
3
4
- 必须保证业务系统和流程中心redis同源同库
3、消息接收类
添加消息接收类,实现接口IReceiver
参考示例如下:
/**
* <功能描述/>
* 接收流程状态消息,更新业务状态
*/
@Service
@Slf4j
public class MessageReceiver implements IReceiver {
// 实例项目业务
@Autowired
private IProjectService projectService;
// 请假业务
@Autowired
private ILeaveDetailsService leaveDetailsService;
// value按【业务状态通知实体(BusinessNotifyModel)】解析
public void getMessage(String value) {
try {
if (log.isDebugEnabled()) {
log.debug("消息: {}", value);
}
JSONObject jo = JSONObject.parseObject(value);
String id = jo.getString("id");
Integer billStatus = jo.getInteger("state");
if (id != null) {
if (jo.getString("buseinessCode").equals("FLOW_LEAVE_DECLARE")){
LeaveDetailsEntity entity = leaveDetailsService.getById(id);
entity.setBillStatus(billStatus);
leaveDetailsService.updateById(entity);
}
ProjectEntity entity = projectService.getById(id);
entity.setBillStatus(billStatus);
projectService.updateById(entity);
}
} catch (Exception e) {
log.error(String.format("格式出现错误: %s", value), e);
}
}
@Override
public void getMessage(String message, String topic) {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 工程容器化
- dockerfile文件
# 后端服务基础镜像
FROM registry.cn-beijing.aliyuncs.com/crcc_yth/nepoch-server-basis:V1.0
# copy 资源
ARG JAR_FILE
COPY target/${JAR_FILE} /opt/nepoch/app.jar
ENTRYPOINT java -Xms2048m -Xmx2048m -jar -Duser.timezone=GMT+08 /opt/nepoch/app.jar
2
3
4
5
6
- 打包镜像命令
mvn -DskipTests=true -Ddocker.skip=false clean deploy -Ddocker.registry.name.prefix=docker仓库地址