风之子博客-测试源码分享 - 网站搭建技术
type: channel
link: https://www.canace.cn/index.php/category/web/
items: SpringBoot集成RabbitMq，实现监听数据变化后更新内容
items: springboot3+canal+rabbitMQ实现对缓存进行清除
items: 如何查询树形结构的数据
items: 完美解决方案-雪花算法ID到前端之后精度丢失问题
items: tymeleaf教程 标签内，js中使用表达式
items: vue框架下部署上线后刷新报404问题解决方案
items: Thymeleaf常用语法：模板注释
items: Mybatis-plus自动代码生成工具
items: SpingBoot配置七牛云存储
items: vue单页面标题设置 title

SpringBoot集成RabbitMq，实现监听数据变化后更新内容
type: item
date: 2024-02-23T16:49:00+08:00
description: 集成Spring AMQP1.可通过如下 Docker 命令 安装 RabbiMQ: docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.10-management2.登录 RabbiMQ 的 web 管理界面，创建虚拟主机novel: 3.项目中加入如下的 maven 依赖： org.springframework.boot spring-boot-starter-amqp 4.在 application.yml 配置文件中加入 RabbitMQ 的连接配置:spring: rabbitmq: addresses: "amqp://guest:guest@47.106.243.172" virtual-host: novel template: retry: # 开启重试 enabled: true # 最大重试次数 max-attempts: 3 # 第一次和第二次重试之间的持续时间 initial-interval: "3s"相关配置1.建立相关常量:/** * @author canace * @version 1.0 * @description AMQP 相关常量 * @date 2024/2/23 10:15 */ public class AmqpConsts { /** * 小说信息改变MQ */ public static class BookChangeMQ { /** * 小说信息改变交换机 */ public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE"; /** * Elasticsearch book 索引更新的队列 */ public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE"; /** * Redis book 缓存更新的队列 */ public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE"; } }2.创建 AMQP 配置类，配置各个交换机、队列以及绑定关系：/** * @author canace * @version 1.0 * @description AMPQ 配置类 * @date 2024/2/23 10:25 */ @Configuration public class AmqpConfig { /** * 小说信息改变交换机(广播模式) */ @Bean public FanoutExchange bookChangeExchange() { return new FanoutExchange(AmqpConsts.BookChangeMQ.EXCHANGE_NAME); } /** * Elasticsearch book 索引更新队列 */ @Bean public Queue esBookUpdateQueue() { return new Queue(AmqpConsts.BookChangeMQ.QUEUE_ES_UPDATE); } /** * Elasticsearch book 索引更新队列绑定到小说信息改变交换机 */ @Bean public Binding esBookUpdateQueueBinding() { return BindingBuilder.bind(esBookUpdateQueue()).to(bookChangeExchange()); } }3.配置 AMQP 消息管理类：/** * @author canace * @version 1.0 * @description AMQP 消息管理器 * @date 2024/2/23 10:32 */ @Component @RequiredArgsConstructor public class AmqpMsgManager { private final AmqpTemplate amqpTemplate; // 判断配置是否启动 RabbitMq @Value("${spring.amqp.enabled:false}") private boolean amqpEnabled; // 在小说进行信息更新或者新建时，只需调用这个接口即可实现 // 这里没有写更新书籍信息的接口，不做演示 // amqpMsgManager.sendBookChangeMsg(dto.getBookId()); public void sendBookChangeMsg…

springboot3+canal+rabbitMQ实现对缓存进行清除
type: item
date: 2024-01-30T19:11:51+08:00
description: Canal 工作原理Canal 模拟 MySQL slave 的交互协议，伪装自己为 MySQL slave ，向 MySQL master 发送dump 协议MySQL master 收到 dump 请求，开始推送 binary log 给 slave (即 canal )Canal 解析 binary log 对象(原始为 byte 流)环境准备本项目所有的环境都是在 Docker 环境下进行了，需要自行在安装 DockerMysql安装# 拉取镜像 docker pull mysql # 创建文件并写入内容 vim /usr/local/mysql/conf/my.cnf [client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] # 设置东八区时区 default-time_zone = '+8:00' # 设置密码验证规则，default_authentication_plugin参数已被废弃 # 改为authentication_policy #default_authentication_plugin=mysql_native_password authentication_policy=mysql_native_password # 限制导入和导出的数据目录 # 为空，不限制导入到处的数据目录； # 指定目录，必须从该目录导入到处，且MySQL不会自动创建该目录； # 为NULL，禁止导入与导出功能 #secure_file_priv=/var/lib/mysql secure_file_priv= init_connect='SET collation_connection = utf8mb4_0900_ai_ci' init_connect='SET NAMES utf8mb4' character-set-server=utf8mb4 collation-server=utf8mb4_0900_ai_ci skip-character-set-client-handshake skip-name-resolve [mysqld] log-bin=mysql-bin # 开启binlog binlog-format=ROW # 选择ROW模式 server_id=1 # 配置MySQL replaction需要定义，不和Canal的slaveId重复即可 # 运行mysql容器 docker run -p 3306:3306 --name mysql --restart=always --privileged=true \ -v /usr/local/mysql/log:/var/log/mysql \ -v /usr/local/mysql/data:/var/lib/mysql \ -v /usr/local/mysql/conf:/etc/mysql \ -v /etc/localtime:/etc/localtime:ro \ -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latestCanal安装这里建议使用v1.1.6版本，v1.1.7版本使用时报错# mysql创建授权账号 CREATE USER canal IDENTIFIED BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; -- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ; FLUSH PRIVILEGES; # 先创建一个临时容器 docker run --name canal -p 11111:11111 \ -d canal/canal-server:v1.1.6 # 复制配置文件 docker cp canal:/home/admin/canal-server/conf/canal.properties /mydata/canal/conf docker cp canal:/home/admin/canal-server/conf/example/instance.properties /mydata/canal/conf # 删除该临时容器 docker rm -f canal # 新建canal容器 docker run --name canal -p 11111:11111 \ -v /mydata/canal/conf/instance.properties:/home/admin/canal-server/con…

如何查询树形结构的数据
type: item
date: 2023-04-14T11:44:00+08:00
description: 如何查询树形结构的数据在遇到数据库保存的数据为树形结构，如多级分类列表或者评论等，此处以分类列表举例我们需要向前端返回如下部分数据[ { "id": "1-1", "name": "前端开发", "label": "前端开发", "parentid": "1", "isShow": 1, "orderby": 1, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-1-1", "name": "HTML/CSS", "label": "HTML/CSS", "parentid": "1-1", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-10", "name": "其它", "label": "其它", "parentid": "1-1", "isShow": 1, "orderby": 10, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-2", "name": "JavaScript", "label": "JavaScript", "parentid": "1-1", "isShow": 1, "orderby": 2, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-3", "name": "jQuery", "label": "jQuery", "parentid": "1-1", "isShow": 1, "orderby": 3, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-4", "name": "ExtJS", "label": "ExtJS", "parentid": "1-1", "isShow": 1, "orderby": 4, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-5", "name": "AngularJS", "label": "AngularJS", "parentid": "1-1", "isShow": 1, "orderby": 5, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-6", "name": "ReactJS", "label": "ReactJS", "parentid": "1-1", "isShow": 1, "orderby": 6, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-7", "name": "Bootstrap", "label": "Bootstrap", "parentid": "1-1", "isShow": 1, "orderby": 7, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-8", "name": "Node.js", "label": "Node.js", "parentid": "1-1", "isShow": 1, "orderby": 8, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-1-9", "name": "Vue", "label": "Vue", "parentid": "1-1", "isShow": 1, "orderby": 9, "isLeaf": 1, "childrenTreeNodes": null } ] }, { "id": "1-10", "name": "研发管理", "label": "研发管理", "parentid": "1", "isShow": 1, "orderby": 10, "isLeaf": 0, "childrenTreeNodes": [ { "id": "1-10-1", "name": "敏捷开发", "label": "敏捷开发", "parentid": "1-10", "isShow": 1, "orderby": 1, "isLeaf": 1, "childrenTreeNodes": null }, { "id": "1-10-2", "name": "软件设计", "label": "软件设计", "parentid": "1-10", "isShow": 1, "orderby": 2,…

完美解决方案-雪花算法ID到前端之后精度丢失问题
type: item
date: 2023-04-07T22:15:00+08:00
description: 一、现象是这样的下面我把异常的现象给大家描述一下，小伙伴建了一张表，表的主键是id BigINT，用来存储雪花算法生成的ID，嗯，这个没有问题！CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', #其他字段省略 );使用Long 类型对应数据库ID数据。嗯，也没有问题，雪花算法生成的就是一串数字，Long类型属于标准答案！@Data public class User { private Long id; //其他成员变量省略在后端下断点。看到数据响应以JSON响应给前端，正常{ id：1297873308628307970, //其他属性省略 }最后，这条数据返回给前端，前端接收到之后，修改这条数据，后端再次接收回来。奇怪的问题出现了：后端重新接收回来的id变成了： 12978733086283000000 ，不再是 1297873308628307970 二、分析问题我的第一感觉是，开发小伙伴把数据给搞混了，张冠李戴了，把XXX的对象ID放到了YYY对象的ID上。所以，就按照代码从前端到后端、从后端到前端调试跟踪了一遍。从代码的逻辑角度上没有任何问题。这时，我有点烦躁了，真的是耽误我下班了！但开工没有回头箭，既然坐下来了就得帮他解决，不然以后这队伍怎么带？想到这我又静下心来，开始思考。1297873308628300000 ---> 1297873308628307970 这两个数长得还挺像的，似乎是被四舍五入了。此时脑袋里面冒出一个想法，是精度丢失了么？哪里能导致精度丢失？服务端都是Long类型的id，不可能丢失前端是什么类型，JSON字符串转js对象，接收Long类型的是number上网查了一下Number精度是16位（雪花ID是19位的），So：JS的Number数据类型导致的精度丢失。问题是找到了！小伙伴投来敬佩的眼光，5分钟就把这问题发现了。可是发现了有什么用？得解决问题啊！三、解决问题开发小伙伴说：那我把所有的数据库表设计，id字段由Long类型改成String类型吧。我问他你有多少张表？他说100多张吧。100多张表还有100多个实体类需要改还有各种使用到实体类的Service层要改Service等改完Controller层要改关键的是String和Long都是常用类型，他还不敢批量替换小伙伴拿起电话打算订餐，说今晚的加班是无法避免了。我想了想说：你最好别改，String做ID查询性能会下降，我再想想！后端A到前端B出现精度丢失，要么改前端，要么改后端，要么…… 。“哎哎，你等等先别订餐，后端A到前端B你用的什么做的序列化？” 小伙伴告诉我说使用的是Jackson，这就好办了，Jackson我熟悉啊！解决思路：后端的ID(Long) ==> Jackson(Long转String) ==> 前端使用String类型的ID,前端使用js string精度就不会丢失了。 那前端再把String类型的19位数字传回服务端的时候，可以用Long接收么？当然可以，这是Spring反序列化参数接收默认支持的行为。最终方案就是：前端用String类型的雪花ID保持精度，后端及数据库继续使用Long(BigINT)类型不影响数据库查询执行效率。剩下的问题就是：在Spring Boot应用中，使用Jackson进行JSON序列化的时候怎么将Long类型ID转成String响应给前端。方案如下： import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; /** * 解决后端传给前端Long类型，前端处理后有可能失精问题 * 雪花算法1…

tymeleaf教程 标签内，js中使用表达式
type: item
date: 2021-03-12T08:52:11+08:00
description: 表达式同样可以在javascript中使用。先用属性声明一下：javascript ( th:inline=”javascript” )，然后我们开始在js中声明变量： /[[…]]/表达式的理解如下：/…/中的内容在用浏览器打开静态 网页时，会被忽略.‘Sebastian’会在浏览器中显示。静态时。Thymeleaf解析时，会解析/[[…]]/的内容，并把获得的值替换掉/[[…]]/后面的内容。所以执行的结果如下： 你也可以不用注释： 这会让它在静态显示时出现错误。注意：引擎求值后注入是智能的，它可以智能赋值一下类型的数据。StringsNumbersBooleansArraysCollectionsMapsBeans (objects with getter and setter methods)举个栗子：script th:inline="javascript"> /* */ ... var user = /*[[${session.user}]]*/ null; ... /* */ ${session.user}会获取一个user对象。写入后如下： 引擎同样允许增加和删除代码块，增加代码块：var x = 23; /*[+ var msg = 'This is a working application'; +]*/ var f = function() { ...解析如下：var x = 23; var msg = 'This is a working application'; var f = function() { ...删除代码块：var x = 23; /*[- */ var msg = 'This is a non-working template'; /* -]*/ var f = function(){ ...解析如下：var x = 23; var f = function(){ ...

vue框架下部署上线后刷新报404问题解决方案
type: item
date: 2021-03-03T10:11:00+08:00
description: nginx配置location / { try_files $uri $uri/ /index.html; }vue history模式下nginx配置在宝塔的网站设置中修改 server { listen 80; server_name testwx.wangshibo.com; root /Data/app/xqsj_wx/dist; index index.html; access_log /var/log/testwx.log main; ## 注意从这里开始 location / { try_files $uri $uri/ @router; index index.html; } location @router { rewrite ^.*$ /index.html last; } }

Thymeleaf常用语法：模板注释
type: item
date: 2021-03-01T09:38:00+08:00
description: 1、标准HTML/XML注释直接通过浏览器打开，不显示，Thymeleaf模板引擎解析也不处理，但查看网页源代码，注释有在里面。语法： 浏览器界面不显示，查看浏览器网页源码有显示；2、解析层注释直接通过浏览器打开，会显示。但Thymeleaf模板引擎解析时会移除注释标签和里面内容。一般开发人员使用，减少对原型页面的破坏。语法： 内容 3、原型注释直接通过浏览器打开，不显示。但Thymeleaf模板引擎解析时只移除注释标签，不会移除里面内容，最后会正常输出。一般界面设计人员使用。语法：

Mybatis-plus自动代码生成工具
type: item
date: 2021-02-08T20:52:10+08:00
description: 添加依赖 com.baomidou mybatis-plus-boot-starter 3.2.0 org.springframework.boot spring-boot-starter-freemarker com.baomidou mybatis-plus-generator 3.2.0 import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.ArrayList; import java.util.List; import java.util.Scanner; /** * 代码生成器 */ // 演示例子，执行 main 方法控制台输入模块表名回车自动生成对应项目目录中 public class CodeGenerator { /** * * 读取控制台内容 * */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + "："); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "！"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); // gc.setOutputDir("D:\\test"); // gc.setAuthor("LXQ"); gc.setOpen(false); // gc.setSwagger2(true); 实体属性 Swagger2 注解 gc.setServiceName("%sService"); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC"); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); mpg.setDataSource(dsc); // 包…

SpingBoot配置七牛云存储
type: item
date: 2021-02-02T21:09:00+08:00
description: 1、七牛云存储相关依赖 com.qiniu qiniu-java-sdk 7.2.28 com.google.code.gson gson 2.8.5 junit junit 2、yml文件配置#七牛云存储配置 qiniu: accessKey: SVc4nB8AQffickdBj4MoW-lxFX-P8ZzLmuEAcQhh secretKey: s8-4wL8RkOsCooBEcCJbPOZMuavhBL6HNgKW6vYV # 对象储存 bucket: music-web # 空间名称 path: https://file.canace.cn/ # 访问域名3、七牛云Config类配置import com.google.gson.Gson; import com.qiniu.common.QiniuException; import com.qiniu.common.Zone; import com.qiniu.http.Response; import com.qiniu.storage.Configuration; import com.qiniu.storage.UploadManager; import com.qiniu.storage.model.DefaultPutRet; import com.qiniu.util.Auth; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.FileInputStream; /** * 七牛云文件上传 */ @Data @Component public class QiNiuYunConfig { @Value("${qiniu.accessKey}") private String accessKey; @Value("${qiniu.secretKey}") private String secretKey; @Value("${qiniu.bucket}") private String bucket; @Value("${qiniu.path}") private String path; public String uploadToQiNiu(FileInputStream file, String filename) { // 构造一个带指定Zone对象的配置类，注意后面的zone各个地区不一样的 //zone2为华南地区 Configuration cfg = new Configuration(Zone.zone2()); // 其他参数参考类注释 UploadManager uploadManager = new UploadManager(cfg); // 生成密钥 Auth auth = Auth.create(accessKey, secretKey); try { String upToken = auth.uploadToken(bucket); try { Response response = uploadManager.put(file, filename, upToken, null, null); // 解析上传成功的结果 DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class); // 这个returnPath是获得到的外链地址 String returnPath = getPath() +putRet.key; return returnPath; } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { //ignore } } } catch (Exception e) { e.printStackTrace(); } return ""; } }4、文件上传Controller（） //获取文件名 String fileName = avatorFile.getOriginalFilename(); FileInputStream inputStream = (FileInputStream) avatorFil…

vue单页面标题设置 title
type: item
date: 2021-02-01T20:59:00+08:00
description: 推荐使用vue-wechat-title插件安装npm install vue-wechat-title --save用法1，在 main.js 中引入import VueWechatTitle from 'vue-wechat-title'Vue.use(VueWechatTitle)2，在路由文件 index.js 中给每个路由添加 title// 挂载路由const router = new Router({ mode: 'history', routes:[ { path: '/', name: 'Index', component: Index, meta: { title: '首页' // 标题设置 } }]});3，在 app.vue 中修改 router-view 组件 自定义加载的图片地址 默认是 ./favicon.ico 可以是相对或者绝对的