A086_HRM_02_SaaS introduction_ Institutional settlement_ exception handling

1. Content introduction

1. System management front end construction
2. SaaS platform design (Master)
3. Organization type management
4. Institution entry (mastery)
5. Global exception handling (Master)

2. System front end construction

Our project is based on the front and back-end separation architecture. The clients that can access our application include PC, mobile (applet, wechat public, etc.), third-party programs, etc. we mainly consider PC. The front section is divided into the front section of system management and the front section of portal website, while the portal website on PC side is divided into many sub sites, including course site, user site, position site and so on.

2.1. Create project

In order to facilitate management, the front end is also built in the way of parent-child engineering, but it is a static web application built in the front section

Hrm-website-parent        //Build an empty project
	Hrm-system-website    //System management front, static web project
	Hrm-user-website    
	Hrm-course-website   
2.1.1. Create parent project

Create an empty project named HRM website parent

The effect is as follows

2.1.2. Create sub module

Click HRM website parent to create a sub module named HRM system website

Select a static web project

Enter the name of the subproject as HRM system website. Note that the project path should be under HRM website parent

The effect is as follows

2.1.3. Copy previous code

In the first section of this project, use the prepared Vue cli template to copy the relevant data to the HRM system website directory without executing install

2.1.4. Start project

Use terminal, enter the project with CD HRM system website command, execute npm run dev to start the project, and access it according to the address of the console

2.2. Front section configuration

2.2.1. Routing configuration

Modify router JS can increase navigation

2.2.2.axios configuration

Modify main The baseUrl of axios in JS uniformly accesses the address prefixed with zuul

3.SaaS platform design

3.1. Understanding of SaaS platform

SaaS providers build all the network infrastructure, software and hardware operation platforms required by informatization for enterprises, and are responsible for a series of services such as early implementation and later maintenance. Enterprises can use the information system through the Internet without purchasing software and hardware, building computer rooms and recruiting IT personnel. Just as you can use water by turning on the tap, enterprises rent software services from SaaS providers according to their actual needs.

Problems caused by SaaS mode:
The software is aimed at the data of multiple customer enterprises, and the amount of data becomes larger, such as millions or tens of millions of data: consider clustering, sub database and sub table
How to isolate data from different customers and enterprises
How to handle enterprise permissions of different customers: enterprises, packages, roles, permissions and resources need to be considered

3.2.SaaS platform data isolation

3.2.1. Independent database

This is the first scheme, that is, one tenant and one database. This scheme has the highest user data isolation level, the best security, but the cost is high.
Advantages
Provide independent databases for different tenants, which helps to simplify the expansion design of data model and meet the unique needs of different tenants; In case of failure, it is easy to recover data.
Shortcomings
The number of database installations is increased, which leads to the increase of maintenance cost and purchase cost.
This scheme is similar to the traditional one customer, one set of data and one set of deployment, except that the software is uniformly deployed in the operators. If you are facing tenants who need a very high level of data isolation, such as banks and hospitals, you can choose this mode to improve the rental pricing. If the price is low and the product takes the low price route, this scheme is generally unbearable for operators.

3.2.2. Shared database, isolated data schema

This is the second scheme, that is, multiple or all tenants share the Database, but each tenant has a Schema (also known as a user).

Advantages
It provides a certain degree of logical data isolation for tenants with high security requirements, but not complete isolation; Each database can support more tenants.
Shortcomings
In case of failure, data recovery is difficult, because restoring the database will involve the data of other tenants;
If cross tenant statistics are required, there are some difficulties.

3.2.3. Shared database, shared data schema, using foreign keys to distinguish data

This is the third scheme, that is, tenants share the same Database and Schema and share tables, but add TenantID multi tenant data fields to the tables. This is the mode with the highest degree of sharing and the lowest level of isolation.
Advantages
Compared with the three schemes, the third scheme has the lowest maintenance and purchase cost and allows the largest number of tenants supported by each database.
Disadvantages:
The isolation level is the lowest and the safety is the lowest. It is necessary to increase the development of safety during design and development;
Data backup and recovery is the most difficult. It needs to be backed up and restored table by table and item by item.
If you want to provide services to the most tenants with the least servers, and the tenants accept to sacrifice the isolation level in exchange for reducing the cost, this scheme is the most suitable.

When querying the data of different tenants, you need to add a condition: where tenant_id = login tenant ID

3.2.4. Shared database, shared data schema, different institutions use different tables

Different organizations use different tables, which are distinguished by suffixes, such as: t_employee_ali , t_employee_tx

3.3.SaaS platform permission design

RBAC: role based permission control: employee / user - > role - > permission - > resource

Based on the authority control of traditional RBAC, three roles of tenant, package and function package (non essential) are added here. When a tenant purchases a package, the package is associated with the corresponding permission. All functions enjoyed by the tenant are limited to the functions of the purchased package. The same is true for employees under tenants.
Package:

Relationship between package and permission: package table n -- N permission table
Relationship between tenant and package: Tenant n -- package n
Relationship between tenant and employee: Tenant 1 -- employee n

Tenants N-N packages N-N function packs N-N permissions 1-1 resources

Tenants 1-N employees N-N roles N-N permissions 1-1 resources

Platform administrator: create packages and allocate functions

Tenant entry: purchase packages, create employees, create roles, assign permissions, and assign roles to employees

Platform super management: create permission list, create function package, assign permissions, create package and assign function package
Tenant purchase package: purchase package, pay, and establish relationship between tenant and package
Tenants create employees: create employees, create roles, assign permissions to roles (restricted by packages), and assign roles to their own employees
All permissions must be restricted by the package

3.4. Table design of SaaS platform

4. Organization classification management

We need to choose the type of institution when the institution moves in. We have made the type of institution first

4.1. Mechanism type table design

See SQL script: tenant in HRM system database_ type

4.2. Basic code generation

Use the code generator module to generate the basic code, and use Swagger to test

4.3. Front end CRUD implementation

Connect the HRM system website of the system management front end to realize the basic CURD function of mechanism classification

4.4.Zuul configuring cross domain

Set cross domain in 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;

//Cross domain configuration
@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        //1. Add CORS configuration information
        CorsConfiguration config = new CorsConfiguration();
        //1) Do not write *, otherwise the cookie will not be used
        config.addAllowedOrigin("http://localhost:6001");
        config.addAllowedOrigin("http://127.0.0.1:6001");
        //2) Send Cookie information
        config.setAllowCredentials(true);
        //3) Allowed request mode
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        // 4) Allowed header information
        config.addAllowedHeader("*");
        //2. Add a mapping path and we will intercept all requests
        UrlBasedCorsConfigurationSource configSource = new
                UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);
        //3. Return to the new CorsFilter
        return new CorsFilter(configSource);
    }
}

5. Organization management

5.1. Mechanism table design

See Database: HRM system t_ tenant

5.2. code generation

The code generator generates the basic CRUD code of the organization

5.3. Front end CRUD implementation

Connect the HRM system website of the system management front end to realize the basic CURD function of the mechanism

5.4. Institutional settlement

5.4.1. Check in page design

The basic information of the company (saved to the tenant table) needs to be saved when the institution is settled. At the same time, a tenant account needs to be initialized (the tenant's administrator account is saved to the Employee package). At the same time, the tenant selects the package (saving is related to the intermediate table t_tenant_meal)


The form data needs to be saved to three tables: Organization tenant, administrator employee and package t_tenant_meal

5.4.2. Check in parameter submission
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. Parameter DTO encapsulation
//Accept enterprise settlement parameters
public class TenantEnteringDto {
    private Employee employee;
    private Tenant tenant;
    private Long mealId;
. . .
5.4.4. Check in Controller interface
//Institutional settlement
@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("Failed to save object!"+e.getMessage());
    }
}
5.4.5. Check in Service implementation
@Override
public void entering(TenantEnteringDto dto) {
    Tenant tenant = dto.getTenant();
    Long mealId = dto.getMealId();
    Employee employee = dto.getEmployee();
    //Judgment parameters
    if(StringUtils.isBlank(tenant.getCompanyName())){
        throw new RuntimeException("Company name cannot be empty");
    }
    //Depository
    Date now = new Date();
    tenant.setRegisterTime(now);
    baseMapper.insert(tenant);

    //Save employee
    employee.setTenantId(tenant.getId());

    //Salt: Registration: plaintext + salt (save database) = ciphertext, login: plaintext + salt (database) = ciphertext
    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);

    //Save package relationship
     Date expireDate = DateUtils.addDays(now,7);
     baseMapper.insertRelationWithMeal(
mealId,tenant.getId(),expireDate);
}
5.4.6. Save institutions and packages
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 needs to be associated with tenant id
employee password encryption and salt: add salt field
When saving the intermediate table, remember to label @ Param in the mapper interface

6. Exception handling

6.1. Custom exception

Exceptions in the system can be divided into predictable exceptions and unknown system exceptions. For predictable exceptions such as null value judgment, user name error, password error and so on, we need to return to the client. For internal exceptions such as SQL syntax error and parameter format conversion error, we need to uniformly package them into friendly prompts and then return to the client, Otherwise, the user cannot understand the exceptions inside the system.

The scheme is to use custom exceptions to encapsulate the exceptions we can predict and distinguish them from the exceptions unknown to the system. The custom exceptions are as follows:

//Global exception
public class GlobalException extends RuntimeException {

    public GlobalException(String message){
        super(message);
    }
}

Use custom exception

if(StringUitls.isNull(username)){
    throw new GlobalException("User name cannot be empty ");
}

Catch different exceptions at the controller layer

@RequestMapping(value="/{id}",method=RequestMethod.DELETE)
public AjaxResult delete(@PathVariable("id") Long id){
    try {
        tenantService.deleteById(id);
        return AjaxResult.me();
    } catch (GlobalException e) { //Custom exception, message returned directly
        e.printStackTrace();
        return AjaxResult.me().setSuccess(false).setMessage(e.getMessage());
    } catch (Exception e) { //Unknown exception, wrapped as a friendly prompt and returned
        e.printStackTrace();
        return AjaxResult.me().setSuccess(false)
							.setMessage("The system is abnormal. Please contact the administrator");
    }
}

Problem: as a result, a lot of duplicate try{}catch() code needs to be written in the controller

6.2. Assertion tool extraction

You can extract the assertion tool to judge the data, and then throw an exception, that is, you can

if(StringUitls.isNull(username)){
    throw new GlobalException("User name cannot be empty ");
}

Drawing tools such as ValidUtils

//Assertion is not empty
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);
        }
    }
}

This is all you need to do in the future:

ValidUtils.assertNotNull(tenant.getCompanyName(),"Company name cannot be blank");

ValidUtils assertion tool:

import cn.itsource.exception.GlobalException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

//Tool for parameter validation
public class ValidUtils {

    //Verification rules of mobile phone number
    private static final String PHONE_REGEX = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0-9]))\\d{8}$";

    //Verification rules of mailbox
    private static final String EMAIL_REGEX = "[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[\\w](?:[\\w-]*[\\w])?";

    //Assertion Verification mobile number
    public static void assertPhone(String phone,String message){
        //Format judgment
        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,"Invalid phone number");
    }
    //Assertion validation mailbox
    public static void assertEmail(String email,String message){
        //Format judgment
        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,"Invalid mailbox");
    }
    //Assertion is not empty
    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,"Cannot be empty");
    }
}

6.3. Global exception handling

We can extract a large number of repeated try{} code in the controller into special classes for processing

//Global exception handling
@RestControllerAdvice //You can do some things before and after the controller executes
//@ResponseBody
public class GlobalExceptionHandler {


    @ExceptionHandler(GlobalException.class)   //Handling exceptions
    public AjaxResult globalExceptionHandler(GlobalException e){
        e.printStackTrace();
        return AjaxResult.me().setSuccess(false).setMessage(e.getMessage());
    }

    @ExceptionHandler(Exception.class)   //Handling exceptions
    public AjaxResult exceptionHandler(Exception e){
        e.printStackTrace();
        return AjaxResult.me().setSuccess(false).setMessage("Ah, the system is abnormal. We're beating the programmer...");
    }
}

The RestControllerAdvice + ExceptionHandler annotation here can intercept exceptions, do different interceptions for different exceptions, and then make different processing. When the controller executes non package exceptions, it will return the correct results. If an exception occurs, go to the GlobalExceptionHandler exception interception class for processing.

How to write Controller:

@RequestMapping(value="/entering",method= RequestMethod.POST)
public AjaxResult entering(@RequestBody TenantEnteringDto dto){
    tenantService.entering(dto);
    return AjaxResult.me();
}

Don't think about try{}-catch anymore

6.4. Read configuration in yml

6.4.1. Method 1: @ valve

If configured

hrm:
  tenant:
    register:
      saltlen: 18
      trialdays: 7

Read from code

@Value("${hrm.tenant.register.saltlen}")
Private int saltlen;

@Value("${hrm.tenant.register.trialdays}")
Private int trialdays;
6.4.2. Method 2: @ ConfigurationProperties

Through ConfigurationProperties, you can read a bunch of configurations into an object, and the field name of the object should be consistent with the configured key

Create object:

//Automatically filter out a bunch of configurations from the configuration file through the prefix, and automatically bind the same name to the field of the object
@ConfigurationProperties(prefix = "hrm.tenant.register")
@Component
@Data
public class TenantRegisterProperties {
    private int saltlen = 14;
    private int trialdays = 7;
}

Use the configuration object to inject

@Autowired
private TenantRegisterProperties properties;

6.5. Error code extraction

6.5.1. Error code extraction enumeration
//Encapsulation error code
public enum ErrorCode {
    //Non nullable error code
    CODE_100_NULL_COMPANYNAME(100,"Company name cannot be empty"),
    CODE_101_FORMAT_INVALID_PHONE(101,"Invalid phone format"),
    CODE_102_EXIST_COMPANYNAME(102,"The company name has been registered");

    //Error code
    private  int code;
    //error message
    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. Error code usage
ValidUtils.isNotNull(tenant.getCompanyName(), ErrorCode.CODE_100_NULL_COMPANYNAME.);
6.5.3. Assertion tool modification
//Assertion is not empty
public static void isNotNull(String str , String errorMessage,Integer code){
    if(null == str || str.length() == 0){
        throw new ParamException(errorMessage,code);
    }
}
//The assertion is not empty. An error code is passed in
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. Exception object modification
//Global exception
public class GlobalException extends RuntimeException {
	//Exception code
	private Integer code = 0;

    public GlobalException(String message){
        super(message);
    }
	public GlobalException(String message ,Integer code){
	        super(message);
	this.code = code;
	    }
    //Get error information and error code from error enumeration
	public GlobalException(ErrorCode errorCode){
	        super(errorCode.getMessage());
	this.code = errorCode.getCode();
    }
}
6.5.5. Add error code to Ajax result
//Class of Ajax request response object
public class AjaxResult {
    private boolean success = true;
    private String message = "Operation succeeded!";
    private Integer code = 0;
    //Return to foreground object
    private Object resultObj;
    public Integer getCode() {
        return code;
    }

    public AjaxResult setCode(Integer code) {
        this.code = code;
        return this;
    }
...ellipsis...
6.5.6. Global exception tool modification
//After the controller executes and reports an exception, the exception is captured uniformly
//@ExceptionHandler: handle exceptions and catch exceptions
@ExceptionHandler(value = ParamException.class)
public AjaxResult handlerParamException(ParamException e){
    e.printStackTrace();
    return AjaxResult.me().setSuccess(false).setMessage(e.getMessage())
.setCode(e.getCode());
}

7. Course summary

7.1. difficulty

OSS enables file upload

7.2. How to master?

1. Sort out your ideas before writing code
2. After typing the code, make a summary

7.3. Interview questions

1. How are your documents managed?
2. What about the fastdfs construction process?

Keywords: Java

Added by paulmo on Tue, 25 Jan 2022 15:18:25 +0200