# 业务系统后端开发

后端开发需具备java、maven、git、springboot等基础知识。

# demo启动

下载模板工程(stms)、修改模板配置为所对接平台的相应配置;包括nacos、db(平台配置中已经包含redis、kafka相关配置)

stms工程下载地址

# 修改配置文件

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
1
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
1
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();
    }

}
1
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>
1
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();
1
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();
}
1
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;
1
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();
}

1
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;
1
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();
}

1
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;
1
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();
    }

}
1
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;
1
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());
            }
        });
    }
}
1
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);
}
1
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());
        }
    }
}
1
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;
}
1
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());
            }
        });
    }
}

1
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);
}
1
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;
    }
1
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>
1
2
3
4
5

2、导入流程SDK配置类,即在启动类上添加如下注解:

@Import({WorkFlowClientConfiguration.class})
public class StmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(StmsApplication.class, args);
    }
}
1
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);
        }
    }
1
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>
1
2
3
4
5

2、业务系统对应的nacos中添加如下配置

nepoch:
  message:
    notify: redis
    consumer: true
1
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) {

    }
}
1
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
1
2
3
4
5
6
  • 打包镜像命令
mvn -DskipTests=true -Ddocker.skip=false clean deploy -Ddocker.registry.name.prefix=docker仓库地址
1