平台端
引言
平台端是指圣钰SaaS的平台端,是用于管理所有租户的一个大型平台。
- 管理租户端的维护,租户可使用的套餐、用户数等相关信息
- 短信、文件管理、代码生成、接口文档、监控等基础服务
- 插件管理体系
- 平台端自己本身的用户、权限、菜单等完全体的端服务
本文档指在告诉用户如何在平台端开发自己想要的功能以及相关注意事项
一、数据库业务表创建
1、平台端的表明通常以platform_前缀开头,以这个开头的表面系统会自动处理,不会进行租户idtenant_id的隔离操作(假定这个表有租户id的前提)。比如如下表名,
这里我们以系统已有的用户表为例:
CREATE TABLE `platform_users` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户账号',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '密码',
`nickname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
`post_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '岗位编号数组',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '用户邮箱',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '手机号码',
`sex` tinyint DEFAULT '0' COMMENT '用户性别',
`avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '头像地址',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '最后登录IP',
`login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_username` (`username`,`update_time`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=126 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息表';注意点:
- 主键id请以
bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID'格式来书写,其中bigint对应后端Java的Long类型 - 如下统一字段被创建。
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',2、如果你的表明希望不以platform_前缀开头但又希望不自动隔离租户id,则需要调整如下配置
├─shengyu-server
| ├─src
| | ├─main
| | | ├─resources
| | | | └application.yaml其中application.yaml文件如下地方调整。添加上你想要的前缀格式或表名
tenant: # 多租户相关配置项
#忽略多租户的表
ignore-tables:
- system_saas_user
#忽略多租户表的前缀
ignore-tables-prefix:
- platform
- tenant
- infra二、业务代码处理
1、首先第一步得清晰的认识你想要开发在哪个模块
├─shengyu-module-platform #平台端模块
| ├─shengyu-module-platform-biz #平台端业务子模块
| | ├─src
| | | ├─main
| | | | ├─java
| | | | | ├─com
| | | | | | ├─shengyu
| | | | | | | ├─module
| | | | | | | | ├─platform
| | | | | | | | | ├─util #工具包
| | | | | | | | | ├─service #业务接口与实现
| | | | | | | | | ├─mq #mq
| | | | | | | | | ├─framework #核心配置
| | | | | | | | | ├─dal #实体类
| | | | | | | | | ├─convert #转换模块
| | | | | | | | | ├─controller #控制器
| | | | | | | | | | ├─platform #app端
| | | | | | | | | | ├─app #web端
| | | | | | | | | ├─api #对外api接口实现
| ├─shengyu-module-platform-api #平台端对外api
├─shengyu-module-infra #基础模块
| ├─shengyu-module-infra-biz #平台端基础模块:文件管理、监控管理、接口管理、代码生成等
| ├─shengyu-module-infra-api #平台端基础模块对外api2、首先开发接口总体分为几个步骤
orm数据库持久层
dal:你的实体类放在该包下面,可以参照其它实体。
mysql:通过继承extends BaseMapperX的方式获得一定的orm操作数据库的能力。
package com.shengyu.module.platform.dal.mysql.user;
import com.shengyu.framework.common.pojo.PageResult;
import com.shengyu.framework.mybatis.core.mapper.BaseMapperX;
import com.shengyu.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserExportReqVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserPageReqVO;
import com.shengyu.module.platform.dal.dataobject.user.PlatformUserDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
@Mapper
public interface PlatformUserMapper extends BaseMapperX<PlatformUserDO> {
default PlatformUserDO selectByUsername(String username) {
return selectOne(PlatformUserDO::getUsername, username);
}
default PlatformUserDO selectByEmail(String email) {
return selectOne(PlatformUserDO::getEmail, email);
}
default PlatformUserDO selectByMobile(String mobile) {
return selectOne(PlatformUserDO::getMobile, mobile);
}
default PageResult<PlatformUserDO> selectPage(UserPageReqVO reqVO, Collection<Long> deptIds) {
return selectPage(reqVO, new LambdaQueryWrapperX<PlatformUserDO>()
.likeIfPresent(PlatformUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(PlatformUserDO::getMobile, reqVO.getMobile())
.eqIfPresent(PlatformUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PlatformUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(PlatformUserDO::getDeptId, deptIds)
.orderByDesc(PlatformUserDO::getId));
}
default List<PlatformUserDO> selectList(UserExportReqVO reqVO, Collection<Long> deptIds) {
return selectList(new LambdaQueryWrapperX<PlatformUserDO>()
.likeIfPresent(PlatformUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(PlatformUserDO::getMobile, reqVO.getMobile())
.eqIfPresent(PlatformUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PlatformUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(PlatformUserDO::getDeptId, deptIds));
}
default List<PlatformUserDO> selectListByNickname(String nickname) {
return selectList(new LambdaQueryWrapperX<PlatformUserDO>().like(PlatformUserDO::getNickname, nickname));
}
default List<PlatformUserDO> selectListByStatus(Integer status) {
return selectList(PlatformUserDO::getStatus, status);
}
default List<PlatformUserDO> selectListByDeptIds(Collection<Long> deptIds) {
return selectList(PlatformUserDO::getDeptId, deptIds);
}
}上述以用户的orm持久层交互为例讲解:
- Java8以上已支持
default,接口层面直接提供默认的方法实现。上述提供了分页列表、列表、根据某一属性查询用户对象
控制器层
package com.shengyu.module.platform.controller.platform.user;
import static com.shengyu.framework.common.pojo.CommonResult.success;
import static com.shengyu.framework.common.util.collection.CollectionUtils.convertList;
import static com.shengyu.framework.common.util.collection.CollectionUtils.convertSet;
import static com.shengyu.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
import cn.hutool.core.collection.CollUtil;
import com.shengyu.framework.common.enums.CommonStatusEnum;
import com.shengyu.framework.common.enums.common.SexEnum;
import com.shengyu.framework.common.pojo.CommonResult;
import com.shengyu.framework.common.pojo.PageResult;
import com.shengyu.framework.common.util.collection.MapUtils;
import com.shengyu.framework.common.util.object.BeanUtils;
import com.shengyu.framework.excel.core.util.ExcelUtils;
import com.shengyu.framework.operatelog.core.annotations.OperateLog;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserCreateReqVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserExcelVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserExportReqVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserImportExcelVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserImportRespVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserPageItemRespVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserPageReqVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserRespVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserSimpleRespVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserUpdatePasswordReqVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserUpdateReqVO;
import com.shengyu.module.platform.controller.platform.user.vo.user.UserUpdateStatusReqVO;
import com.shengyu.module.platform.dal.dataobject.dept.PlatformDeptDO;
import com.shengyu.module.platform.dal.dataobject.user.PlatformUserDO;
import com.shengyu.module.platform.service.dept.PlatformDeptService;
import com.shengyu.module.platform.service.user.PlatformUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@Tag(name = "管理后台 - 用户")
@RestController
@RequestMapping("/system/user")
@Validated
public class PlatformUserController {
@Resource
private PlatformUserService platformUserService;
@Resource
private PlatformDeptService platformDeptService;
@PostMapping("/create")
@Operation(summary = "新增用户")
@PreAuthorize("@ps.hasPermission('system:user:create')")
public CommonResult<Long> createUser(@Valid @RequestBody UserCreateReqVO reqVO) {
Long id = platformUserService.createUser(reqVO);
return success(id);
}
@PutMapping("update")
@Operation(summary = "修改用户")
@PreAuthorize("@ps.hasPermission('system:user:update')")
public CommonResult<Boolean> updateUser(@Valid @RequestBody UserUpdateReqVO reqVO) {
platformUserService.updateUser(reqVO);
return success(true);
}
}注意点
@PreAuthorize("@ps.hasPermission('system:user:create')")是属于平台的接口权限注解,根据ps这个标识符来确定是属于平台的。开发者 将这个已经封装在了底层PlatformUserController控制器名称取名时最好注意点,尽量不要跟其他模块同名了,建议你自己的控制器名称也跟作者一样加上Platform保持统一@Valid接口上面这个属于校验注解,入参dto里面当有validator这个包下面的校验注解时,接口需要加上这个Valid校验才会生效@RequestBody如果是post请求需要加上此注解。- 控制器层面的接口不要写过多的业务,尽量言简意赅。
业务实现层service
1、顾名思义业务实现层是主要编写业务的,业务集中在service的impl实现类中。我们以下面用户的业务实现类举例(下面的实现类只实际提取了部分,误直接摘抄)
package com.shengyu.module.platform.service.user;
/**
* 后台用户 Service 实现类
*
* @author 圣钰科技
*/
@Service
@Slf4j
public class PlatformUserServiceImpl implements PlatformUserService {
@Value("${sys.user.init-password:shengyuyuanma}")
private String userInitPassword;
@Resource
private PlatformUserMapper userMapper;
@Resource
private PlatformDeptService platformDeptService;
@Resource
private PlatformPostService platformPostService;
@Resource
private PlatformPermissionService platformPermissionService;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private PlatformUserPostMapper platformUserPostMapper;
@Resource
private FileApi fileApi;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createUser(UserCreateReqVO reqVO) {
// 校验正确性
validateUserForCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
reqVO.getDeptId(), reqVO.getPostIds());
// 插入用户
PlatformUserDO user = BeanUtils.toBean(reqVO, PlatformUserDO.class);
user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
user.setPassword(encodePassword(reqVO.getPassword())); // 加密密码
userMapper.insert(user);
// 插入关联岗位
if (CollectionUtil.isNotEmpty(user.getPostIds())) {
platformUserPostMapper.insertBatch(convertList(user.getPostIds(),
postId -> new PlatformUserPostDO().setUserId(user.getId()).setPostId(postId)));
}
return user.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserUpdateReqVO reqVO) {
// 校验正确性
validateUserForCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
reqVO.getDeptId(), reqVO.getPostIds());
// 更新用户
PlatformUserDO updateObj = BeanUtils.toBean(reqVO, PlatformUserDO.class);
userMapper.updateById(updateObj);
// 更新岗位
updateUserPost(reqVO, updateObj);
}
private void validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
Long deptId, Set<Long> postIds) {
// 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确
DataPermissionUtils.executeIgnore(() -> {
// 校验用户存在
validateUserExists(id);
// 校验用户名唯一
validateUsernameUnique(id, username);
// 校验手机号唯一
validateMobileUnique(id, mobile);
// 校验邮箱唯一
validateEmailUnique(id, email);
// 校验部门处于开启状态
platformDeptService.validateDeptList(CollectionUtils.singleton(deptId));
// 校验岗位处于开启状态
platformPostService.validatePostList(postIds);
});
}
}注意点
1、比如如上图创建createUser,编辑updateUser等方法。这些需要操作数据库调整的统一需要添加@Transactional(rollbackFor = Exception.class)事务注解
2、validateUserForCreateOrUpdate这个方法在新增和编辑中都会用到,可以参照这个来进行封装统一的校验方法,减少代码冗余量。
3、
@Resource
private PlatformUserMapper userMapper;这种**Mapper结尾的依赖都是直接跟数据库打交道的,简称orm持久层交互。如果是业务中直接跟数据库交互的可以直接这种依赖其他业务模块的Mapper持久层接口操作数据库
4、
@Resource
private PlatformDeptService platformDeptService;这种**Service结尾的依赖属于其他模块参杂了业务的调用。比如用户实现层需要操作部门的数据,但是操作部门之前部门本身也有一些业务需要处理,则这种依赖部门的Service业务接口,然后 部门自己的业务在他自己的实现里面编写,用户模块进行调用即可。总结自己的业务在自己模块进行编写,其他业务模块的互相调用即可,达到解耦的目的
5、
@Resource
private FileApi fileApi;这种**Api结尾的属于其他pom模块的远程调用了,属于platform调用infra模块了。比如下图
├─shengyu-module-platform #平台端模块
| ├─shengyu-module-platform-biz #平台端业务子模块
| ├─shengyu-module-platform-api #平台端对外api
├─shengyu-module-infra #基础模块
| ├─shengyu-module-infra-biz #平台端基础模块:文件管理、监控管理、接口管理、代码生成等
| ├─shengyu-module-infra-api #平台端基础模块对外api这种调用稍微比较麻烦,之所以这样设计是为了方便后期做微服务迁移改造。说白点,现在的编写部分规范点(麻烦点)是为了后续更好的维护。调用链路建议读者根据代码 自行跟踪阅读一下。 大致的调用链路为:
- 如若platform调用infra
infra(biz)-->infra(api)-->platform(biz) - 如若infra调用platform
platform(biz)-->platform(api)-->infra(biz)
6、重中之重、注释一定要写完备!!!
三、数据权限 Configuration
package com.shengyu.module.platform.framework.datapermission.config;
import com.shengyu.module.platform.dal.dataobject.dept.PlatformDeptDO;
import com.shengyu.module.platform.dal.dataobject.user.PlatformUserDO;
import com.shengyu.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* platform 模块的数据权限 Configuration
*
* @author 圣钰科技
*/
@Configuration(proxyBeanMethods = false)
public class PlatformDataPermissionConfiguration {
@Bean
public DeptDataPermissionRuleCustomizer psDeptDataPermissionRuleCustomizer() {
return rule -> {
// dept
rule.addDeptColumn(PlatformUserDO.class);
rule.addDeptColumn(PlatformDeptDO.class, "id");
// user
rule.addUserColumn(PlatformUserDO.class, "id");
};
}
}1、数据权限配置在com.shengyu.module.platform.framework.datapermission.config下面的PlatformDataPermissionConfiguration配置类。 如上图所示增加了部门级别的数据权限过滤,用户级别的数据权限过滤