文章目录
里面包含了尚上优选的一部分代码,主要是梳理各个组件的使用方法和自己不会的东西,比如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操作,同时提供了自动分页,代码生成,逻辑删除等丰富的功能。
- 配置Mapper扫描
- 配置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参数名 | 作用 | 默认值 |
---|---|---|---|
String | title | 标题 | Api Documentation |
String | description | 描述 | Api Documentation |
String | termsOfServiceUrl | 服务条款网址 | urn:tos |
String | license | 许可 | Apache 2.0 |
String | licenseUrl | 许可链接 | http://www.apache.org/licenses/LICENSE-2.0 |
String | version | 版本 | 1.0 |
Contact | Contact | 维护人信息 | null |
contact参数表
类型 | 参数名 | 作用 | 默认值 |
---|---|---|---|
String | name | 维护人姓名 | “” |
String | url | 维护人url地址 | “” |
String | 维护人邮箱 | “” |
工具类(重要)
统一返回结果类
项目中我们会将接口返回的数据封装成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();
}
}
这里我有几点疑惑
- super(message)是在干什么,对super的理解不到位,这里为什么要调用父类的构造器
- 异常处理器的第二个error方法,前端收到的message都仅仅是失败,无法根据message来判断问题,为什么不将Result的message设置成异常的message,是因为只想让用户知道成功与否,不想让用户知道后端出了问题。
这里的代码完全照抄,做不到自己手敲
实体类
直接复制代码,要注意包名需要修改
idea全局查找快捷键ctrl+shift+f,全局替换快捷键ctrl+shift+q
平台管理系统
前端
安装相关软件
需要安装nodejs(16.19版本)和vscode
vscode:
- 先建立一个文件夹
- 使用vscode打开这个文件夹
- 另存工作区
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请求的一种限制
解决方式:
- 在controller加上@CrossOrigin
- 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
- 在实体类中属性加上@TableLogic注解,表示该字段是逻辑删除字段
- 增加注解后调用BaseMapper的deleteById(id)或者IService的removeById(id),是逻辑删除。如果没有增加该注解,是真删除
- @TableLogic注解参数
- value = “未删除的值,默认值为0”
- delval = “删除后的值,默认值为1”
- 如果不设置,就使用默认值
- 当使用了@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<>();
}
分配角色
- 显示所有角色
- 显示用户已经分配的角色
- 分配角色
菜单接口
尚上优选的菜单是分级菜单,分级菜单在查询的时候需要将级别和归属菜单的关系也表现出来
基本功能
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
- 登录阿里云网站并注册
- 进入OSS管理控制台
- 创建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;
}
}
}
营销管理接口
需求
- 营销活动的增删改查
- 给某一条营销活动指定规则
- 规格分为满减形式和优惠券形式,可以选择指定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主要提供以下四大功能:
-
服务发现和服务健康监测
-
动态配置服务
-
动态DNS服务
-
服务及其元数据管理
-
-
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
-
在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); } }
-
创建service-client模块和service-product-client子模块
-
配置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>
-
在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); }
-
在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>
-
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); } }
-
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); } }
-
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操作系统
-
卸载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
-
设置源仓库
新主机上首次安装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
-
安装docker
sudo yum install -y docker-ce docker-ce-cli containerd.io
如果不需要docker-ce-cli或containerd.io可直接执行如下命令:
yum install -y docker-ce
-
启动docker
sudo systemctl start docker
通过运行hello-world镜像来验证是否正确安装了Docker Engine-Community。
// 拉取镜像 sudo docker pull hello-world // 执行hello-world sudo docker run hello-world
如果执行之后,控制台显示如下信息,则说明Docker安装和启动成功:
-
其他命令:
- 守护进程重启: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
-
在common搭建rabbit_util模块
-
在rabbit_util引入依赖
<dependencies> <!--rabbitmq消息队列--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency> </dependencies>
-
添加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; } }
-
配置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(); } }
说明:默认是字符串转换器
-
添加消息的确认配置
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); } }
-
添加常量类
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/
-
创建一个rabbitmq-util,里面包含rabbitmq的工具类,常量和整合的发送信息方法(不是必要但是结构很好用)
-
商品在上架和下架的过程中不仅对数据库进行操作还给rabbitmq发送一条消息
-
在给rabbitmq发送信息的时候调用rabbitMQService.sendMessage方法,传入参数为交换机,路由key和需要发送的消息
-
search服务在接收的完消息后调用openfeign去product服务去查询消息
-
接收消息的方法为@RabbitListener注解,需要配置通道,交换机和路由key,来找到对应的消息
-
将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); }
-
整个业务流程:前端发送上架请求,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中几个重要的概念。
- **路由。**路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配
- 断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。
- 过滤器。一个标准的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注解只能存在一个