1.内容介绍
1. 系统管理前端搭建
2. SaaS平台设计(掌握)
3. 机构类型管理
4. 机构入驻(掌握)
5. 全局异常处理(掌握)
2.系统前端搭建
我们的项目是基于前后端分离架构,可以访问我们应用的客户端包括 PC端,移动端(小程序,微信公众等等),三方程序等,我们主要考虑PC端。而前段分为系统管理的前段和门户网站的前段,而PC端的门户网站分为很多个子站点组成,包括课程站点,用户站点,职位站点等等。
2.1.创建项目
为了方便管理,前端也使用父子工程的方式来搭建,只不过前段搭建但是静态的web应用
Hrm-website-parent //搭建一个空项目
Hrm-system-website //系统管理前段,静态的web项目
Hrm-user-website
Hrm-course-website
2.1.1.创建父工程
创建空项目,命名为Hrm-website-parent
效果如下
2.1.2.创建子模块
点中hrm-website-parent创建子模块,命名为Hrm-system-website
选择静态的web项目
输入子项目名字为 hrm-system-website,注意项目路径,应该在hrm-website-parent 下面
效果如下
2.1.3.拷贝前段代码
这个项目的前段使用准备好的vue-cli模板,拷贝相关资料到hrm-system-website目录中,无需执行install
2.1.4.启动项目
使用terminal ,使用cd hrm-system-website 命令进入到项目中,执行 npm run dev 启动项目,根据控制台的地址访问
2.2.前段配置
2.2.1.路由配置
修改router.js可以增加导航
2.2.2.axios配置
修改main.js中的axios的baseUrl统一访问前缀为zuul的地址
3.SaaS平台设计
3.1.SaaS平台的认识
SaaS提供商为企业搭建信息化所需要的所有网络基础设施及软件、硬件运作平台,并负责所有前期的实施、后期的维护等一系列服务,企业无需购买软硬件、建设机房、招聘IT人员,即可通过互联网使用信息系统。就像打开自来水龙头就能用水一样,企业根据实际需要,从SaaS提供商租赁软件服务。
SaaS模式带来的问题:
软件面向的是多个客户企业的数据 , 数据量变大,比如达到上百万,千万数据:考虑做集群,分库,分表
不同客户企业的数据如何隔离
不同客户企业权限如何处理:需要考虑企业,套餐,角色,权限,资源
3.2.SaaS平台数据隔离
3.2.1.独立数据库
这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
优点
为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
缺点
增多了数据库的安装数量,随之带来维护成本和购置成本的增加。
这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。
3.2.2.共享数据库,隔离数据架构
这是第二种方案,即多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。
优点
为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
缺点
如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;
如果需要跨租户统计数据,存在一定困难。
3.2.3.共享数据库,共享数据架构,使用外键区分数据
这是第三种方案,即租户共享同一个Database、同一个Schema,共享表,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。
优点
三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。
缺点:
隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;
数据备份和恢复最困难,需要逐表逐条备份和还原。
如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。
查询不同的租户的数据的时候,需要增加条件:where tenant_id = 登录的租户ID
3.2.4.共享数据库,共享数据架构,不同的机构使用不同的表
不同的机构使用不同的表,表使用后缀进行区分如:t_employee_ali , t_employee_tx
3.3.SaaS平台权限设计
RBAC:基于角色的权限控制: 员工/用户 -> 角色 -> 权限 -> 资源
这里在传统的RBAC的权限控制基础上增加了租户,套餐,功能包(非必须)三个角色。租户购买套餐,套餐关联了对应的权限,租户享有的所有功能限制于购买的套餐的功能。租户下的员工亦是如此。
套餐:
套餐和权限的关系:套餐表 N ---- N 权限表
租户和套餐的关系:租 户 N ---- N 套 餐
租户和员工的关系:租 户 1 ---- N 员 工
租户 N — N 套餐 N — N 功能包 N — N 权限 1 — 1 资源
租户 1 — N 员工 N — N 角色N — N 权限 1 — 1 资源
平台管理员: 创建套餐,分配功能
租户入驻: 购买套餐 ,创建员工 , 创建角色分配权限, 把角色分配给员工
平台超级管理:创建权限列表 ,创建功能包,分配权限,创建套餐,分配功能包
租户购买套餐:购买套餐,支付,租户和套餐建立关系
租户创建员工:创建员工 , 创建角色 ,分配权限给角色(收到套餐的限制) , 把角色分配给自己的员工
所有的权限,一定收到套餐的限制
3.4.SaaS平台相关表设计
4.机构分类管理
在机构入住的时候需要选择机构的类型,我们先把机构类型做了
4.1.机构类型表设计
表见SQL脚本: hrm-system数据库中的 tenant_type
4.2.基础代码生成
使用代码生成器模块生成基础代码,使用Swagger进行测试
4.3.前端CRUD实现
对接系统管理前端 hrm-system-website 对机构分类实现基本CURD功能
4.4.Zuul配置跨域
在zuul设置跨域即可
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
//跨域配置
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,不要写*,否则cookie就无法使用了
config.addAllowedOrigin("http://localhost:6001");
config.addAllowedOrigin("http://127.0.0.1:6001");
//2) 是否发送Cookie信息
config.setAllowCredentials(true);
//3) 允许的请求方式
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
// 4)允许的头信息
config.addAllowedHeader("*");
//2.添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource configSource = new
UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
5.机构管理
5.1.机构表设计
见数据库:hrm-system.t_tenant
5.2.代码生成
代码生成器生成机构的基本CRUD代码
5.3.前端CRUD实现
对接系统管理前端 hrm-system-website 对机构实现基本CURD功能
5.4.机构入驻
5.4.1.入住页面设计
机构入驻需要保存公司的基本信息(保存到Teanant租户表),同时需要初始化一个租户账号(租户的管理员账号保存到Employee包) ,同时租户选择了套餐(保存关系到中间表t_tenant_meal)
表单数据需要保存到三张表 ,机构tenant, 管理员 employee,套餐 t_tenant_meal
5.4.2.入住参数提交
var param = {
"tenant" : {
"companyName":this.employee.companyName,
"companyNum":this.employee.companyNum,
"address":this.employee.address,
"logo":this.employee.logo,
"tenantType":this.employee.tenantTypeId,
},
"employee":{
"username" :this.employee.username,
"tel" :this.employee.tel,
"email" :this.employee.email,
"password" :this.employee.password,
},
"mealId":this.employee.mealId
}
5.4.3.入住参数DTO封装
//接受企业入驻参数
public class TenantEnteringDto {
private Employee employee;
private Tenant tenant;
private Long mealId;
. . .
5.4.4.入住Controller接口
//机构入驻
@RequestMapping(value="/entering",method= RequestMethod.POST)
public AjaxResult entering(@RequestBody TenantEnteringDto tenantEnteringDto){
try {
tenantService.entering(tenantEnteringDto);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("保存对象失败!"+e.getMessage());
}
}
5.4.5.入住Service实现
@Override
public void entering(TenantEnteringDto dto) {
Tenant tenant = dto.getTenant();
Long mealId = dto.getMealId();
Employee employee = dto.getEmployee();
//判断参数
if(StringUtils.isBlank(tenant.getCompanyName())){
throw new RuntimeException("公司名称不可为空哦");
}
//保存机构
Date now = new Date();
tenant.setRegisterTime(now);
baseMapper.insert(tenant);
//保存员工
employee.setTenantId(tenant.getId());
//盐 :注册: 明文 + 盐(保存数据库) = 密文 ,登录: 明文 + 盐(数据库) = 密文
String uuid = UUID.randomUUID().toString();
String md5 = MD5.getMD5(employee.getPassword()+uuid);
employee.setPassword(md5);
employee.setSalt(uuid);
employee.setInputTime(now);
employee.setType(Employee.TYPE_TENANT_ADMIN);
employeeMapper.insert(employee);
//保存套餐关系
Date expireDate = DateUtils.addDays(now,7);
baseMapper.insertRelationWithMeal(
mealId,tenant.getId(),expireDate);
}
5.4.6.保存机构和套餐
public interface TenantMapper extends BaseMapper<Tenant> {
void insertRelationWithMeal(@Param("mealId") Long mealId,
@Param("tenantId")Long tenantId,
@Param("expireDate")Date expireDate);
}
<insert id="insertRelationWithMeal"
useGeneratedKeys="true" keyProperty="id" keyColumn="id">
INSERT INTO t_tenant_meal(meal_id,tenant_id,expireDate)
VALUES (
#{mealId},
#{tenantId},
#{expireDate}
)
</insert>
employee需要关联tenant的id
employee密码加密加盐:增加salt字段
中间表保存,mapper接口记得打标签@Param
6.异常处理
6.1.自定义异常
系统中的异常可以分为我们能预知的异常和未知的系统异常,对于我们能预知的异常如空值判断,用户名错误,密码错误等异常我们需要返回客户端,对于系统内部异常如SQL语法错误,参数格式转换错误等需要统一包装成友好的提示后再返回客户端,否则用户也看不懂系统内部的异常。
方案是可以自定义异常来封装我们能够预知的异常,和系统未知的异常做一个区分,自定义异常如下:
//全局异常
public class GlobalException extends RuntimeException {
public GlobalException(String message){
super(message);
}
}
使用自定义异常
if(StringUitls.isNull(username)){
throw new GlobalException(“用户名不可为空哦”);
}
在控制器层捕获不同的异常
@RequestMapping(value="/{id}",method=RequestMethod.DELETE)
public AjaxResult delete(@PathVariable("id") Long id){
try {
tenantService.deleteById(id);
return AjaxResult.me();
} catch (GlobalException e) { //自定义异常,message直接返回
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage(e.getMessage());
} catch (Exception e) { //未知异常 ,包装成友好提示返回
e.printStackTrace();
return AjaxResult.me().setSuccess(false)
.setMessage("系统异常啦,请联系管理员");
}
}
问题:这样一来,controller中需要写大量重复的try{}catch() 代码
6.2.断言工具抽取
可以抽取断言工具进行数据判断,然后抛出异常,即可以把
if(StringUitls.isNull(username)){
throw new GlobalException(“用户名不可为空哦”);
}
抽成工具如:ValidUtils
//断言不为空
public static void assertNotNull(Object obj,String message){
if(null == obj){
throw new GlobalException(message);
}
if(obj instanceof String){
String objStr = (String) obj;
if(objStr.length() == 0){
throw new GlobalException(message);
}
}
}
以后就只需要这样用:
ValidUtils.assertNotNull(tenant.getCompanyName(),"公司名称不可为空");
ValidUtils断言工具:
import cn.itsource.exception.GlobalException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//参数验证的工具
public class ValidUtils {
//手机号的验证规则
private static final String PHONE_REGEX = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0-9]))\\d{8}$";
//邮箱的校验规则
private static final String EMAIL_REGEX = "[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[\\w](?:[\\w-]*[\\w])?";
//断言验证手机号
public static void assertPhone(String phone,String message){
//格式判断
Pattern p = Pattern.compile(PHONE_REGEX);
Matcher m = p.matcher(phone);
if(!m.matches()){
throw new GlobalException(message);
}
}
public static void assertPhone(String phone){
assertPhone(phone,"无效的手机号");
}
//断言验证邮箱
public static void assertEmail(String email,String message){
//格式判断
Pattern p = Pattern.compile(EMAIL_REGEX);
Matcher m = p.matcher(email);
if(!m.matches()){
throw new GlobalException(message);
}
}
public static void assertEmail(String email){
assertEmail(email,"无效的邮箱");
}
//断言不为空
public static void assertNotNull(Object obj,String message){
if(null == obj){
throw new GlobalException(message);
}
if(obj instanceof String){
String objStr = (String) obj;
if(objStr.length() == 0){
throw new GlobalException(message);
}
}
}
public static void assertNotNull(Object obj){
assertNotNull(obj,"不可为空");
}
}
6.3.全局异常处理
我们可以吧controller中大量重复的try{}代码抽取到专门的类中进行处理
//全局异常处理
@RestControllerAdvice //可以在controller执行前,后做一些事情
//@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(GlobalException.class) //处理异常
public AjaxResult globalExceptionHandler(GlobalException e){
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage(e.getMessage());
}
@ExceptionHandler(Exception.class) //处理异常
public AjaxResult exceptionHandler(Exception e){
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("啊,系统异常啦,我们正在殴打程序员...");
}
}
这里的 RestControllerAdvice + ExceptionHandler 注解可以拦截到异常,针对不同的异常做不同的拦截,然后做出不同的处理,在controller执行不包异常的时候就会返回正确的结果,如果出现异常,走GlobalExceptionHandler 异常拦截类进行处理。
Controller的写法:
@RequestMapping(value="/entering",method= RequestMethod.POST)
public AjaxResult entering(@RequestBody TenantEnteringDto dto){
tenantService.entering(dto);
return AjaxResult.me();
}
不再考虑try{}-catch
6.4.读取yml中的配置
6.4.1.方式一:@Valvue
如有配置
hrm:
tenant:
register:
saltlen: 18
trialdays: 7
代码中读取
@Value(“${hrm.tenant.register.saltlen}”)
Private int saltlen;
@Value(“${hrm.tenant.register.trialdays}”)
Private int trialdays;
6.4.2.方式二:@ConfigurationProperties
通过ConfigurationProperties可以吧一堆配置读取到一个对象中,对象的字段名和配置的key要一致
创建对象:
//自动从配置文件中,通过前缀过滤出一堆配置,自动同名绑定到该对象的字段上
@ConfigurationProperties(prefix = "hrm.tenant.register")
@Component
@Data
public class TenantRegisterProperties {
private int saltlen = 14;
private int trialdays = 7;
}
使用配置对象,注入即可使用
@Autowired
private TenantRegisterProperties properties;
6.5.错误码抽取
6.5.1.错误码抽取枚举
//封装错误码
public enum ErrorCode {
//不可为空的错误码
CODE_100_NULL_COMPANYNAME(100,"公司名不可为空"),
CODE_101_FORMAT_INVALID_PHONE(101,"无效的手机格式"),
CODE_102_EXIST_COMPANYNAME(102,"公司名已经被注册");
//错误码
private int code;
//错误信息
private String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
6.5.2.错误码使用
ValidUtils.isNotNull(tenant.getCompanyName(), ErrorCode.CODE_100_NULL_COMPANYNAME.);
6.5.3.断言工具修改
//断言不为空
public static void isNotNull(String str , String errorMessage,Integer code){
if(null == str || str.length() == 0){
throw new ParamException(errorMessage,code);
}
}
//断言不为空,传入错误码
public static void isNotNull(String str , ErrorCode errorCode){
if(null == str || str.length() == 0){
//throw new ParamException(errorCode.getMessage(),errorCode.getCode());
throw new ParamException(errorCode);
}
}
6.5.4.异常对象修改
//全局异常
public class GlobalException extends RuntimeException {
//异常码
private Integer code = 0;
public GlobalException(String message){
super(message);
}
public GlobalException(String message ,Integer code){
super(message);
this.code = code;
}
//从错误枚举中获取错误信息和错误码
public GlobalException(ErrorCode errorCode){
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
}
6.5.5.AjaxResult增加错误码
//Ajax请求响应对象的类
public class AjaxResult {
private boolean success = true;
private String message = "操作成功!";
private Integer code = 0;
//返回到前台对象
private Object resultObj;
public Integer getCode() {
return code;
}
public AjaxResult setCode(Integer code) {
this.code = code;
return this;
}
...省略...
6.5.6.全局异常工具修改
//在controller执行,报异常后,统一捕获异常
//@ExceptionHandler:处理异常,捕获异常的
@ExceptionHandler(value = ParamException.class)
public AjaxResult handlerParamException(ParamException e){
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage(e.getMessage())
.setCode(e.getCode());
}
7.课程总结
7.1.难点
OSS实现文件上传
7.2.如何掌握?
1.写代码前先整理思路
2.代码敲完,做一遍总结
7.3.面试题
1.你们的文件是怎么管理的?
2.Fastdfs搭建流程说一下?