[Project Combat: Nucleic Acid Detection Platform] Chapter 3 Sharp Weapons

Chapter Three Sharp Weapons

Abstract: As the saying goes, if a good worker wants to be good at his work, he must first sharpen his tools. If the framework is well built, it will be very comfortable to develop, but if it is not well built, it will be very painful to develop.

A programmer can only write business code, at most he can be regarded as a code farmer, the ability to build frameworks, the ability to solve problems encountered, and algorithm ability are the skills that determine your worth, and they need long-term practice.

Objectives of this chapter

Complete the project framework construction and basic data construction

Emphasis: it's all about

overview

In the previous chapter, the overall design was completed. In this chapter, the work of building the project framework is completed, that is, the development of business functions can be achieved.

Whether a programmer is competent depends on whether he can do a good job in the development of business functions, and how much it is worth depends on what kind of framework you can build.

Before building a framework, you need to think about how to build the framework, what is the basis, and what problems to solve.

The basis must be the overall design and project requirements. When building the framework, you should consider whether there are public functions in the project requirements and overall design. The requirements are completed first. These functions are usually functions that other business modules must rely on, such as: login, authorization verification and so on.

This project avoids permission verification through the design of the deployment structure, so this part of the function can be ignored when building the framework.

Generally speaking, I think that building a framework mainly solves three types of problems:

  1. Unified business module development method

    What does development method mean? To give the simplest example, there are many ways to add a record to the database. You can use MyBatis or MyBatisPllus. MyBatis can also write to xml or to annotations. This is just adding a record, and there are not too many ways to achieve it. I often see in some old projects that there are many ways to write similar functions, which looks really messy.

    For another example, there are many database naming conventions that use underscores to separate words in field naming, but entity classes require camel naming, so you must write entity class mapping files in xml, otherwise fields cannot be mapped to On the entity class, to be honest, I really hate this way of writing. And the field naming directly uses the camel naming method, so there is no need to write a mapping file, which is cool and unnecessary.

    There is also the problem of naming the ID field. It happened in my previous team. Some people wrote it as Id, and some people wrote it as ID, which caused a lot of trouble. Every time you write an ID, you must first check how it is named in the database.

    There are countless rollover incidents like this, so if the development methods are not unified, it will cause many problems.

    The unified development method usually includes: request method, basic naming convention, paging processing method, basic CRUD writing method, common development steps of a page from front to back (this involves front-end routing, AJAX request, parameter verification, packaging, back-end parameter verification packaging, DAO layer writing method, return data encapsulation, etc.), log processing method, transaction processing method, exception handling mechanism, etc.

  2. Unified front-end and back-end interface formats

    It is very common practice to unify the front-end and back-end interface formats. The unified interface format not only unifies the data format, but also expands the message processing mechanism of the front-end and back-end. , and to achieve this effect, both the front end and the back end need to be processed. At this time, you can also encapsulate the type of pop-up window prompts in a unified interface format, such as Success, Info, Warning, and Error.

    The interface format is not only the unification of the return format, but also the unification of the request format. Some students said that there is a unified request format, isn’t it all JSON? In fact, it is not the case. For example, for commonly used paging requests, the basic request parameters are the same no matter what table it is for. Then we can change the paging request format. unite.

    In addition, the exception handling mechanism is also related to the interface format, because the request will return data to the front end regardless of success or failure. If you don’t want to lose control, you can unify them, so that even if the interface is abnormal, you can also give it very friendly response.

  3. Write a public class library

    The public class library is a must-have for the framework, including meeting processing, date format processing, null value processing, mathematical operations, encryption and decryption, and so on.

    In addition to the above content, there is also the definition of public enumeration types. Many times we will use some int types to represent enumerations, but the meaning expressed by the int type will often be confused after a long time, so the best way is Define it as an enumeration to reduce the chance of error as much as possible. Such as the return code of the project, the status field in the business process, and so on.

Let's solve it in turn

1. Create a project

backend project

In the previous chapter, we defined the project document structure. The backend contains three projects, all of which are placed in the server folder. This has the advantage of sub-modules. Several backends can become Modules and appear in the same IDEA project. Avoid the hassle of switching projects back and forth.

    ├─server												后端代码,idea中项目的根目录,下面的几个文件夹是idea中的模块
    │  ├─collector_server						
    │  ├─manager_server
    │  └─other_server

The specific method is to first create an IDEA project in the Server directory. If you have already created a folder, you may be prompted that the folder already exists when you create the project in this step. At this time, you can delete the server folder first.

When creating, the first step is to select the Maven project, and then create an empty project.

insert image description here

It should be like this after creation, where the src folder can be deleted, because the server is just a directory, we need to create several modules (Module) in the server.
insert image description here

Create a module like this:

insert image description here

In the next step, things like artifact and package still need to be modified, do not use the default ones, because our folder naming does not conform to Java naming habits.

insert image description here

Choose the default Maver package:

insert image description here

After the three projects collector_server, manager_server, and other_server are created, create a common module to put some common codes for reference by the other three modules. Note that the common module should not be created with the Spring wizard, just create an empty maven project directly.

After the final creation, it looks like this,

insert image description here

front-end project

Then let's build the front-end project structure. The method is actually similar to the back-end. I use HBuilderX. Open the project from the web directory, so you can naturally see the following folders after opening.

    └─web						    前端代码,vscode/hbuilder的根目录,省去打开多个项目窗口来回切换的麻烦
        ├─collect_web
        ├─manager_web
        ├─reciever_web
        ├─transfer_web
        └─uploader_web

Then open the terminal window, switch to the web folder, and create the projects one by one. Note that the five subfolders are deleted first when creating, because the folder will be created automatically when the npm init vue command creates the project.

Next, enter the following commands in sequence to create a vue project. By default, a VUE3 project will be created.

npm init vue
npx: installed 1 in 1.348s

Vue.js - The Progressive JavaScript Framework

√ Project name: ... collector_web    //注意这一步文件名字
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes  //这一步选择YES,其它默认选NO就可以了。
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add Cypress for both Unit and End-to-End testing? ... No / Yes
√ Add ESLint for code quality? ... No / Yes
Scaffolding project in D:\Code\NATPlatform\demo\collector_web...

Done. Now run:

  cd collector_web				
  npm install
  npm run dev

After the creation is completed, it is like this. It is recommended to create a mobile terminal first, and copy the files directly to other folders after setting up the framework.

The PC side can be built separately. Or you can also choose the method of creating projects provided by vant on the mobile terminal, and choose the creation method provided by elementUI on the PC terminal.

insert image description here

2. Agreed development method

Several development methods were exemplified above, and the conventions are listed below. Fortunately, the actions are unified during the development process. This is especially important in team development. The code written by an excellent team looks like it was written by one person, and it is easy to read. Comfortable.

  1. The Mapping annotation on the backend is given priority to use PostMapping, prohibits the use of RequestMapping, uniformly uses RequestBody for parameter reception, and only accepts Json request parameters. The POST method is preferred for sending requests at the front end, and the Get request method is not used as much as possible, and the submitted data is in JSON format. Except for file upload requests.

    @PostMapping("login")
    public ResultModel<Collector> login(@RequestBody @Valid LoginModel model)  {
          
          
    }
    
  2. When the front-end submits data, even if an ID is passed, it must be in Josn format. It is forbidden to directly pass numbers, and the back-end can use entity classes or BO classes to receive.

  3. When submitting the form, the front-end needs to be verified, and the back-end also needs to be verified. The verification method uses annotations.

  4. The business exception is thrown using the business exception class defined by itself, and the exception data package is returned by the global exception processing. Business exception refers to the abnormal process judged in the business logic, for example: the user password is wrong, the test tube code has been used when the tube is opened, and there is an unsealed test tube code in the box code when the box is sealed, etc.

    throw new BusinessException("用户名或密码不正确",ResultCodeEnum.LOGIN_ERROR);
    
  5. Description of the common module

    1. The common module stores the common code to be used by each module, such as global exception handling
    2. Entity classes are stored in the common module, pay attention to refer to resultType to write the complete package name when writing xml.
    3. The public controller interface can be placed in the common module, such as the query interface related to administrative divisions.
    4. The common module prohibits the interface of the write operation, and the interface of the write operation is only allowed to be placed in the business module
  6. SQL statements are forbidden to be written in the annotations of the method, and are uniformly written in the XML file, and the use of MyBatisPlus is prohibited.

        <select id="login" resultType="com.hawkon.common.pojo.Collector">
            select *
            from collector
            where tel = #{tel} and password=#{password};
        </select>
    
  7. The naming convention for the identity column in the database table is: personId, userId. The following naming rules are prohibited: id, ID, userID, user_id, user_ID. This naming has two advantages. One is that there is no need to write a mapping; the other is that even if you want to write a table connection, you do not need to write a mapping xml.

  8. Words in database table names are separated by underscores. The reason is that in the Windows system, mysql table names do not distinguish between uppercase and lowercase. If you use camel naming, they will all become lowercase in the end, which brings some troubles to development.

  9. The return type of xml query returns the entity class in principle. If the entity class cannot meet the needs, it is defined as the VO class. The VO class can be combined with the entity class for extension.

    It is especially suitable for the situation where there is userId in the table and userName is required when the interface is displayed.

    public class Point{
          
          
        private Integer pointId;
        private String pointName;
    	  private Integer createUserId;
    }
    
    // 查询的SQL语句:
    // select p.*,u.userName as createUserName from point p inner join user u on u.userId = p.createUserId
    public class PointVO extends Point{
          
          
      	private String createUserName;
    }
    
  10. The field names in the table adopt the camel naming method, and the use of underscores to separate words is prohibited. In this way, there is no need to configure the mapping relationship in the xml file. Reduce the amount of code written.

  11. All pojo classes are annotated with @Data to automatically generate get\set methods. If it is necessary to do additional processing, you can write the get\set method by hand. For example: the following paging query parameter entity class defines the resetRowBegin method and calls it in the set method of page and size to automatically generate rowBegin, which is the parameter used after the limit in the SQL statement.

    @Data
    public class BasePagedSearchModel {
          
          
        /**
         * 页码,从1开始
         */
        private Integer page;
        private Integer size;
        private Integer rowBegin;
        //此方法的目的是page和size更改之后自动计算rowBegin
        private void resetRowBegin(){
          
          
            rowBegin = (page-1)*size;
        }
    
        public Integer getPage() {
          
          
            return page;
        }
    
        public void setPage(Integer page) {
          
          
            this.page = page;
            resetRowBegin();
        }
    
        public Integer getSize() {
          
          
            return size;
        }
    
        public void setSize(Integer size) {
          
          
            this.size = size;
            resetRowBegin();
        }
    }
    
  12. The input parameters for access must define the input parameter pojo class, and put them in the pojo.bo package. If the method is: Login, then the pojo class is named: LoginBO. If the public BO class can meet the needs, it does not need to be defined separately.

    @RequestMapping("login")
    public ResultModel<Collector> login(@RequestBody @Valid LoginModel model)  {
          
          
    }
    
    
    @Data
    public class LoginModel {
          
          
        @NotEmpty(message = "手机号不能为空")
        @Size(min = 11, max = 11,message = "手机号必须是11位")
        private String tel;
        @NotEmpty(message = "密码不能为空")
        @Size(min = 6,message = "密码至少6位 ")
        private String password;
    }
    
  13. The front-end VUE files are uniformly written in a combined way.

    <script setup>
    import { ref } from 'vue';
    import { Toast } from 'vant';
    import api from '@/common/api.js';
    import { useRouter } from "vue-router";
    const router = useRouter();
    
    const loginForm = ref({tel:'1877777777',password:'123'});
    const now = new Date();
    const onSubmit = (values) => {
    	api.post("/collector/login",loginForm.value)
    	.then(res=>{
    		window.sessionStorage["user"] = JSON.stringify(res.data);
    		router.push("/SelectPoint");
    	})
    	.catch(res=>{
    		console.log("错误",res)
    	})
    	
    };
    </script>
    
  14. Time format processing: By default, the format of 2022-01-09 is used. If there is a time type in the entity class, you need to add annotations to set the default format.

        @DateTimeFormat(pattern = "yyyy-MM-dd")
        @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
        private Date registTime;
    

Some of the conventions of the above development methods may not necessarily be widely used naming conventions, and some are not the most efficient way of development, such as prohibiting the use of MyBatisPlus, but in fact, as long as the agreement is unified in the team, what kind of code does everyone have when problems arise? Where, in this way, the efficiency is not necessarily much worse when it is actually developed. In the final analysis, the development specification is just a choice, and there is no right or wrong.

Many conventions are summed up in actual work. Beginners often do not realize that this is a problem. When encountering problems at work, think more and summarize more, and gradually find a development method and convention that suits you and your team.

3. Agree on the front-end and back-end interface formats

Many beginners return data directly, such as returning an object, and the json given to the front end is:

{
    
    
	"name":'tom',
	"age":32
}

In the actual development of the enterprise, it may be like this:

{
    
    
	code:200,
	data:{
    
    
		"name":'tom',
  	"age":32
	},
	message:'',
    dialogType:'success'
}

They packaged the actual returned data in a unified way. In addition to the data, it also contains some other information. This information can be used by the front end for unified exception handling, message reminders, and feedback mechanisms.

The interface format of our project is defined as:

{
    
    
	code:0,代码接口响应代码,0表示成功,100开头表示服务器错误
	data:{
    
    
		//数据
	},
	errMsg:''
}

The backend defines a unified return type:

package com.hawkon.common.pojo;

import lombok.Data;

@Data
public class ResultModel<T> {
    
    
    private Integer code;
    private T data;
    private String errMsg;

    public ResultModel(ResultCodeEnum codeEnum, T data, String errMsg) {
    
    
        this.code = codeEnum.getCode();
        this.data = data;
        this.errMsg = errMsg;
    }
  	//大多数据情况下使用该方法返回成功的响应结果,尽量减少业务代码编写量
    public static<T> ResultModel<T>  success(T data){
    
    
        return new ResultModel<>(ResultCodeEnum.SUCCESS,data,"");
    }
}

Among them, ResultCodeEnum represents the interface response code, and the enumeration definition is used to prevent random definition of the response code and cause unnecessary trouble.

package com.hawkon.common.enums;

/**
 * 接口响应代码
 */
public enum ResultCodeEnum {
    
    

    SUCCESS(0),
    /**
     * 其它服务器代码错误
     */
    ERROR(100),
    /**
     * 登录错误
     */
    LOGIN_ERROR(101),
    /**
     * 登录状态失效
     */
    NOT_LOGIN(102),
    /**
     * 参数验证错误
     */
    PARAMS_ERROR(103);

    private int code;
    ResultCodeEnum(int i) {
    
    
        this.code = i;
    }
    public int getCode() {
    
    
        return code;
    }
}

4. Global exception handling

Global exception handling has been mentioned many times before. For these specific methods, first define BusinessException, which is used to specifically throw business exceptions.

package com.hawkon.common.exception;

import com.hawkon.common.pojo.ResultCodeEnum;
import lombok.Data;

@Data
public class BusinessException extends  Exception{
    
    
    private ResultCodeEnum resultCode;

    public BusinessException(String message, ResultCodeEnum resultCode) {
    
    
        super(message);
        this.resultCode = resultCode;
    }
}

package com.hawkon.common.exception;

import com.hawkon.common.enums.ResultCodeEnum;
import com.hawkon.common.pojo.ResultModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.stream.Collectors;

@Slf4j
@ControllerAdvice
public class GolbalException {
    
    

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultModel<Object> handle(Exception e) {
    
    
        if (e instanceof BusinessException) {
    
    
            BusinessException be = (BusinessException) e;
            log.error("业务逻辑处理异常:{}", (be).getMessage());
            log.trace(e.getStackTrace().toString());
            e.printStackTrace();
            return new ResultModel<>(be.getResultCode(),null,be.getMessage());
        }
        if(e instanceof MethodArgumentNotValidException){
    
    
            MethodArgumentNotValidException me = (MethodArgumentNotValidException) e;
            log.error("业务逻辑处理异常:{}", (me).getMessage());
            log.trace(e.getStackTrace().toString());
            e.printStackTrace();
            String errMsg = me.getBindingResult().getAllErrors()
                    .stream().map(err->err.getDefaultMessage()).collect(Collectors.joining(","));
            return  new ResultModel<>(ResultCodeEnum.PARAMS_ERROR,null,errMsg);
        }
        log.error("系统异常:{}", e);
        return new ResultModel<>(ResultCodeEnum.ERROR,null,"系统错误");
    }
}

Because each back-end module needs to be used, these two classes are written in the common module, and the common common code module needs to be referenced in the business module. See the fifth section for the reference method.

Then you need to add the CompnentScan annotation to the SpringBoot startup class to ensure that the code in the common class library will be scanned when SpringBoot starts

@ComponentScan(basePackages = {
    
    "com.hawkon.collector.*","com.hawkon.common"})

Five, public class library

The code in the public class library mainly includes:

  • global exception handling
  • Unified interface format definition
  • Paging query interface parameter pojo class
  • Tools

Backend public class library

The class structure of the completed module is as follows:

insert image description here

The code class that was not posted before is posted below:

Global.java: Mainly used to obtain request and response objects globally, a must-have artifact for Web projects. In this way, you don't need to pass the request and response objects back and forth.

package com.hawkon.common.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class Global {
    
    
    public static HttpServletRequest request;
    public static HttpServletResponse response;

    @Autowired
    HttpServletRequest httpServletRequest;
    @Autowired
    HttpServletResponse httpServletResponse;
    @PostConstruct
    public void init(){
    
    
        request = httpServletRequest;
        response = httpServletResponse;
    }
}

Md5Util.java: md5 encryption algorithm tool class, needless to say. This project first uses the most basic version of the encryption method, and then extends the more advanced encryption method in the last chapter (continue to practice).

package com.hawkon.common.utils;

import sun.misc.BASE64Encoder;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Md5Util {
    
    
    public static String encode(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
    
    
        //确定计算方法
        MessageDigest md5=MessageDigest.getInstance("MD5");
        BASE64Encoder base64en = new BASE64Encoder();
        //加密后的字符串
        String newstr=base64en.encode(md5.digest(str.getBytes("utf-8")));
        return newstr;
    }
}

Note that not all common codes are suitable to be placed in the common module. For example, in the collector module, I wrote the common code for setting and obtaining the session login status, and this part is only suitable to be placed in the collector module.

Also note that the code of the public class library is written in the common module, and it needs to be referenced by other modules. The reference method is:

Step 1: Right click on the module and select Open Module Setting

insert image description here

Select Dependencies, Module Dependency. Select the common module.

insert image description here

After the operation is completed, you will see the following code in the pom.xml file. If you are proficient in writing pom files, you can also directly modify the pom file.

        <dependency>
            <groupId>com.hawkon</groupId>
            <artifactId>common</artifactId>
            <version>1.0</version>
            <scope>compile</scope>
        </dependency>

front-end common class library

The front-end public class library has to do

module configuration

Vue3 does not enable LAN access by default, so you cannot use mobile phones to design front-end pages. The opening method is also very simple, find the dev in scripts in package.json, and add –host after it.

/package.json

{
    
    
  "name": "collector_web",
  "version": "0.0.0",
  "scripts": {
    
    
    "dev": "vite --host",
    "build": "vite build",
    "preview": "vite preview --port 4173"
  },
  //....
}

The second main configuration is the reverse proxy, which must be done for the front-end joint debugging, otherwise the joint debugging cannot be done. Find vite.config.js,

Add the configuration content of the server node. Among them, proxy refers to the proxy, and "/api" refers to the fact that all requests starting with /api initiated by the front end will be forwarded by the proxy.

Forward to the server defined by target, but our backend server address does not actually have a path starting with /api, so we wrote rewrite to replace /api with an empty string. For example, the front-end access address is: http://localhost:5143, then the address of the login function request is: http://localhost:5143/api/collector/login, and it will be sent to http://localhost:8091 after being proxied /collecotr/login, which happens to be the interface address of our backend server.

But this request is not directly sent to 8091 by the front-end page, but is sent to 8091 by the reverse proxy of the vite js module in nodejs, so there is no problem of cross-domain requests.

The complete code is as follows:

/vite.config.js

import {
    
    
	fileURLToPath,
	URL
} from 'node:url'

import {
    
    
	defineConfig
} from 'vite'
import vue from '@vitejs/plugin-vue'

import Components from 'unplugin-vue-components/vite';
import {
    
    
	VantResolver
} from 'unplugin-vue-components/resolvers';

// https://vitejs.dev/config/
export default defineConfig({
    
    
	plugins: [vue(),
		Components({
    
    
			resolvers: [VantResolver()],
		}),
	],
	resolve: {
    
    
		alias: {
    
    
			'@': fileURLToPath(new URL('./src',
				import.meta.url))
		}
	},
	server: {
    
    
		proxy: {
    
    
			'/api': {
    
    // 
				target: 'http://localhost:8091', // 后端代码地址和端口
				changeOrigin: true,
				rewrite: path => path.replace(/^\/api/, '') // 重写路径
			}
		}
	}
})

The third configuration, the mobile terminal module uses the vantUI component. For convenience, global references can be made, so that there is no need to write reference codes for each page.

Step 1: Install vantUI, execute npm install vant in the module directory

Step 2: Add codes in vite.config.js, which are lines 12-14 and 18-22 in the above vite.config.js file.

import Components from 'unplugin-vue-components/vite';
import {
    
    
	VantResolver
} from 'unplugin-vue-components/resolvers';

	plugins: [vue(),
		Components({
    
    
			resolvers: [VantResolver()],
		}),
	],

Step 3: Refer to the vantUI style in main.js, add a line of code to correspond to the 4th line of code in /src/main.js below

import 'vant/es/toast/style';

public method

Define some constants and tool classes used globally.

/src/common/common.js

export default {
    
    
    VERSION: "V1.0",  //需要显示版本号的地方使用
	urlPrefix:"/api", //后端请求的统一前缀,用于在api.js中发送请求时统一加前缀。再由反向代码把api开头的请求转发到后端服务器
    //返回代码常量,要和后台的常量定义保持一致
    RESULT_CODE: {
    
    
      SUCCESS: 0, ERROR: 100, LOGIN_ERROR: 101,NOT_LOGIN:102,PARAMS_ERROR:103,REGISTER_ERROR:104,BUSSINESS_ERROR:105
    },
    utils: {
    
    
      nullToEmpty(obj) {
    
    
        if (obj == null || typeof (obj) == "undefined") {
    
    
          return "";
        }
        return obj;
      }
    }
  }

Package axios

/src/common/api.js

import axios from 'axios'
import {
    
    
	Toast
} from 'vant'
import common from './common.js'
import router from "../router/index.js";
import cookie from "vue-cookie";

// 创建一个axios实例
const api = axios.create()
api.defaults.baseURL = common.urlPrefix;
//设置请求时把token从cookie中取出,放到header中。
api.interceptors.request.use(function(config) {
    
    
	// 发送请求的相关逻辑
	// config:对象  与 axios.defaults 相当
	// 借助config配置token
	let token = cookie.get("token")
	// 判断token存在再做配置
	if (token) {
    
    
		config.headers.token = token
	}
	return config
}, function(error) {
    
    
	// Do something with request error
	return Promise.reject(error)
})

// 设置axios拦截器: 响应拦截器
api.interceptors.response.use(res => {
    
    
	Toast.clear();
	// 成功响应的拦截
	return Promise.resolve(res.data);
}, err => {
    
    
	Toast.clear();
	var res = err.response.data
	if (res.code) {
    
    
		// 失败响应的status需要在response中获得
		switch (res.code) {
    
    
			// 对得到的状态码的处理,common,是在前端定义的错误代码常量
			case common.RESULT_CODE.ERROR: //100
				console.log('服务器错误')
				Toast.fail("服务器错误");
				break;
			case common.RESULT_CODE.LOGIN_ERROR: //101
				console.log('登录失败')
				Toast.fail(res.errMsg);
				break;
			case common.RESULT_CODE.NOT_LOGIN: //102
				console.log('未登录')
				Toast.fail('登录状态失效,请重新登录');
				sessionStorage.clear();
				router.push("/");
				console.log('跳转')
				break;
			case common.RESULT_CODE.PARAMS_ERROR: //103
				console.log('参数错误')
				Toast.fail(res.errMsg); //如果是参数错误,说明是对参数实体类的验证,有必要提示具体内容
				break;
			case common.RESULT_CODE.REGISTER_ERROR: //104
				console.log('参数错误')
				Toast.fail(res.errMsg); //注册错误,信息提示由后端给
				break;
			case common.RESULT_CODE.BUSSINESS_ERROR: //105
				Toast.fail(res.errMsg); //业务逻辑错误,错误信息要提示。
				break;
			default:
				console.log('其他错误')
				break
		}
	} else {
    
    
		Toast.fail("请求异常");
	}
	return Promise.reject(res)
})



export default api;

date filter

The date filter references dayjs, and the reference method is to execute npm installl dayjs in the module directory first

Then add the code in main.js, line 5, line 12-25.

main.js

import {
    
     createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'vant/es/toast/style';
import dayjs from 'dayjs'

const app = createApp(App)

app.use(router)


//添加全局日期过滤器
//用法:<span>{
    
    { $filters.format(scope.row.createDt) }}</span>
app.config.globalProperties.$filters = {
    
    
    format(value,format) {
    
    
        if (value) {
    
    
			if(!format){
    
    
				format="YYYY-MM-DD";
			}
            return dayjs(value).format(format)
        } else {
    
    
            return ''
        }
    }
}

app.mount('#app')

6. Develop a completed function point (login)

The agreement has been completed above, and the public code has also been completed. Next, a function point can be developed.

Why build a framework to develop a function point? Haven't you heard that the working mode of programmers is CV Dafa? Although it is a CV Dafa, you also need to know where to start from C, where to go to V, and how to change after the CV is over. This is a qualified programmer.

Here comes the problem. If the framework does not give a standard function point, then if it is developed by multiple people, the writing method of the function point will be various, because everyone has no reference.

The first function point is to choose login is the most appropriate, that is, there are sql statements, there are business logic, and it also involves login verification, which is also one of the components of the overall framework. The project has multiple modules, and I choose to log in to the collection point module.

After completion, the code can be copied to other modules and then modified.

rear end

Ok, let's write the backend interface first.

The database table structure is:
insert image description here

First, write the configuration file:

# 应用名称
spring.application.name=collector_server
#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath*:mappers/*.xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=com.hawkon.collector.pojo
# 应用服务 WEB 访问端口
server.port=8091
# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://localhost:3306/natDb?serverTimezone=UTC&characterEncoding=utf8&allowMultiQueries=true
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=123
logging.level.com.hawkon.collector.dao=debug
# 设定项目部署的城市
cityCode=4103
provinceCode=41

Entity class

package com.hawkon.common.pojo;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.Date;

@Data
public class Collector {
    
    
    private Integer collectorId;
    @NotEmpty(message="电话号码不能为空")
    private String tel;
    @NotEmpty(message="身份证号不能为空")
    @Pattern(regexp="^([1-6][1-9]|50)\\d{4}(18|19|20)\\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$")
    private String idcard;
    private String password;
    @NotEmpty(message = "姓名不能为空")
    private String name;
    private Integer collectorType;
    private Integer organizationId;
    @NotNull(message = "所在行政区划不能为空")
    private Long areaId;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date registTime;
}

Parameter BO:

package com.hawkon.collector.pojo.bo;

import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

@Data
public class LoginModel {
    
    
    @NotEmpty(message = "手机号不能为空")
    @Size(min = 11, max = 11,message = "手机号必须是11位")
    private String tel;
    @NotEmpty(message = "密码不能为空")
    @Size(min = 6,message = "密码至少6位 ")
    private String password;
}

CollectorMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.hawkon.collector.dao.CollectorDao">
    <select id="login" resultType="com.hawkon.common.pojo.Collector">
        select *
        from collector
        where tel = #{tel}
          and password = #{password};
    </select>
</mapper>
package com.hawkon.collector.dao;

import com.hawkon.collector.pojo.Collector;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface CollectorDao {
    
    
    public Collector login(@Param("tel") String tel,@Param("password") String password);
}

Service layer

package com.hawkon.collector.service;

import com.hawkon.common.pojo.Collector;
import com.hawkon.collector.pojo.bo.LoginModel;
import com.hawkon.common.exception.BusinessException;
import org.springframework.stereotype.Service;

import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;

@Service
public interface ICollectorService {
    
    
    public Collector login(LoginModel model) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException;

    public Collector getCollectorByToken(String token);
}

package com.hawkon.collector.service.impl;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.hawkon.collector.dao.CollectorDao;
import com.hawkon.collector.utils.SessionUtil;
import com.hawkon.common.pojo.Collector;
import com.hawkon.collector.pojo.bo.LoginModel;
import com.hawkon.collector.service.ICollectorService;
import com.hawkon.common.enums.CollectorType;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.enums.ResultCodeEnum;
import com.hawkon.common.utils.Global;
import com.hawkon.common.utils.Md5Util;
import com.hawkon.common.utils.NullUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.Cookie;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

@Service
public class CollectorService implements ICollectorService {
    
    
    @Autowired
    CollectorDao collectorDao;

    @Override
    public Collector login(LoginModel model) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
    
    
        String md5Password = Md5Util.encode(model.getPassword());
        Collector collector = collectorDao.login(model.getTel(), md5Password);
        if (collector == null) {
    
    
            throw new BusinessException("用户名或密码不正确", ResultCodeEnum.LOGIN_ERROR);
        }
        String token = getToken(collector);

        //把token存到cokkie中,并设置过期时间,一天
        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setMaxAge(7 * 24 * 60 * 60);
        Global.response.addCookie(cookie);
        //返回前端之前要把密文的密码清除掉。
        collector.setPassword(null);
        return collector;
    }
  
    public String getToken(Collector user) {
    
    
        Date start = new Date();
        long currentTime = System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000;//7天有效时间
        Date end = new Date(currentTime);
        String token = "";

        token = JWT.create().withAudience(user.getCollectorId().toString()).withIssuedAt(start).withExpiresAt(end)
                .sign(Algorithm.HMAC256(user.getPassword()));
        return token;
    }

    @Override
    public Collector getCollectorByToken(String token) {
    
    
        String userId = JWT.decode(token).getAudience().get(0);
        if (NullUtil.isEmpty(userId)) {
    
    
            return null;
        }
        Integer collectorId = Integer.parseInt(userId);
        Collector collector = collectorDao.getCollectorById(collectorId);
        return collector;
    }
}

Controller layer

package com.hawkon.collector.controller;

import com.hawkon.common.pojo.Collector;
import com.hawkon.collector.pojo.bo.LoginModel;
import com.hawkon.collector.service.ICollectorService;
import com.hawkon.collector.utils.SessionUtil;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.pojo.ResultModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;

@RestController
@RequestMapping("/collector")
public class CollectorController {
    
    
    @Autowired
    ICollectorService collectorService;

    @PostMapping("login")
    public ResultModel<Collector> login(@RequestBody @Valid LoginModel model) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
    
    
        Collector collector = collectorService.login(model);
        //session也存储一下,取登录用户直接从session取,减少sql查询请求
        SessionUtil.setCurrentUser(collector);
        return ResultModel.success(collector);
    }
    @PostMapping("logout")
    public ResultModel<Object> logout() {
    
    
        SessionUtil.clear();
        //清除cookie
        Cookie cookie = new Cookie("token", "");
        cookie.setPath("/");
        cookie.setMaxAge(0);
        Global.response.addCookie(cookie);
        return ResultModel.success(null);
    }
}

Common code in the module

package com.hawkon.collector.common;

public class Consts {
    
    
    /**
     * 定义统一的会话名称
     */
    public static final String SESSION_USER_KEY = "collector";
}

package com.hawkon.collector.utils;

import com.hawkon.collector.common.Consts;
import com.hawkon.common.pojo.Collector;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.enums.ResultCodeEnum;
import com.hawkon.common.utils.Global;

/**
 * session工具类
 */
public class SessionUtil {
    
    
    public static Collector getCurrentUser() throws BusinessException {
    
    
        Object obj = Global.request.getSession().getAttribute(Consts.SESSION_USER_KEY);
        if (obj == null) {
    
    
            return null;
        }
        if (obj instanceof Collector) {
    
    
            return (Collector) obj;
        } else {
    
    
            return null;
        }
    }

    public static void setCurrentUser(Collector collector) throws BusinessException {
    
    
        Global.request.getSession().setAttribute(Consts.SESSION_USER_KEY, collector);
    }

    public static void clear() {
    
    
        Global.request.getSession().invalidate();
    }
}

Finally, deal with the login interceptor so that access to the business interface is not allowed without login. Strictly speaking, this part of the code is actually part of the public code.

package com.hawkon.collector.common;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.hawkon.collector.service.ICollectorService;
import com.hawkon.common.enums.ResultCodeEnum;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.pojo.Collector;
import com.hawkon.collector.utils.SessionUtil;
import com.hawkon.common.utils.Global;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.websocket.Session;
import java.lang.reflect.Method;
@Component
public class UserLoginInterceptor implements HandlerInterceptor {
    
    

    @Autowired
    ICollectorService collectorService;

    /***
     * 在请求处理之前进行调用(Controller方法调用之前)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //统一拦截(查询当前session是否存在collecotr)
        Collector user = SessionUtil.getCurrentUser();
        if (user != null) {
    
    
            return true;
        }
//        return false;

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
    
    
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
    
    
                return true;
            }
        }
        String token = Global.request.getHeader("token");// 从 http 请求头中取出 token
        if (token == null) {
    
    
            throw new BusinessException("非法请求,无登录令牌", ResultCodeEnum.NOT_LOGIN);
        }
        Collector collector = collectorService.getCollectorByToken(token);
        if (collector == null) {
    
    
            throw new BusinessException("登录状态过期", ResultCodeEnum.NOT_LOGIN);
        }
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(collector.getPassword())).build();
        try {
    
    
            jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
    
    
            throw new BusinessException("登录令牌过期,请重新登录",ResultCodeEnum.LOGIN_ERROR);
        }
        //如果令牌有效,把登录信息存到session中,这样如果需要用到登录信息不用总到数据库查询。
        SessionUtil.setCurrentUser(collector);
        return true;
        //该方法没有做异常处理,因为在SessionUtil中已经处理了登录状态的异常。只要getCurrentUser()返回有值肯定就是成功的。
    }

    /***
     * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
    
    }

    /***
     * 整个请求结束之后被调用,也就是在DispatchServlet渲染了对应的视图之后执行(主要用于进行资源清理工作)
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    
    }

}

Write a configuration class, without this, the interceptor cannot take effect.

package com.hawkon.collector.config;

import com.hawkon.collector.common.UserLoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class LoginConfig implements WebMvcConfigurer {
    
    
    @Autowired
    UserLoginInterceptor userLoginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        //注册TestInterceptor拦截器
        InterceptorRegistration registration = registry.addInterceptor(userLoginInterceptor);
        registration.addPathPatterns("/**"); //所有路径都被拦截
        //本项目计划是前后端分享部署,其实下面的代码除了/login需要以外,其它并不是必须的。不过可以保留。
        registration.excludePathPatterns(    //添加不拦截路径
                "/collector/login",                    //登录路径
                "/collector/register",                    //注册方法
                "/collector/forget",                    //忘记密码
                "/area/**/*",                    //请求区域数据
                "/**/*.html",                //html静态资源
                "/**/*.js",                  //js静态资源
                "/**/*.css"                  //css静态资源
        );
    }

}

PassToken.java: Skip Token verification, which method does not require login permission, you can add a comment, and you don’t need to always change the config.

package com.hawkon.collector.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 用来跳过Token验证的注解
 */
@Target({
    
    ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    
    
    boolean required() default true;
}

After writing the backend, you can use apipost to see the effect of the request first, and pay attention to the red box.

wrong situation

insert image description here

successful case

insert image description here

Pay attention to saving the token when the login is successful, and add it to the header when testing other interfaces

insert image description here

The code structure after the backend is completed, the screenshot contains some codes of other functions.

insert image description here

Let's take a look at the code structure of the common module

insert image description here

front end

To realize the complete login function, the front end must have at least two pages, one is the login page, and the other is the page to be redirected after login.

In addition to implementing the login business logic, it is also necessary to implement the login route guard to ensure that the business interface cannot be accessed without login.

Then the front end also needs to save the login status, we can use sessionStorage to save it. To be honest, this method is relatively low-level, and it is not recommended to do this in actual projects, because people who know a little bit about development can bypass it and directly enter the business interface. So the backend also needs to judge the login status.

After the project is created, the original code should be cleaned up first.

Among them, App.vue should pay attention when cleaning, and keep a RouterView label. After cleaning, the code is as follows:

<script setup>
import { RouterView } from 'vue-router'
</script>

<template>
  <RouterView />
</template>

<style scoped>
</style>

After cleaning, create the Login.vue page and SelectPoint.vue page first.

Login.vue uses the vantui component.

/src/views/Login.vue

<script setup>
	import {
		ref
	} from 'vue';
	import {
		Toast
	} from 'vant';
	import {
		RouterLink,useRouter
	} from 'vue-router'
	//引用封装过后的axios组件
	import api from '@/common/api.js';
	const router = useRouter();

	const loginForm = ref({
		tel: '18638898990',
		password: '156011'
	});
	const now = new Date();
	const onSubmit = (values) => {
		api.post("/collector/login", loginForm.value)
			.then(res => {
				//代码到这里一定是登录成功,因为失败的时候会被api.js中的拦截器处理掉。
				//成功的时候把返回的数据保存在sessionStorage中,因为sessionStorage只能保存String,所以要用JSON.stringify转换一下。
				window.sessionStorage["user"] = JSON.stringify(res.data);
				//跳转路由
				router.push("/SelectPoint");
			})
			.catch(res => {
				console.log("错误", res)
			})
	};
	
</script>

<template>
	<van-row>
		<van-col span="24">
			<h2 style="text-align: center;">全场景疫情病原体检测信息系统</h2>
		</van-col>
		<van-col span="24">
			<van-form @submit="onSubmit">
				<van-cell-group inset>
					<van-field v-model="loginForm.tel" name="tel" label="手机号" placeholder="请输入手机号"
						:rules="[{ required: true, message: '请填写手机号' }]" />
					<van-field v-model="loginForm.password" type="password" name="password" label="密码"
						placeholder="默认密码为身份证后6位" :rules="[{ required: true, message: '请填写密码' }]" />
				</van-cell-group>
				<div style="margin: 16px;">
					<van-button round block type="primary" native-type="submit">
						提交
					</van-button>
				</div>
			</van-form>
		</van-col>
		<van-col span="12" style="padding-left: 1em;">
			<RouterLink to="/Register">注册</RouterLink>
		</van-col>
		<van-col span="12" style="text-align: right;padding-right: 1em">
			<RouterLink to="/Forget">忘记密码</RouterLink>
		</van-col>
	</van-row>
</template>

SelectPoint.vue: Just be able to display the page first.

/src/views/SelectPoint.vue

<script setup>
</script>

<template>
	选择采集点
</template>

Then modify the routing file /src/router/index.js. Note that there are two ways to write the reference component in the route, and it is recommended to use the way of writing in line 23. Because there are too many routes, it is annoying to drag the scroll bar up and down when writing this part of the code.

By the way, write the routing guard function, so that you can’t access the business interface when you are not logged in

router.beforeEach((to, from) => {
    
    
	//避免路由循环,会带来灾难性BUG:页面卡死,权限不受控的页面都要判断
	if (to.name == 'Login'||to.name=="Home"||to.name=="Register"||to.name=="Forget") {
    
    
		return true;
	}
	//如果sessionStorage["user"]中有值,说明有可能是登录状态
	if (sessionStorage["user"]) {
    
    
		var user = JSON.parse(sessionStorage["user"]);
		//再判断一下SessionStorage中保存的user是否有collectorId字段,避免被人为放入一个非法的字符串
		if (sessionStorage["user"] && user && user.collectorId) {
    
    
			return true;
		}
	}
	return {
    
    name:"Login"};
})

/src/router/index.js

import {
    
    
	createRouter,
	createWebHistory
} from 'vue-router'
import Login from '../views/Login.vue'

const router = createRouter({
    
    
	history: createWebHistory(
		import.meta.env.BASE_URL),
	routes: [{
    
    
			path: '/',
			name: 'Home',
			component: Login
		},
		{
    
    
			path: '/Login',
			name: 'Login',
			component: Login
		},
		{
    
    
			path: '/SelectPoint',
			name: 'SelectPoint',
			component: () => import('../views/SelectPoint.vue')
		}
	]
})


router.beforeEach((to, from) => {
    
    
	//避免路由循环,会带来灾难性BUG:页面卡死,权限不受控的页面都要判断
	if (to.name == 'Login'||to.name=="Home"||to.name=="Register"||to.name=="Forget") {
    
    
		return true;
	}
	//如果sessionStorage["user"]中有值,说明有可能是登录状态
	if (sessionStorage["user"]) {
    
    
		var user = JSON.parse(sessionStorage["user"]);
		//再判断一下SessionStorage中保存的user是否有collectorId字段,避免被人为放入一个非法的字符串
		if (sessionStorage["user"] && user && user.collectorId) {
    
    
			return true;
		}
	}
	return {
    
    name:"Login"};
})

export default router

Finally, let's take a look at the effect of logging in.

insert image description here

Finally, our framework is finished. Don’t forget to submit it to GIT. As the saying goes, if you want to do a good job, you must first sharpen your tools. If the framework is set up well, it will be very comfortable to develop, but if it is not well set up, it will be very painful to develop. .

A programmer can only write business code, at most he can be regarded as a code farmer, the ability to build frameworks, the ability to solve problems encountered, and algorithm ability are the skills that determine your worth, and they need long-term practice.

Students who want to knock out this project together can do it three times, so as not to get lost.
insert image description here

Guess you like

Origin blog.csdn.net/aley/article/details/128048702