Compare commits

...

8 Commits

Author SHA1 Message Date
c6ac7193c1 feat(count): 引入批量消费机制优化粉丝计数处理
- 添加 BufferTrigger依赖以支持消息聚合
- 实现消息批量消费逻辑,提升处理效率
- 配置批量大小为 1000,缓存队列最大容量为50000
- 设置聚合间隔为1 秒,均衡实时性与性能
- 新增测试用例验证大量消息发送与消费流程
- 日志记录聚合消息内容,便于调试与监控
2025-10-15 22:33:20 +08:00
31ab7c3d86 feat(count): 实现关注与粉丝数统计功能
- 新增关注数与粉丝数 MQ 消费者
- 在用户关系服务中新增关注/取关时发送 MQ 消息逻辑
- 新增关注/取关类型枚举类
- 新增用于统计的 MQ 常量定义
- 调整应用主类包路径以符合项目结构(致命,查半天)
- 移除配置文件中不再使用的 MQ 消费者限流配置项
2025-10-15 22:25:59 +08:00
e17ab857b9 refactor(note):优化NoteLikeDOServiceImpl的导入顺序- 调整了类导入顺序,将Spring注解与MyBatis相关依赖分开
- 移除了未使用的List导入
-重新组织了包导入顺序以提高可读性
-保持了@Service注解的位置不变- 确保所有必需的依赖仍然正确导入
2025-10-15 19:33:21 +08:00
ee99654e7c refactor(core):优化服务实现类代码结构
- 调整了多个服务实现类中的 import 语句顺序- 移除了未使用的注解和依赖注入相关导入
- 统一了 MyBatis 注解的导入方式-优化了数据源配置类中的 DataSource 导入位置
- 移除了过滤器中不必要的 Component 注解- 简化了部分重复的代码结构以提升可读性
2025-10-15 19:31:02 +08:00
3904e8510e feat(count): 新增笔记和用户计数相关数据结构和服务
- 新增笔记收藏表(NoteCollectionDO)及相关Mapper和服务实现
- 新增笔记计数表(NoteCountDO)及相关Mapper和服务实现
- 新增笔记点赞表(NoteLikeDO)及相关Mapper和服务实现
- 新增用户计数表(UserCountDO)及相关Mapper和服务实现
- 配置RedisTemplate以支持JSON格式序列化
- 引入RocketMQ依赖并配置自动装配
- 在count模块中添加Redis和RocketMQ相关配置类
2025-10-15 19:26:18 +08:00
893f52e5aa feat(count): 初始化计数服务模块
- 新增 han-note-count 模块及其子模块 han-note-count-api 和 han-note-count-biz
- 配置 application.yml 和 bootstrap.yml,支持 Nacos 注册与配置中心
- 添加 MyBatis-Plus、Redis、MySQL 等基础依赖
- 集成日志配置 logback-spring.xml,支持异步输出及多环境配置
- 设置 .gitignore 忽略本地开发配置文件
- 更新 IDEA 编码配置,指定新增模块的字符集为 UTF-8- 在主 pom.xml 中注册新模块 han-note-count
2025-10-15 19:10:48 +08:00
84d6914b1c feat(sql): 新增笔记点赞、收藏及计数相关表结构
- 创建笔记点赞表 t_note_like,记录用户对笔记的点赞状态
- 创建笔记收藏表 t_note_collection,记录用户对笔记的收藏状态
- 创建笔记计数表 t_note_count,统计笔记的点赞、收藏和评论数量
- 创建用户计数表 t_user_count,统计用户的粉丝、关注、笔记及获赞数据
2025-10-15 17:56:10 +08:00
9e3c35043e feat(relation): 实现用户粉丝列表查询功能
- 新增查询用户粉丝列表接口
- 定义粉丝列表请求参数类 FindFansListReqVO- 定义粉丝信息响应类 FindFansUserRspVO
- 在 RelationController 中添加 /fans/list POST 接口
- 在 RelationService 中定义 findFansList 方法
- 在 RelationServiceImpl 中实现粉丝列表查询逻辑
- 支持 Redis 缓存查询与数据库分页查询
- 实现粉丝列表异步同步至 Redis 功能
- 添加 HTTP 客户端测试用例
2025-10-15 17:40:36 +08:00
56 changed files with 1341 additions and 21 deletions

1
.gitignore vendored
View File

@@ -51,3 +51,4 @@ build/
/han-note-note/han-note-note-biz/src/main/resources/application-dev.yml
/han-note-user-relation/han-note-user-relation-biz/src/main/resources/application-dev.yml
/han-note-user-relation/han-note-user-relation-biz/logs/
/han-note-count/han-note-count-biz/src/main/resources/application-dev.yml

6
.idea/encodings.xml generated
View File

@@ -3,6 +3,12 @@
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="file://$PROJECT_DIR$/han-note-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-count/han-note-count-api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-count/han-note-count-api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-count/han-note-count-biz/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-count/han-note-count-biz/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-count/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-count/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/han-note-distributed-id-generator-api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/han-note-distributed-id-generator-api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/han-note-distributed-id-generator/han-note-distributed-id-generator-biz/src/main/java" charset="UTF-8" />

View File

@@ -1,6 +1,9 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ConstantValue" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPORT_CONSTANT_REFERENCE_VALUES" value="false" />
</inspection_tool>
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="56" name="Java" />

View File

@@ -0,0 +1,25 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定父项目 -->
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-count</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-count-api</artifactId>
<name>${project.artifactId}</name>
<description>RPC层, 供其他服务调用</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,117 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定父项目 -->
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-count</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-count-biz</artifactId>
<name>${project.artifactId}</name>
<description>计数服务业务模块</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<!-- 业务接口日志组件 -->
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<!-- 上下文组件 -->
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
<!-- Jackson 组件 -->
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- Druid 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<!-- 提供 Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Rocket MQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<!-- 快手 Buffer Trigger -->
<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>buffer-trigger</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package com.hanserwei.hannote.count.biz;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.hanserwei.hannote.count.biz.domain.mapper")
public class HannoteCountBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteCountBizApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
package com.hanserwei.hannote.count.biz.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

View File

@@ -0,0 +1,10 @@
package com.hanserwei.hannote.count.biz.config;
import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(RocketMQAutoConfiguration.class)
public class RocketMQConfig {
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.hannote.count.biz.constant;
public interface MQConstants {
/**
* Topic: 关注数计数
*/
String TOPIC_COUNT_FOLLOWING = "CountFollowingTopic";
/**
* Topic: 粉丝数计数
*/
String TOPIC_COUNT_FANS = "CountFansTopic";
}

View File

@@ -0,0 +1,37 @@
package com.hanserwei.hannote.count.biz.consumer;
import com.github.phantomthief.collection.BufferTrigger;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
@Component
@RocketMQMessageListener(
consumerGroup = "han_note_group_" + MQConstants.TOPIC_COUNT_FANS,
topic = MQConstants.TOPIC_COUNT_FANS
)
@Slf4j
public class CountFansConsumer implements RocketMQListener<String> {
private final BufferTrigger<String> bufferTrigger = BufferTrigger.<String>batchBlocking()
.bufferSize(50000) // 缓存队列的最大容量
.batchSize(1000) // 一批次最多聚合 1000 条
.linger(Duration.ofSeconds(1)) // 多久聚合一次
.setConsumerEx(this::consumeMessage)
.build();
@Override
public void onMessage(String body) {
// 往 bufferTrigger 中添加元素
bufferTrigger.enqueue(body);
}
private void consumeMessage(List<String> bodys) {
log.info("==> 聚合消息, size: {}", bodys.size());
log.info("==> 聚合消息, {}", JsonUtils.toJsonString(bodys));
}
}

View File

@@ -0,0 +1,20 @@
package com.hanserwei.hannote.count.biz.consumer;
import com.hanserwei.hannote.count.biz.constant.MQConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
consumerGroup = "han_note_group_" + MQConstants.TOPIC_COUNT_FOLLOWING,
topic = MQConstants.TOPIC_COUNT_FOLLOWING
)
@Slf4j
public class CountFollowingConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String body) {
log.info("## 消费了 MQ [计数:关注数]: {}", body);
}
}

View File

@@ -0,0 +1,50 @@
package com.hanserwei.hannote.count.biz.domain.dataobject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 笔记计数表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_note_count")
public class NoteCountDO {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 笔记ID
*/
@TableField(value = "note_id")
private Long noteId;
/**
* 获得点赞总数
*/
@TableField(value = "like_total")
private Long likeTotal;
/**
* 获得收藏总数
*/
@TableField(value = "collect_total")
private Long collectTotal;
/**
* 被评论总数
*/
@TableField(value = "comment_total")
private Long commentTotal;
}

View File

@@ -0,0 +1,50 @@
package com.hanserwei.hannote.count.biz.domain.dataobject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 笔记计数表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_note_count")
public class TNoteCount {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 笔记ID
*/
@TableField(value = "note_id")
private Long noteId;
/**
* 获得点赞总数
*/
@TableField(value = "like_total")
private Long likeTotal;
/**
* 获得收藏总数
*/
@TableField(value = "collect_total")
private Long collectTotal;
/**
* 被评论总数
*/
@TableField(value = "comment_total")
private Long commentTotal;
}

View File

@@ -0,0 +1,62 @@
package com.hanserwei.hannote.count.biz.domain.dataobject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户计数表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_user_count")
public class UserCountDO {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
@TableField(value = "user_id")
private Long userId;
/**
* 粉丝总数
*/
@TableField(value = "fans_total")
private Long fansTotal;
/**
* 关注总数
*/
@TableField(value = "following_total")
private Long followingTotal;
/**
* 发布笔记总数
*/
@TableField(value = "note_total")
private Long noteTotal;
/**
* 获得点赞总数
*/
@TableField(value = "like_total")
private Long likeTotal;
/**
* 获得收藏总数
*/
@TableField(value = "collect_total")
private Long collectTotal;
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.count.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.count.biz.domain.dataobject.NoteCountDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NoteCountDOMapper extends BaseMapper<NoteCountDO> {
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.count.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.count.biz.domain.dataobject.UserCountDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserCountDOMapper extends BaseMapper<UserCountDO> {
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.count.biz.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.hannote.count.biz.domain.dataobject.NoteCountDO;
public interface NoteCountDOService extends IService<NoteCountDO>{
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.count.biz.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.hannote.count.biz.domain.dataobject.UserCountDO;
public interface UserCountDOService extends IService<UserCountDO>{
}

View File

@@ -0,0 +1,11 @@
package com.hanserwei.hannote.count.biz.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.count.biz.domain.dataobject.NoteCountDO;
import com.hanserwei.hannote.count.biz.domain.mapper.NoteCountDOMapper;
import com.hanserwei.hannote.count.biz.service.NoteCountDOService;
import org.springframework.stereotype.Service;
@Service
public class NoteCountDOServiceImpl extends ServiceImpl<NoteCountDOMapper, NoteCountDO> implements NoteCountDOService{
}

View File

@@ -0,0 +1,11 @@
package com.hanserwei.hannote.count.biz.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.count.biz.domain.dataobject.UserCountDO;
import com.hanserwei.hannote.count.biz.domain.mapper.UserCountDOMapper;
import com.hanserwei.hannote.count.biz.service.UserCountDOService;
import org.springframework.stereotype.Service;
@Service
public class UserCountDOServiceImpl extends ServiceImpl<UserCountDOMapper, UserCountDO> implements UserCountDOService{
}

View File

@@ -0,0 +1,31 @@
server:
port: 8090 # 项目启动的端口
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境
servlet:
multipart:
max-file-size: 20MB # 单个文件最大大小
max-request-size: 100MB # 单次请求最大大小(包含多个文件)
data:
redis:
database: 5 # Redis 数据库索引(默认为 0
host: 127.0.0.1 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: redis # Redis 服务器连接密码(默认为空)
timeout: 5s # 读超时时间
connect-timeout: 5s # 链接超时时间
lettuce:
pool:
max-active: 200 # 连接池最大连接数
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
banner: false
mapper-locations: classpath*:/mapperxml/*.xml

View File

@@ -0,0 +1,19 @@
spring:
application:
name: han-note-count # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
config:
server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
file-extension: yaml # 配置文件格式
refresh-enabled: true # 是否开启动态刷新

View File

@@ -0,0 +1,58 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property scope="context" name="appName" value="count"/>
<!-- 自定义日志输出路径,以及日志名称前缀 -->
<property name="LOG_FILE" value="./logs/${appName}.%d{yyyy-MM-dd}"/>
<!-- 每行日志输出的格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的命名格式 -->
<fileNamePattern>${LOG_FILE}-%i.log</fileNamePattern>
<!-- 保留 30 天的日志文件 -->
<maxHistory>30</maxHistory>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>0</totalSizeCap>
<!-- 重启服务时,是否清除历史日志,不推荐清理 -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 是否丢弃日志, 0 表示不丢弃。默认情况下,如果队列满 80%, 会丢弃 TRACE、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 本地 dev 开发环境 -->
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/> <!-- 输出控制台日志 -->
<appender-ref ref="ASYNC_FILE"/> <!-- 打印日志到文件中。PS: 本地环境下,如果不想打印日志到文件,可注释掉此行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/> <!-- 生产环境下,仅打印日志到文件中 -->
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,17 @@
<?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.hanserwei.hannote.count.biz.domain.mapper.NoteCountDOMapper">
<resultMap id="BaseResultMap" type="com.hanserwei.hannote.count.biz.domain.dataobject.NoteCountDO">
<!--@mbg.generated-->
<!--@Table t_note_count-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="note_id" jdbcType="BIGINT" property="noteId" />
<result column="like_total" jdbcType="BIGINT" property="likeTotal" />
<result column="collect_total" jdbcType="BIGINT" property="collectTotal" />
<result column="comment_total" jdbcType="BIGINT" property="commentTotal" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, note_id, like_total, collect_total, comment_total
</sql>
</mapper>

View File

@@ -0,0 +1,19 @@
<?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.hanserwei.hannote.count.biz.domain.mapper.UserCountDOMapper">
<resultMap id="BaseResultMap" type="com.hanserwei.hannote.count.biz.domain.dataobject.UserCountDO">
<!--@mbg.generated-->
<!--@Table t_user_count-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="fans_total" jdbcType="BIGINT" property="fansTotal" />
<result column="following_total" jdbcType="BIGINT" property="followingTotal" />
<result column="note_total" jdbcType="BIGINT" property="noteTotal" />
<result column="like_total" jdbcType="BIGINT" property="likeTotal" />
<result column="collect_total" jdbcType="BIGINT" property="collectTotal" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, user_id, fans_total, following_total, note_total, like_total, collect_total
</sql>
</mapper>

26
han-note-count/pom.xml Normal file
View File

@@ -0,0 +1,26 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定父项目 -->
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>han-note</artifactId>
<version>${revision}</version>
</parent>
<!-- 多模块项目需要配置打包方式为 pom -->
<packaging>pom</packaging>
<!-- 子模块管理 -->
<modules>
<module>han-note-count-api</module>
<module>han-note-count-biz</module>
</modules>
<artifactId>han-note-count</artifactId>
<!-- 项目名称 -->
<name>${project.artifactId}</name>
<!-- 项目描述 -->
<description>计数服务</description>
</project>

View File

@@ -2,7 +2,6 @@ package com.hanserwei.hannote.distributed.id.generator.biz.config;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
import jakarta.annotation.PostConstruct;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -11,6 +10,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Slf4j
@Configuration
@RequiredArgsConstructor

View File

@@ -1,12 +1,7 @@
package com.hanserwei.hannote.distributed.id.generator.biz.core.segment.dao;
import com.hanserwei.hannote.distributed.id.generator.biz.core.segment.model.LeafAlloc;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.*;
import java.util.List;

View File

@@ -0,0 +1,51 @@
package com.hanserwei.hannote.note.biz.domain.dataobject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 笔记收藏表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_note_collection")
public class NoteCollectionDO {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
@TableField(value = "user_id")
private Long userId;
/**
* 笔记ID
*/
@TableField(value = "note_id")
private Long noteId;
/**
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
/**
* 收藏状态(0取消收藏 1收藏)
*/
@TableField(value = "`status`")
private Byte status;
}

View File

@@ -0,0 +1,51 @@
package com.hanserwei.hannote.note.biz.domain.dataobject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 笔记点赞表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_note_like")
public class NoteLikeDO {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
@TableField(value = "user_id")
private Long userId;
/**
* 笔记ID
*/
@TableField(value = "note_id")
private Long noteId;
/**
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
/**
* 点赞状态(0取消点赞 1点赞)
*/
@TableField(value = "`status`")
private Byte status;
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.note.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteCollectionDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NoteCollectionDOMapper extends BaseMapper<NoteCollectionDO> {
}

View File

@@ -0,0 +1,9 @@
package com.hanserwei.hannote.note.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NoteLikeDOMapper extends BaseMapper<NoteLikeDO> {
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.note.biz.service;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteCollectionDO;
import com.baomidou.mybatisplus.extension.service.IService;
public interface NoteCollectionDOService extends IService<NoteCollectionDO>{
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.note.biz.service;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
import com.baomidou.mybatisplus.extension.service.IService;
public interface NoteLikeDOService extends IService<NoteLikeDO>{
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.note.biz.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteCollectionDO;
import com.hanserwei.hannote.note.biz.domain.mapper.NoteCollectionDOMapper;
import com.hanserwei.hannote.note.biz.service.NoteCollectionDOService;
import org.springframework.stereotype.Service;
@Service
public class NoteCollectionDOServiceImpl extends ServiceImpl<NoteCollectionDOMapper, NoteCollectionDO> implements NoteCollectionDOService {
}

View File

@@ -0,0 +1,11 @@
package com.hanserwei.hannote.note.biz.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
import com.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper;
import com.hanserwei.hannote.note.biz.service.NoteLikeDOService;
import org.springframework.stereotype.Service;
@Service
public class NoteLikeDOServiceImpl extends ServiceImpl<NoteLikeDOMapper, NoteLikeDO> implements NoteLikeDOService{
}

View File

@@ -0,0 +1,17 @@
<?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.hanserwei.hannote.note.biz.domain.mapper.NoteCollectionDOMapper">
<resultMap id="BaseResultMap" type="com.hanserwei.hannote.note.biz.domain.dataobject.NoteCollectionDO">
<!--@mbg.generated-->
<!--@Table t_note_collection-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="note_id" jdbcType="BIGINT" property="noteId" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="status" jdbcType="TINYINT" property="status" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, user_id, note_id, create_time, `status`
</sql>
</mapper>

View File

@@ -0,0 +1,17 @@
<?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.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper">
<resultMap id="BaseResultMap" type="com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO">
<!--@mbg.generated-->
<!--@Table t_note_like-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="note_id" jdbcType="BIGINT" property="noteId" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="status" jdbcType="TINYINT" property="status" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, user_id, note_id, create_time, `status`
</sql>
</mapper>

View File

@@ -16,4 +16,14 @@ public interface MQConstants {
* 取关标签
*/
String TAG_UNFOLLOW = "Unfollow";
/**
* Topic: 关注数计数
*/
String TOPIC_COUNT_FOLLOWING = "CountFollowingTopic";
/**
* Topic: 粉丝数计数
*/
String TOPIC_COUNT_FANS = "CountFansTopic";
}

View File

@@ -7,6 +7,8 @@ import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
import com.hanserwei.hannote.user.relation.biz.constant.RedisKeyConstants;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
import com.hanserwei.hannote.user.relation.biz.enums.FollowUnfollowTypeEnum;
import com.hanserwei.hannote.user.relation.biz.model.dto.CountFollowUnfollowMqDTO;
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO;
import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
@@ -15,13 +17,17 @@ import com.hanserwei.hannote.user.relation.biz.util.DateUtils;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
@@ -32,8 +38,8 @@ import java.util.Objects;
@Component
@RocketMQMessageListener(
consumerGroup = "han_note_group_" + MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW,
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW,
consumerGroup = "han_note_group_" + MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW, //han_note_group_FollowUnfollowTopic
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW, //FollowUnfollowTopic
consumeMode = ConsumeMode.ORDERLY
)
@Slf4j
@@ -47,6 +53,8 @@ public class FollowUnfollowConsumer implements RocketMQListener<Message> {
private RateLimiter rateLimiter;
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@Resource
private RocketMQTemplate rocketMQTemplate;
@Override
public void onMessage(Message message) {
@@ -114,6 +122,17 @@ public class FollowUnfollowConsumer implements RocketMQListener<Message> {
String fansRedisKey = RedisKeyConstants.buildUserFansKey(unfollowUserId);
// 删除指定粉丝
redisTemplate.opsForZSet().remove(fansRedisKey, userId);
// 发送MQ消息通知计数服务统计关注数
// 构建DTO对象
CountFollowUnfollowMqDTO countFollowUnfollowMqDTO = CountFollowUnfollowMqDTO.builder()
.userId(userId)
.targetUserId(unfollowUserId)
.type(FollowUnfollowTypeEnum.UNFOLLOW.getCode())
.build();
// 发送MQ
sendMQ(countFollowUnfollowMqDTO);
}
}
@@ -177,6 +196,53 @@ public class FollowUnfollowConsumer implements RocketMQListener<Message> {
// 执行Lua脚本
redisTemplate.execute(script, Collections.singletonList(fansZSetKey), userId, timestamp);
// 发送MQ消息通知计数服务统计关注数
// 构建消息体
CountFollowUnfollowMqDTO countFollowUnfollowMqDTO = CountFollowUnfollowMqDTO.builder()
.userId(userId)
.targetUserId(followUserId)
.type(FollowUnfollowTypeEnum.FOLLOW.getCode())
.build();
sendMQ(countFollowUnfollowMqDTO);
}
}
/**
* 发送MQ消息
*
* @param countFollowUnfollowMqDTO 消息体
*/
private void sendMQ(CountFollowUnfollowMqDTO countFollowUnfollowMqDTO) {
// 构建MQ消息体
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countFollowUnfollowMqDTO))
.build();
// 异步发送 MQ 消息
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_FOLLOWING, message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("==> 【计数服务关注数】MQ 发送成功SendResult: {}", sendResult);
}
@Override
public void onException(Throwable throwable) {
log.error("==> 【计数服务关注数】MQ 发送异常: ", throwable);
}
});
// 发送 MQ 通知计数服务:统计粉丝数
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_FANS, message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("==> 【计数服务粉丝数】MQ 发送成功SendResult: {}", sendResult);
}
@Override
public void onException(Throwable throwable) {
log.error("==> 【计数服务粉丝数】MQ 发送异常: ", throwable);
}
});
}
}

View File

@@ -5,6 +5,8 @@ import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.dto.req.FindFollowingListReqVO;
import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO;
import com.hanserwei.hannote.user.relation.biz.model.dto.FindFansUserRspVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.FindFansListReqVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
import com.hanserwei.hannote.user.relation.biz.service.RelationService;
@@ -41,4 +43,10 @@ public class RelationController {
public PageResponse<FindFollowingUserRspVO> findFollowingList(@Validated @RequestBody FindFollowingListReqVO findFollowingListReqVO) {
return relationService.findFollowingList(findFollowingListReqVO);
}
@PostMapping("/fans/list")
@ApiOperationLog(description = "查询用户粉丝列表")
public PageResponse<FindFansUserRspVO> findFansList(@Validated @RequestBody FindFansListReqVO findFansListReqVO) {
return relationService.findFansList(findFansListReqVO);
}
}

View File

@@ -0,0 +1,17 @@
package com.hanserwei.hannote.user.relation.biz.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum FollowUnfollowTypeEnum {
// 关注
FOLLOW(1),
// 取关
UNFOLLOW(0),
;
private final Integer code;
}

View File

@@ -0,0 +1,29 @@
package com.hanserwei.hannote.user.relation.biz.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CountFollowUnfollowMqDTO {
/**
* 原用户
*/
private Long userId;
/**
* 目标用户
*/
private Long targetUserId;
/**
* 1:关注 0:取关
*/
private Integer type;
}

View File

@@ -0,0 +1,39 @@
package com.hanserwei.hannote.user.relation.biz.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindFansUserRspVO {
/**
* 用户ID
*/
private Long userId;
/**
* 头像
*/
private String avatar;
/**
* 昵称
*/
private String nickname;
/**
* 粉丝总数
*/
private Long fansTotal;
/**
* 笔记总数
*/
private Long noteTotal;
}

View File

@@ -0,0 +1,20 @@
package com.hanserwei.hannote.user.relation.biz.model.vo;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindFansListReqVO {
@NotNull(message = "查询用户 ID 不能为空")
private Long userId;
@NotNull(message = "页码不能为空")
private Integer pageNo = 1; // 默认值为第一页
}

View File

@@ -1,7 +1,7 @@
package com.hanserwei.hannote.user.relation.biz.service;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
public interface FansDOService extends IService<FansDO>{

View File

@@ -1,7 +1,7 @@
package com.hanserwei.hannote.user.relation.biz.service;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
public interface FollowingDOService extends IService<FollowingDO>{

View File

@@ -4,6 +4,8 @@ import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.dto.req.FindFollowingListReqVO;
import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO;
import com.hanserwei.hannote.user.relation.biz.model.dto.FindFansUserRspVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.FindFansListReqVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
@@ -30,4 +32,13 @@ public interface RelationService {
* @return 响应
*/
PageResponse<FindFollowingUserRspVO> findFollowingList(FindFollowingListReqVO findFollowingListReqVO);
/**
* 查询粉丝列表
*
* @param findFansListReqVO 查询粉丝列表请求
* @return 响应
*/
PageResponse<FindFansUserRspVO> findFansList(FindFansListReqVO findFansListReqVO);
}

View File

@@ -1,12 +1,10 @@
package com.hanserwei.hannote.user.relation.biz.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
import com.hanserwei.hannote.user.relation.biz.domain.mapper.FansDOMapper;
import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
import org.springframework.stereotype.Service;
@Service
public class FansDOServiceImpl extends ServiceImpl<FansDOMapper, FansDO> implements FansDOService{

View File

@@ -1,12 +1,10 @@
package com.hanserwei.hannote.user.relation.biz.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.user.relation.biz.domain.mapper.FollowingDOMapper;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
import com.hanserwei.hannote.user.relation.biz.domain.mapper.FollowingDOMapper;
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
import org.springframework.stereotype.Service;
@Service
public class FollowingDOServiceImpl extends ServiceImpl<FollowingDOMapper, FollowingDO> implements FollowingDOService{

View File

@@ -14,14 +14,18 @@ import com.hanserwei.hannote.user.dto.resp.FindFollowingUserRspVO;
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
import com.hanserwei.hannote.user.relation.biz.constant.RedisKeyConstants;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
import com.hanserwei.hannote.user.relation.biz.enums.LuaResultEnum;
import com.hanserwei.hannote.user.relation.biz.enums.ResponseCodeEnum;
import com.hanserwei.hannote.user.relation.biz.model.dto.FindFansUserRspVO;
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO;
import com.hanserwei.hannote.user.relation.biz.model.vo.FindFansListReqVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
import com.hanserwei.hannote.user.relation.biz.rpc.UserRpcService;
import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
import com.hanserwei.hannote.user.relation.biz.service.RelationService;
import com.hanserwei.hannote.user.relation.biz.util.DateUtils;
@@ -59,6 +63,8 @@ public class RelationServiceImpl implements RelationService {
private RocketMQTemplate rocketMQTemplate;
@Resource(name = "relationTaskExecutor")
private ThreadPoolTaskExecutor taskExecutor;
@Resource
private FansDOService fansDOService;
@Override
public Response<?> follow(FollowUserReqVO followUserReqVO) {
@@ -321,7 +327,6 @@ public class RelationServiceImpl implements RelationService {
log.info("==> 批量查询用户信息用户ID: {}", userIds);
// RPC: 批量查询用户信息
//noinspection ConstantValue
findFollowingUserRspVOS = rpcUserServiceAndDTO2VO(userIds, findFollowingUserRspVOS);
}
} else {
@@ -358,7 +363,6 @@ public class RelationServiceImpl implements RelationService {
List<Long> userIds = followingDOS.stream().map(FollowingDO::getFollowingUserId).toList();
// RPC: 调用用户服务,并将 DTO 转换为 VO
//noinspection ConstantValue
findFollowingUserRspVOS = rpcUserServiceAndDTO2VO(userIds, findFollowingUserRspVOS);
// 异步将关注列表全量同步到 Redis
@@ -372,6 +376,157 @@ public class RelationServiceImpl implements RelationService {
total);
}
@Override
public PageResponse<FindFansUserRspVO> findFansList(FindFansListReqVO findFansListReqVO) {
// 要查询的用户ID
Long userId = findFansListReqVO.getUserId();
// 页码
Integer pageNo = findFansListReqVO.getPageNo();
// 先从Redis中查询
String fansListRedisKey = RedisKeyConstants.buildUserFansKey(userId);
// 查询目标用户粉丝列表 ZSet 的总大小
Long total = redisTemplate.opsForZSet().zCard(fansListRedisKey);
// 构建回参
List<FindFansUserRspVO> findFansUserRspVOS = null;
// 每页展示10条数据
long limit = 10L;
if (total != null && total > 0) {
// 缓存有数据
// 计算一共多少页
long totalPage = PageResponse.getTotalPage(total, limit);
// 请求页码超过总页数
if (pageNo > totalPage) {
log.info("==> 查询粉丝列表请求页码超过总页数,返回空数据");
return PageResponse.success(null, pageNo, total);
}
// 准备从 Redis 中查询 ZSet 分页数据
// 每页 10 个元素,计算偏移量
long offset = PageResponse.getOffset(pageNo, limit);
// 使用 ZREVRANGEBYSCORE 命令按 score 降序获取元素,同时使用 LIMIT 子句实现分页
Set<Object> followingUserIdsSet = redisTemplate.opsForZSet()
.reverseRangeByScore(fansListRedisKey, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, offset, limit);
if (CollUtil.isNotEmpty(followingUserIdsSet)) {
// 提取所有用户 ID 到集合中
List<Long> userIds = followingUserIdsSet.stream().map(object -> Long.valueOf(object.toString())).toList();
// RPC: 批量查询用户信息
findFansUserRspVOS = rpcUserServiceAndCountServiceAndDTO2VO(userIds, findFansUserRspVOS);
}
} else {
// 若 Redis 中没有数据,则从数据库查询
// 先查询记录总量
total = fansDOService.count(new LambdaQueryWrapper<>(FansDO.class).eq(FansDO::getUserId, userId));
// 获取一共多少页
long totalPage = PageResponse.getTotalPage(total, limit);
// 请求的页码超出了总页数(只允许查询前 500 页)
if (pageNo > totalPage || pageNo > 500) {
log.info("==> 查询粉丝列表页码大于总页数或者请求的页码超出了总页数,返回空数据");
return PageResponse.success(null, pageNo, total);
}
// 偏移量
long offset = PageResponse.getOffset(pageNo, limit);
// 分页查询
Page<FansDO> page = fansDOService.page(new Page<>(offset / limit + 1, limit),
new LambdaQueryWrapper<>(FansDO.class)
.select(FansDO::getFansUserId)
.eq(FansDO::getUserId, userId)
.orderByDesc(FansDO::getCreateTime));
List<FansDO> fansDOS = page.getRecords();
log.info("==> 查询到粉丝列表:{}", JsonUtils.toJsonString(fansDOS));
// 若记录不为空
if (CollUtil.isNotEmpty(fansDOS)) {
// 提取所有用户 ID 到集合中
List<Long> userIds = fansDOS.stream().map(FansDO::getFansUserId).toList();
// RPC: 调用用户服务、计数服务,并将 DTO 转换为 VO
findFansUserRspVOS = rpcUserServiceAndCountServiceAndDTO2VO(userIds, findFansUserRspVOS);
// 异步将粉丝列表同步到 Redis最多5000条
taskExecutor.submit(() -> syncFansList2Redis(userId));
}
}
return PageResponse.success(findFansUserRspVOS, pageNo, total);
}
private void syncFansList2Redis(Long userId) {
// 同步粉丝列表到 Redis
// 查询粉丝列表最多5000条
Page<FansDO> page = fansDOService.page(new Page<>(1, 5000),
new LambdaQueryWrapper<>(FansDO.class)
.select(FansDO::getFansUserId, FansDO::getCreateTime)
.eq(FansDO::getUserId, userId)
.orderByDesc(FansDO::getCreateTime));
List<FansDO> fansDOS = page.getRecords();
if (CollUtil.isNotEmpty(fansDOS)) {
// 用户粉丝列表的Redis Key
String fansListRedisKey = RedisKeyConstants.buildUserFansKey(userId);
// 随机过期时间,保底一天+随机秒数
long expireSeconds = 86400 + RandomUtil.randomLong(0, 86400);
// 构建 Lua 参数
Object[] luaArgs = buildFansZSetLuaArgs(fansDOS, expireSeconds);
// 执行 Lua 脚本,批量同步关注关系数据到 Redis 中
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/follow_batch_add_and_expire.lua")));
script.setResultType(Long.class);
redisTemplate.execute(script, Collections.singletonList(fansListRedisKey), luaArgs);
}
}
/**
* 构建 Lua 脚本参数 :粉丝列表
*
* @param fansDOS 粉丝DO列表
* @param expireSeconds 过期时间
* @return 参数列表
*/
private Object[] buildFansZSetLuaArgs(List<FansDO> fansDOS, long expireSeconds) {
int argsLength = fansDOS.size() * 2 + 1; // 每个粉丝关系有 2 个参数score 和 value再加一个过期时间
Object[] luaArgs = new Object[argsLength];
int i = 0;
for (FansDO fansDO : fansDOS) {
luaArgs[i] = DateUtils.localDateTime2Timestamp(fansDO.getCreateTime()); // 粉丝的关注时间作为 score
luaArgs[i + 1] = fansDO.getFansUserId(); // 粉丝的用户 ID 作为 ZSet value
i += 2;
}
luaArgs[argsLength - 1] = expireSeconds; // 最后一个参数是 ZSet 的过期时间
return luaArgs;
}
private List<FindFansUserRspVO> rpcUserServiceAndCountServiceAndDTO2VO(List<Long> userIds, List<FindFansUserRspVO> findFansUserRspVOS) {
// RPC: 批量查询用户信息
List<FindUserByIdRspDTO> findUserByIdRspDTOS = userRpcService.findByIds(userIds);
// TODO RPC: 批量查询用户的计数数据(笔记总数、粉丝总数)
// 若不为空DTO 转 VO
if (CollUtil.isNotEmpty(findUserByIdRspDTOS)) {
findFansUserRspVOS = findUserByIdRspDTOS.stream()
.map(dto -> FindFansUserRspVO.builder()
.userId(dto.getId())
.avatar(dto.getAvatar())
.nickname(dto.getNickName())
.noteTotal(0L) // TODO: 这块的数据暂无,后续补充
.fansTotal(0L) // TODO: 这块的数据暂无,后续补充
.build())
.toList();
}
return findFansUserRspVOS;
}
/**
* 全量同步关注列表到 Redis
*

View File

@@ -2,6 +2,8 @@ package com.hanserwei.hannote.user.relation.biz;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
import com.hanserwei.hannote.user.relation.biz.enums.FollowUnfollowTypeEnum;
import com.hanserwei.hannote.user.relation.biz.model.dto.CountFollowUnfollowMqDTO;
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO;
import jakarta.annotation.Resource;
@@ -125,4 +127,37 @@ class MQTests {
}
}
/**
* 测试:发送计数 MQ, 以统计粉丝数
*/
@Test
void testSendCountFollowUnfollowMQ() {
// 循环发送 3200 条 MQ
for (long i = 0; i < 3200; i++) {
// 构建消息体 DTO
CountFollowUnfollowMqDTO countFollowUnfollowMqDTO = CountFollowUnfollowMqDTO.builder()
.userId(i + 1) // 关注者用户 ID
.targetUserId(27L) // 目标用户
.type(FollowUnfollowTypeEnum.FOLLOW.getCode())
.build();
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countFollowUnfollowMqDTO))
.build();
// 发送 MQ 通知计数服务:统计粉丝数
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_FANS, message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("==> 【计数服务粉丝数】MQ 发送成功SendResult: {}", sendResult);
}
@Override
public void onException(Throwable throwable) {
log.error("==> 【计数服务粉丝数】MQ 发送异常: ", throwable);
}
});
}
}
}

View File

@@ -8,7 +8,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

View File

@@ -170,6 +170,16 @@ POST http://localhost:8000/relation/relation/following/list
Content-Type: application/json
Authorization: Bearer {{token}}
{
"userId": 100,
"pageNo": 1
}
### 查询用户粉丝列表
POST http://localhost:8000/relation/relation/fans/list
Content-Type: application/json
Authorization: Bearer {{token}}
{
"userId": 100,
"pageNo": 1

View File

@@ -21,6 +21,7 @@
<module>han-note-note</module>
<module>han-note-note/han-note-note-biz</module>
<module>han-note-user-relation</module>
<module>han-note-count</module>
</modules>
<properties>
@@ -63,6 +64,7 @@
<zookeeper.version>3.9.4</zookeeper.version>
<rocketmq-spring-boot.version>2.3.4</rocketmq-spring-boot.version>
<rocketmq-client.version>5.3.2</rocketmq-client.version>
<buffertrigger.version>0.2.21</buffertrigger.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -284,6 +286,12 @@
<artifactId>rocketmq-acl</artifactId>
<version>${rocketmq-client.version}</version>
</dependency>
<!-- 快手 Buffer Trigger -->
<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>buffer-trigger</artifactId>
<version>${buffertrigger.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -179,4 +179,66 @@ ALTER TABLE t_following ADD UNIQUE uk_user_id_following_user_id(user_id, followi
ALTER TABLE t_fans ADD UNIQUE uk_user_id_fans_user_id(user_id, fans_user_id);
-- 表t_note_like
CREATE TABLE `t_note_like`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`note_id` bigint NOT NULL COMMENT '笔记ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '点赞状态(0取消点赞 1点赞)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_user_id_note_id` (`user_id`, `note_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='笔记点赞表';
-- 表t_note_collection
CREATE TABLE `t_note_collection`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`note_id` bigint NOT NULL COMMENT '笔记ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '收藏状态(0取消收藏 1收藏)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_user_id_note_id` (`user_id`, `note_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='笔记收藏表';
-- 表t_note_count
CREATE TABLE `t_note_count`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`note_id` bigint unsigned NOT NULL COMMENT '笔记ID',
`like_total` bigint DEFAULT '0' COMMENT '获得点赞总数',
`collect_total` bigint DEFAULT '0' COMMENT '获得收藏总数',
`comment_total` bigint DEFAULT '0' COMMENT '被评论总数',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_note_id` (`note_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='笔记计数表';
-- 表t_user_count
CREATE TABLE `t_user_count`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint unsigned NOT NULL COMMENT '用户ID',
`fans_total` bigint DEFAULT '0' COMMENT '粉丝总数',
`following_total` bigint DEFAULT '0' COMMENT '关注总数',
`note_total` bigint DEFAULT '0' COMMENT '发布笔记总数',
`like_total` bigint DEFAULT '0' COMMENT '获得点赞总数',
`collect_total` bigint DEFAULT '0' COMMENT '获得收藏总数',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci
COMMENT ='用户计数表';