A086_HRM_02_SaaS介绍_机构入驻_异常处理

1.内容介绍

1. 系统管理前端搭建
2. SaaS平台设计(掌握)
3. 机构类型管理
4. 机构入驻(掌握)
5. 全局异常处理(掌握)

2.系统前端搭建

我们的项目是基于前后端分离架构,可以访问我们应用的客户端包括 PC端,移动端(小程序,微信公众等等),三方程序等,我们主要考虑PC端。而前段分为系统管理的前段和门户网站的前段,而PC端的门户网站分为很多个子站点组成,包括课程站点,用户站点,职位站点等等。
A086_HRM_02_SaaS介绍_机构入驻_异常处理

2.1.创建项目

为了方便管理,前端也使用父子工程的方式来搭建,只不过前段搭建但是静态的web应用

Hrm-website-parent        //搭建一个空项目
	Hrm-system-website    //系统管理前段,静态的web项目
	Hrm-user-website    
	Hrm-course-website   
2.1.1.创建父工程

创建空项目,命名为Hrm-website-parent
A086_HRM_02_SaaS介绍_机构入驻_异常处理
效果如下
A086_HRM_02_SaaS介绍_机构入驻_异常处理

2.1.2.创建子模块

点中hrm-website-parent创建子模块,命名为Hrm-system-website
A086_HRM_02_SaaS介绍_机构入驻_异常处理
选择静态的web项目
A086_HRM_02_SaaS介绍_机构入驻_异常处理
输入子项目名字为 hrm-system-website,注意项目路径,应该在hrm-website-parent 下面
A086_HRM_02_SaaS介绍_机构入驻_异常处理
效果如下
A086_HRM_02_SaaS介绍_机构入驻_异常处理

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提供商租赁软件服务。
A086_HRM_02_SaaS介绍_机构入驻_异常处理
SaaS模式带来的问题:
软件面向的是多个客户企业的数据 , 数据量变大,比如达到上百万,千万数据:考虑做集群,分库,分表
不同客户企业的数据如何隔离
不同客户企业权限如何处理:需要考虑企业,套餐,角色,权限,资源

3.2.SaaS平台数据隔离

3.2.1.独立数据库

这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
优点
为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
缺点
增多了数据库的安装数量,随之带来维护成本和购置成本的增加。
这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。
A086_HRM_02_SaaS介绍_机构入驻_异常处理

3.2.2.共享数据库,隔离数据架构

这是第二种方案,即多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。

优点
为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
缺点
如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;
如果需要跨租户统计数据,存在一定困难。
A086_HRM_02_SaaS介绍_机构入驻_异常处理

3.2.3.共享数据库,共享数据架构,使用外键区分数据

这是第三种方案,即租户共享同一个Database、同一个Schema,共享表,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。
优点
三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。
缺点:
隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;
数据备份和恢复最困难,需要逐表逐条备份和还原。
如果希望以最少的服务器为最多的租户提供服务,并且租户接受牺牲隔离级别换取降低成本,这种方案最适合。
A086_HRM_02_SaaS介绍_机构入驻_异常处理
查询不同的租户的数据的时候,需要增加条件:where tenant_id = 登录的租户ID

3.2.4.共享数据库,共享数据架构,不同的机构使用不同的表

不同的机构使用不同的表,表使用后缀进行区分如:t_employee_ali , t_employee_tx
A086_HRM_02_SaaS介绍_机构入驻_异常处理

3.3.SaaS平台权限设计

RBAC:基于角色的权限控制: 员工/用户 -> 角色 -> 权限 -> 资源

这里在传统的RBAC的权限控制基础上增加了租户,套餐,功能包(非必须)三个角色。租户购买套餐,套餐关联了对应的权限,租户享有的所有功能限制于购买的套餐的功能。租户下的员工亦是如此。
套餐:
A086_HRM_02_SaaS介绍_机构入驻_异常处理
套餐和权限的关系:套餐表 N ---- N 权限表
租户和套餐的关系:租 户 N ---- N 套 餐
租户和员工的关系:租 户 1 ---- N 员 工

租户 N — N 套餐 N — N 功能包 N — N 权限 1 — 1 资源

租户 1 — N 员工 N — N 角色N — N 权限 1 — 1 资源

平台管理员: 创建套餐,分配功能

租户入驻: 购买套餐 ,创建员工 , 创建角色分配权限, 把角色分配给员工
A086_HRM_02_SaaS介绍_机构入驻_异常处理
平台超级管理:创建权限列表 ,创建功能包,分配权限,创建套餐,分配功能包
租户购买套餐:购买套餐,支付,租户和套餐建立关系
租户创建员工:创建员工 , 创建角色 ,分配权限给角色(收到套餐的限制) , 把角色分配给自己的员工
所有的权限,一定收到套餐的限制

3.4.SaaS平台相关表设计

A086_HRM_02_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)
A086_HRM_02_SaaS介绍_机构入驻_异常处理
A086_HRM_02_SaaS介绍_机构入驻_异常处理
表单数据需要保存到三张表 ,机构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搭建流程说一下?

上一篇:saas和paas的区别


下一篇:SaaS模式、技术与案例详解——第14章 实用性及扩展性