尚上优选平台管理模块笔记

里面包含了尚上优选的一部分代码,主要是梳理各个组件的使用方法和自己不会的东西,比如Wrapper,@TableLogic,跨域等

尚上优选

bilibili地址:https://www.bilibili.com/video/BV19M4y1q7Lt

gitee地址:https://gitee.com/zhanpengyu/ssyx

百度网盘:https://pan.baidu.com/s/1JxCTrz84fTuUjEqwJVlHzg?pwd=lens

需要的软件都放在百度网盘中

项目准备阶段

技术栈
  • SpringBoot
  • SpringCloud
  • Mybatis-Plus
  • Redis
  • ES
  • OSS
  • Docker
  • 微信小程序
前后端分离开发

在这里插入图片描述

搭建后端环境

parent父工程:公共依赖和版本依赖

  • common子模块(公共部分)
    • common-util
    • service-util
  • model子模块(实体类)
  • service子模块(微服务模块)
工具类和配置类
配置类
MybatisPlus配置类

MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。只需简单配置即可快速进行单表的CURD操作,同时提供了自动分页,代码生成,逻辑删除等丰富的功能。

  1. 配置Mapper扫描
  2. 配置MyBatisPlus分页插件

具体代码需要去https://baomidou.com/pages/2976a3/

package com.zhan_py.ssyx.common.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhan_py
 * @Date 2024/1/26 13:44
 */

@MapperScan(basePackages = "com.zhan_py.ssyx.*.mapper")
@Configuration
public class MybatisPlusConfig {

    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

@MapperScan(basePackages = “com.zhan_py.ssyx.*.mapper”)中的mapper报红,运行不报错,没找到解决办法。

Swagger配置类

前后端分离开发模式中,API文档是最好的沟通方式。Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

在配置类里有几个参数

apiInfo参数表

类型apiInfo参数名作用默认值
Stringtitle标题Api Documentation
Stringdescription描述Api Documentation
StringtermsOfServiceUrl服务条款网址urn:tos
Stringlicense许可Apache 2.0
StringlicenseUrl许可链接http://www.apache.org/licenses/LICENSE-2.0
Stringversion版本1.0
ContactContact维护人信息null

contact参数表

类型参数名作用默认值
Stringname维护人姓名“”
Stringurl维护人url地址“”
Stringemail维护人邮箱“”
工具类(重要)
统一返回结果类

项目中我们会将接口返回的数据封装成Json数据格式,一般我们会将所有接口的数据格式统一, 使前端(iOS Android, Web)对数据的操作更一致、轻松。一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码、返回消息、数据这几部分内容。
返回结果类

package com.zhan_py.ssyx.common.result;

import lombok.Data;

/**
 * @author zhan_py
 * @Date 2024/1/26 15:16
 */
@Data
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    private Result() {

    }

    public static <T> Result<T> build(T data, Integer code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        if (data != null) {
            result.setData(data);
        }
        return result;
    }

    public static <T> Result<T> build(T data, ResultCodeEnum resultCodeEnum) {
        Result<T> result = new Result<>();
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        if (data != null) {
            result.setData(data);
        }
        return result;
    }

    public static <T> Result<T> success(T data) {
        return build(data, ResultCodeEnum.SUCCESS);
    }

    public static <T> Result<T> fail(T data) {
        return build(data, ResultCodeEnum.FAIL);
    }
}

返回结果枚举类

package com.zhan_py.ssyx.common.result;

import lombok.Getter;

/**
 * @author zhan_py
 */

@Getter
public enum ResultCodeEnum {

    SUCCESS(200,"成功"),
    FAIL(201, "失败"),
    SERVICE_ERROR(2012, "服务异常"),
    DATA_ERROR(204, "数据异常"),
    ILLEGAL_REQUEST(205, "非法请求"),
    REPEAT_SUBMIT(206, "重复提交"),

    LOGIN_AUTH(208, "未登陆"),
    PERMISSION(209, "没有权限"),

    ORDER_PRICE_ERROR(210, "订单商品价格变化"),
    ORDER_STOCK_FALL(204, "订单库存锁定失败"),
    CREATE_ORDER_FAIL(210, "创建订单失败"),

    COUPON_GET(220, "优惠券已经领取"),
    COUPON_LIMIT_GET(221, "优惠券已发放完毕"),

    URL_ENCODE_ERROR( 216, "URL编码失败"),
    ILLEGAL_CALLBACK_REQUEST_ERROR( 217, "非法回调请求"),
    FETCH_ACCESSTOKEN_FAILD( 218, "获取accessToken失败"),
    FETCH_USERINFO_ERROR( 219, "获取用户信息失败"),


    SKU_LIMIT_ERROR(230, "购买个数不能大于限购个数"),
    REGION_OPEN(240, "该区域已开通"),
    REGION_NO_OPEN(240, "该区域未开通"),
    ;

    private Integer code;

    private String message;

    ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
统一异常处理类

系统在运行过程中如果出现了异常,默认会直接返回异常信息,比如500错误提示。但是我们想让异常结果也显示为统一的返回结果对象,并且统一处理系统的异常信息,那么需要进行统一异常处理。

异常处理类

package com.zhan_py.ssyx.common.exception;

import com.zhan_py.ssyx.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author zhan_py
 * @Date 2024/1/26 15:31
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(SsyxException.class)
    @ResponseBody
    public Result<Object> error(SsyxException e) {
        e.printStackTrace();
        return Result.fail(null);
    }
    
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result<Object> error(Exception e) {
        e.printStackTrace();
        return Result.fail(null);
    }
    
}

自定义异常类

package com.zhan_py.ssyx.common.exception;

import com.zhan_py.ssyx.common.result.ResultCodeEnum;
import lombok.Data;

/**
 * @author zhan_py
 * @Date 2024/1/26 15:48
 */
@Data
public class SsyxException extends RuntimeException {

    private Integer code;

    public SsyxException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public SsyxException(ResultCodeEnum resultCodeEnum) {
        super(resultCodeEnum.getMessage());
        this.code = resultCodeEnum.getCode();
    }
    
}

这里我有几点疑惑

  1. super(message)是在干什么,对super的理解不到位,这里为什么要调用父类的构造器
  2. 异常处理器的第二个error方法,前端收到的message都仅仅是失败,无法根据message来判断问题,为什么不将Result的message设置成异常的message,是因为只想让用户知道成功与否,不想让用户知道后端出了问题。

这里的代码完全照抄,做不到自己手敲

实体类

直接复制代码,要注意包名需要修改

idea全局查找快捷键ctrl+shift+f,全局替换快捷键ctrl+shift+q

平台管理系统

前端
安装相关软件

需要安装nodejs(16.19版本)和vscode

vscode:

  1. 先建立一个文件夹
  2. 使用vscode打开这个文件夹
  3. 另存工作区
Vue 脚手架

Vue-cli(俗称:Vue 脚手架)是 vue 官方提供的、快速生成 vue 工程化项目的工具。
特点:① 开箱即用,② 基于 webpack,③ 功能丰富且易于扩展,④ 支持创建 vue2 和 vue3 的项目

#全局安装命令行工具
npm install --location=global @vue/cli
#创建一个项目
vue create vue-test #选择vue2
#启动程序(需要在项目目录下)
npm run serve

将前端的代码解压到工作区内,代码在vue文件夹下

不会前端,对这部分没有什么理解,完全按照视频的操作去做,就不做笔记了

登录接口

新建一个模块service-acl

建立SpringBoot的配置文件

appliction.yml

spring:
  application:
    name: service-acl
  profiles:
    active: dev

appliction-dev.yml

server:
  port: 8201

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shequ-acl?characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
新建启动类

ServiceAclApplication.java

package com.zhan_py.ssyx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author zhan_py
 * @Date 2024/1/30 17:32
 */
@SpringBootApplication
public class ServiceAclApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceAclApplication.class, args);
    }
}
登录接口

IndexController.java

package com.zhan_py.ssyx.acl.controller;

import com.zhan_py.ssyx.common.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * @author zhan_py
 * @Date 2024/1/30 17:43
 */

// @RestController = @Controller + @ResponseBody
@RestController
@RequestMapping(("/admin/acl/index"))
@Api(tags = "登录接口")
public class IndexController {

    @ApiOperation("登录")
    @PostMapping("/login")
    public Result<Map<String, String>> login(){
        Map<String, String> map = new HashMap<>();
        map.put("token","token-admin");
        return Result.success(map);
    }

    @ApiOperation("获取信息")
    @GetMapping("/info")
    public Result<Map<String, String>> info(){
        Map<String, String> map = new HashMap<>();
        map.put("name", "admin");
        map.put("avatar", "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
        return Result.success(map);
    }

    @ApiOperation("登出")
    @PostMapping("/logout")
    public Result<Map<String, String>> logout(){
        return Result.success(null);
    }
}
跨域

http://localhost:8201/admin//acl/index/login

访问协议-http、ip-localhost、端口号-8201,有任意一个不相同,都无法相互访问,这被称为跨域

跨域的本质是浏览器对ajax请求的一种限制

解决方式:

  1. 在controller加上@CrossOrigin
  2. TODO
角色接口
接口构建

创建RoleController,RoleService,RoleMapper

RoleService extend IService (可以不用)

RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService (可以不用)

RoleMapper extends BaseMapper<Role>

@Mapper和@Repository
  • 相同点:@Mapper和@Repository都是作用在dao层接口,使得其生成代理对象bean,交给spring 容器管理,对于mybatis来说,都可以不用写mapper.xml文件

  • 不同点:

    • @Repository需要在Spring中配置扫描地址,然后生成Dao层的Bean才能被注入到Service层中

    • @Mapper不需要配置扫描地址,可以单独使用,如果有多个mapper文件,可以在项目启动类中加入@MapperScan(“mapper文件所在包”),这样就不需要每个mapper文件都加@Mapper注解

Wapper

在这里插入图片描述

Wrapper : 条件构造抽象类,最顶端父类

  • AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件

    • QueryWrapper : Entity 对象封装操作类,不是用lambda语法

    • UpdateWrapper : Update 条件封装,用于Entity对象更新操作

    • AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。

      • LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper
      • LambdaUpdateWrapper : Lambda 更新封装Wrapper

使用Wrapper对象进行条件构造,常用的方法有:

  • eq(String column, Object value):等于查询
  • ne(String column, Object value):不等于查询
  • gt(String column, Object value):大于查询
  • ge(String column, Object value):大于等于查询
  • lt(String column, Object value):小于查询
  • le(String column, Object value):小于等于查询
  • like(String column, Object value):模糊查询
  • in(String column, Collection<?> values):IN查询
  • isNull(String column):为空查询
  • isNotNull(String column):不为空查询
  • orderByAsc(String... columns):升序排序
  • orderByDesc(String... columns):降序排序

例如:

wrapper.eq("name", "张三")
       .like("address", "北京")
       .in("age", Arrays.asList(18, 20, 25))
       .orderByAsc("age");

List<T> userList = userDao.selectList(wrapper);
params和data
	getPageList(page, limit, searchObj) {
        return request({
            url: `${api_name}/${page}/${limit}`,
            method: 'get',
            params: searchObj // url查询字符串或表单键值对
        })
    },
        
    save(role) {
        return request({
            url: `${api_name}/save`,
            method: 'post',
            data: role
        })
    },
  • params传的是对象,直接用对象接收
  • data传的是Json数据,使用@RequestBody将参数进行封装
用户接口
@TableLogic
  1. 在实体类中属性加上@TableLogic注解,表示该字段是逻辑删除字段
  2. 增加注解后调用BaseMapper的deleteById(id)或者IService的removeById(id),是逻辑删除。如果没有增加该注解,是真删除
  3. @TableLogic注解参数
    1. value = “未删除的值,默认值为0”
    2. delval = “删除后的值,默认值为1”
    3. 如果不设置,就使用默认值
  4. 当使用了@TableLogic注解,调用update方法修改该字段的值是不会将该字段放入修改字段中,而是在条件字段中where条件后
@Data
public class BaseEntity implements Serializable {

    @ApiModelProperty(value = "id")
    @TableId(type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField("create_time")
    private Date createTime;

    @ApiModelProperty(value = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField("update_time")
    private Date updateTime;

    @ApiModelProperty(value = "逻辑删除(1:已删除,0:未删除)")
    @JsonIgnore
    @TableLogic
    @TableField("is_deleted")
    private Integer isDeleted;

    @ApiModelProperty(value = "其他参数")
    @TableField(exist = false)
    private Map<String,Object> param = new HashMap<>();
}
分配角色
  1. 显示所有角色
  2. 显示用户已经分配的角色
  3. 分配角色
菜单接口

尚上优选的菜单是分级菜单,分级菜单在查询的时候需要将级别和归属菜单的关系也表现出来

基本功能

Controller

package com.zhan_py.ssyx.acl.controller;

import com.zhan_py.ssyx.acl.service.PermissionService;
import com.zhan_py.ssyx.acl.service.RolePermissionService;
import com.zhan_py.ssyx.common.result.Result;
import com.zhan_py.ssyx.model.acl.Permission;
import com.zhan_py.ssyx.model.acl.RolePermission;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zhan_py
 * @Date 2024/2/2 16:43
 */
@RestController
@RequestMapping("/admin/acl/permission")
@Api(tags = "菜单接口")
@CrossOrigin
public class PermissionController {

    @Resource
    private PermissionService permissionService;

    @Resource
    private RolePermissionService rolePermissionService;

    @ApiOperation("查询所有菜单")
    @GetMapping
    public Result<List<Permission>> getPermissionList(){
        List<Permission> permissionList = permissionService.getPermissionList();
        return Result.success(permissionList);
    }

    @ApiOperation("删除权限")
    @DeleteMapping("/remove/{id}")
    public Result<Object> removePermission(@PathVariable Long id){
        permissionService.removeChildById(id);
        return Result.success(null);
    }

    @ApiOperation("添加权限")
    @PostMapping("/save")
    public Result<Object> addPermission(@RequestBody Permission permission){
        permissionService.save(permission);
        return Result.success(null);
    }

    @ApiOperation("更新权限")
    @PutMapping("/update")
    public Result<Object> updatePermission(@RequestBody Permission permission){
        permissionService.updateById(permission);
        return Result.success(null);
    }

    @ApiOperation("查看角色权限列表")
    @GetMapping("/toAssign/{roleId}")
    public Result<List<Permission>> toAssign(@PathVariable Long roleId){
        List<Permission> allPermissionList = permissionService.getPermissionList();
        rolePermissionService.toAssign(roleId, allPermissionList);
        return Result.success(allPermissionList);
    }

    @ApiOperation("角色授权")
    @PostMapping("/doAssign")
    public Result<Object> doAssign(@RequestParam Long roleId, @RequestParam List<Long> permissionId){
        rolePermissionService.doAssign(roleId, permissionId);
        return Result.success(null);
    }
}

Service

package com.zhan_py.ssyx.acl.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhan_py.ssyx.acl.mapper.PermissionMapper;
import com.zhan_py.ssyx.acl.service.PermissionService;
import com.zhan_py.ssyx.acl.utils.PermissionUtil;
import com.zhan_py.ssyx.model.acl.Permission;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zhan_py
 * @Date 2024/2/2 16:47
 */
@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {

    @Override
    public List<Permission> getPermissionList() {
        List<Permission> allPermissionList = baseMapper.selectList(null);
        return PermissionUtil.build(allPermissionList);
    }

    @Override
    public void removeChildById(Long id) {
        List<Long> idList = new ArrayList<>();
        this.getAllPermissionId(id, idList);
        baseMapper.deleteBatchIds(idList);
    }

    private void getAllPermissionId(Long id, List<Long> idList) {
        idList.add(id);
        LambdaQueryWrapper<Permission> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Permission::getPid, id);
        List<Permission> childList = baseMapper.selectList(wrapper);
        if (!childList.isEmpty()){
            for (Permission permission:childList){
                getAllPermissionId(permission.getId(), idList);
            }
        }
    }
}

Utils

package com.zhan_py.ssyx.acl.utils;

import com.zhan_py.ssyx.model.acl.Permission;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zhan_py
 * @Date 2024/2/4 10:16
 */
public class PermissionUtil {
    public static List<Permission> build(List<Permission> permissionList) {
        List<Permission> list = new ArrayList<>();
        for (Permission permission : permissionList) {
            if (permission.getPid() == 0) {
                permission.setLevel(1);
                list.add(findChildrenPermission(permission, permissionList));
            }
        }
        return list;
    }

    private static Permission findChildrenPermission(Permission permission, List<Permission> permissionList) {
        List<Permission> children = new ArrayList<>();
        for (Permission child : permissionList) {
            // 判断封装类的值相等的时候调用.xxxValue()
            if (child.getPid().longValue() == permission.getId().longValue()) {
                Integer level = permission.getLevel();
                child.setLevel(level + 1);
                children.add(findChildrenPermission(child, permissionList));
            }
        }

        permission.setChildren(children);
        return permission;
    }
}

表结构

CREATE TABLE `permission` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `pid` bigint NOT NULL DEFAULT '0' COMMENT '所属上级',
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
  `code` varchar(50) DEFAULT NULL COMMENT '名称code',
  `to_code` varchar(100) DEFAULT NULL COMMENT '跳转页面code',
  `type` tinyint NOT NULL DEFAULT '0' COMMENT '类型(1:菜单,2:按钮)',
  `status` tinyint DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:不可用 1:可用)',
  PRIMARY KEY (`id`),
  KEY `idx_pid` (`pid`)
) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限';
分配权限
package com.zhan_py.ssyx.acl.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhan_py.ssyx.acl.mapper.RolePermissionMapper;
import com.zhan_py.ssyx.acl.service.RolePermissionService;
import com.zhan_py.ssyx.model.acl.Permission;
import com.zhan_py.ssyx.model.acl.RolePermission;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zhan_py
 * @Date 2024/2/2 17:20
 */
@Service
public class RolePermissionServiceImpl extends ServiceImpl<RolePermissionMapper, RolePermission> implements RolePermissionService {
    @Override
    public void toAssign(Long roleId, List<Permission> allPermissionList) {
        LambdaQueryWrapper<RolePermission> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(RolePermission::getRoleId,roleId);
        List<RolePermission> rolePermissionList = baseMapper.selectList(wrapper);
        for (RolePermission rolePermission:rolePermissionList){
            toAssignChildren(rolePermission, allPermissionList);
        }
    }

    private void toAssignChildren(RolePermission rolePermission, List<Permission> allPermissionList) {
        for (Permission permission : allPermissionList){
            if (rolePermission.getPermissionId().longValue() == permission.getId().longValue()){
                permission.setSelect(true);
            }
            if (!permission.getChildren().isEmpty()){
                toAssignChildren(rolePermission, permission.getChildren());
            }
        }
    }


    @Override
    @Transactional
    public void doAssign(Long roleId, List<Long> permissionId) {
        LambdaQueryWrapper<RolePermission> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(RolePermission::getRoleId, roleId);
        baseMapper.delete(wrapper);
        List<RolePermission> list = new ArrayList<>();
        for (Long id : permissionId){
            RolePermission rolePermission = new RolePermission();
            rolePermission.setRoleId(roleId);
            rolePermission.setPermissionId(id);
            list.add(rolePermission);
        }
        saveBatch(list);
    }
}
SpringSecurity

整个权限管理仅仅实现了显示功能和基本的增删改查,并为实现真正的权限管理,因此需要继续学习SpringSecurity+OAuth2

bilibili:https://www.bilibili.com/video/BV14b4y1A7Wz/?spm_id_from=333.999.0.0

区域管理接口
MyBatis-Plus代码自动生成

https://baomidou.com/pages/779a6e/

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>最新版本</version>
</dependency>

快速生成

FastAutoGenerator.create("url", "username", "password")
    .globalConfig(builder -> {
        builder.author("baomidou") // 设置作者
            .enableSwagger() // 开启 swagger 模式
            .fileOverride() // 覆盖已生成文件
            .outputDir("D://"); // 指定输出目录
    })
    .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
        int typeCode = metaInfo.getJdbcType().TYPE_CODE;
        if (typeCode == Types.SMALLINT) {
            // 自定义类型转换
            return DbColumnType.INTEGER;
        }
        return typeRegistry.getColumnType(metaInfo);

    }))
    .packageConfig(builder -> {
        builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名
            .moduleName("system") // 设置父包模块名
            .pathInfo(Collections.singletonMap(OutputFile.xml, "D://")); // 设置mapperXml生成路径
    })
    .strategyConfig(builder -> {
        builder.addInclude("t_simple") // 设置需要生成的表名
            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
    })
    .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
    .execute();
整合Nginx

修改conf ->nginx.conf文件

http {
   ............
    server {
        listen       9001;
        server_name  localhost;

        location ~ /acl/ {           
            proxy_pass http://localhost:8201;
        }      

        location ~ /sys/ {           
            proxy_pass http://localhost:8202;
        }
    }
}
商品管理接口

MyBatis-Plues逆向工程

MP版本需要在3.5以下,3.5以上版本去官网查看

引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.1</version>
</dependency>

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.0</version>
</dependency>
public class CodeGet {

    public static void main(String[] args) {

        // 1、创建代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 2、全局配置
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        gc.setOutputDir("D:\\Software\\Java\\JetBrains\\IdeaProjects\\ssyx-parent\\service\\service-product"+"/src/main/java");

        gc.setServiceName("%sService"); //去掉Service接口的首字母I
        gc.setAuthor("zhan_py");
        gc.setOpen(false);
        mpg.setGlobalConfig(gc);

        // 3、数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/shequ-product?characterEncoding=utf-8&useSSL=false");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        dsc.setDbType(DbType.MYSQL);
        mpg.setDataSource(dsc);

        // 4、包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.zhan_py.ssyx");
        pc.setModuleName("product"); //模块名
        pc.setController("controller");
        pc.setService("service");
        pc.setMapper("mapper");
        mpg.setPackageInfo(pc);

        // 5、策略配置
        StrategyConfig strategy = new StrategyConfig();

        strategy.setInclude("attr", "attr_group", "category", "sku_attr_value", "sku_info",
                "sku_image", "sku_poster", "sku_detail"); //需要生成的表名

        strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略

        strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
        strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作

        strategy.setRestControllerStyle(true); //restful api风格控制器
        strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符

        mpg.setStrategy(strategy);

        // 6、执行
        mpg.execute();

        System.out.println("执行完成");
    }
}

商品管理需要以下几个表

  • 商品属性
  • 商品属性分组
  • 商品类别表
  • 商品信息
  • 商品分类
  • 商品详细信息
  • 商品属性值
  • 商品海报
  • 商品图片
添加商品sku信息
    @ApiOperation("添加商品sku信息")
    @PostMapping("/save")
    public Result<Object> save(@RequestBody SkuInfoVo skuInfoVo){
        skuInfoService.saveSkuInfo(skuInfoVo);
        return Result.success(null);
    }

	@Resource
    private SkuImageService skuImageService;

    @Resource
    private SkuPosterService skuPosterService;

    @Resource
    private SkuAttrValueService skuAttrValueService;

    @Override
    public void saveSkuInfo(SkuInfoVo skuInfoVo) {
        // 添加sku基本属性
        SkuInfo skuInfo = new SkuInfo();
        System.out.println("Sku的id:"+skuInfo.getId());
        BeanUtils.copyProperties(skuInfoVo, skuInfo);
        System.out.println("Sku的id:"+skuInfo.getId());
        baseMapper.insert(skuInfo);
        System.out.println("Sku的id:"+skuInfo.getId());
        // 添加sku海报
        List<SkuPoster> skuPosterList = skuInfoVo.getSkuPosterList();
        if (!CollectionUtils.isEmpty(skuPosterList)){
            for (SkuPoster skuPoster: skuPosterList){
                skuPoster.setSkuId(skuInfo.getId());
            }
            skuPosterService.saveBatch(skuPosterList);
        }
        // 添加sku属性
        List<SkuAttrValue> skuAttrValueList = skuInfoVo.getSkuAttrValueList();
        if (!CollectionUtils.isEmpty(skuAttrValueList)){
            for (SkuAttrValue skuAttrValue: skuAttrValueList){
                skuAttrValue.setSkuId(skuInfo.getId());
            }
            skuAttrValueService.saveBatch(skuAttrValueList);
        }
        // 添加sku图片
        List<SkuImage> skuImagesList = skuInfoVo.getSkuImagesList();
        if (!CollectionUtils.isEmpty(skuImagesList)){
            for (SkuImage skuImage: skuImagesList){
                skuImage.setSkuId(skuInfo.getId());
            }
            skuImageService.saveBatch(skuImagesList);
        }

    }
阿里云OSS

图片和海报需要上传到阿里云OSS

  1. 登录阿里云网站并注册
  2. 进入OSS管理控制台
  3. 创建bucket和密钥
pom.xml引入依赖
<dependencies>
    <!-- 阿里云oss依赖 -->
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.9.1</version>
    </dependency>
    <!-- 日期工具栏依赖 -->
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.10.1</version>
    </dependency>
</dependencies>
application-dev.yml添加配置
aliyun:
  endpoint: oss-cn-beijing.aliyuncs.com
  keyid: LTAI5tK6q6QVDqZAY3mD459s
  keysecret: lCaDcmvm7AvfvJ2BhC6456luf45FgM
  bucketname: ssyx-guigu
添加FileUploadController方法
package com.atguigu.ssyx.product.controller;

import com.atguigu.ssyx.common.result.Result;
import com.atguigu.ssyx.product.service.FileUploadService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Api(tags = "文件上传接口")
@RestController
@RequestMapping("admin/product")
public class FileUploadController {

    @Autowired
    private FileUploadService fileUploadService;

    //文件上传
    @PostMapping("fileUpload")
    public Result fileUpload(MultipartFile file) throws Exception{
        return Result.ok(fileUploadService.fileUpload(file));
    }
}
添加FileUploadService方法
package com.atguigu.ssyx.product.service;

import org.springframework.web.multipart.MultipartFile;

public interface FileUploadService {

    //文件上传
    String fileUpload(MultipartFile file) throws Exception;
}

添加FileUploadServiceImpl方法
package com.atguigu.ssyx.product.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.atguigu.ssyx.product.service.FileUploadService;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

@Service
public class FileUploadServiceImpl implements FileUploadService {

    @Value("${aliyun.endpoint}")
    private String endPoint;
    @Value("${aliyun.keyid}")
    private String accessKey;
    @Value("${aliyun.keysecret}")
    private String secreKey;
    @Value("${aliyun.bucketname}")
    private String bucketName;

    @Override
    public String fileUpload(MultipartFile file) throws Exception {
        try {
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKey, secreKey);
            // 上传文件流。
            InputStream inputStream = file.getInputStream();
            String fileName = file.getOriginalFilename();
            //生成随机唯一值,使用uuid,添加到文件名称里面
            String uuid = UUID.randomUUID().toString().replaceAll("-","");
            fileName = uuid+fileName;
            //按照当前日期,创建文件夹,上传到创建文件夹里面
            //  2021/02/02/01.jpg
            String timeUrl = new DateTime().toString("yyyy/MM/dd");
            fileName = timeUrl+"/"+fileName;
            //调用方法实现上传
            ossClient.putObject(bucketName, fileName, inputStream);
            // 关闭OSSClient。
            ossClient.shutdown();
            //上传之后文件路径
            // https://ssyx-atguigu.oss-cn-beijing.aliyuncs.com/01.jpg
            String url = "https://"+bucketName+"."+endPoint+"/"+fileName;
            //返回
            return url;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}
营销管理接口
需求
  1. 营销活动的增删改查
  2. 给某一条营销活动指定规则
  3. 规格分为满减形式和优惠券形式,可以选择指定sku或者按商品类型等
表设计

在这里插入图片描述

Controller
package com.zhan_py.ssyx.activity.controller;


import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zhan_py.ssyx.activity.service.ActivityInfoService;
import com.zhan_py.ssyx.common.result.Result;
import com.zhan_py.ssyx.enums.ActivityType;
import com.zhan_py.ssyx.model.activity.ActivityInfo;
import com.zhan_py.ssyx.model.product.SkuInfo;
import com.zhan_py.ssyx.vo.activity.ActivityRuleVo;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

/**
 *
 * @author zhan_py
 * @since 2024-02-29
 */
@RestController
@RequestMapping("/admin/activity/activityInfo")
public class ActivityInfoController {
    @Resource
    private ActivityInfoService activityInfoService;

    @GetMapping("/{page}/{limit}")
    public Result<IPage<ActivityInfo>> getPageList(@PathVariable Long page, @PathVariable Long limit){
        Page<ActivityInfo> pageParam = new Page<>(page, limit);
        IPage<ActivityInfo> iPage = activityInfoService.getPageList(pageParam);
        return Result.success(iPage);
    }

    @PostMapping("/save")
    public Result<Object> save(@RequestBody ActivityInfo activityInfo){
        activityInfoService.save(activityInfo);
        return Result.success(null);
    }

    @GetMapping("/get/{id}")
    public Result<ActivityInfo> getById(@PathVariable Long id){
        ActivityInfo activityInfo = activityInfoService.getById(id);
        activityInfo.setActivityTypeString(activityInfo.getActivityType().getComment());
        return Result.success(activityInfo);
    }

    @PutMapping("/update")
    public Result<Object> updateById(@RequestBody ActivityInfo activityInfo){
        activityInfoService.updateById(activityInfo);
        return Result.success(null);
    }

    @DeleteMapping("/remove/{id}")
    public Result<Object> removeById(@PathVariable Long id){
        activityInfoService.removeById(id);
        return Result.success(null);
    }

    @DeleteMapping("/batchRemove")
    public Result<Object> removeRows(@RequestBody List<Long> idList){
        activityInfoService.removeByIds(idList);
        return Result.success(null);
    }

    @GetMapping("/findActivityRuleList/{id}")
    public Result<Map<String, Object>> findActivityRuleList(@PathVariable Long id){
        Map<String, Object> activityRuleMap = activityInfoService.findActivityRuleList(id);
        return Result.success(activityRuleMap);
    }

    @PostMapping("/saveActivityRule")
    public Result saveActivityRule(@RequestBody ActivityRuleVo activityRuleVo){
        activityInfoService.saveActivityRule(activityRuleVo);
        return Result.success(null);
    }

    @GetMapping("/findSkuInfoByKeyword/{keyword}")
    public Result<List<SkuInfo>> findSkuInfoByKeyword(@PathVariable String keyword){
        List<SkuInfo> skuInfoList = activityInfoService.findSkuInfoByKeyword(keyword);
        return Result.success(skuInfoList);
    }
}
Service
package com.zhan_py.ssyx.activity.service;


import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zhan_py.ssyx.model.activity.ActivityInfo;
import com.zhan_py.ssyx.model.product.SkuInfo;
import com.zhan_py.ssyx.vo.activity.ActivityRuleVo;

import java.util.List;
import java.util.Map;

/**
 * @author zhan_py
 * @since 2024-02-29
 */
public interface ActivityInfoService extends IService<ActivityInfo> {

    IPage<ActivityInfo> getPageList(Page<ActivityInfo> pageParam);

    Map<String, Object> findActivityRuleList(Long id);

    void saveActivityRule(ActivityRuleVo activityRuleVo);

    List<SkuInfo> findSkuInfoByKeyword(String keyword);
}
package com.zhan_py.ssyx.activity.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zhan_py.ssyx.activity.mapper.ActivityInfoMapper;
import com.zhan_py.ssyx.activity.mapper.ActivityRuleMapper;
import com.zhan_py.ssyx.activity.mapper.ActivitySkuMapper;
import com.zhan_py.ssyx.activity.service.ActivityInfoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhan_py.ssyx.client.product.ProductFeignClient;
import com.zhan_py.ssyx.enums.ActivityType;
import com.zhan_py.ssyx.model.activity.ActivityInfo;
import com.zhan_py.ssyx.model.activity.ActivityRule;
import com.zhan_py.ssyx.model.activity.ActivitySku;
import com.zhan_py.ssyx.model.product.SkuInfo;
import com.zhan_py.ssyx.vo.activity.ActivityRuleVo;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 *
 * @author zhan_py
 * @since 2024-02-29
 */
@Service
public class ActivityInfoServiceImpl extends ServiceImpl<ActivityInfoMapper, ActivityInfo> implements ActivityInfoService{
    @Resource
    private ActivityRuleMapper activityRuleMapper;

    @Resource
    private ActivitySkuMapper activitySkuMapper;

    @Resource
    private ProductFeignClient productFeignClient;

    @Override
    public IPage<ActivityInfo> getPageList(Page<ActivityInfo> pageParam) {
        Page<ActivityInfo> activityInfoPage = baseMapper.selectPage(pageParam, null);
        List<ActivityInfo> records = activityInfoPage.getRecords();
        // 营销活动的类型是一个枚举类,需要将枚举类的值返回给前端
        records.forEach(item -> {
            item.setActivityTypeString(item.getActivityType().getComment());
        });

        return activityInfoPage;
    }

    @Override
    public Map<String, Object> findActivityRuleList(Long id) {
        Map<String, Object> result = new HashMap<>();
        LambdaQueryWrapper<ActivityRule> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(ActivityRule::getActivityId, id);
        List<ActivityRule> activityRuleList = activityRuleMapper.selectList(lambdaQueryWrapper);
        result.put("activityRuleList",activityRuleList);

        List<ActivitySku> activitySkuList = activitySkuMapper.selectList(
                new LambdaQueryWrapper<ActivitySku>().eq(ActivitySku::getActivityId, id));
        List<Long> skuIdList = activitySkuList.stream()
                .map(ActivitySku::getSkuId).collect(Collectors.toList());

        List<SkuInfo> skuInfoList = productFeignClient.findSkuInfoList(skuIdList);
        result.put("skuInfoList", skuInfoList);

        return result;
    }

    @Override
    public void saveActivityRule(ActivityRuleVo activityRuleVo) {
        Long activityId = activityRuleVo.getActivityId();
        activityRuleMapper.delete(new LambdaQueryWrapper<ActivityRule>().eq(ActivityRule::getActivityId, activityId));
        activitySkuMapper.delete(new LambdaQueryWrapper<ActivitySku>().eq(ActivitySku::getActivityId, activityId));

        List<ActivityRule> activityRuleList = activityRuleVo.getActivityRuleList();
        ActivityInfo activityInfo = baseMapper.selectById(activityId);
        for (ActivityRule activityRule : activityRuleList){
            activityRule.setActivityId(activityId);
            activityRule.setActivityType(activityInfo.getActivityType());
            activityRuleMapper.insert(activityRule);
        }

        List<ActivitySku> activitySkuList = activityRuleVo.getActivitySkuList();
        for (ActivitySku activitySku : activitySkuList){
            activitySku.setActivityId(activityId);
            activitySkuMapper.insert(activitySku);
        }
    }

    @Override
    public List<SkuInfo> findSkuInfoByKeyword(String keyword) {
        // 根据关键字远程调用查询sku列表
        List<SkuInfo> skuInfoList = productFeignClient.findSkuInfoByKeyword(keyword);
        if (CollectionUtils.isEmpty(skuInfoList)){
            return skuInfoList;
        }

        // 筛选出还没有参加活动的sku列表
        List<Long> skuIdList = skuInfoList.stream().map(SkuInfo::getId).collect(Collectors.toList());
        // 查询sku列表内已经参加活动skuId
        List<Long> skuIdListExist = baseMapper.selectSkuIdListExist(skuIdList);
        List<SkuInfo> finalSkuInfoList = new ArrayList<>();
        for (SkuInfo skuInfo:skuInfoList){
            if (!skuIdListExist.contains(skuInfo.getId())){
                finalSkuInfoList.add(skuInfo);
            }
        }
        return finalSkuInfoList;
    }
}
Mapper
package com.zhan_py.ssyx.activity.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zhan_py.ssyx.model.activity.ActivityInfo;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * @author zhan_py
 * @since 2024-02-29
 */
public interface ActivityInfoMapper extends BaseMapper<ActivityInfo> {

    List<Long> selectSkuIdListExist(@Param("skuIdList") List<Long> skuIdList);
}

在这里插入图片描述

<?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.zhan_py.ssyx.activity.mapper.ActivityInfoMapper">
    <select id="selectSkuIdListExist" resultType="Long">
        SELECT sku_id
        FROM `activity_info` info
        INNER JOIN `activity_sku` sku ON info.id = sku.activity_id
        <where>
            <if test="skuIdList != null">
                AND sku.sku_id in
                <foreach collection="skuIdList" index="index" item="item" open="(" close=")" separator=",">
                    #{item}
                </foreach>
            </if>
        </where>
        AND NOW() BETWEEN info.start_time and info.end_time;
    </select>
</mapper>

Spring Cloud Nacos

SpringCloud相关概念

微服务的由来

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

微服务本质

  • 微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。

  • 微服务的目的是有效的拆分应用,实现敏捷开发和部署 。

  • 微服务提倡的理念团队间应该是 inter-operate, not integrate 。inter-operate是定义好系统的边界和接口,在一个团队内全栈,让团队自治,原因就是因为如果团队按照这样的方式组建,将沟通的成本维持在系统内部,每个子系统就会更加内聚,彼此的依赖耦合能变弱,跨系统的沟通成本也就能降低。

什么样的项目适合微服务

微服务可以按照业务功能本身的独立性来划分,如果系统提供的业务是非常底层的,如:操作系统内核、存储系统、网络系统、数据库系统等等,这类系统都偏底层,功能和功能之间有着紧密的配合关系,如果强制拆分为较小的服务单元,会让集成工作量急剧上升,并且这种人为的切割无法带来业务上的真正的隔离,所以无法做到独立部署和运行,也就不适合做成微服务了。

微服务开发框架

目前微服务的开发框架,最常用的有以下四个:

Spring Cloud:http://projects.spring.io/spring-cloud(现在非常流行的微服务架构)

Dubbo:http://dubbo.io

Dropwizard:http://www.dropwizard.io (关注单个微服务的开发)

Consul、etcd&etc.(微服务的模块)

什么是Spring Cloud

Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性简化了分布式系统基础设施的开发,如服务发现、服务注册、配置中心、消息总线、负载均衡、 熔断器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过SpringBoot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包

Spring Cloud和Spring Boot是什么关系

Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。

Nacos基本概念
  • Nacos 是阿里巴巴推出来的一个新开源项目,是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
  • 常见的注册中心:
    • Eureka(原生,2.0遇到性能瓶颈,停止维护)
    • Zookeeper(支持,专业的独立产品。例如:dubbo)
    • Consul(原生,GO语言开发)
    • Nacos

相对于 Spring Cloud Eureka 来说,Nacos 更强大。Nacos = Spring Cloud Eureka + Spring Cloud Config

Nacos 可以与 Spring, Spring Boot, Spring Cloud 集成,并能代替 Spring Cloud Eureka, Spring Cloud Config

通过 Nacos Server 和 spring-cloud-starter-alibaba-nacos-discovery 实现服务的注册与发现。

  • Nacos是以服务为主要服务对象的中间件,Nacos支持所有主流的服务发现、配置和管理。

    Nacos主要提供以下四大功能:

    1. 服务发现和服务健康监测

    2. 动态配置服务

    3. 动态DNS服务

    4. 服务及其元数据管理

  • Nacos结构图

在这里插入图片描述

Nacos下载和安装

(1)下载地址和版本

下载地址:https://github.com/alibaba/nacos/releases

下载版本:nacos-server-2.2.1.tar.gz或nacos-server-2.2.1.zip,解压没有中文没有空格目录即可

(2)修改配置文件

参考官方文档:https://nacos.io/zh-cn/docs/v2/guide/user/auth.html

在这里插入图片描述

添加如下内容:

在这里插入图片描述

(3)启动nacos服务

Linux/Unix/Mac

  • 启动命令(standalone代表着单机模式运行,非集群模式)
  • 启动命令:sh startup.sh -m standalone

Windows

  • 启动方式,cmd打开,执行命令: startup.cmd -m standalone。
  • 访问:http://localhost:8848/nacos
  • 用户名密码:nacos/nacos

在这里插入图片描述

在这里插入图片描述

服务注册

把各个微服务注册到注册中心,其他模块步骤相同

在service模块配置pom

配置Nacos客户端的pom依赖

<!--服务注册-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
添加服务配置信息

配置application.yml,在客户端微服务中添加注册Nacos服务的配置信息

  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
添加Nacos客户端注解

在客户端微服务启动类中添加注解

@EnableDiscoveryClient
启动客户端微服务

启动注册中心

启动已注册的微服务,可以在Nacos服务列表中看到被注册的微服务

在这里插入图片描述

Elasticsearch

目的:用来存储商品的信息,提高商品的查询性能

下载elasticsearch7.17.18和elasticsearch-analysis-ik-7.17.18

注意elasticsearch和elasticsearch-analysis-ik最好版本一致

Elasticsearch (简称ES)是一个分布式、高扩展、高实时的、RESTful 风格的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。

Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:

{
    "name" :     "John",
    "sex" :      "Male",
    "age" :      25,
    "birthDate": "1990/05/01",
    "about" :    "I love to go rock climbing",
    "interests": [ "sports", "music" ]
}

用Mysql这样的数据库存储就会容易想到建立一张User表,有各个字段等,在ElasticSearch里这就是一个文档,当然这个文档会属于一个User的类型,各种各样的类型存在于一个索引当中。这里有一份简易的将Elasticsearch和关系型数据术语对照表:

在这里插入图片描述

下载和配置

下载ElasticSearch,kibana和ik分词器组件,需要注意这三个软件的版本需要一致

ik分词器的版本更新速度较慢,以ik分词器看齐

将ik分词器解压在elasticsearch\plugins目录下

修改kibana的配置文件kibana.yml

server.port: 5601
elasticsearch.hosts: ["http://localhost:9200"]
i18n.locale: "zh-CN"
整合ES

在这里插入图片描述

product服务负责商品上下架,执行的同时给消息队列发送上架商品的命令

ES服务监听消息队列接收到上架商品的命令,使用openfeign远程调用product服务查询商品详细信息

product服务根据skuid查询商品的分类信息和sku信息,返回给search服务

商品详细信息上传到ES数据库,存储商品的信息,提高商品的查询性能

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
</dependencies>
server:
  port: 8204

feign:
  sentinel:
    enabled: true
  client:
    config:
      default: #配置全局的feign的调用超时时间  如果 有指定的服务配置 默认的配置不会生效
        connectTimeout: 30000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接  单位是毫秒
        readTimeout: 50000  # 指定的是调用服务提供者的 服务 的超时时间()  单位是毫秒

spring:
  main:
    allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  elasticsearch:
    rest:
      uris: http://localhost:9200
  rabbitmq:
    host: 192.168.197.128
    port: 5672
    username: guest
    password: guest
    publisher-confirm-type: CORRELATED
    publisher-returns: true
    listener:
      simple:
        prefetch: 1
        concurrency: 3
        acknowledge-mode: manual
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 1800000
    password:
    lettuce:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0     #最小空闲
package com.zhan_py.ssyx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author zhan_py
 * @Date 2024/2/22 14:08
 */
// 配置文件没有配置MySQL数据库,在启动的时候排除数据库的自动装配
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceSearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceSearchApplication.class, args);
    }
}
整合OpenFeign
  1. 在product开发查询商品的分类信息和sku信息接口

    package com.zhan_py.ssyx.product.controller;
    
    import com.zhan_py.ssyx.model.product.Category;
    import com.zhan_py.ssyx.model.product.SkuInfo;
    import com.zhan_py.ssyx.product.service.CategoryService;
    import com.zhan_py.ssyx.product.service.SkuInfoService;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    /**
     * @author zhan_py
     * @Date 2024/2/22 14:42
     */
    @RestController
    @RequestMapping("/api/product")
    public class ProductInnerController {
        @Resource
        private CategoryService categoryService;
    
        @Resource
        private SkuInfoService skuInfoService;
        @GetMapping("/inner/getCategory/{categoryId}")
        public Category getCategory(@PathVariable Long categoryId){
            if (categoryId != null) {
                return categoryService.getById(categoryId);
            }
            return null;
        }
    
        @GetMapping("/inner/getSkuInfo/{skuId}")
        public SkuInfo getSkuInfo(@PathVariable Long skuId){
            if (skuId == null) {
                return null;
            }
            return skuInfoService.getById(skuId);
        }
    }
    
  2. 创建service-client模块和service-product-client子模块

    在这里插入图片描述

  3. 配置service-client的pom.xml

    <dependencies>
        <dependency>
            <groupId>com.zhan_py</groupId>
            <artifactId>common-util</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>provided </scope>
        </dependency>
    
        <dependency>
            <groupId>com.zhan_py</groupId>
            <artifactId>model</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>provided </scope>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided </scope>
        </dependency>
    
        <!-- 服务调用feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <scope>provided </scope>
        </dependency>
    </dependencies>
    
  4. 在serivce-product-client创建接口com.zhan_py.ssyx.client.product.ProductFeignClient.java,

    需要注意,由于SpringBoot扫描规则是扫描当前文件夹以及子文件夹,因此路径com.zhan_py.ssyx.需要与其他服务保持一致

    package com.zhan_py.ssyx.client.product;
    
    import com.zhan_py.ssyx.model.product.Category;
    import com.zhan_py.ssyx.model.product.SkuInfo;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    @FeignClient(value = "service-product")
    public interface ProductFeignClient {
    
        @GetMapping("/api/product/inner/getCategory/{categoryId}")
        Category getCategory(@PathVariable("categoryId") Long categoryId);
    
        @GetMapping("/api/product//inner/getSkuInfo/{skuId}")
        SkuInfo getSkuInfo(@PathVariable("skuId") Long skuId);
    }
    
  5. 在service-search服务中引入serivce-product-client

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    
        <dependency>
            <groupId>com.zhan_py</groupId>
            <artifactId>service-product-client</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    
  6. SkuApiController

    package com.zhan_py.ssyx.search.controller;
    
    import com.zhan_py.ssyx.common.result.Result;
    import com.zhan_py.ssyx.search.service.SkuService;
    import org.springframework.web.bind.annotation.*;
    
    import javax.annotation.Resource;
    
    /**
     * @author zhan_py
     * @Date 2024/2/22 14:13
     */
    @RestController
    @RequestMapping("api/search/sku")
    @CrossOrigin
    public class SkuApiController {
        @Resource
        private SkuService skuService;
    
        @GetMapping("/inner/upperSku/{skuId}")
        public Result<Object> upperSku(@PathVariable Long skuId){
            skuService.upperSku(skuId);
            return Result.success(null);
        }
    
        @GetMapping("/inner/lowerSku/{skuId}")
        public Result<Object> lowerSku(@PathVariable Long skuId){
            skuService.lowerSku(skuId);
            return Result.success(null);
        }
    }
    
  7. SkuServiceImpl调用OpenFeign

    package com.zhan_py.ssyx.search.service.impl;
    
    import com.zhan_py.ssyx.client.product.ProductFeignClient;
    import com.zhan_py.ssyx.enums.SkuType;
    import com.zhan_py.ssyx.model.product.Category;
    import com.zhan_py.ssyx.model.product.SkuInfo;
    import com.zhan_py.ssyx.model.search.SkuEs;
    import com.zhan_py.ssyx.search.repository.SkuRepository;
    import com.zhan_py.ssyx.search.service.SkuService;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    
    /**
     * @author zhan_py
     * @Date 2024/2/22 14:15
     */
    @Service
    public class SkuServiceImpl implements SkuService {
        @Resource
        private SkuRepository skuRepository;
    
        @Resource
        private ProductFeignClient productFeignClient;
        @Override
        public void upperSku(Long skuId) {
            SkuEs skuEs = new SkuEs();
            SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
            if (skuInfo == null){
                return;
            }
            Category category = productFeignClient.getCategory(skuInfo.getCategoryId());
            if (category != null) {
                skuEs.setCategoryId(category.getId());
                skuEs.setCategoryName(category.getName());
            }
            skuEs.setId(skuInfo.getId());
            skuEs.setKeyword(skuInfo.getSkuName()+","+skuEs.getCategoryName());
            skuEs.setWareId(skuInfo.getWareId());
            skuEs.setIsNewPerson(skuInfo.getIsNewPerson());
            skuEs.setImgUrl(skuInfo.getImgUrl());
            skuEs.setTitle(skuInfo.getSkuName());
            if(skuInfo.getSkuType().equals(SkuType.COMMON.getCode())) {
                skuEs.setSkuType(0);
                skuEs.setPrice(skuInfo.getPrice().doubleValue());
                skuEs.setStock(skuInfo.getStock());
                skuEs.setSale(skuInfo.getSale());
                skuEs.setPerLimit(skuInfo.getPerLimit());
            } else {
                //TODO 待完善-秒杀商品
            }
    
            skuRepository.save(skuEs);
        }
    
        @Override
        public void lowerSku(Long skuId) {
            skuRepository.deleteById(skuId);
        }
    }
    
  8. SkuRepository

    package com.zhan_py.ssyx.search.repository;
    
    import com.zhan_py.ssyx.model.search.SkuEs;
    import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
    
    public interface SkuRepository extends ElasticsearchRepository<SkuEs, Long> {
    }
    

Docker

安装docker

安装环境VMware虚拟机,Centos7操作系统

  1. 卸载docker

    这一步是可选的,建议做一下,以免出现报错

    yum remove docker \
                      docker-client \
                      docker-client-latest \
                      docker-common \
                      docker-latest \
                      docker-latest-logrotate \
                      docker-logrotate \
                      docker-selinux \
                      docker-engine-selinux \
                      docker-engine \
                      docker-ce
    
  2. 设置源仓库

    新主机上首次安装Docker Engine-Community之前,需要设置Docker仓库。在设置仓库之前,需先按照所需的软件包。yum-utils提供了yum-config-manager,并且device mapper存储驱动程序需要device-mapper-persistent-data和lvm2。

    sudo yum install -y yum-utils \
      device-mapper-persistent-data \
      lvm2
    

    官方源地址设置:

    sudo yum-config-manager \
      --add-repo \
      https://download.docker.com/linux/centos/docker-ce.repo
    

    清华数据源地址设置:

    sudo yum-config-manager \
      --add-repo \
      https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo
    
  3. 安装docker

    sudo yum install -y docker-ce docker-ce-cli containerd.io
    

    如果不需要docker-ce-cli或containerd.io可直接执行如下命令:

    yum install -y docker-ce
    
  4. 启动docker

    sudo systemctl start docker
    

    通过运行hello-world镜像来验证是否正确安装了Docker Engine-Community。

    // 拉取镜像
    sudo docker pull hello-world
    // 执行hello-world
    sudo docker run hello-world
    

    如果执行之后,控制台显示如下信息,则说明Docker安装和启动成功:

    在这里插入图片描述

  5. 其他命令:

    • 守护进程重启:systemctl daemon-reload
    • 重启Docker服务:systemctl restart docker / service docker restart
    • 关闭Docker服务:docker service docker stop / docker systemctl stop docker
    • 查看Docker容器:docker ps -a (包含未运行的)
    • 删除Docker容器:docker rm CONTAINER ID
    • 查看Docker状态:systemctl status docker
    • 设置Docker开机自启:sudo systemctl enable docke

RabbitMQ

RabbitMQ简介

以商品订单场景为例,如果商品服务和订单服务是两个不同的微服务,在下单的过程中订单服务需要调用商品服务进行扣库存操作。按照传统的方式,下单过程要等到调用完毕之后才能返回下单成功,如果网络产生波动等原因使得商品服务扣库存延迟或者失败,会带来较差的用户体验,如果在高并发的场景下,这样的处理显然是不合适的,那怎么进行优化呢?这就需要消息队列登场了。

消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处理网络通信,如果网络连接不可用,消息被暂存于队列当中,当网络畅通的时候在将消息转发给相应的应用程序或者服务,当然前提是这些服务订阅了该队列。如果在商品服务和订单服务之间使用消息中间件,既可以提高并发量,又降低服务之间的耦合度。

RabbitMQ就是这样一款消息队列。RabbitMQ是一个开源的消息代理的队列服务器,用来通过普通协议在完全不同的应用之间共享数据。

典型应用场景:

流量削峰。例如秒杀活动,在短时间内访问量急剧增加,使用消息队列,当消息队列满了就拒绝响应,跳转到错误页面,这样就可以使得系统不会因为超负载而崩溃

在这里插入图片描述

安装rabbitMQ
#拉取镜像,安装最新版的rabbitmq
docker pull rabbitmq:management
#创建容器启动
docker run -d --restart=always -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management
#关闭防火墙
systemctl stop firewalld.service
#查看防火墙状态
systemctl status firewalld.service

rabbitMQ管理后台:http://IP:15672

在这里插入图片描述
在这里插入图片描述

整合MQ
  1. 在common搭建rabbit_util模块

    在这里插入图片描述

  2. 在rabbit_util引入依赖

    <dependencies>
        <!--rabbitmq消息队列-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
    </dependencies>
    
  3. 添加service方法

    package com.zhan_py.ssyx.common.service;
    
    import org.springframework.amqp.AmqpException;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.core.MessagePostProcessor;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class RabbitService {
    
        //  引入操作rabbitmq 的模板
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        /**
         * 发送消息
         * @param exchange  交换机
         * @param routingKey    路由键
         * @param message   消息
         * @return
         */
        public boolean sendMessage(String exchange,String routingKey, Object message){
            //  调用发送数据的方法
            rabbitTemplate.convertAndSend(exchange,routingKey,message);
            return true;
        }
    
        /**
         * 发送延迟消息的方法
         * @param exchange  交换机
         * @param routingKey    路由键
         * @param message   消息内容
         * @param delayTime 延迟时间
         * @return
         */
        public boolean sendDelayMessage(String exchange,String routingKey, Object message, int delayTime){
    
            //  在发送消息的时候设置延迟时间
            rabbitTemplate.convertAndSend(exchange, routingKey, message, new MessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    //  设置一个延迟时间
                    message.getMessageProperties().setDelay(delayTime*1000);
                    return message;
                }
            });
            return true;
        }
    }
    
  4. 配置mq消息转换器

    package com.zhan_py.ssyx.common.config;
    
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class MQConfig {
    
        @Bean
        public MessageConverter messageConverter(){
            return new Jackson2JsonMessageConverter();
        }
    }
    
    

    说明:默认是字符串转换器

  5. 添加消息的确认配置

    package com.zhan_py.ssyx.common.config;
    
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.connection.CorrelationData;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import javax.annotation.PostConstruct;
    
    @Component
    public class MQProducerAckConfig implements RabbitTemplate.ReturnCallback,RabbitTemplate.ConfirmCallback {
    
        //  我们发送消息使用的是 private RabbitTemplate rabbitTemplate; 对象
        //  如果不做设置的话 当前的rabbitTemplate 与当前的配置类没有任何关系!
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        //  设置 表示修饰一个非静态的void方法,在服务器加载Servlet的时候运行。并且只执行一次!
        @PostConstruct
        public void init(){
            rabbitTemplate.setReturnCallback(this);
            rabbitTemplate.setConfirmCallback(this);
        }
    
        /**
         * 表示消息是否正确发送到了交换机上
         * @param correlationData   消息的载体
         * @param ack   判断是否发送到交换机上
         * @param cause 原因
         */
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            if(ack){
                System.out.println("消息发送成功!");
            }else {
                System.out.println("消息发送失败!"+cause);
            }
        }
    
        /**
         * 消息如果没有正确发送到队列中,则会走这个方法!如果消息被正常处理,则这个方法不会走!
         * @param message
         * @param replyCode
         * @param replyText
         * @param exchange
         * @param routingKey
         */
        @Override
        public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
            System.out.println("消息主体: " + new String(message.getBody()));
            System.out.println("应答码: " + replyCode);
            System.out.println("描述:" + replyText);
            System.out.println("消息使用的交换器 exchange : " + exchange);
            System.out.println("消息使用的路由键 routing : " + routingKey);
        }
    }
    
  6. 添加常量类

    package com.zhan_py.ssyx.common.constant;
    
    public class MqConst {
        /**
         * 消息补偿
         */
        public static final String MQ_KEY_PREFIX = "ssyx.mq:list";
        public static final int RETRY_COUNT = 3;
    
        /**
         * 商品上下架
         */
        public static final String EXCHANGE_GOODS_DIRECT = "ssyx.goods.direct";
        public static final String ROUTING_GOODS_UPPER = "ssyx.goods.upper";
        public static final String ROUTING_GOODS_LOWER = "ssyx.goods.lower";
        //队列
        public static final String QUEUE_GOODS_UPPER  = "ssyx.goods.upper";
        public static final String QUEUE_GOODS_LOWER  = "ssyx.goods.lower";
    
        /**
         * 团长上下线
         */
        public static final String EXCHANGE_LEADER_DIRECT = "ssyx.leader.direct";
        public static final String ROUTING_LEADER_UPPER = "ssyx.leader.upper";
        public static final String ROUTING_LEADER_LOWER = "ssyx.leader.lower";
        //队列
        public static final String QUEUE_LEADER_UPPER  = "ssyx.leader.upper";
        public static final String QUEUE_LEADER_LOWER  = "ssyx.leader.lower";
    
        //订单
        public static final String EXCHANGE_ORDER_DIRECT = "ssyx.order.direct";
        public static final String ROUTING_ROLLBACK_STOCK = "ssyx.rollback.stock";
        public static final String ROUTING_MINUS_STOCK = "ssyx.minus.stock";
    
        public static final String ROUTING_DELETE_CART = "ssyx.delete.cart";
        //解锁普通商品库存
        public static final String QUEUE_ROLLBACK_STOCK = "ssyx.rollback.stock";
        public static final String QUEUE_SECKILL_ROLLBACK_STOCK = "ssyx.seckill.rollback.stock";
        public static final String QUEUE_MINUS_STOCK = "ssyx.minus.stock";
        public static final String QUEUE_DELETE_CART = "ssyx.delete.cart";
    
        //支付
        public static final String EXCHANGE_PAY_DIRECT = "ssyx.pay.direct";
        public static final String ROUTING_PAY_SUCCESS = "ssyx.pay.success";
        public static final String QUEUE_ORDER_PAY  = "ssyx.order.pay";
        public static final String QUEUE_LEADER_BILL  = "ssyx.leader.bill";
    
        //取消订单
        public static final String EXCHANGE_CANCEL_ORDER_DIRECT = "ssyx.cancel.order.direct";
        public static final String ROUTING_CANCEL_ORDER = "ssyx.cancel.order";
        //延迟取消订单队列
        public static final String QUEUE_CANCEL_ORDER  = "ssyx.cancel.order";
    
        /**
         * 定时任务
         */
        public static final String EXCHANGE_DIRECT_TASK = "ssyx.exchange.direct.task";
        public static final String ROUTING_TASK_23 = "ssyx.task.23";
        //队列
        public static final String QUEUE_TASK_23  = "ssyx.queue.task.23";
    }
    
完善上下架功能

service-search和service-product引入rabbitmq-util包

<dependency>
    <groupId>com.zhan_py</groupId>
    <artifactId>rabbitmq_util</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

上下架功能

package com.zhan_py.ssyx.product.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhan_py.ssyx.constant.MQConstant;
import com.zhan_py.ssyx.model.product.SkuAttrValue;
import com.zhan_py.ssyx.model.product.SkuImage;
import com.zhan_py.ssyx.model.product.SkuInfo;
import com.zhan_py.ssyx.model.product.SkuPoster;
import com.zhan_py.ssyx.product.mapper.SkuInfoMapper;
import com.zhan_py.ssyx.product.service.SkuAttrValueService;
import com.zhan_py.ssyx.product.service.SkuImageService;
import com.zhan_py.ssyx.product.service.SkuInfoService;
import com.zhan_py.ssyx.product.service.SkuPosterService;
import com.zhan_py.ssyx.service.RabbitMQService;
import com.zhan_py.ssyx.vo.product.SkuInfoQueryVo;
import com.zhan_py.ssyx.vo.product.SkuInfoVo;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.List;

/**
 * <p>
 * sku信息 服务实现类
 * </p>
 *
 * @author zhan_py
 * @since 2024-02-05
 */
@Service
public class SkuInfoServiceImpl extends ServiceImpl<SkuInfoMapper, SkuInfo> implements SkuInfoService {

    @Resource
    private RabbitMQService rabbitMQService;

    @Override
    public void publish(Long skuId, Integer status) {
        if(status == 1) {
            SkuInfo skuInfoUp = new SkuInfo();
            skuInfoUp.setId(skuId);
            skuInfoUp.setPublishStatus(1);
            baseMapper.updateById(skuInfoUp);
            // 商品上架 发送mq消息更新es数据
            rabbitMQService.sendMessage(MQConstant.EXCHANGE_GOODS_DIRECT,
                    MQConstant.ROUTING_GOODS_UPPER, skuId);
        } else {
            SkuInfo skuInfoUp = new SkuInfo();
            skuInfoUp.setId(skuId);
            skuInfoUp.setPublishStatus(0);
            baseMapper.updateById(skuInfoUp);
            // 商品下架 送mq消息更新es数据
            rabbitMQService.sendMessage(MQConstant.EXCHANGE_GOODS_DIRECT,
                    MQConstant.ROUTING_GOODS_LOWER, skuId);
        }
    }
}

在search服务中新建一个模块

package com.zhan_py.ssyx.search.receiver;

import com.rabbitmq.client.Channel;
import com.zhan_py.ssyx.constant.MQConstant;
import com.zhan_py.ssyx.search.service.SkuService;
import com.zhan_py.ssyx.service.RabbitMQService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;

/**
 * @author zhan_py
 * @Date 2024/2/28 15:30
 */
@Component
public class SkuReceiver {
    @Resource
    private SkuService skuService;

    // 商品上架
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MQConstant.QUEUE_GOODS_UPPER, durable = "true"),
            exchange = @Exchange(value = MQConstant.EXCHANGE_GOODS_DIRECT),
            key = {MQConstant.ROUTING_GOODS_UPPER}))
    public void upperSku(Long skuId, Message message, Channel channel) throws IOException {
        if (null != skuId) {
            skuService.upperSku(skuId);
        }
        /*
         * 第一个参数:表示收到的消息的标号
         * 第二个参数:如果为true表示可以签收多个消息
         */
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

    // 商品下架
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MQConstant.QUEUE_GOODS_LOWER, durable = "true"),
            exchange = @Exchange(value = MQConstant.EXCHANGE_GOODS_DIRECT),
            key = {MQConstant.ROUTING_GOODS_LOWER}
    ))
    public void lowerSku(Long skuId, Message message, Channel channel) throws IOException {
        if (null != skuId) {
            skuService.lowerSku(skuId);
        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
总结

Kibana:http://localhost:5601/

RabbitMQ:http://localhost:15672/

  1. 创建一个rabbitmq-util,里面包含rabbitmq的工具类,常量和整合的发送信息方法(不是必要但是结构很好用)

  2. 商品在上架和下架的过程中不仅对数据库进行操作还给rabbitmq发送一条消息

  3. 在给rabbitmq发送信息的时候调用rabbitMQService.sendMessage方法,传入参数为交换机,路由key和需要发送的消息

  4. search服务在接收的完消息后调用openfeign去product服务去查询消息

  5. 接收消息的方法为@RabbitListener注解,需要配置通道,交换机和路由key,来找到对应的消息

  6. 将openfeign新建一个模块用来作为两个服务的桥梁,openfeign的配置很简单,定义一个接口,需要配置服务名,服务地址,和方法。调用方只需要自动注入这个类然后调用方法就可以。

    如果参数是请求地址最好加上@PathVariable注解

    @FeignClient(value = "service-product")
    public interface ProductFeignClient {
    
        @GetMapping("/api/product/inner/getCategory/{categoryId}")
        Category getCategory(@PathVariable("categoryId") Long categoryId);
    
        @GetMapping("/api/product//inner/getSkuInfo/{skuId}")
        SkuInfo getSkuInfo(@PathVariable("skuId") Long skuId);
    }
    
  7. 整个业务流程:前端发送上架请求,product服务给rabbitmq发送需要上架的skuid,search接收到消息后调用openfeign查询商品详细信息,将详细信息上传到ES里,ES的上传操作是通过继承ElasticsearchRepository的接口操作的,@Document设置了ES上的信息

    public interface SkuRepository extends ElasticsearchRepository<SkuEs, Long> {
    }
    @Document(indexName = "skues" ,shards = 3,replicas = 1)
    public class SkuEs {}
    

Spring Cloud Gateway

Gateway

网关提供API全托管服务,丰富的API管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等贡呢。一般来说网关对外暴露的URL或者接口信息,我们统称为路由信息。如果研发过网关中间件或者使用过Zuul的人,会知道网关的核心是Filter以及Filter Chain(Filter责任链)。Sprig Cloud Gateway也具有路由和Filter的概念。下面介绍一下Spring Cloud Gateway中几个重要的概念。

  1. **路由。**路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配
  2. 断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。
  3. 过滤器。一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理

在这里插入图片描述

如图所示,Spring Cloud Gateway发出请求。然后再由Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler再通过指定的过滤器链将请求发送到实际的服务执行业务逻辑,然后返回。

引入依赖
<dependencies>
    <dependency>
        <groupId>com.zhan_py</groupId>
        <artifactId>common-util</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>
配置文件
application.yml
spring:
  application:
    name: server-gateway
  profiles:
    active: dev
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
application-dev.yml
server:
  port: 8200

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: service-acl
          uri: lb://service-acl
          predicates:
            - Path=/*/acl/**

        - id: service-sys
          uri: lb://service-sys
          predicates:
            - Path=/*/sys/**

        - id: service-product
          uri: lb://service-product
          predicates:
            - Path=/*/product/**

        - id: service-activity
          uri: lb://service-activity
          predicates:
            - Path=/*/activity/**

        - id: service-order
          uri: lb://service-order
          predicates:
            - Path=/*/order/**

        - id: service-payment
          uri: lb://service-payment
          predicates:
            - Path=/*/payment/**

        - id: service-user
          uri: lb://service-user
          predicates:
            - Path=/*/user/**

        - id: service-search
          uri: lb://service-search
          predicates:
            - Path=/*/search/**

        - id: service-home
          uri: lb://service-home
          predicates:
            - Path=/*/home/**

        - id: service-cart
          uri: lb://service-cart
          predicates:
            - Path=/*/cart/**
启动类
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceGatewayApplication.class, args);
    }

}
解决跨域配置
package com.zhan_py.ssyx.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * @author zhan_py
 * @Date 2024/2/29 18:50
 */

@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

跨域和@CrossOrigin注解只能存在一个

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值