【Spring Cloud】黑马头条 用户服务创建、登录功能实现

点击去看上一篇

一、创建用户 model

1.创建用户数据库库 leadnews_user

核心表 ap_user

建库建表语句

这里一定要使用 navicat,执行SQL 文件,以防止 cmd 中的编码问题

先将 SQL 语句,保存在电脑中,再使用 navicat 打开

CREATE DATABASE IF NOT EXISTS leadnews_user DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE leadnews_user;
SET NAMES utf8;
/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50721
Source Host           : localhost:3306
Source Database       : leadnews_user

Target Server Type    : MYSQL
Target Server Version : 50721
File Encoding         : 65001

Date: 2021-04-12 13:58:42
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for ap_user
-- ----------------------------
DROP TABLE IF EXISTS `ap_user`;
CREATE TABLE `ap_user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `salt` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码、通信等加密盐',
  `name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
  `password` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码,md5加密',
  `phone` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
  `image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
  `sex` tinyint(1) unsigned DEFAULT NULL COMMENT '0 男\r\n            1 女\r\n            2 未知',
  `is_certification` tinyint(1) unsigned DEFAULT NULL COMMENT '0 未\r\n            1 是',
  `is_identity_authentication` tinyint(1) DEFAULT NULL COMMENT '是否身份认证',
  `status` tinyint(1) unsigned DEFAULT NULL COMMENT '0正常\r\n            1锁定',
  `flag` tinyint(1) unsigned DEFAULT NULL COMMENT '0 普通用户\r\n            1 自媒体人\r\n            2 大V',
  `created_time` datetime DEFAULT NULL COMMENT '注册时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户信息表';

-- ----------------------------
-- Records of ap_user
-- ----------------------------
INSERT INTO `ap_user` VALUES ('1', 'abc', 'zhangsan', 'abc', '13511223453', null, '1', null, null, '1', '1', '2020-03-19 23:22:07');
INSERT INTO `ap_user` VALUES ('2', 'abc', 'lisi', 'abc', '13511223454', '', '1', null, null, '1', '1', '2020-03-19 23:22:07');
INSERT INTO `ap_user` VALUES ('3', 'sdsa', 'wangwu', 'wangwu', '13511223455', null, null, null, null, null, '1', null);
INSERT INTO `ap_user` VALUES ('4', '123abc', 'admin', '81e158e10201b6d7aee6e35eaf744796', '13511223456', null, '1', null, null, '1', '1', '2020-03-30 16:36:32');
INSERT INTO `ap_user` VALUES ('5', '123', 'suwukong', 'suwukong', '13511223458', null, '1', null, null, '1', '1', '2020-08-01 11:09:57');
INSERT INTO `ap_user` VALUES ('6', null, null, null, null, null, null, null, null, null, null, null);

-- ----------------------------
-- Table structure for ap_user_fan
-- ----------------------------
DROP TABLE IF EXISTS `ap_user_fan`;
CREATE TABLE `ap_user_fan` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` int(11) unsigned DEFAULT NULL COMMENT '用户ID',
  `fans_id` int(11) unsigned DEFAULT NULL COMMENT '粉丝ID',
  `fans_name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '粉丝昵称',
  `level` tinyint(1) unsigned DEFAULT NULL COMMENT '粉丝忠实度\r\n            0 正常\r\n            1 潜力股\r\n            2 勇士\r\n            3 铁杆\r\n            4 老铁',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `is_display` tinyint(1) unsigned DEFAULT NULL COMMENT '是否可见我动态',
  `is_shield_letter` tinyint(1) unsigned DEFAULT NULL COMMENT '是否屏蔽私信',
  `is_shield_comment` tinyint(1) unsigned DEFAULT NULL COMMENT '是否屏蔽评论',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户粉丝信息表';

-- ----------------------------
-- Records of ap_user_fan
-- ----------------------------

-- ----------------------------
-- Table structure for ap_user_follow
-- ----------------------------
DROP TABLE IF EXISTS `ap_user_follow`;
CREATE TABLE `ap_user_follow` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` int(11) unsigned DEFAULT NULL COMMENT '用户ID',
  `follow_id` int(11) unsigned DEFAULT NULL COMMENT '关注作者ID',
  `follow_name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '粉丝昵称',
  `level` tinyint(1) unsigned DEFAULT NULL COMMENT '关注度\r\n            0 偶尔感兴趣\r\n            1 一般\r\n            2 经常\r\n            3 高度',
  `is_notice` tinyint(1) unsigned DEFAULT NULL COMMENT '是否动态通知',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户关注信息表';

-- ----------------------------
-- Records of ap_user_follow
-- ----------------------------

-- ----------------------------
-- Table structure for ap_user_realname
-- ----------------------------
DROP TABLE IF EXISTS `ap_user_realname`;
CREATE TABLE `ap_user_realname` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` int(11) unsigned DEFAULT NULL COMMENT '账号ID',
  `name` varchar(20) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户名称',
  `idno` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '资源名称',
  `font_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '正面照片',
  `back_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '背面照片',
  `hold_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手持照片',
  `live_image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '活体照片',
  `status` tinyint(1) unsigned DEFAULT NULL COMMENT '状态\r\n            0 创建中\r\n            1 待审核\r\n            2 审核失败\r\n            9 审核通过',
  `reason` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '拒绝原因',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `submited_time` datetime DEFAULT NULL COMMENT '提交时间',
  `updated_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP实名认证信息表';

-- ----------------------------
-- Records of ap_user_realname
-- ----------------------------
INSERT INTO `ap_user_realname` VALUES ('1', '1', 'zhangsan', '512335455602781278', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '9', '', '2019-07-30 14:34:28', '2019-07-30 14:34:30', '2019-07-12 06:48:04');
INSERT INTO `ap_user_realname` VALUES ('2', '2', 'lisi', '512335455602781279', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '1', '', '2019-07-11 17:21:18', '2019-07-11 17:21:20', '2019-07-12 06:48:04');
INSERT INTO `ap_user_realname` VALUES ('3', '3', 'wangwu6666', '512335455602781276', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '9', '', '2019-07-11 17:21:18', '2019-07-11 17:21:20', '2019-07-12 06:48:04');
INSERT INTO `ap_user_realname` VALUES ('5', '5', 'suwukong', '512335455602781279', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '1', '', '2020-08-01 11:10:31', '2020-08-01 11:10:34', '2020-08-01 11:10:36');

2.在 heima-leadnews-model 模块底下创建 com.heima.model.user 包

3.在 com.heima.model.user 包底下创建 pojos 包

dtos 包的创建暂时忽略,不过这里也会讲解 dtos 包的大概作用(存放 DTO 类型)

POJO

POJO 的全程有几种,如 pure old java object 、plain ordinary java object 等。但意思都差不多,可以理解为就是简单的 Java 对象,符合没有被继承类、没有实现接口、属性私有、拥有无参构造方法等规则的 Java 对象

POJO 可划分为 VO、DTO、BO、DO 等

POJO全称作用
VOView Object前端会将 VO 内的属性渲染到页面上
DTOData Transfer Object前后端通过 DTO 传输数据、后端的服务与服务之间也可以通过 DTO 传输数据
BOBusiness ObjectBO 内会封装多个 DO,即业务直接处理 BO,间接处理 DO。如简历相关业务,简历就是 BO,专业技能、实习经历、项目经历、个人信息等为 DO
DOData ObjectDO 中的属性与数据库表中的属性一一对应,即实体类

这里黑马头条中的包划分可能不够严谨,也可能有其他考量,不是重点,暂时不关心

在阿里开发规范中,规定类名中的 DO/BO/DTO/VO/AO/PO/UID 需要大写

4.在 pojos 包中创建 ApUser 类

package com.heima.model.user.pojos;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * APP用户信息表
 * </p>
 *
 * @author itheima
 */
@Data
@TableName("ap_user")
public class ApUser implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 密码、通信等加密盐
     */
    @TableField("salt")
    private String salt;

    /**
     * 用户名
     */
    @TableField("name")
    private String name;

    /**
     * 密码,md5加密
     */
    @TableField("password")
    private String password;

    /**
     * 手机号
     */
    @TableField("phone")
    private String phone;

    /**
     * 头像
     */
    @TableField("image")
    private String image;

    /**
     * 0 男
            1 女
            2 未知
     */
    @TableField("sex")
    private Boolean sex;

    /**
     * 0 未
            1 是
     */
    @TableField("is_certification")
    private Boolean certification;

    /**
     * 是否身份认证
     */
    @TableField("is_identity_authentication")
    private Boolean identityAuthentication;

    /**
     * 0正常
            1锁定
     */
    @TableField("status")
    private Boolean status;

    /**
     * 0 普通用户
            1 自媒体人
            2 大V
     */
    @TableField("flag")
    private Short flag;

    /**
     * 注册时间
     */
    @TableField("created_time")
    private Date createdTime;

}
@MyBatisPlus 注解
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

ApUser 类使用了 MybatisPlus 中的 4 个注解

注解作用
@TableName

让 MybatisPlus 识别当前类对应的表,ApUser > ap_user

@TableId让 MybatisPlus 识别当前属性为表的主键,id > id
@IdType让 MybatisPlus 识别当前主键的类型,IdType.AUTO > 自增类型
@TableField让 MybatisPlus 识别当前属性对应的表字段,name > name

添加注解是为了防止数据库字段或 Java 代码属性的命名不规范、不统一

蛇形驼峰转化

如果满足以下三个条件,即可开启蛇形驼峰转化,可以不用上面的 MybatisPlus 注解来进行识别:

  • Java 严格按照驼峰命名(名字中单词首字母大写,如属性名 helloWorld、类名 HelloWorld)
  • 数据库严格按照蛇形命名(名字中的单词之间用下划线分割,如 hello_world)
  • 名字都一一对应完全相同(名字都是 hello world 只是命名形式不同)

在 yml 中开启蛇形驼峰转化:

    黑马提供的初始项目的 model 模块下并没有 resources 目录,需要自己创建。然后在 resources 目录下创建 application.yml 文件来进行配置(黑马并没有使用 MybatisPlus 的自动命名转化配置,为了不给后面埋雷,不建议大家做这个配置)

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true

MyBatisPlus POM 依赖

在 heima-leadnews-model 模块的 pom.xml 中添加依赖


<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

实际上 mybatis-plus-boot-starter 中包含了 mybatis,但是可能和导入的 mybatis 相比版本什么的有点不同,建议按照黑马的来

@Lombok 注解
import lombok.Data;
注解作用
@Data为当前类的所有属性添加 getter & setter 方法

Lombok POM 依赖

该依赖已经在父工程 pom.xml 中的 dependencies 标签下被引入,在该标签下被引入的依赖会在所有子模块中生效,因此无需再次引入

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
    <scope>provided</scope>
</dependency>

二、用户 service 微服务搭建

微服务搭建口诀:建 module、改 pom、写 yml、主启动、业务类(不绝对,仅提供参考)

1.在heima-leadnews-service 模块底下的 pom.xml 中添加所有微服务公用的依赖

黑马提供的初始工程中已经添加好了,不必再次添加

引入其他子模块
<!-- 引入依赖模块 -->
<dependency>
    <groupId>com.heima</groupId>
    <artifactId>heima-leadnews-model</artifactId>
</dependency>

<dependency>
    <groupId>com.heima</groupId>
    <artifactId>heima-leadnews-common</artifactId>
</dependency>

<dependency>
    <groupId>com.heima</groupId>
    <artifactId>heima-leadnews-feign-api</artifactId>
</dependency>
依赖作用
heima-leadnews-model导入实体类等,如 ApUser
heima-leadnews-common导入全局配置、全局功能等,如全局异常捕获
heima-leadnews-feign-api导入该模块中统一管理的 feign 远程调用接口(一般微服务之间的调用,都通过 feign 来调用)
引入开发和测试相关依赖
<!-- Spring boot starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
依赖作用
spring-boot-starter-web

包含 @Controller、@Service、@RequestMapping 等 web 相关注解,实现相关 web 功能

spring-boot-starter-test包含 @SpringBootTest、@Test 等测试相关注解,实现相关测试功能

SpringBoot 把许多场景抽象为了启动器 starter。当前我们需要一个搭建 web 服务的应用场景,因此使用 starter-web 启动器,即可自动完成 web 服务的所有相关配置(使用默认值),如将使用了 @Bean 注解的类注册进 spring 容器、将内置的 tomcat 配置好等等。与 Spring 相比方便了许多,减少了大量配置文件、配置操作

引入 nacos 服务注册、配置中心相关依赖
<!-- Spring Cloud Alibaba Nacos-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
依赖作用
spring-cloud-starter-alibaba-nacos-discovery

实现 nacos 相关服务发现、服务注册等功能

spring-cloud-starter-alibaba-nacos-config实现 nacos 相关配置拉取功能

注册中心

在微服务架构中,会有某些服务需要由许多机器/容器来提供,而每个机器/容器的 url 都是不一样的。因此我们需要通过服务注册中心将不同的 url 注册到同一个服务名底下,之后再调用该服务名的时候,会从注册中心将服务注册表(包含着 url 与服务名之间映射关系)拉取到本地,然后 nacos 内置的 Ribbon 会将服务名转化为具体的 url(默认轮询,这次是第一个 url,下次就轮到另一个 url),再进行服务调用

配置中心

由于在微服务架构下有许许多多的服务,因此也会产生许许多多的配置文件,每个配置文件也会有许许多多的配置项。当有需求变更时,修改起配置来十分头疼,管理起来也很混乱,很容易改错配置,开发环境直接成为修罗炼狱。而配置中心可以让项目中的配置全部统一管理,并且还可以实现动态修改、运行环境区分(开发、测试、生产等)、配置回滚等功能,很好的解决上述问题

2.在 heima-leadnews-service 模块底下创建 heima-leadnews-user 子模块

黑马提供初始项目中已经建好了的,不必再重建

3.修改 heima-leadnews-user 底下的 pom.xml

由于我们已经在 heima-leadnews-user 的父工程 heima-leadnews-service 中引入了必要的依赖,因此这里暂时不需要添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>heima-leadnews-service</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>heima-leadnews-user</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

4.编写 heima-leadnews-user 的 yml 配置

在 heima-leadnews-user 底下的 resources 目录中新建 bootstrap.yml 文件

SpringBoot 核心配置文件有两种:bootstrap 和 application

bootstrap

bootstrap 是应用程序的父上下文,由父 ApplicationContext 加载,优先级更高。一般用来做系统层级的参数配置

application

application 是当前应用程序的上下文,优先级较低。一般用来做应用层级的参数配置

为什么用 bootstrap

由于 Spring 根据优先级会先加载 bootstrap 中 nacos 服务的 url(没有就先使用 nacos 默认的),并进行连接尝试,如果你的 nacos 不在默认的 url 上,那么就会报错。因此一定要在 bootstrap 上做好配置,避免报错,才能启动成功

编写 bootstarp 文件的内容
server:
  port: 51801
spring:
  application:
    name: leadnews-user
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
  profiles:
    active: dev

注意使用自己的 nacos 服务地址

nacos 下载 

Releases · alibaba/nacos · GitHub

通过 GitHub 下载,可以自行选择版本(我当时下载的是 1.1.4),点击版本下的 Assets 中的 nacos-server-1.1.4.zip(不论 tar.gz/zip 在 linux 和 windows 都能使用,应该)

https://github.com/alibaba/nacos/releases/download/1.1.4/nacos-server-1.1.4.zip

nacos 启动

解压后双击 bin/startup.cmd 启动 nacos(默认端口为 8848)

在 nocos 配置中心添加 heima-leadnews-user 的服务配置

左边导航栏选择配置列表,点击列表右上方的加号,添加配置

Data ID = spring.application.name (即 leadnews-user) + "-" + spring.profiles.active (即 dev) + "." + spring.cloud.nacos.config.file-extension (即 yml) = leadnews-user-dev.yml

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: 159357
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.heima.model.user.pojos

5.完善 heima-leadnews-user 的主启动类 UserApplication

创建 com.heima.user.UserApplication 主启动类(黑马提供的初始工程也建好了)

package com.heima.user;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;


@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.user.mapper")
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class,args);
    }
}
@SpringBootApplication 注解

用来标记当前类为 SpringBoot 的主配置类,SpringBoot 应用由当前类的 main 方法启动,当 mian 方法执行下面这条代码的时候,

SpringApplication.run(UserApplication.class,args);

SpringBoot 将会把当前包底下的 @Bean 方法返回的对象以及 @Component 类的实例化对象自动存放入 Spring 容器中,并且将所有的配置项以默认值自动装配(约定大于配置)

@EnableDiscoveryClient 注解

该注解是由 SpringBoot 提供的,拥有服务注册与订阅的相关功能。使用该注解后,SpringBoot 应用会自动向配置的服务注册中心注册自己,并定时发送心跳包告知注册中心自己的服务状态

@MapperScan 注解

由 heima-leadnews-model 中的 MyBatis 依赖所提供的注解,可以自动扫描指定包下的类,将扫描到的接口作为 Mapper 并将其实现。有了这个注解可以不必再 XXXMapper 接口上再写 @Mapper 注解了。如果当前服务有许多的 Mapper 接口,那每一个接口上都要写 @Mapper 注解,就会有许多的 @Mapper 注解。如果使用 @MapperScan 注解,即可用一个 @MapperScan 注解代替大量 @Mapper 注解

6.构建 heima-leadnews-user 的微服务目录

config、controller、service、mapper

config

存放配置相关类

controller

为前端提供访问接口,不关心具体业务逻辑,只负责接收参数和返回结果

service

向 contrller 提供业务逻辑接口,serviceImpl 负责将接口中的方法实现

mapper

向 service 提供操作持久层数据的接口,mapper.xml 负责实现接口

为什么这样设计

这种设计方式体现了一种思想 MVC(即 model + view + controller)

分层作用
view从 controller 获取数据,并展示给用户
controller接收 view 的请求,让 model 进行处理,并返回处理结果给 view
model操作数据完成业务,将结果返回给 controller

view 对应项目中的 前端,controller 对应项目中的 controller,model 对应项目中的 service + controller。controller 就如一个中介者,封装 view 和 model 之间的交互,松散耦合,集中控制交互。而 service 与 serviceIpml 分开,mapper 与 mapper.xml 分开,体现了依赖倒转,使调用方不依赖于实现,而是依赖于抽象接口

7.开发 heima-leadnews-user 登录功能相关的 controller 层代码

在 controller 下新建 v1 包,表示第一个版本

在 v1 包底下新建 ApUserLoginController 类

登录接口
接口路径/api/v1/login/login_auth
请求方式POST
参数LoginDto
响应结果ResponseResult

LoginDto

在 heima-leadnews-model 模块下的 com.heima.model.user.dtos 包下创建 LoginDto

@Data
public class LoginDto {

    /**
     * 手机号
     */
    private String phone;

    /**
     * 密码
     */
    private String password;
}

ResponseResult

在 heima-leadnews-model 中的 com.heima.model.common.dtos 包底下创建 ResponseResult 类


/**
 * 通用的结果返回类
 * @param <T>
 */
public class ResponseResult<T> implements Serializable {

    private String host;

    private Integer code;

    private String errorMessage;

    private T data;

    public ResponseResult() {
        this.code = 200;
    }

    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.errorMessage = msg;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.errorMessage = msg;
    }

    public static ResponseResult errorResult(int code, String msg) {
        ResponseResult result = new ResponseResult();
        return result.error(code, msg);
    }

    public static ResponseResult okResult(int code, String msg) {
        ResponseResult result = new ResponseResult();
        return result.ok(code, null, msg);
    }

    public static ResponseResult okResult(Object data) {
        ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());
        if(data!=null) {
            result.setData(data);
        }
        return result;
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums){
        return setAppHttpCodeEnum(enums,enums.getErrorMessage());
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums, String errorMessage){
        return setAppHttpCodeEnum(enums,errorMessage);
    }

    public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
        return okResult(enums.getCode(),enums.getErrorMessage());
    }

    private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String errorMessage){
        return okResult(enums.getCode(),errorMessage);
    }

    public ResponseResult<?> error(Integer code, String msg) {
        this.code = code;
        this.errorMessage = msg;
        return this;
    }

    public ResponseResult<?> ok(Integer code, T data) {
        this.code = code;
        this.data = data;
        return this;
    }

    public ResponseResult<?> ok(Integer code, T data, String msg) {
        this.code = code;
        this.data = data;
        this.errorMessage = msg;
        return this;
    }

    public ResponseResult<?> ok(T data) {
        this.data = data;
        return this;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

}

AppHttpCodeEnum

在 heima-leadnews-model 模块中的 com.heima.model.common.enums 包下新建 AppHttpCodeEnum 类

public enum AppHttpCodeEnum {

    // 成功段固定为200
    SUCCESS(200,"操作成功"),
    // 登录段1~50
    NEED_LOGIN(1,"需要登录后操作"),
    LOGIN_PASSWORD_ERROR(2,"密码错误"),
    // TOKEN50~100
    TOKEN_INVALID(50,"无效的TOKEN"),
    TOKEN_EXPIRE(51,"TOKEN已过期"),
    TOKEN_REQUIRE(52,"TOKEN是必须的"),
    // SIGN验签 100~120
    SIGN_INVALID(100,"无效的SIGN"),
    SIG_TIMEOUT(101,"SIGN已过期"),
    // 参数错误 500~1000
    PARAM_REQUIRE(500,"缺少参数"),
    PARAM_INVALID(501,"无效参数"),
    PARAM_IMAGE_FORMAT_ERROR(502,"图片格式有误"),
    SERVER_ERROR(503,"服务器内部错误"),
    // 数据错误 1000~2000
    DATA_EXIST(1000,"数据已经存在"),
    AP_USER_DATA_NOT_EXIST(1001,"ApUser数据不存在"),
    DATA_NOT_EXIST(1002,"数据不存在"),
    // 数据错误 3000~3500
    NO_OPERATOR_AUTH(3000,"无权限操作"),
    NEED_ADMIND(3001,"需要管理员权限");

    int code;
    String errorMessage;

    AppHttpCodeEnum(int code, String errorMessage){
        this.code = code;
        this.errorMessage = errorMessage;
    }

    public int getCode() {
        return code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}
ApUserLoginController 代码
@RestController
@RequestMapping("/api/v1/login")
public class ApUserLoginController {

    @Autowired
    private ApUserService apUserService;

    @PostMapping("/login_auth")
    public ResponseResult login(@RequestBody LoginDto dto){
        return apUserService.login(dto);
    }
}

@RestController 注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

本质是个复合注解,即 @RestController = @Controller + @ResponseBody

注解作用
@Target

规定注解的使用范围(

ElementType.TYPE 类注解,

ElementType.FIELD 属性注解,

ElementType.METHOD 方法注解...

@Retention

规定注释的有效时期(

RetentionPolicy.SOUTCE 代码注解,

RetenionPolicy.CLASS 字节码注解,

RetentionPolicy.RUNTIME 运行注解...

@Documented

将该注解写入 JAVADOC 中

@Inherited使用该注解的类的子类,可以继承该注解
注解作用
@Controller包含 @Component 注解,可以将使用该注解的类的实例化对象注册到 Spring IOC 容器中
@ResponseBody将方法的返回值直接写入 HTTP Response 的 body 中

@RequestMapping 注解

当请求路径与 @RequestMapping 的 value 值一致时,对请求进行路由,路由到当前类或方法

@AutoWired 注解

自动装载,将注册进容器中的 ApUserService 类的实例化对象取出并注入到当前的 ApUserController 类中。为 ApUserController 提供业务方法(ApUserService 还没建好,可以先不注入)

@RequestBody 与 @RequestParam 的区别

@RequestBody 发送

@RequestBody 接收

@RestController
public class TestController {
    @RequestMapping(value = "/test")
    public void test(@RequestBody String body) {
        System.out.println(body);
    }
}

@RequestParam 发送

第一种 query params

第二种 body form-data

@RequestParam 接收

@RestController
public class TestController {
    @RequestMapping(value = "/test")
    public void test(@RequestParam String param) {
        System.out.println(param);
    }
}

8.开发 heima-leadnews-user 登录功能相关的 service 层代码

在 service 下新建 apUserService 接口

public interface ApUserService extends IService<ApUser> {
    /**
     * app端登录功能
     * @param dto
     * @return
     */
    public ResponseResult login(LoginDto dto);
}

IService 是由 MyBatis Plus 提供的一个接口,MyBatis 也提供了对应的实现类 ServiceImpl

ServiceImpl 类注入了 baseMapper 接口,而 MyBatis Plus 也实现了该接口

因此我们可以使用 ServiceImpl 中的丰富的基础数据处理方法间接调用 mapper 来进行数据库操作

在 service 下新建 impl 包并创建 ApUserServiceImpl 类实现 ApUserService 接口

@Service
@Transactional
@Slf4j
public class ApUserServiceImpl extends ServiceImpl<ApUserMapper, ApUser> implements ApUserService {
    /**
     * app端登录功能
     * @param dto
     * @return
     */
    @Override
    public ResponseResult login(LoginDto dto) {
        //1.正常登录 用户名和密码
        if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())){
            //1.1 根据手机号查询用户信息
            ApUser dbUser = getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));
            if(dbUser == null){
                return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
            }

            //1.2 比对密码
            String salt = dbUser.getSalt();
            String password = dto.getPassword();
            String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
            if(!pswd.equals(dbUser.getPassword())){
                return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
            }

            //1.3 返回数据  jwt  user
            String token = AppJwtUtil.getToken(dbUser.getId().longValue());
            Map<String,Object> map = new HashMap<>();
            map.put("token",token);
            dbUser.setSalt("");
            dbUser.setPassword("");
            map.put("user",dbUser);

            return ResponseResult.okResult(map);
        }else {
            //2.游客登录
            Map<String,Object> map = new HashMap<>();
            map.put("token",AppJwtUtil.getToken(0L));
            return ResponseResult.okResult(map);
        }
    }
}
判断用户名和密码是否为空

使用 StringUtils 的 isNotBlank 方法判断字符串是否为空、是否有长度、是否含有空白字符

if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())) {
    // 1.正常登录
} else {
    // 2.游客登录
    Map<String,Object> map = new HashMap<>();
    map.put("token",AppJwtUtil.getToken(0L));
    return ResponseResult.okResult(map);
}

游客登陆返回

{
    "host": "",
    "code": 200,    // AppHttpCodeEnum.SUCCESS
    "errorMessage": "操作成功",
    "data": {
        "token": // 使用 AppJwtUtil.getToken(0L) 方法生成游客 token,具体实现下面会讲
    }
}
判断该用户是否存在

使用 ServiceImpl 中的 getOne 方法,用 Wrapper 构造查询条件( dbUser.getPhone() 等与 dto.getPhone)

//1.1 根据手机号查询用户信息
ApUser dbUser = getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));
if(dbUser == null){
    return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
}

不存在返回

{
    "host": "",
    "code": 1002,    // AppHttpCodeEnum.DATA_NOT_EXIST
    "errorMessage": "用户信息不存在",
    "data": {}
}
判断密码是否正确

数据库中的密码加密是通过先加盐再MD5的方式完成的

MD5 为一种单向加密算法,只能加密不能解密。因此很适合用来加密密码,这样连数据库所有者都无法知道用户的真实密码,大大提高安全性。但是既然这么安全为什么还要加盐呢?因为MD5的广泛使用,黑客认为有利可图,于是使用暴力学习,将许多密码进行 MD5 加密,并将映射关系存起来,这样就能用映射表,将 MD5 加密后的密码转为真实的密码。所以,我们通过加盐,也就是给原有的密码加上其他的字符/字符串再进行 MD5 加密,使得暴力解密后的密码也不是真正的密码

使用 org.springframework.util 中的 DigestUtils 类里的 md5DigestAsHex 方法对 用户输入的密码 + 数据库中获取的盐值 进行加密,然后将加密后的输入密码与数据库中的比较

//1.2 比对密码
String salt = dbUser.getSalt();
String password = dto.getPassword();
String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
if(!pswd.equals(dbUser.getPassword())){
    return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}

密码错误返回

{
    "host": "",
    "code": 2,    // AppHttpCodeEnum.LOGIN_PASSWORD_ERROR
    "errorMessage": "密码错误",
    "data": {
        "token": // 使用 AppJwtUtil.getToken(0L) 方法生成游客 token,具体实现下面会讲
    }
}
生成 JWT

JWT,即 Json Web Token。它由三部分组成

header

JWT头

一个 JSON 对象用来描述 JWT 的签名算法、令牌类型

payload

JWT载荷

一个 JSON 对象用来传递数据

signature

JWT签名

使用 base64 加密后的 header + base64 加密后的 payload + 服务器生成的密钥,通过指定算法生成签名

形如 headerheaderheader.payloadpayloadpayload.signaturesignaturesignature

即 header 与 payload 与 signature 之间使用 '.' 点号隔开

eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQrDMAzA_uJzA_biuKa_cWnKUigEnMLG2N_nHnaTEPrAMRosUFhUmCmZUEmsImnN-kgZ90q2Z1EWmKDZgIVmRBRS1gn8WuP2t4963t099FnbaWF2bWHWe3B99f9Z-D5bNPz-AKKQJrqAAAAA.0ZNpu7dGgNtoF-UBGM3LvFZxEsltZFHazfByij_IW5KIv82oNxV4VremFE4zAx_lFAxE2XuOZ53LGb1u2mY7Lg

由于 base64 可以解密,因此不要在 payload 中存放重要的隐私信息

为什么需要 JWT

JWT 用来存储客户端与服务器之间的身份识别信息,相当于是存会话信息。既然是会话,为什么不用 session 呢?因为 session 依赖于 cookie,session 中会存放用户信息,浏览器的 cookie 中会存 session 的 id,每次浏览器发送请求的时候就会携带 cookie,这样也能将 cookie 中的 sessionID 发送给服务器,这样服务器就能识别出这个请求时哪个用户发出的。可是有些客户端并没有 cookie,因此也无法存储 sessionID,于是我们就使用 JWT,客户端发送请求的时候带上 JWT,而 JWT 中又有用户的信息,这样服务器也能识别出请求的来源。抛弃掉 session cookie,实现前后端分离

在 heima-leadnews-utils 模块下创建 com.heima.utils.common.AppJwtUtil 类

public class AppJwtUtil {

    // TOKEN的有效期一天(S)
    private static final int TOKEN_TIME_OUT = 3_600;
    // 加密KEY
    private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
    // 最小刷新间隔(S)
    private static final int REFRESH_TIME = 300;

    // 生产ID
    public static String getToken(Long id){
        Map<String, Object> claimMaps = new HashMap<>();
        claimMaps.put("id",id);
        long currentTime = System.currentTimeMillis();
        return Jwts.builder()
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date(currentTime))  //签发时间
                .setSubject("system")  //说明
                .setIssuer("heima") //签发者信息
                .setAudience("app")  //接收用户
                .compressWith(CompressionCodecs.GZIP)  //数据压缩方式
                .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
                .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000))  //过期时间戳
                .addClaims(claimMaps) //cla信息
                .compact();
    }

    /**
     * 获取token中的claims信息
     *
     * @param token
     * @return
     */
    private static Jws<Claims> getJws(String token) {
            return Jwts.parser()
                    .setSigningKey(generalKey())
                    .parseClaimsJws(token);
    }

    /**
     * 获取payload body信息
     *
     * @param token
     * @return
     */
    public static Claims getClaimsBody(String token) {
        try {
            return getJws(token).getBody();
        }catch (ExpiredJwtException e){
            return null;
        }
    }

    /**
     * 获取hearder body信息
     *
     * @param token
     * @return
     */
    public static JwsHeader getHeaderBody(String token) {
        return getJws(token).getHeader();
    }

    /**
     * 是否过期
     *
     * @param claims
     * @return -1:有效,0:有效,1:过期,2:过期
     */
    public static int verifyToken(Claims claims) {
        if(claims==null){
            return 1;
        }
        try {
            claims.getExpiration()
                    .before(new Date());
            // 需要自动刷新TOKEN
            if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
                return -1;
            }else {
                return 0;
            }
        } catch (ExpiredJwtException ex) {
            return 1;
        }catch (Exception e){
            return 2;
        }
    }

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

}

生成过程

//1.3 返回数据  jwt  user
String token = AppJwtUtil.getToken(dbUser.getId().longValue());
Map<String,Object> map = new HashMap<>();
map.put("token",token);
dbUser.setSalt("");
dbUser.setPassword("");
map.put("user",dbUser);

return ResponseResult.okResult(map);
public static String getToken(Long id){
    Map<String, Object> claimMaps = new HashMap<>();
    claimMaps.put("id",id);
    long currentTime = System.currentTimeMillis();
    return Jwts.builder()
            .setId(UUID.randomUUID().toString())
            .setIssuedAt(new Date(currentTime))  //签发时间
            .setSubject("system")  //说明
            .setIssuer("heima") //签发者信息
            .setAudience("app")  //接收用户
            .compressWith(CompressionCodecs.GZIP)  //数据压缩方式
            .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
            .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000))  //过期时间戳
            .addClaims(claimMaps) //cla信息
            .compact();
}

使用 jjwt 依赖提供的 jsonwebtoken 包中的 Jwts.builder 方法构建 jwt,并将用户 id 存放在 jwt 的 payload 中,从而让服务器能够识别请求的发送者

三、测试接口

{
    "host": null,
    "code": 200,
    "errorMessage": "操作成功",
    "data": {
        "user": {
            "id": 4,
            "salt": "",
            "name": "admin",
            "password": "",
            "phone": "13511223456",
            "image": null,
            "sex": true,
            "certification": null,
            "identityAuthentication": null,
            "status": true,
            "flag": 1,
            "createdTime": "2020-03-30T08:36:32.000+00:00"
        },
        "token": "eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWL0QrDIAwA_yXPFZLqau3fpBqZg4IQCxuj_770YW93HPeF12iwAfqEiWRxNefiAu-rYx-z48dMNZKUtSaYoPGAjSIizcHjMoGeu9360SHH3VVNn9IONuOzmHHvxvLu_zOm-2zWwvUD7al0KoAAAAA.5GEoiqO9MH7WCZeBWM95XAlAtlrkHvrbzUqZOO0HktuJjcCCJ20MVprJjXXa-DuNY9qdHIy0yt7Z1ziaflTHUw"
    }
}

基础用户登录功能完成

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/154652.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

代码静态测试工具之cppcheck

cppcheck简介 Cppcheck is an analysis tool for C/C code. It provides unique code analysis to detect bugs and focuses on detecting undefined behaviour and dangerous coding constructs. The goal is to detect only real errors in the code, and generate as few f…

数据库期末考前复习题(单选+多选+判断+解答)

文章目录 #数据库考前复习题一、 选择1.单选题2.多选题 二、判断题三、解答请描述数据库中的三大范式关系型数据库ACID特性 #数据库考前复习题 一、 选择 1.单选题 1.使用limit进行分页查询&#xff0c;其中每页10条数据&#xff0c;查询第5页应该写为&#xff1f; SELECT *…

java: 程序包XXX.XXX.XXX不存在解决方法

背景介绍&#xff1a; com.DXG.bean 来源于同一个项目底下的另一个包 问题所在&#xff1a; 明明已经引入了相关包 但是编译的时候报错&#xff1a;java: 程序包com.DXG.bean不存在 问题分析&#xff1a; 怀疑是拆模块以后引入相关包没有将相关包下载到本地maven仓库中 所以…

Qt QLable 字符过长省略

前言&#xff1a; 项目中常用到字符过长问题&#xff0c;Qt默认的省略并不好用&#xff0c;不是自己想要的&#xff1b; QFontMetri 可使用 QFontMetri 当text的像素宽度超过width&#xff0c;将返回字符串的一个省略版本取决于mode。否则将返回原字符串&#xff1b; mode…

C++面向对象编程(3)——常用关键字介绍(TODO)

本篇会逐步添加一些C的关键字&#xff0c;持续更新... 一. default 1.1 场景 如果对构造函数进行了重载&#xff0c;则编译器不会隐式的生成一个默认的构造函数&#xff0c;此时如果调用了默认构造函数会在编译时报错&#xff0c;但是很多时候我们是需要默认构造函数的。如何…

基于IDEA创建Maven工程及注意事项

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 1. 概念梳理Maven工程的GAVP Maven工程相对之前的项目&#xff0c;多出一组gavp属性&#xff0c;gav需要我们在创建项目的时候指定&#xff0c;p有默认值&#xff0c;我们先行了解下这组属性的含义&#xff1a; Ma…

Mac电脑VSCode配置PHP开发环境

1.安装 PHP 首先&#xff0c;打开终端&#xff0c;安装 Homebrew&#xff0c;输入如下命令&#xff1a; $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 安装了 Homebrew 之后&#xff0c;你可以使用下面的…

Apache Airflow (八) :DAG任务依赖设置

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

User parameters自定义用户参数 (zabbix监控)

1、介绍和用法 ① 介绍 自定义用户参数&#xff0c;也就是自定义key 有时&#xff0c;你可能想要运行一个代理检查&#xff0c;而不是Zabbix的预定义 你可以编写一个命令来检索需要的数据&#xff0c;并将其包含在代理配置文件("UserParameter"配置参数)的用户参数中…

LabVIEW关于USRPRIO的示例代码

LabVIEW关于USRPRIO的示例代码 USRPRIO 通常以两种方式使用&#xff1a; 1 基于 FPGA 的编程 对于希望修改USRP上的底层FPGA代码以添加自定义DSP模块的应用&#xff0c;请使用USRP示例项目。它可作为构建 USRP RIO 流式处理应用程序的起点&#xff0c;可从“创建项目”对话框…

服务注册发现 springcloud netflix eureka

文章目录 前言角色&#xff08;三个&#xff09; 工程说明基础运行环境工程目录说明启动顺序&#xff08;建议&#xff09;&#xff1a;运行效果注册与发现中心服务消费者&#xff1a; 代码说明服务注册中心&#xff08;Register Service&#xff09;服务提供者&#xff08;Pro…

基于JavaWeb+SSM+社区居家养老服务平台—颐养者端微信小程序系统的设计和实现

基于JavaWebSSM社区居家养老服务平台—颐养者端微信小程序系统的设计和实现 源码获取入口前言主要技术系统设计功能截图Lun文目录订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 前言 在复杂社会化网络中&#xff0c;灵活运用社会生活产生的大数据&am…

Shopee买家号有什么作用?如何才能拥有大量的虾皮买家号?

对于卖家而言&#xff0c;用shopee买家号进行测评有以下几点好处&#xff1a; 1、随时随地可以给自己店铺下单、评价、点星 2、成本很低&#xff1a;都是自己准备一些资料进行注册的&#xff0c;因此成本也是比较可控的。 3、自己管理更加安全可控&#xff1a;每个账号都是独…

03-关系和非关系型数据库对比

关系和非关系型数据库对比 关系型数据库(RDBMS)&#xff1a;MySQL、Oracl、DB2、SQLServer 非关系型数据库(NoSql)&#xff1a;Redis、Mongo DB、MemCached 插入数据结构的区别 传统关系型数据库是结构化数据,向表中插入数据时都需要严格的约束信息(如字段名,字段数据类型,字…

C#WPF中的实现读取和写入文件的几种方式

说明&#xff1a;C#中实现读取和写入的类根据需要来选择。 1、File类 File类是用于操作文件的工具类&#xff0c;提供了对文件进行创建、复制、删除、移动和打开单一文件的静态方法。但需要注意的是&#xff0c;WPF中使用File的类&#xff0c;需要先引用System.IO下的命名空间。…

从零开始:打造疫苗预约抖音小程序的技术指南

这篇文章小编将与大家一同探讨如何开发一款疫苗预约的抖音小程序。 第一步&#xff1a;项目准备和规划 确定用户需要提供的信息&#xff0c;例如个人信息、接种地点偏好等。同时&#xff0c;考虑系统的用户界面设计&#xff0c;确保用户友好性和易用性。 第二步&#xff1a;…

验证k8s中HPA功能及测试

部署 使用yaml部署服务 apiVersion: apps/v1 kind: Deployment metadata:name: php-apachenamespace: tools spec:replicas: 1selector:matchLabels:app: php-apachetemplate:metadata:labels:app: php-apachespec:containers:- name: php-apacheimage: registry.cn-beijing.…

基于SpringBoot+Vue的新能源汽车充电桩管理系统

基于SpringBootVue的新能源汽车充电桩管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 充电桩详情 管理员界面 摘要 本项目是基于Spring Boot 和 …

百数低代码——为教育行业打开数字化转型之路

教育一直是人类社会发展与进步的基石&#xff0c;也是国之大计。教育既需要满足“千人千面”的获取知识需求&#xff0c;又需要保证教育本身的质量&#xff0c;因此教育培训方式一直在改变。 而如今&#xff0c;大数据、人工智能等新一代信息技术推动着传统教育向数字化教育发展…

event事件分发器||静态类型转换

由于类型不一样在event事件分发器中要进行静态类型转换&#xff0c;将基类转换为派生类进行处理 对event事件分发器拦截后最后要将其他函数交给父类处理&#xff0c;否则不会运行
最新文章