diff --git a/docs/dev-ops/docker-compose-environment-aliyun.yml b/docs/dev-ops/docker-compose-environment-aliyun.yml
deleted file mode 100644
index 5997e92..0000000
--- a/docs/dev-ops/docker-compose-environment-aliyun.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-# 命令执行 docker-compose -f docker-compose-environment-aliyun.yml up -d
-version: '3.9'
-services:
- mysql:
- image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/mysql:8.0.32
- container_name: mysql
- command: --default-authentication-plugin=mysql_native_password
- restart: always
- environment:
- TZ: Asia/Shanghai
- MYSQL_ROOT_PASSWORD: 123456
- ports:
- - "13306:3306"
- volumes:
- - ./mysql/my.cnf:/etc/mysql/conf.d/mysql.cnf:ro
- - ./mysql/sql:/docker-entrypoint-initdb.d
- healthcheck:
- test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
- interval: 5s
- timeout: 10s
- retries: 10
- start_period: 15s
- networks:
- - my-network
-
- # phpmyadmin https://hub.docker.com/_/phpmyadmin
- phpmyadmin:
- image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/phpmyadmin:5.2.1
- container_name: phpmyadmin
- hostname: phpmyadmin
- ports:
- - 8899:80
- environment:
- - PMA_HOST=mysql
- - PMA_PORT=3306
- - MYSQL_ROOT_PASSWORD=123qwe!@#QWE
- depends_on:
- mysql:
- condition: service_healthy
- networks:
- - my-network
-
- # Redis
- redis:
- image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/redis:6.2
- container_name: redis
- restart: always
- hostname: redis
- privileged: true
- ports:
- - 16379:6379
- volumes:
- - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
- command: redis-server /usr/local/etc/redis/redis.conf
- networks:
- - my-network
- healthcheck:
- test: [ "CMD", "redis-cli", "ping" ]
- interval: 10s
- timeout: 5s
- retries: 3
-
- # RedisAdmin https://github.com/joeferner/redis-commander
- # 账密 admin/admin
- redis-admin:
- image: registry.cn-hangzhou.aliyuncs.com/xfg-studio/redis-commander:0.8.0
- container_name: redis-admin
- hostname: redis-commander
- restart: always
- ports:
- - 8081:8081
- environment:
- - REDIS_HOSTS=local:redis:6379
- - HTTP_USER=admin
- - HTTP_PASSWORD=admin
- - LANG=C.UTF-8
- - LANGUAGE=C.UTF-8
- - LC_ALL=C.UTF-8
- networks:
- - my-network
- depends_on:
- redis:
- condition: service_healthy
-
-networks:
- my-network:
- driver: bridge
\ No newline at end of file
diff --git a/docs/dev-ops/docker-compose-environment.yml b/docs/dev-ops/docker-compose-environment.yml
index 794cf38..c4b0c67 100644
--- a/docs/dev-ops/docker-compose-environment.yml
+++ b/docs/dev-ops/docker-compose-environment.yml
@@ -80,6 +80,24 @@ services:
redis:
condition: service_healthy
+ # rabbitmq
+ # 账密 admin/admin
+ # rabbitmq-plugins enable rabbitmq_management
+ rabbitmq:
+ image: rabbitmq:3.8-management
+ container_name: rabbitmq
+ restart: always
+ ports:
+ - "5672:5672"
+ - "15672:15672"
+ environment:
+ RABBITMQ_DEFAULT_USER: admin
+ RABBITMQ_DEFAULT_PASS: admin
+ command: rabbitmq-server
+ volumes:
+ - ./rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
+ - ./rabbitmq/mq-data:/var/lib/rabbitmq
+
networks:
my-network:
driver: bridge
\ No newline at end of file
diff --git a/docs/dev-ops/nginx/html/css/index.css b/docs/dev-ops/nginx/html/css/index.css
index 7ec93be..f35e1f4 100644
--- a/docs/dev-ops/nginx/html/css/index.css
+++ b/docs/dev-ops/nginx/html/css/index.css
@@ -14,7 +14,7 @@ body{
/* ========== 轮播图 ========== */
.swiper-container{
- width:100%;height:375px;position:relative;overflow:hidden;
+ width:100%;height:375px;position:relative;overflow:hidden;margin-top: 10px;
}
.swiper-wrapper{display:flex;transition:transform .3s;}
.swiper-slide{flex:0 0 100%;height:375px;}
@@ -62,7 +62,14 @@ body{
}
/* ========== 拼单列表 ========== */
-.group-buying{background:#fff;padding:15px;margin-bottom:10px;position:relative;overflow:hidden;}
+.group-buying {
+ background: #fff;
+ padding: 15px;
+ margin-bottom: 10px; /* 如果还想保留一点外边距 */
+ min-height: 230px; /* 根据需要调整数值 */
+ position: relative;
+ overflow: hidden;
+}
.section-title{
font-size:16px;font-weight:bold;margin-bottom:12px;position:relative;padding-left:10px;
}
@@ -71,7 +78,7 @@ body{
width:3px;height:16px;background:#ff5000;border-radius:2px;
}
-.group-users{height:120px;position:relative;overflow:hidden;}
+.group-users{height:180px;position:relative;overflow:hidden;}
.user-list{position:absolute;top:0;left:0;width:100%;transition:transform .5s ease;}
.user-item{
diff --git a/docs/dev-ops/rabbitmq/enabled_plugins b/docs/dev-ops/rabbitmq/enabled_plugins
new file mode 100644
index 0000000..90fdaa3
--- /dev/null
+++ b/docs/dev-ops/rabbitmq/enabled_plugins
@@ -0,0 +1 @@
+[rabbitmq_management].
\ No newline at end of file
diff --git a/docs/tag/v2.0/nginx/html/js/index.js b/docs/tag/v2.0/nginx/html/js/index.js
index 7217b72..e899bf2 100644
--- a/docs/tag/v2.0/nginx/html/js/index.js
+++ b/docs/tag/v2.0/nginx/html/js/index.js
@@ -86,7 +86,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
userList.innerHTML = '';
list.forEach(t => userList.appendChild(makeItem(t, groupPrice)));
- initUserMarquee();
+ initUserMarquee(3); // 显示 3 条
initCountdown();
}
@@ -137,28 +137,61 @@ document.addEventListener('DOMContentLoaded', () => {
/* =====================================================
* 2. 拼单列表纵向轮播
* =================================================== */
- function initUserMarquee() {
- const items = userList.querySelectorAll('.user-item');
- if (items.length <= 1) return;
+ function initUserMarquee(visibleCount = 3, interval = 3000, duration = 500) {
+ const box = document.querySelector('.group-users');
+ const listEl = userList;
+ let originals = Array.from(listEl.children);
+ const total = originals.length;
- const itemH = items[0].offsetHeight;
- userList.appendChild(items[0].cloneNode(true));
+ if (total === 0) return;
+ // 如果原本就 <= 可见数,直接定高,不滚
+ if (total <= visibleCount) {
+ const h0 = originals[0].offsetHeight;
+ box.style.height = (h0 * visibleCount) + 'px';
+ return;
+ }
- let idx = 0;
- userList.addEventListener('transitionend', () => {
- if (idx >= items.length) {
- userList.style.transition = 'none';
- userList.style.transform = 'translateY(0)';
- idx = 0;
- void userList.offsetWidth;
+ // 1. 复制整份加入末尾
+ originals.forEach(item => listEl.appendChild(item.cloneNode(true)));
+
+ // 2. 重新测量单条高度(此时 DOM 完整)
+ const itemH = listEl.children[0].offsetHeight;
+
+ // 3. 设定窗口高度
+ box.style.height = (itemH * visibleCount) + 'px';
+
+ // 4. 状态
+ let index = 0;
+ let ticking = false;
+
+ function step() {
+ index++;
+ listEl.style.transition = `transform ${duration}ms ease`;
+ listEl.style.transform = `translateY(-${index * itemH}px)`;
+ }
+
+ listEl.addEventListener('transitionend', () => {
+ ticking = false;
+ // 5. 到达复制段的“第一帧”(index === total)就无缝重置
+ if (index === total) {
+ listEl.style.transition = 'none';
+ listEl.style.transform = 'translateY(0)';
+ index = 0;
+ // 强制 reflow 再恢复 transition
+ void listEl.offsetHeight;
+ listEl.style.transition = `transform ${duration}ms ease`;
}
});
- setInterval(() => {
- idx++;
- userList.style.transition = 'transform .5s ease';
- userList.style.transform = `translateY(${-idx * itemH}px)`;
- }, 3000);
+ const timer = setInterval(() => {
+ if (!ticking) {
+ ticking = true;
+ step();
+ }
+ }, interval);
+
+ // 可选:鼠标悬停暂停
+ listEl.addEventListener('mouseenter', () => clearInterval(timer));
}
/* =====================================================
diff --git a/group-buying-sys-app/pom.xml b/group-buying-sys-app/pom.xml
index 0d6f19a..1eb6c9e 100644
--- a/group-buying-sys-app/pom.xml
+++ b/group-buying-sys-app/pom.xml
@@ -78,7 +78,10 @@
converter-gson
2.9.0
-
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
com.squareup.okhttp3
diff --git a/group-buying-sys-app/src/main/java/edu/whut/config/RabbitMQConfig.java b/group-buying-sys-app/src/main/java/edu/whut/config/RabbitMQConfig.java
new file mode 100644
index 0000000..b376e38
--- /dev/null
+++ b/group-buying-sys-app/src/main/java/edu/whut/config/RabbitMQConfig.java
@@ -0,0 +1,33 @@
+package edu.whut.config;
+
+import org.springframework.amqp.core.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+
+//@Configuration
+public class RabbitMQConfig {
+
+ @Value("${spring.rabbitmq.config.producer.exchange}")
+ private String exchangeName;
+
+ /**
+ * 专属交换机
+ */
+ @Bean
+ public TopicExchange topicExchange() {
+ return new TopicExchange(exchangeName, true, false);
+ }
+
+ /**
+ * 绑定队列到交换机
+ */
+ @Bean
+ public Binding topicTeamSuccessBinding(
+ @Value("${spring.rabbitmq.config.producer.topic_team_success.routing_key}") String routingKey,
+ @Value("${spring.rabbitmq.config.producer.topic_team_success.queue}") String queue) {
+ return BindingBuilder.bind(new Queue(queue, true))
+ .to(topicExchange())
+ .with(routingKey);
+ }
+
+}
diff --git a/group-buying-sys-app/src/main/resources/application-dev.yml b/group-buying-sys-app/src/main/resources/application-dev.yml
index 9f4a4c2..f01bfb1 100644
--- a/group-buying-sys-app/src/main/resources/application-dev.yml
+++ b/group-buying-sys-app/src/main/resources/application-dev.yml
@@ -30,6 +30,29 @@ spring:
connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
connection-test-query: SELECT 1
type: com.zaxxer.hikari.HikariDataSource
+ # RabbitMQ
+ rabbitmq:
+ addresses: 192.168.10.218
+ port: 5672
+ username: admin
+ password: admin
+ listener:
+ simple:
+ prefetch: 1 # 每次投递n个消息,消费完在投递n个
+ template:
+ delivery-mode: persistent # 确保全局默认设置为持久化
+ # 消息配置
+ config:
+ # 生产者
+ producer:
+ # 1. 用哪个交换机(Exchange)
+ exchange: group_buy_market_exchange
+ # 2. 针对不同“业务消息”定义 routing key 和 queue
+ topic_team_success:
+ # 消息主题
+ routing_key: topic.team_success
+ # 消费队列
+ queue: group_buy_market_queue_2_topic_team_success
# MyBatis 配置【如需使用记得打开】
mybatis:
diff --git a/group-buying-sys-app/src/main/resources/application.yml b/group-buying-sys-app/src/main/resources/application.yml
index 9f96606..3d7808a 100644
--- a/group-buying-sys-app/src/main/resources/application.yml
+++ b/group-buying-sys-app/src/main/resources/application.yml
@@ -1,3 +1,3 @@
spring:
profiles:
- active: prod
+ active: dev
diff --git a/group-buying-sys-app/src/test/java/edu/whut/test/ApiTest.java b/group-buying-sys-app/src/test/java/edu/whut/test/ApiTest.java
index 025c536..134e587 100644
--- a/group-buying-sys-app/src/test/java/edu/whut/test/ApiTest.java
+++ b/group-buying-sys-app/src/test/java/edu/whut/test/ApiTest.java
@@ -1,19 +1,40 @@
package edu.whut.test;
+import edu.whut.infrastructure.event.EventPublisher;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
+import javax.annotation.Resource;
+import java.util.concurrent.CountDownLatch;
+
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApiTest {
+ @Resource
+ private EventPublisher publisher;
+
+ @Value("${spring.rabbitmq.config.producer.topic_team_success.routing_key}")
+ private String routingKey;
+
@Test
- public void test() {
- log.info("测试完成");
+ public void test_rabbitmq() throws InterruptedException {
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+
+ publisher.publish(routingKey, "订单结算:ORD-20231234");
+ publisher.publish(routingKey, "订单结算:ORD-20231235");
+ publisher.publish(routingKey, "订单结算:ORD-20231236");
+ publisher.publish(routingKey, "订单结算:ORD-20231237");
+ publisher.publish(routingKey, "订单结算:ORD-20231238");
+
+ // 等待,消息消费。测试后,可主动关闭。
+ countDownLatch.await();
}
+
}
diff --git a/group-buying-sys-infrastructure/pom.xml b/group-buying-sys-infrastructure/pom.xml
index 74a5f89..4c8c2f4 100644
--- a/group-buying-sys-infrastructure/pom.xml
+++ b/group-buying-sys-infrastructure/pom.xml
@@ -28,13 +28,15 @@
com.squareup.okhttp3
okhttp-sse
-
cn.hutool
hutool-all
5.8.26
-
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
edu.whut
diff --git a/group-buying-sys-infrastructure/src/main/java/edu/whut/infrastructure/adapter/repository/TagRepository.java b/group-buying-sys-infrastructure/src/main/java/edu/whut/infrastructure/adapter/repository/TagRepository.java
index ed09e61..9c6ce4a 100644
--- a/group-buying-sys-infrastructure/src/main/java/edu/whut/infrastructure/adapter/repository/TagRepository.java
+++ b/group-buying-sys-infrastructure/src/main/java/edu/whut/infrastructure/adapter/repository/TagRepository.java
@@ -73,13 +73,13 @@ public class TagRepository implements ITagRepository {
try {
crowdTagsDetailDao.addCrowdTagsUserId(crowdTagsDetailReq);
- // 获取BitSet
- RBitSet bitSet = redisService.getBitSet(tagId);
- bitSet.set(redisService.getIndexFromUserId(userId));
} catch (DuplicateKeyException ignore) {
log.info("用户id{}已在人群标签{}中",userId,tagId);
// 忽略唯一索引冲突
}
+ // 获取BitSet
+ RBitSet bitSet = redisService.getBitSet(tagId);
+ bitSet.set(redisService.getIndexFromUserId(userId));
}
/**
* 更新标签的统计量(例如已命中的用户总数)。
diff --git a/group-buying-sys-infrastructure/src/main/java/edu/whut/infrastructure/event/EventPublisher.java b/group-buying-sys-infrastructure/src/main/java/edu/whut/infrastructure/event/EventPublisher.java
new file mode 100644
index 0000000..94a9927
--- /dev/null
+++ b/group-buying-sys-infrastructure/src/main/java/edu/whut/infrastructure/event/EventPublisher.java
@@ -0,0 +1,36 @@
+package edu.whut.infrastructure.event;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.MessageDeliveryMode;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * 消息发送
+ */
+@Slf4j
+@Component
+public class EventPublisher {
+
+ @Autowired
+ private RabbitTemplate rabbitTemplate;
+
+ @Value("${spring.rabbitmq.config.producer.exchange}")
+ private String exchangeName;
+
+ public void publish(String routingKey, String message) {
+ try {
+ rabbitTemplate.convertAndSend(exchangeName, routingKey, message, m -> {
+ // 持久化消息配置
+ m.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
+ return m;
+ });
+ } catch (Exception e) {
+ log.error("发送MQ消息失败 team_success message:{}", message, e);
+ throw e;
+ }
+ }
+
+}
diff --git a/group-buying-sys-trigger/pom.xml b/group-buying-sys-trigger/pom.xml
index ac38fb9..19bc5a4 100644
--- a/group-buying-sys-trigger/pom.xml
+++ b/group-buying-sys-trigger/pom.xml
@@ -30,6 +30,10 @@
org.redisson
redisson-spring-boot-starter
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
edu.whut
diff --git a/group-buying-sys-trigger/src/main/java/edu/whut/trigger/listener/TeamSuccessTopicListener.java b/group-buying-sys-trigger/src/main/java/edu/whut/trigger/listener/TeamSuccessTopicListener.java
new file mode 100644
index 0000000..8f67270
--- /dev/null
+++ b/group-buying-sys-trigger/src/main/java/edu/whut/trigger/listener/TeamSuccessTopicListener.java
@@ -0,0 +1,30 @@
+package edu.whut.trigger.listener;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.ExchangeTypes;
+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;
+
+/**
+ * @author Fuzhengwei bugstack.cn @小傅哥
+ * 结算完成消息监听
+ */
+@Slf4j
+@Component
+public class TeamSuccessTopicListener {
+
+ @RabbitListener(
+ bindings = @QueueBinding(
+ value = @Queue(value = "${spring.rabbitmq.config.producer.topic_team_success.queue}"),
+ exchange = @Exchange(value = "${spring.rabbitmq.config.producer.exchange}", type = ExchangeTypes.TOPIC),
+ key = "${spring.rabbitmq.config.producer.topic_team_success.routing_key}"
+ )
+ )
+ public void listener(String message) {
+ log.info("接收消息:{}", message);
+ }
+
+}
diff --git a/group-buying-sys-types/src/main/java/edu/whut/types/event/BaseEvent.java b/group-buying-sys-types/src/main/java/edu/whut/types/event/BaseEvent.java
new file mode 100644
index 0000000..c9dd79e
--- /dev/null
+++ b/group-buying-sys-types/src/main/java/edu/whut/types/event/BaseEvent.java
@@ -0,0 +1,30 @@
+package edu.whut.types.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+
+/**
+ * 基础时间
+ */
+@Data
+public abstract class BaseEvent {
+
+ public abstract EventMessage buildEventMessage(T data);
+
+ public abstract String topic();
+
+ @Data
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ public static class EventMessage {
+ private String id;
+ private Date timestamp;
+ private T data;
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 5760469..579d704 100644
--- a/pom.xml
+++ b/pom.xml
@@ -132,6 +132,12 @@
logging-interceptor
3.14.9
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+ 3.2.0
+