✨ 集群环境-负载均衡完成;
This commit is contained in:
parent
f1e7394450
commit
6fcf7a4b75
|
@ -615,6 +615,8 @@ void buildExchangeOverflowTest() {
|
|||
>
|
||||
> - 最大延迟时间:**2天(48小时)**
|
||||
> - 必须匹配RabbitMQ版本
|
||||
>
|
||||
> **插件版本必须与RabbitMQ严格匹配(如3.13.x需使用v3.13.x插件)。**
|
||||
|
||||
#### 1、确认docker数据卷
|
||||
|
||||
|
@ -746,8 +748,8 @@ try {
|
|||
|
||||
---
|
||||
|
||||
### **2. 生产环境推荐方案**
|
||||
#### **(1)Confirm 模式(轻量级确认)**
|
||||
### 生产环境推荐方案
|
||||
#### (1)Confirm 模式(轻量级确认)
|
||||
- **原理**:异步确认消息是否成功到达 Broker。
|
||||
- **配置方式**:
|
||||
```java
|
||||
|
@ -763,14 +765,14 @@ try {
|
|||
```
|
||||
- **优点**:性能接近非事务模式,可靠性高。
|
||||
|
||||
#### **(2)消息补偿 + 幂等设计**
|
||||
#### (2)消息补偿 + 幂等设计
|
||||
- **步骤**:
|
||||
1. 消息表记录发送状态(如 `status: sending/success/fail`)。
|
||||
2. 定时任务补偿失败消息。
|
||||
3. 消费者端做幂等处理(如唯一 ID + 去重表)。
|
||||
- **适用场景**:订单支付、库存扣减等关键业务。
|
||||
|
||||
#### **(3)本地消息表(最终一致性)**
|
||||
#### (3)本地消息表(最终一致性)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App
|
||||
|
@ -852,6 +854,8 @@ channel.queueDeclare("myLazyQueue", true, false, false, args);
|
|||
|
||||
## 优先级队列
|
||||
|
||||
**优先级仅在消息堆积时生效(空队列时无意义)**
|
||||
|
||||
**优先级范围**
|
||||
|
||||
- 通过 `x-max-priority` 参数定义队列支持的最大优先级(默认0=无优先级)
|
||||
|
@ -949,6 +953,8 @@ public void processMessagePriority(String dataString, Channel channel, Message m
|
|||
> [!IMPORTANT]
|
||||
>
|
||||
> 如果需要修改,记得修改网关和IP地址。
|
||||
>
|
||||
> 节点间端口要求(如4369/25672需互通),避免防火墙问题.。
|
||||
|
||||
看自己是否有这个需求。
|
||||
|
||||
|
@ -1014,7 +1020,6 @@ sudo systemctl daemon-reload && sudo systemctl restart docker
|
|||
> # 3. 重建目录并设置权限
|
||||
> mkdir -p ~/docker/docker_data/rabbitmq/{rabbit1,rabbit2,rabbit3}/{data,conf,log}
|
||||
> sudo chown -R 999:999 ~/docker/docker_data/rabbitmq
|
||||
> sudo chmod -R 775 ~/docker/docker_data/rabbitmq
|
||||
>
|
||||
> # 4. 重新启动
|
||||
> docker compose up -d
|
||||
|
@ -1253,6 +1258,14 @@ sudo vim /etc/hosts
|
|||
|
||||
#### 3、设置Erlang Cookie
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 普通方式搭建集群时,`.erlang.cookie`的权限必须为`400`
|
||||
>
|
||||
> ```bash
|
||||
> sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||
> ```
|
||||
|
||||
RabbitMQ节点使用Erlang cookie进行认证,所有节点必须使用相同的cookie。
|
||||
|
||||
如果是克隆的虚拟机,可以不检查(最好还是检查下),因为克隆的基本上是一样的。
|
||||
|
@ -1270,8 +1283,8 @@ sudo systemctl stop rabbitmq-server
|
|||
sudo nano /var/lib/rabbitmq/.erlang.cookie
|
||||
|
||||
# 粘贴主节点的cookie内容
|
||||
# 然后设置正确的权限
|
||||
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
||||
# 然后设置正确的权限
|
||||
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||
|
||||
# 重新启动RabbitMQ服务
|
||||
|
@ -1353,6 +1366,14 @@ sudo rabbitmqctl status
|
|||
|
||||
- 如果需要Web管理界面:
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 如果使用Web界面,需要在每个集群上都执行一下,否则情况会出现下面情况,举个例子。
|
||||
>
|
||||
> 如果在主节点上执行,那么主节点可以访问,之后输入账户密码登录之后可以看到主节点的情况,但是其余节点会出现【Node statistics not available】。
|
||||
>
|
||||
> 为了避免这种情况,要在每个节点上都执行一下下面的命令。
|
||||
|
||||
```bash
|
||||
sudo rabbitmq-plugins enable rabbitmq_management
|
||||
```
|
||||
|
@ -1470,26 +1491,26 @@ sudo cat /var/lib/rabbitmq/.erlang.cookie
|
|||
|
||||
1. 停止 RabbitMQ:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo systemctl stop rabbitmq-server
|
||||
```
|
||||
|
||||
2. 修改 Cookie(复制主节点的 Cookie 到其他节点):
|
||||
|
||||
```
|
||||
```bash
|
||||
echo "YOUR_MASTER_NODE_COOKIE" | sudo tee /var/lib/rabbitmq/.erlang.cookie
|
||||
```
|
||||
|
||||
3. 设置权限:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
||||
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||
```
|
||||
|
||||
4. 重启 RabbitMQ:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo systemctl start rabbitmq-server
|
||||
```
|
||||
|
||||
|
@ -1501,14 +1522,14 @@ sudo cat /var/lib/rabbitmq/.erlang.cookie
|
|||
|
||||
确保用户在所有节点都有 **管理员权限**:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo rabbitmqctl set_user_tags <username> administrator
|
||||
sudo rabbitmqctl set_permissions -p / <username> ".*" ".*" ".*"
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo rabbitmqctl set_user_tags admin administrator
|
||||
sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
|
||||
```
|
||||
|
@ -1516,3 +1537,111 @@ sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
|
|||
**情况五 管理界面缓存问题**
|
||||
|
||||
浏览器可能缓存了旧数据,导致显示异常。
|
||||
|
||||
### 集群环境-负载均衡
|
||||
|
||||
#### 1. 安装 HAProxy
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install haproxy -y
|
||||
```
|
||||
|
||||
#### 2. 配置 HAProxy
|
||||
编辑 HAProxy 的配置文件 `/etc/haproxy/haproxy.cfg`:
|
||||
```bash
|
||||
sudo vim /etc/haproxy/haproxy.cfg
|
||||
```
|
||||
添加以下内容(假设 RabbitMQ 节点为 `rabbit1:5672` 和 `rabbit2:5672`):
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 绑定的端口号(`bind *:`)不能重复!!!
|
||||
|
||||
```ini
|
||||
frontend rabbitmq_front
|
||||
bind *:22222
|
||||
mode tcp
|
||||
default_backend rabbitmq_back
|
||||
|
||||
backend rabbitmq_back
|
||||
mode tcp
|
||||
balance roundrobin
|
||||
# 在之前设置了hosts文件,所以这里没有写端口号 == server rabbit1 192.168.3.144:5672 check
|
||||
server rabbit1 rabbit1:5672 check
|
||||
server rabbit2 rabbit2:5672 check
|
||||
server rabbit3 rabbit3:5672 check
|
||||
server rabbit4 rabbit4:5672 check
|
||||
|
||||
# 可选:启用 RabbitMQ 管理界面的负载均衡(默认端口 15672)
|
||||
frontend rabbitmq_admin
|
||||
bind *:12222
|
||||
mode http
|
||||
default_backend rabbitmq_admin_back
|
||||
|
||||
backend rabbitmq_admin_back
|
||||
mode http
|
||||
balance roundrobin
|
||||
# 在之前设置了hosts文件,所以这里没有写端口号 == server rabbit1 192.168.3.144:15672 check
|
||||
server rabbit1 rabbit1:15672 check
|
||||
server rabbit2 rabbit2:15672 check
|
||||
server rabbit3 rabbit3:15672 check
|
||||
server rabbit4 rabbit4:15672 check
|
||||
```
|
||||
|
||||
#### 3. 重启 HAProxy
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 重启报错可以看下报错内容: journalctl -xeu haproxy.service
|
||||
>
|
||||
> 如果使用一些远程软件,修改时可能会出现,多行配置变一行问题,需要手动通过vim查看。
|
||||
|
||||
```bash
|
||||
sudo systemctl restart haproxy
|
||||
```
|
||||
### 集群环境的连接
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 如果是新建环境,需要将之前写的监听内容注释,否则启动容易报错。
|
||||
|
||||
使用Java程序进行连接。如果搭建了集群环境还配置了负载均衡,就是用负载均衡的方式进行连接。
|
||||
|
||||
负载均衡配置的是端口是:`5672`对应上面的文件是`22222`。所以在连接时候是`22222`这个 端口(参考【2. 配置 HAProxy】)
|
||||
|
||||
#### Java配置
|
||||
|
||||
```yaml
|
||||
# application.yaml
|
||||
rabbitmq:
|
||||
host: ${bunny.rabbitmq.host}
|
||||
port: ${bunny.rabbitmq.port}
|
||||
username: ${bunny.rabbitmq.username}
|
||||
password: ${bunny.rabbitmq.password}
|
||||
virtual-host: ${bunny.rabbitmq.virtual-host}
|
||||
|
||||
# application-dev.yaml
|
||||
bunny:
|
||||
rabbitmq:
|
||||
host: 192.168.3.144
|
||||
# port: 5672
|
||||
# 集群环境的端口号
|
||||
port: 22222
|
||||
virtual-host: /
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
|
||||
#### 创建环境
|
||||
|
||||
**交换机:**exchange.cluster.test
|
||||
|
||||

|
||||
|
||||
**队列:**queue.cluster.test
|
||||
|
||||

|
||||
|
||||
**路由键:**routing.key.cluster.test
|
||||
|
||||

|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
|
@ -0,0 +1,20 @@
|
|||
package cn.bunny.mq.mqdemo.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping
|
||||
public class IndexController {
|
||||
|
||||
@GetMapping("")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping("dialog")
|
||||
public String dialog() {
|
||||
return "dialog";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package cn.bunny.mq.mqdemo.mq.listener;
|
||||
|
||||
import com.rabbitmq.client.Channel;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ClusterEnvironmentListener {
|
||||
|
||||
@Resource
|
||||
private RabbitTemplate rabbitTemplate;
|
||||
|
||||
/* 测试优先级队列 */
|
||||
@RabbitListener(queues = "queue.cluster.test")
|
||||
public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
log.info("<<集群环境下消息>>----<cluster>{}", dataString);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,8 @@
|
|||
package cn.bunny.mq.mqdemo.mq.listener;
|
||||
|
||||
import com.rabbitmq.client.Channel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MessageListenerOrder {
|
||||
|
@ -81,17 +74,17 @@ public class MessageListenerOrder {
|
|||
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
||||
// }
|
||||
|
||||
/* 测试延迟消息 */
|
||||
@RabbitListener(queues = "queue.test.delay")
|
||||
public void processMessageDelay(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
log.info("<延迟消息>----消息本身{}", dataString);
|
||||
log.info("<延迟消息>----当前时间{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||
}
|
||||
|
||||
/* 测试优先级队列 */
|
||||
@RabbitListener(queues = "queue.test.priority")
|
||||
public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
log.info("<<优先级队列>>----<priority>{}", dataString);
|
||||
}
|
||||
// /* 测试延迟消息 */
|
||||
// @RabbitListener(queues = "queue.test.delay")
|
||||
// public void processMessageDelay(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
// log.info("<延迟消息>----消息本身{}", dataString);
|
||||
// log.info("<延迟消息>----当前时间{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||
// }
|
||||
//
|
||||
// /* 测试优先级队列 */
|
||||
// @RabbitListener(queues = "queue.test.priority")
|
||||
// public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
// log.info("<<优先级队列>>----<priority>{}", dataString);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ server:
|
|||
bunny:
|
||||
rabbitmq:
|
||||
host: 192.168.3.144
|
||||
port: 5672
|
||||
# port: 5672
|
||||
# 集群环境的端口号
|
||||
port: 22222
|
||||
virtual-host: /
|
||||
username: admin
|
||||
password: admin
|
||||
|
|
|
@ -0,0 +1,480 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<title>优雅的Bootstrap弹窗表单</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4e73df;
|
||||
--secondary-color: #2e59d9;
|
||||
--success-color: #1cc88a;
|
||||
--danger-color: #e74a3b;
|
||||
--light-color: #f8f9fc;
|
||||
--dark-color: #5a5c69;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f8f9fc;
|
||||
}
|
||||
|
||||
/* 自定义弹窗样式 */
|
||||
.custom-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1050;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.custom-modal-dialog {
|
||||
max-width: 500px;
|
||||
margin: 1.75rem auto;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-modal-header {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-close-custom {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
font-size: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.btn-close-custom:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-modal-body {
|
||||
padding: 1.5rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.custom-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--light-color);
|
||||
border-top: 1px solid #e3e6f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control-custom {
|
||||
border: 1px solid #d1d3e2;
|
||||
border-radius: 0.35rem;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control-custom:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
|
||||
}
|
||||
|
||||
/* 验证样式 */
|
||||
.is-invalid {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.is-valid {
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: var(--danger-color);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.valid-feedback {
|
||||
color: var(--success-color);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary-custom {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary-custom:hover {
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline-secondary-custom {
|
||||
border-color: #d1d3e2;
|
||||
color: var(--dark-color);
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-outline-secondary-custom:hover {
|
||||
background-color: #f8f9fc;
|
||||
border-color: #d1d3e2;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-show {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal-show .custom-modal-dialog {
|
||||
animation: slideInDown 0.3s;
|
||||
}
|
||||
|
||||
/* 浮动标签效果 */
|
||||
.form-floating-custom {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-floating-custom label {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
left: 1rem;
|
||||
color: #6e707e;
|
||||
transition: all 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-floating-custom input:focus ~ label,
|
||||
.form-floating-custom input:not(:placeholder-shown) ~ label {
|
||||
top: -0.75rem;
|
||||
left: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
background-color: white;
|
||||
padding: 0 0.25rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 密码强度指示器 */
|
||||
.password-strength {
|
||||
height: 4px;
|
||||
background-color: #e3e6f0;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.password-strength-bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
transition: width 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 text-center">
|
||||
<h1 class="display-4 mb-4">优雅的Bootstrap弹窗示例</h1>
|
||||
<p class="lead mb-5">点击下方按钮查看带有表单验证的现代化弹窗</p>
|
||||
<button class="btn btn-primary btn-lg px-4 py-2" id="openModalBtn">
|
||||
<i class="bi bi-box-arrow-in-down-right me-2"></i>打开注册弹窗
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义弹窗 -->
|
||||
<div class="custom-modal" id="customModal">
|
||||
<div class="custom-modal-dialog">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">
|
||||
<i class="bi bi-person-plus-fill me-2"></i>用户注册
|
||||
</h5>
|
||||
<button class="btn-close-custom" id="closeModalBtn" type="button">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<form id="registrationForm" novalidate>
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="username" placeholder="用户名" required
|
||||
type="text">
|
||||
<label for="username">用户名</label>
|
||||
<div class="invalid-feedback">请输入有效的用户名(4-20个字符)</div>
|
||||
<div class="valid-feedback">用户名可用</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="email" placeholder="电子邮箱" required
|
||||
type="email">
|
||||
<label for="email">电子邮箱</label>
|
||||
<div class="invalid-feedback">请输入有效的电子邮箱地址</div>
|
||||
<div class="valid-feedback">邮箱格式正确</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="password" minlength="8" placeholder="密码"
|
||||
required type="password">
|
||||
<label for="password">密码</label>
|
||||
<div class="invalid-feedback">密码至少需要8个字符</div>
|
||||
<div class="password-strength">
|
||||
<div class="password-strength-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="confirmPassword" placeholder="确认密码"
|
||||
required type="password">
|
||||
<label for="confirmPassword">确认密码</label>
|
||||
<div class="invalid-feedback">密码不匹配</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" id="terms" required type="checkbox">
|
||||
<label class="form-check-label" for="terms">
|
||||
我已阅读并同意 <a class="text-primary" href="#">服务条款</a> 和 <a class="text-primary"
|
||||
href="#">隐私政策</a>
|
||||
</label>
|
||||
<div class="invalid-feedback">必须同意条款才能继续</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button class="btn btn-outline-secondary-custom me-2" id="cancelBtn" type="button">
|
||||
取消
|
||||
</button>
|
||||
<button class="btn btn-primary-custom" id="submitBtn" type="button">
|
||||
<i class="bi bi-check-lg me-1"></i> 提交注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 弹窗控制
|
||||
const modal = document.getElementById('customModal');
|
||||
const openModalBtn = document.getElementById('openModalBtn');
|
||||
const closeModalBtn = document.getElementById('closeModalBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const form = document.getElementById('registrationForm');
|
||||
|
||||
// 打开弹窗
|
||||
openModalBtn.addEventListener('click', () => {
|
||||
modal.classList.add('modal-show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
|
||||
// 关闭弹窗
|
||||
function closeModal() {
|
||||
modal.classList.remove('modal-show');
|
||||
document.body.style.overflow = 'auto';
|
||||
form.reset();
|
||||
resetValidation();
|
||||
}
|
||||
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
cancelBtn.addEventListener('click', closeModal);
|
||||
|
||||
// 点击弹窗外部关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
function resetValidation() {
|
||||
const inputs = form.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.classList.remove('is-invalid', 'is-valid');
|
||||
});
|
||||
}
|
||||
|
||||
// 实时验证
|
||||
form.addEventListener('input', (e) => {
|
||||
const input = e.target;
|
||||
validateInput(input);
|
||||
|
||||
// 密码强度检测
|
||||
if (input.id === 'password') {
|
||||
updatePasswordStrength(input.value);
|
||||
}
|
||||
|
||||
// 确认密码验证
|
||||
if (input.id === 'confirmPassword' || input.id === 'password') {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
document.getElementById('confirmPassword').classList.add('is-invalid');
|
||||
document.getElementById('confirmPassword').classList.remove('is-valid');
|
||||
} else if (confirmPassword) {
|
||||
document.getElementById('confirmPassword').classList.add('is-valid');
|
||||
document.getElementById('confirmPassword').classList.remove('is-invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function validateInput(input) {
|
||||
if (input.id === 'username') {
|
||||
const isValid = input.value.length >= 4 && input.value.length <= 20;
|
||||
updateValidationState(input, isValid);
|
||||
} else if (input.id === 'email') {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const isValid = emailRegex.test(input.value);
|
||||
updateValidationState(input, isValid);
|
||||
} else if (input.id === 'password') {
|
||||
const isValid = input.value.length >= 8;
|
||||
updateValidationState(input, isValid);
|
||||
} else if (input.id === 'terms') {
|
||||
updateValidationState(input, input.checked);
|
||||
}
|
||||
}
|
||||
|
||||
function updateValidationState(input, isValid) {
|
||||
if (input.value === '' && input.required) {
|
||||
input.classList.remove('is-invalid', 'is-valid');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
input.classList.add('is-valid');
|
||||
input.classList.remove('is-invalid');
|
||||
} else {
|
||||
input.classList.add('is-invalid');
|
||||
input.classList.remove('is-valid');
|
||||
}
|
||||
}
|
||||
|
||||
// 密码强度检测
|
||||
function updatePasswordStrength(password) {
|
||||
const strengthBar = document.querySelector('.password-strength-bar');
|
||||
let strength = 0;
|
||||
|
||||
// 长度检测
|
||||
if (password.length >= 8) strength += 20;
|
||||
if (password.length >= 12) strength += 20;
|
||||
|
||||
// 复杂度检测
|
||||
if (/[A-Z]/.test(password)) strength += 20;
|
||||
if (/[0-9]/.test(password)) strength += 20;
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength += 20;
|
||||
|
||||
// 更新UI
|
||||
strengthBar.style.width = `${strength}%`;
|
||||
|
||||
if (strength < 40) {
|
||||
strengthBar.style.backgroundColor = '#e74a3b'; // 红色
|
||||
} else if (strength < 80) {
|
||||
strengthBar.style.backgroundColor = '#f6c23e'; // 黄色
|
||||
} else {
|
||||
strengthBar.style.backgroundColor = '#1cc88a'; // 绿色
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
submitBtn.addEventListener('click', () => {
|
||||
let isValid = true;
|
||||
const inputs = form.querySelectorAll('input[required]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
validateInput(input);
|
||||
if (input.classList.contains('is-invalid')) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查密码匹配
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
document.getElementById('confirmPassword').classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// 模拟提交
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> 提交中...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
alert('注册成功!');
|
||||
closeModal();
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-lg me-1"></i> 提交注册';
|
||||
submitBtn.disabled = false;
|
||||
}, 1500);
|
||||
} else {
|
||||
// 滚动到第一个错误
|
||||
const firstInvalid = form.querySelector('.is-invalid');
|
||||
if (firstInvalid) {
|
||||
firstInvalid.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,23 @@
|
|||
package cn.bunny.mq.mqdemo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
public class ClusterEnvironmentTest {
|
||||
|
||||
@Autowired
|
||||
private RabbitTemplate rabbitTemplate;
|
||||
|
||||
/* 集群环境下消息 */
|
||||
@Test
|
||||
void clusterTest() {
|
||||
for (int i = 0; i <= 10; i++) {
|
||||
rabbitTemplate.convertAndSend("exchange.cluster.test",
|
||||
"routing.key.cluster.test",
|
||||
"集群环境下消息-" + i);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue