微服务项目完整解析
一、项目整体架构概述这是一个基于 Spring Cloud Alibaba 的微服务项目,项目名为 micro2505,采用 Maven 多模块聚合架构。项目实现了一个类似 "小红书" 的种草笔记平台,包含用户管理、笔记发布、点赞、积分等核心功能。
1.1 技术栈总览
| 类别 | 技术 / 组件 | 版本 |
|---|---|---|
| 基础框架 | Spring Boot | 2.6.13 |
| 微服务框架 | Spring Cloud | 2021.0.4 |
| 微服务套件 | Spring Cloud Alibaba | 2021.0.4.0 |
| 注册中心 | Nacos | - |
| 网关 | Spring Cloud Gateway | - |
| 远程调用 | OpenFeign + LoadBalancer | - |
| 熔断降级 | Sentinel | - |
| 分布式事务 | Seata (AT 模式) | - |
| 持久层 | MyBatis-Plus | 3.5.2 |
| 连接池 | Druid | 1.2.20 |
| 缓存 | Redis + Lettuce | - |
| 分布式锁 | Redisson | 3.18.0 |
| 消息队列 | RocketMQ | 2.2.3 |
| 对象存储 | MinIO | 8.2.2 |
| 短信服务 | 阿里云 SMS | 2.2.1 |
| 内容审核 | 百度 AI SDK | 4.16.6 |
| 定时任务 | XXL-JOB | 2.4.0 |
| 工具包 | Hutool | 5.8.30 |
| 二维码 | ZXing | 3.5.2 |
1.2 模块结构
plaintext
java
micro2505/ # 根项目(聚合POM)
├── micro-gateway/ # API网关服务
├── micro-api/ # Feign远程调用接口模块
├── micro-common/ # 公共模块(聚合POM)
│ ├── micro-common-core/ # 核心工具模块
│ ├── micro-common-mybatisplus/ # MyBatis-Plus配置模块
│ ├── micro-common-redis/ # Redis缓存模块
│ ├── micro-common-minio/ # MinIO对象存储模块
│ ├── micro-common-sms/ # 短信服务模块
│ ├── micro-common-xxljob/ # 分布式定时任务模块
│ ├── micro-common-baidu/ # 百度内容审核模块
│ └── micro-common-mq/ # RocketMQ消息队列模块
└── micro-server/ # 业务服务模块(聚合POM)
├── micro-user/ # 用户服务
└── micro-grass/ # 笔记服务
二、父 POM 配置详解根 POM 作为整个项目的父级配置,主要职责:2.1 聚合管理
xml
java
<modules>
<module>micro-common</module>
<module>micro-server</module>
<module>micro-gateway</module>
<module>micro-api</module>
</modules>
2.2 依赖版本统一管理通过
<dependencyManagement>统一管理所有模块的依赖版本,子模块引入依赖时无需指定版本号:Spring Boot BOM (spring-boot-dependencies)Spring Cloud BOM (spring-cloud-dependencies)Spring Cloud Alibaba BOM (spring-cloud-alibaba-dependencies)各个第三方组件的精确版本控制
三、micro-common 公共模块详解3.1 micro-common-core - 核心工具模块这是整个项目的基础模块,提供通用功能。3.1.1 统一响应封装 R.java
java
运行
java
@Data
public class R<T> {
private int code; // 状态码:200成功,500失败
private String msg; // 状态信息
private T data; // 响应数据
public static R ok() { ... } // 成功无数据
public static <T> R ok(T data) { ... } // 成功带数据
public static R fail(String msg) { ... } // 失败带消息
}
作用:统一前后端数据交互格式,所有 Controller 返回的数据都封装为 R 对象,便于前端统一处理。3.1.2 JWT 工具类 JwtUtils.java
java
运行
java
public class JwtUtils {
private final static String secret = "123456789qwertyui"; // 密钥
// 生成JWT Token
public static String createJwt(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims) // 设置载荷
.setExpiration(new Date(System.currentTimeMillis() + 180000000)) // 过期时间
.signWith(SignatureAlgorithm.HS256, secret) // HS256签名
.compact();
}
// 解析JWT Token
public static Claims parseJWT(String jsonWebToken) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jsonWebToken).getBody();
}
}
作用:实现无状态的用户身份认证,Token 中存储用户 ID 等关键信息。3.1.3 用户上下文工具 UserUtils.java
java
运行
java
public class UserUtils {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void setUid(Integer uid) { threadLocal.set(uid); }
public static Integer getUid() { return threadLocal.get(); }
public static void removeUid() { threadLocal.remove(); }
}
作用:使用 ThreadLocal 存储当前请求的用户 ID,实现线程隔离,避免多线程环境下数据混乱。在业务层任何地方都可以通过 UserUtils.getUid () 获取当前登录用户。3.1.4 登录拦截器 LoginInterceptor.java
java
运行
java
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 检查方法是否有@NoLogin注解,有则放行
if (method.isAnnotationPresent(NoLogin.class)) {
return true;
}
// 验证Token
String token = request.getHeader("Authorization");
if (!StringUtils.hasLength(token)) {
throw new RuntimeException("请重新登录");
}
// 解析Token并存入ThreadLocal
Claims claims = JwtUtils.parseJWT(token);
UserUtils.setUid(claims.get("uid", Integer.class));
}
return true;
}
@Override
public void afterCompletion(...) {
UserUtils.removeUid(); // 请求完成后清理ThreadLocal,防止内存泄漏
}
}
工作流程:
- 拦截所有请求
- 判断方法是否有 @NoLogin 注解,有则跳过验证
- 从请求头获取 Authorization Token
- 解析 Token 获取用户 ID,存入 ThreadLocal
- 请求完成后清理 ThreadLocal3.1.5 免登录注解 NoLogin.java
java
运行
java
@Target(ElementType.METHOD) // 作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface NoLogin {
}
作用:标记不需要登录验证的接口,如登录、注册、发送验证码等。3.1.6 拦截器配置 MyInterceptorConfig.java
java
运行
java
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**"); // 拦截所有请求
}
}
3.1.7 线程池配置 ThreadPoolConfig.java
java
运行
java
@Configuration
public class ThreadPoolConfig {
// 普通线程池
@Bean
public ThreadPoolExecutor threadPoolExecutor() {
return new ThreadPoolExecutor(
8, // 核心线程数
20, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(100) // 任务队列
);
}
// Spring Task线程池(用于@Async异步任务)
@Bean("taskPool")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(15);
executor.setQueueCapacity(20);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("qfedu-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
作用:提供两个线程池,用于异步处理耗时任务(如内容审核、发送积分等),避免阻塞主线程。3.1.8 全局异常处理器 GlobalExceptionHandler.java
java
运行
java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public R exceptionHandler2(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String message = allErrors.stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";"));
return R.fail(message);
}
// 通用异常
@ExceptionHandler(Exception.class)
public R exception(Exception e) {
log.error(e.getMessage(), e);
return R.fail("未知异常");
}
}
作用:统一处理所有 Controller 抛出的异常,返回友好的错误信息给前端。3.1.9 实体类项目定义了以下核心实体类:
| 实体类 | 对应表 | 说明 |
|---|---|---|
| User | user | 用户信息(id, username, password, phone, inviteCode, qrCode, point 等) |
| Grass | grass | 笔记信息(id, uid, title, content, imgPath, checkFlag, likeNum 等) |
| LikeInfo | like_info | 点赞记录(id, uid, gid, likeFlag) |
| PointInfo | point_info | 积分记录(id, uid, type, point) |
| PointRule | point_rule | 积分规则(id, type, point) |
3.2 micro-common-redis - Redis 缓存模块3.2.1 Redis 配置 RedisConfig.java
java
运行
java
@Configuration
public class RedisConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// Key使用String序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
// Value使用Jackson JSON序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// Hash的Key和Value同样配置
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
return redisTemplate;
}
}
配置说明:使用 Lettuce 作为 Redis 客户端(Spring Boot 默认)Key 统一使用 String 序列化,便于在 Redis 客户端查看Value 使用 JSON 序列化,支持存储复杂对象3.2.2 Redisson 配置 RedissonConfig.java
java
运行
java
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setRetryInterval(5000)
.setTimeout(10000)
.setConnectTimeout(10000);
return Redisson.create(config);
}
}
作用:配置 Redisson 客户端,用于实现分布式锁等高级功能。3.2.3 Redis 工具类 RedisCache.java封装了常用的 Redis 操作:
java
运行
java
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
// String操作
public <T> void setCacheObject(String key, T value) { ... }
public <T> void setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) { ... }
public <T> T getCacheObject(String key) { ... }
public boolean deleteObject(String key) { ... }
public Long incrKey(String key, long v) { ... } // 自增
// List操作
public <T> long setCacheList(String key, List<T> dataList) { ... }
public <T> List<T> getCacheList(String key) { ... }
// Hash操作
public <T> void setCacheMap(String key, Map<String, T> dataMap) { ... }
public <T> Map<String, T> getCacheMap(String key) { ... }
public <T> void setCacheMapValue(String key, String hKey, T value) { ... }
public <T> T getCacheMapValue(String key, String hKey) { ... }
public Long incrCacheMapValue(String key, String hKey, long v) { ... } // Hash字段自增
// 通用操作
public boolean expire(String key, long timeout, TimeUnit unit) { ... }
public Boolean hasKey(String key) { ... }
}
3.2.4 分布式锁注解 MyLock.java
java
运行
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLock {
String lockKey() default "lock"; // 锁名称
long waitTime() default 0; // 等待加锁时间
long expireTime() default 0; // 锁过期时间
}
3.2.5 分布式锁切面 MyLockAspect.java
java
运行
java
@Component
@Aspect
public class MyLockAspect {
@Resource
private RedissonClient redissonClient;
@Around("@annotation(com.qfedu.common.redis.annotation.MyLock)")
public Object around(ProceedingJoinPoint joinPoint) {
// 获取注解参数
MyLock annotation = method.getAnnotation(MyLock.class);
String lockKey = annotation.lockKey();
long waitTime = annotation.waitTime();
long expireTime = annotation.expireTime();
// 获取Redisson分布式锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁
if (lock.tryLock(waitTime, expireTime, TimeUnit.SECONDS)) {
return joinPoint.proceed(); // 执行目标方法
}
} finally {
// 释放锁(只有当前线程持有锁才释放)
if (lock.isHeldByCurrentThread() && lock.isLocked()) {
lock.unlock();
}
}
return null;
}
}
使用场景:防止定时任务在分布式环境下重复执行。3.3 micro-common-mybatisplus - MyBatis-Plus 配置模块3.3.1 配置类 MybatisPlusConfig.java
java
运行
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件(指定MySQL方言)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
功能说明:分页插件:自动拦截查询 SQL,添加 LIMIT 语句实现物理分页乐观锁插件:通过版本号机制防止并发更新冲突
3.4 micro-common-minio - MinIO 对象存储模块3.4.1 属性配置类 MinioProperties.java
java
运行
java
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
private String endpoint; // MinIO服务地址
private String accessKey; // 访问密钥
private String secretKey; // 私钥
private String bucketName; // 存储桶名称
}
配置文件示例(application.yml):
yaml
java
minio:
endpoint: http://127.0.0.1:9000
accessKey: minioadmin
secretKey: minioadmin
bucketName: java2505
3.4.2 MinIO 客户端配置 MinioConfig.java
java
运行
java
@Configuration
@EnableConfigurationProperties(MinioProperties.class)
public class MinioConfig {
@Resource
private MinioProperties minioProperties;
@Bean
public MinioClient getMinioClient() {
return MinioClient.builder()
.endpoint(minioProperties.getEndpoint())
.credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
.build();
}
}
3.4.3 MinIO 工具类 MinioUtil.java
java
运行
java
@Component
public class MinioUtil {
@Value("${minio.bucketName}")
private String bucketName;
@Autowired
private MinioClient minioClient;
@Resource
private MinioProperties minioProperties;
// 文件上传
public String upload(InputStream inputStream, String fileName) {
String contentType = getContentType(fileName.substring(fileName.lastIndexOf(".")));
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(inputStream, inputStream.available(), -1)
.contentType(contentType)
.build());
// 返回文件访问URL
return String.format("%s/%s/%s",
minioProperties.getEndpoint(),
minioProperties.getBucketName(),
fileName);
}
// 根据文件扩展名获取ContentType
private String getContentType(String extension) {
if (extension.equalsIgnoreCase(".jpg") || extension.equalsIgnoreCase(".png")) {
return "image/jpeg";
}
// ... 其他类型
return "image/jpeg";
}
}
使用场景:上传用户头像、笔记图片、二维码等文件。
3.5 micro-common-sms - 短信服务模块3.5.1 阿里云短信工具类 AliSmsUtils.java
java
运行
java
public class AliSmsUtils {
private static IAcsClient client;
private static final String ACCESS_KEY_ID = "xxx";
private static final String ACCESS_KEY_SECRET = "xxx";
static {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", ACCESS_KEY_ID, ACCESS_KEY_SECRET);
client = new DefaultAcsClient(profile);
}
public static void sendSms(String signName, String templateCode, String phone, String code) {
SendSmsRequest request = new SendSmsRequest();
request.setSignName(signName); // 短信签名
request.setTemplateCode(templateCode); // 模板编号
request.setPhoneNumbers(phone); // 接收手机号
request.setTemplateParam("{\"code\":\"" + code + "\"}"); // 模板参数
client.getAcsResponse(request);
}
}
使用场景:用户注册时发送验证码。
3.6 micro-common-xxljob - 分布式定时任务模块3.6.1 XXL-JOB 属性配置 XxlJobProperties.java
java
运行
java
@Data
@Component
public class XxlJobProperties {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses; // 调度中心地址
@Value("${xxl.job.accessToken}")
private String accessToken; // 访问令牌
@Value("${xxl.job.executor.appname}")
private String appname; // 执行器名称
@Value("${xxl.job.executor.port}")
private int port; // 执行器端口
@Value("${xxl.job.executor.logpath}")
private String logPath; // 日志路径
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays; // 日志保留天数
}
配置文件示例:
yaml
java
xxl:
job:
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin
accessToken: default_token
executor:
appname: xxl-job-executor-2505
port: 9999
logpath: xxl-job/jobhandler
logretentiondays: 30
3.6.2 XXL-JOB 执行器配置 XxlJobConfig.java
java
运行
java
@Configuration
public class XxlJobConfig {
@Resource
private XxlJobProperties xxlJobProperties;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(xxlJobProperties.getAdminAddresses());
executor.setAppname(xxlJobProperties.getAppname());
executor.setPort(xxlJobProperties.getPort());
executor.setAccessToken(xxlJobProperties.getAccessToken());
executor.setLogPath(xxlJobProperties.getLogPath());
executor.setLogRetentionDays(xxlJobProperties.getLogRetentionDays());
return executor;
}
}
3.6.3 XXL-JOB 动态任务工具类 XxlJobUtil.java
java
运行
java
@Component
public class XxlJobUtil {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appname;
private RestTemplate restTemplate = new RestTemplate();
// 添加并启动任务
public String addAndStart(XxlJobInfo jobInfo) {
// 1. 获取执行器组ID
Map<String, Object> param = new HashMap<>();
param.put("appname", appname);
String result = doPost(adminAddresses + "/jobgroup/getGroupId", JSON.toJSONString(param));
// 2. 设置任务所属组
JSONObject jsonObject = JSON.parseObject(result);
jobInfo.setJobGroup(Integer.parseInt(jsonObject.getString("content")));
// 3. 添加并启动任务
return doPost(adminAddresses + "/jobinfo/addAndStart", JSON.toJSONString(jobInfo));
}
// 其他方法:add、update、remove、pause、start
}
使用场景:实现笔记的定时发布功能,动态创建定时任务。
3.7 micro-common-baidu - 百度内容审核模块3.7.1 内容审核工具类 BaiduTextCheckUtils.java
java
运行
java
public class BaiduTextCheckUtils {
public static final String APP_ID = "xxx";
public static final String API_KEY = "xxx";
public static final String SECRET_KEY = "xxx";
private static AipContentCensor client;
static {
client = new AipContentCensor(APP_ID, API_KEY, SECRET_KEY);
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
}
/**
* 文本内容审核
* @return 审核结果:1-合规,2-不合规,3-疑似,4-审核失败
*/
public static int textCheck(String text) {
JSONObject response = client.textCensorUserDefined(text);
int conclusionType = response.getInt("conclusionType");
// 打印不合规内容
if (conclusionType != 1) {
JSONArray data = response.getJSONArray("data");
for (int i = 0; i < data.length(); i++) {
System.out.println(data.getJSONObject(i).getString("msg"));
}
}
return conclusionType;
}
}
使用场景:审核用户发布的笔记标题和内容,过滤敏感词汇。
3.8 micro-common-mq - RocketMQ 消息队列模块3.8.1 消息 DTO PointDTO.java
java
运行
java
@Data
public class PointDTO {
private Integer uid; // 用户ID
private Integer pointType; // 积分类型
private String msgId; // 消息唯一ID(防重复消费)
}
3.8.2 RocketMQ 工具类 RocketMqUtils.java
java
运行
java
@Component
public class RocketMqUtils {
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* 发送同步消息
*/
public void sendSyncMsg(String topic, String tag, String msg) {
String destination = StringUtils.hasLength(tag) ? topic + ":" + tag : topic;
rocketMQTemplate.syncSend(destination, msg);
}
}
配置文件示例:
yaml
java
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: springboot_producer_grass
sendMessageTimeout: 10000
retryTimesWhenSendFailed: 2
使用场景:发布笔记后异步发送积分,实现服务解耦。
四、micro-api - Feign 远程调用模块4.1 模块说明该模块定义了微服务之间的远程调用接口,使用 OpenFeign 实现声明式 HTTP 客户端。4.2 Feign 配置 FeignConfig.java
java
运行
java
@Configuration
public class FeignConfig implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取当前请求
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// Token透传:将原请求的Token传递给下游服务
requestTemplate.header("Authorization", request.getHeader("Authorization"));
}
}
作用:实现 Token 透传,确保微服务调用链路中用户身份信息不丢失。4.3 积分服务 Feign 接口 PointFeignService.java
java
运行
java
@FeignClient(
value = "micro-user", // 目标服务名称
fallbackFactory = PointFallbackFactory.class, // 熔断降级工厂
configuration = FeignConfig.class // Feign配置类
)
public interface PointFeignService {
// 远程添加积分
@PostMapping("/user/point/add")
R remoteAddPoint(@RequestParam("pointType") Integer pointType);
// 远程批量获取用户列表
@PostMapping("/user/remote/list")
R<List<User>> remoteUserList(@RequestBody List<Integer> uidList);
// 远程获取单个用户信息
@GetMapping("/user/remote/info")
R<User> remoteUserInfo(@RequestParam("uid") Integer uid);
}
4.4 熔断降级工厂 PointFallbackFactory.java
java
运行
java
@Component
public class PointFallbackFactory implements FallbackFactory<PointFeignService> {
@Override
public PointFeignService create(Throwable cause) {
return new PointFeignService() {
@Override
public R remoteAddPoint(Integer pointType) {
System.out.println(cause.getMessage());
return R.fail("远程调用送积分接口失败");
}
@Override
public R<List<User>> remoteUserList(List<Integer> uidList) {
return R.fail("远程调用用户列表接口失败");
}
@Override
public R<User> remoteUserInfo(Integer uid) {
return R.fail("远程调用用户接口失败");
}
};
}
}
作用:当远程服务不可用或超时时,返回降级响应,保证主流程不中断。
五、micro-gateway - API 网关模块5.1 模块说明网关是微服务架构的统一入口,负责路由转发、负载均衡、跨域处理等。5.2 启动类 MicroGatewayApplication.java
java
运行
java
@SpringBootApplication
public class MicroGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(MicroGatewayApplication.class, args);
}
}
5.3 跨域配置 CorsConfig.java
java
运行
java
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*"); // 允许所有HTTP方法
config.addAllowedOrigin("*"); // 允许所有来源
config.addAllowedHeader("*"); // 允许所有请求头
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
5.4 路由配置 application.yml
yaml
java
server:
port: 9100
spring:
application:
name: micro-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 启用服务发现
routes:
- id: user_route
uri: lb://micro-user # 负载均衡到micro-user服务
order: 1
predicates:
- Path=/user/** # 匹配/user/**的请求
- id: grass_route
uri: lb://micro-grass # 负载均衡到micro-grass服务
order: 1
predicates:
- Path=/grass/** # 匹配/grass/**的请求
路由说明:lb:// 表示使用负载均衡,从 Nacos 获取服务实例predicates 定义路由匹配条件请求 /user/** 转发到 micro-user 服务请求 /grass/** 转发到 micro-grass 服务
六、micro-server - 业务服务模块6.1 micro-user - 用户服务6.1.1 服务配置 application.yml
yaml
java
server:
port: 9010
spring:
application:
name: micro-user
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/micro2505?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
redis:
host: 127.0.0.1
port: 6379
database: 10
timeout: 10s
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
minio:
endpoint: http://127.0.0.1:9000
accessKey: minioadmin
secretKey: minioadmin
bucketName: java2505
seata:
enabled: true
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: seata
group: SEATA_GROUP
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: springboot_producer_user
6.1.2 启动类 MicroUserApplication.java
java
运行
java
@MapperScan("com.qfedu.microuser.**.mapper")
@SpringBootApplication(scanBasePackages = "com.qfedu")
public class MicroUserApplication {
public static void main(String[] args) {
SpringApplication.run(MicroUserApplication.class, args);
}
}
关键注解:@MapperScan:扫描 Mapper 接口scanBasePackages = "com.qfedu":扫描 com.qfedu 下所有包,包括 common 模块的配置类6.1.3 用户控制器 UserController.java
java
运行
java
@Validated
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
// 登录(免登录验证)
@NoLogin
@PostMapping("/login")
public R login(@Valid @RequestBody LoginParam loginParam) {
String token = userService.login(loginParam);
return R.ok(token);
}
// 注册(免登录验证)
@NoLogin
@PostMapping("/register")
public R register(@RequestBody RegisterParam registerParam) {
userService.register(registerParam);
return R.ok();
}
// 获取当前用户信息(需要登录)
@GetMapping("/info")
public R userInfo() {
User user = userService.userInfo();
return R.ok(user);
}
// 发送验证码(免登录验证)
@NoLogin
@GetMapping("/send/code")
public R sendCode(String phone) {
userService.sendCode(phone);
return R.ok();
}
// 远程调用:批量获取用户列表
@PostMapping("/remote/list")
public R<List<User>> remoteUserList(@RequestBody List<Integer> uidList) {
List<User> list = userService.remoteUserList(uidList);
return R.ok(list);
}
// 远程调用:获取单个用户信息
@GetMapping("/remote/info")
public R<User> remoteUserInfo(Integer uid) {
User user = userService.remoteUserInfo(uid);
return R.ok(user);
}
}
6.1.4 请求参数类登录参数 LoginParam.java:
java
@Data
public class LoginParam {
@NotBlank(message = "请输入用户名")
private String username;
@NotBlank(message = "请输入密码")
private String password;
}
注册参数 RegisterParam.java:
java
@Data
public class RegisterParam {
private String username;
private String password;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号不合法")
private String phone;
private String inviteCode; // 邀请码
private String validateCode; // 验证码
}
6.1.5 用户服务实现 UserServiceImpl.java
java
package com.qfedu.microuser.user.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.qfedu.common.core.entity.User;
import com.qfedu.common.core.utils.JwtUtils;
import com.qfedu.common.core.utils.UserUtils;
import com.qfedu.common.minio.utils.MinioUtil;
import com.qfedu.common.redis.utils.RedisCache;
import com.qfedu.common.sms.utils.AliSmsUtils;
import com.qfedu.microuser.point.service.PointInfoService;
import com.qfedu.microuser.user.mapper.UserMapper;
import com.qfedu.microuser.user.param.LoginParam;
import com.qfedu.microuser.user.param.RegisterParam;
import com.qfedu.microuser.user.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Classname UserServiceImpl
* @Description TODO
* @Date 2026-01-22 11:46
* @Created by 老任与码
*/
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Resource
private PointInfoService pointInfoService;
@Resource
private MinioUtil minioUtil;
@Resource
private ThreadPoolExecutor threadPoolExecutor;
@Resource
private RedisCache redisCache;
@Override
public String login(LoginParam loginParam) {
// if(loginParam.getUsername() == null || loginParam.getUsername().isEmpty()){
//
// }
User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class)
.eq(User::getUsername, loginParam.getUsername()));
if (user == null) {
throw new RuntimeException("用户名错误");
}
if (!user.getPassword().equals(loginParam.getPassword())) {
throw new RuntimeException("密码错误");
}
Map<String, Object> map = new HashMap<>();
map.put("uid", user.getId());
// 生成jwt
return JwtUtils.createJwt(map);
}
@Override
public void register(RegisterParam registerParam) {
Long userCount = userMapper.selectCount(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, registerParam.getUsername()));
if (userCount > 0) {
throw new RuntimeException("用户名已存在");
}
Long phoneCount = userMapper.selectCount(Wrappers.<User>lambdaQuery()
.eq(User::getPhone, registerParam.getPhone()));
if (phoneCount > 0) {
throw new RuntimeException("手机号已存在");
}
// 判断验证码
String code = (String) redisCache.getCacheObject("code:" + registerParam.getPhone());
if (!registerParam.getValidateCode().equals(code)) {
throw new RuntimeException("验证码错误");
} else {
redisCache.deleteObject("code:" + registerParam.getPhone());
}
Integer inviteUid;
if (StringUtils.hasLength(registerParam.getInviteCode())) {
User inviteUser = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getInviteCode, registerParam.getInviteCode()));
if (inviteUser == null) {
throw new RuntimeException("邀请码对应用户不存在");
}
inviteUid = inviteUser.getId();
} else {
inviteUid = null;
}
// 生成邀请码
String inviteCode = createInviteCode();
// 生成二维码
String qrCode = createQrCode(inviteCode);
User user = User.builder()
.username(registerParam.getUsername())
.password(registerParam.getPassword())
.phone(registerParam.getPhone())
.inviteCode(inviteCode)
.inviteUid(inviteUid)
.qrCode(qrCode)
.build();
// 添加注册信息
userMapper.insert(user);
// 添加积分
if (inviteUid != null) {
// 异步添加积分
threadPoolExecutor.execute(() -> {
pointInfoService.addPoint(1, inviteUid);
});
}
}
@Override
public User userInfo() {
// Integer userId = TokenUtils.getUserId();
// 从ThreadLocal中获取用户id
Integer userId = UserUtils.getUid();
return userMapper.selectById(userId);
}
@Override
public void updateUserPoint(Integer uid, Integer point) {
userMapper.update(null, Wrappers.<User>lambdaUpdate()
.setSql("point=point+" + point)
.eq(User::getId, uid));
}
@Override
public void sendCode(String phone) {
String code = RandomUtil.randomNumbers(4);
// "SMS_154950909", "阿里云短信测试"
AliSmsUtils.sendSms("阿里云短信测试",
"SMS_154950909",
phone, code);
// 将验证码存入redis
redisCache.setCacheObject("code:" + phone, code, 300, TimeUnit.SECONDS);
}
@Override
public List<User> remoteUserList(List<Integer> uidList) {
return userMapper.selectList(Wrappers.<User>lambdaQuery()
// .select(User::getId, User::getUsername, User::getAvatar)
.in(User::getId, uidList));
}
@Override
public User remoteUserInfo(Integer uid) {
return userMapper.selectById(uid);
}
/**
* 生成邀请码,做多重试三次,如果还重复,生成更多位数的邀请码
*
* @return
*/
private String createInviteCode() {
for (int i = 0; i < 3; i++) {
String inviteCode = RandomUtil.randomString(4);
Long codeCount = userMapper.selectCount(Wrappers.<User>lambdaQuery().eq(User::getInviteCode, inviteCode));
if (codeCount == 0) {
return inviteCode;
}
}
// 说明三次都重复
return RandomUtil.randomString(6);
}
/**
* 生成二维码
*
* @param inviteCode
* @return
*/
private String createQrCode(String inviteCode) {
// 生成二维码,二维码中存储注册页面的路径,扫码跳转到注册页面,注册页面自动显示邀请码
String url = "http://www.qfedu.com?inviteCode=" + inviteCode;
// QrCodeUtil.generate(url, 300, 300, FileUtil.file("d:/qrcode.png"));
BufferedImage bufferedImage = QrCodeUtil.generate(url, 300, 300);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 将图像输出到输出流中。
try {
ImageIO.write(bufferedImage, "jpeg", bos);
// 获取二维码图片的输入流对象
InputStream inputStream = new ByteArrayInputStream(bos.toByteArray());
// 上传
String fileName = UUID.randomUUID().toString().replace("-", "") + ".jpg";
return minioUtil.upload(inputStream, fileName);
// return "";
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
注册流程图:
用户注册请求 ↓ 验证用户名/手机号唯一性 ↓ 验证短信验证码(Redis) ↓ 处理邀请码(查询邀请人) ↓ 生成唯一邀请码 ↓ 生成二维码图片 → 上传MinIO → 获取URL ↓ 插入用户数据 ↓ 异步给邀请人加积分6.1.6 积分服务 积分控制器 PointInfoController
java
@RestController
@RequestMapping(\"/user/point\")
public class PointInfoController {
@Resource
private PointInfoService pointInfoService;
// 远程调用接口:添加积分
@PostMapping(\"/add\")
public R remoteAddPoint(Integer pointType) {
Integer uid = UserUtils.getUid();
pointInfoService.addPoint(pointType, uid);
return R.ok();
}
}
积分服务实现 PointInfoServiceImpl
java
@Service
public class PointInfoServiceImpl implements PointInfoService {
@Resource
private PointInfoMapper pointInfoMapper;
@Resource
private PointRuleService pointRuleService;
@Lazy // 延迟注入,解决循环依赖
@Resource
private UserService userService;
@Transactional(rollbackFor = Exception.class)
@Override
public void addPoint(Integer pointType, Integer uid) {
// 1. 根据类型获取积分规则
PointRule pointRule = pointRuleService.pointRuleInfoByType(pointType);
if (pointRule != null) {
// 2. 插入积分记录
PointInfo pointInfo = new PointInfo();
pointInfo.setUid(uid);
pointInfo.setPoint(pointRule.getPoint());
pointInfo.setType(pointRule.getType());
pointInfoMapper.insert(pointInfo);
// 3. 修改用户表的point字段
userService.updateUserPoint(uid, pointRule.getPoint());
}
}
}
积分规则服务 PointRuleServiceImpl.java:
java
@Service
public class PointRuleServiceImpl implements PointRuleService {
@Resource
private PointRuleMapper pointRuleMapper;
@Resource
private RedisCache redisCache;
@Override
public PointRule pointRuleInfoByType(Integer pointType) {
// 1. 从Redis获取积分规则缓存
List<PointRule> pointRules = redisCache.getCacheObject("point:rule");
// 2. 缓存未命中,查询数据库并缓存
if (pointRules == null || pointRules.size() == 0) {
pointRules = pointRuleMapper.selectList(null);
redisCache.setCacheObject("point:rule", pointRules, 24 * 3600, TimeUnit.SECONDS);
}
// 3. 从规则列表中筛选指定类型
return pointRules.stream()
.filter(item -> item.getType().equals(pointType))
.findFirst()
.orElse(null);
}
}
缓存策略说明:积分规则数据相对稳定,使用 Redis 缓存 24 小时减少数据库查询压力,提升响应速度
6.1.7 RocketMQ 消息消费者 PointConsumer.java
java
@Component
@RocketMQMessageListener(topic = "topic_2505", consumerGroup = "group_2505")
public class PointConsumer implements RocketMQListener<MessageExt> {
@Resource
private PointInfoService pointInfoService;
@Resource
private RedisCache redisCache;
@Override
public void onMessage(MessageExt messageExt) {
String tags = messageExt.getTags();
String msg = new String(messageExt.getBody());
JSONObject jsonObject = JSON.parseObject(msg);
String msgId = jsonObject.getString("msgId");
// 幂等性处理:防止重复消费
String key = "mq:relay:" + msgId;
Boolean ret = redisCache.hasKey(key);
if (ret) {
return; // 已消费过,直接返回
}
// 标记消息已消费,1小时过期
redisCache.setCacheObject(key, "1", 3600, TimeUnit.SECONDS);
// 根据Tag处理不同业务
switch (tags) {
case "point":
PointDTO pointDTO = JSON.parseObject(msg, PointDTO.class);
pointInfoService.addPoint(pointDTO.getPointType(), pointDTO.getUid());
break;
}
}
}
消息消费流程:
RocketMQ消息到达
↓
获取消息Tags和内容
↓
提取自定义消息ID
↓
检查Redis是否已消费(幂等校验)
↓
已消费 → 直接返回
↓
未消费 → 标记已消费 → 执行业务逻辑
幂等性设计要点:使用业务唯一消息 ID(而非 MQ 自带的 msgId)在 Redis 中记录已消费的消息 ID设置合理的过期时间(1 小时),避免 Redis 数据无限增长
6.2 micro-grass - 笔记服务
6.2.1 服务配置 application.yml
yaml
XML
server:
port: 9020
spring:
application:
name: micro-grass
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/micro2505?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
redis:
host: 127.0.0.1
port: 6379
database: 10
timeout: 10s
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
# Feign配置
feign:
client:
config:
default:
connect-timeout: 1000 # 连接超时1秒
read-timeout: 5000 # 读取超时5秒
sentinel:
enabled: true # 开启Sentinel熔断
# Seata分布式事务
seata:
enabled: true
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: seata
group: SEATA_GROUP
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
# XXL-JOB配置
xxl:
job:
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin
accessToken: default_token
executor:
appname: xxl-job-executor-2505
port: 9999
logpath: xxl-job/jobhandler
logretentiondays: 30
# RocketMQ配置
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: springboot_producer_grass
sendMessageTimeout: 10000
retryTimesWhenSendFailed: 2
6.2.2 启动类 MicroGrassApplication.java
java
@EnableAsync // 开启异步任务
@EnableScheduling // 开启定时任务
@EnableFeignClients(basePackages = "com.qfedu.api") // 开启Feign,扫描api模块
@MapperScan("com.qfedu.micrograss.**.mapper") // 扫描Mapper
@SpringBootApplication(scanBasePackages = "com.qfedu")
public class MicroGrassApplication {
public static void main(String[] args) {
SpringApplication.run(MicroGrassApplication.class, args);
}
}
注解说明:@EnableAsync:启用 Spring 异步任务,配合 @Async 使用@EnableScheduling:启用 Spring 定时任务,配合 @Scheduled 使用@EnableFeignClients:启用 Feign 客户端,指定扫描 com.qfedu.api 包
6.2.3 笔记控制器 GrassController.java
java
@RestController
@RequestMapping("/grass")
public class GrassController {
@Resource
private GrassService grassService;
// 发布笔记(即时发布)
@PostMapping("/add")
public R addGrass(@RequestBody GrassAddParam grassAddParam) {
grassService.addGrass(grassAddParam);
return R.ok();
}
// 发布笔记(定时发布)
@PostMapping("/add2")
public R addGrass2(@RequestBody GrassAddParam grassAddParam) {
grassService.addGrass2(grassAddParam);
return R.ok("添加成功,笔记审核中");
}
// 分页查询笔记列表
@GetMapping("/page")
public R pageGrass(PageParam pageParam) {
List<GrassVO> list = grassService.grassPage(pageParam);
return R.ok(list);
}
// 获取笔记详情
@GetMapping("/info")
public R<Grass> grassInfo(Integer grassId) {
GrassVO grassVO = grassService.grassInfo(grassId);
return R.ok(grassVO);
}
}
6.2.4 请求参数与响应对象
添加笔记参数 GrassAddParam.java:
java
@Data
public class GrassAddParam {
private String title; // 标题
private String content; // 内容
private List<String> imgList; // 图片URL列表
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date publishTime; // 定时发布时间
}
笔记视图对象 GrassVO.java:
java
@Data
public class GrassVO {
private Integer id;
private String title;
private String content;
private String username; // 作者用户名(远程调用获取)
private String avatar; // 作者头像(远程调用获取)
private int likeNum; // 点赞数
private String coverImg; // 封面图(第一张图片)
private int likeFlag; // 当前用户是否点赞:0-否,1-是
private List<String> imgPathList;// 图片列表(详情页使用)
}
6.2.5 笔记服务实现 GrassServiceImpl.java
核心依赖注入:
java
@Service
public class GrassServiceImpl implements GrassService {
@Resource
private GrassMapper grassMapper;
@Resource
private PointFeignService pointFeignService; // Feign远程调用
@Resource
private LikeInfoService likeInfoService;
@Resource
private RedisCache redisCache;
@Resource
private ThreadPoolExecutor threadPoolExecutor;
@Resource
private XxlJobUtil xxlJobUtil;
@Resource
private RocketMqUtils rocketMqUtils;
发布笔记(即时发布):
java
@Override
public void addGrass(GrassAddParam grassAddParam) {
// 1. 构建笔记实体
Grass grass = new Grass();
BeanUtils.copyProperties(grassAddParam, grass);
grass.setUid(UserUtils.getUid());
String imgPath = grassAddParam.getImgList().stream().collect(Collectors.joining(","));
grass.setImgPath(imgPath);
// 2. 插入数据库
grassMapper.insert(grass);
// 3. 异步内容审核
threadPoolExecutor.execute(() -> {
int conclusionType = BaiduTextCheckUtils.textCheck(grass.getTitle() + grass.getContent());
int checkFlag = 0;
if (conclusionType == 1) { // 合规
checkFlag = 1;
} else if (conclusionType == 2 || conclusionType == 4) { // 不合规或审核失败
checkFlag = 0;
} else if (conclusionType == 3) { // 疑似
checkFlag = 2;
}
// 更新审核状态
grassMapper.update(null, Wrappers.lambdaUpdate(Grass.class)
.set(Grass::getCheckFlag, checkFlag)
.eq(Grass::getId, grass.getId()));
});
// 4. 发送MQ消息,异步添加积分
PointDTO pointDTO = new PointDTO();
pointDTO.setPointType(4); // 类型4:发布笔记积分
pointDTO.setUid(UserUtils.getUid());
pointDTO.setMsgId(UUID.randomUUID().toString()); // 生成唯一消息ID
rocketMqUtils.sendSyncMsg("topic_2505", "point", JSON.toJSONString(pointDTO));
}
发布笔记流程图:
java
接收发布请求
↓
构建笔记实体 → 图片URL列表转逗号分隔字符串
↓
插入数据库(checkFlag默认0-待审核)
↓
异步任务1:百度内容审核
├── 合规 → checkFlag=1
├── 不合规 → checkFlag=0
└── 疑似 → checkFlag=2
↓
异步任务2:发送MQ消息 → micro-user消费 → 添加积分
发布笔记(定时发布):
java
@Override
public void addGrass2(GrassAddParam grassAddParam) {
// 1. 保存笔记(enableFlag默认0-未发布)
Grass grass = new Grass();
BeanUtils.copyProperties(grassAddParam, grass);
grass.setUid(UserUtils.getUid());
String imgPath = grassAddParam.getImgList().stream().collect(Collectors.joining(","));
grass.setImgPath(imgPath);
grassMapper.insert(grass);
// 2. 生成Cron表达式
SimpleDateFormat sdf = new SimpleDateFormat("ss mm HH dd MM ? yyyy");
String cron = sdf.format(grassAddParam.getPublishTime());
// 3. 动态创建XXL-JOB定时任务
XxlJobInfo jobInfo = new XxlJobInfo();
jobInfo.setExecutorParam(grass.getId().toString()); // 任务参数:笔记ID
jobInfo.setScheduleConf(cron); // Cron表达式
jobInfo.setScheduleType("CRON");
jobInfo.setGlueType("BEAN");
jobInfo.setExecutorHandler("updateEnableFlagHandler"); // 执行器Handler名称
jobInfo.setJobDesc("定时发布笔记任务");
jobInfo.setAuthor("renrui");
jobInfo.setExecutorBlockStrategy("SERIAL_EXECUTION");
jobInfo.setExecutorRouteStrategy("FIRST");
jobInfo.setMisfireStrategy("DO_NOTHING");
// 4. 调用XXL-JOB API添加并启动任务
xxlJobUtil.addAndStart(jobInfo);
}
分页查询笔记列表:
java
@Override
public List<GrassVO> grassPage(PageParam pageParam) {
Integer uid = UserUtils.getUid();
// 1. 分页查询已审核通过的笔记
Page<Grass> page = new Page<>(pageParam.getPageNum(), pageParam.getPageSize());
page = grassMapper.selectPage(page, Wrappers.lambdaQuery(Grass.class)
.eq(Grass::getCheckFlag, 1));
List<Grass> records = page.getRecords();
// 2. 提取所有作者ID(去重)
List<Integer> uidList = records.stream()
.map(Grass::getUid)
.distinct()
.collect(Collectors.toList());
// 3. 远程调用获取用户信息
R r = pointFeignService.remoteUserList(uidList);
if (r.getCode() == 500) {
throw new RuntimeException("查询用户信息失败");
}
// 处理Feign返回的LinkedHashMap问题
String jsonString = JSON.toJSONString(r.getData());
List<User> userList = JSON.parseArray(jsonString, User.class);
// 4. 查询当前用户点赞了哪些笔记
List<Integer> gidList = records.stream().map(Grass::getId).collect(Collectors.toList());
List<Integer> likeGidList = likeInfoService.likeListByUidAndGids(uid, gidList);
// 5. 获取Redis中的当天点赞数
String day = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String key = "like:" + day;
// 6. 组装返回数据
List<GrassVO> list = records.stream().map(grass -> {
GrassVO grassVO = new GrassVO();
BeanUtils.copyProperties(grass, grassVO);
// 设置封面图(第一张)
grassVO.setCoverImg(grass.getImgPath().split(",")[0]);
// 设置作者信息
User user = userList.stream()
.filter(u -> u.getId().equals(grass.getUid()))
.findFirst().orElse(null);
if (user != null) {
grassVO.setUsername(user.getUsername());
grassVO.setAvatar(user.getAvatar());
}
// 设置点赞状态
grassVO.setLikeFlag(likeGidList.contains(grass.getId()) ? 1 : 0);
// 设置点赞数(数据库+Redis增量)
Integer redisLikeNum = redisCache.getCacheMapValue(key, grass.getId().toString());
if (redisLikeNum != null) {
grassVO.setLikeNum(grass.getLikeNum() + redisLikeNum);
}
return grassVO;
}).collect(Collectors.toList());
return list;
}
点赞数设计思路:
总点赞数 = 数据库存储的历史点赞数 + Redis存储的当天点赞增量
Redis存储结构(Hash):
Key: like:20260131
Field: 笔记ID
Value: 当天点赞增量(可正可负)
每日凌晨定时任务:
1. 读取昨天的Redis数据
2. 批量更新到数据库
3. 删除Redis中的昨天数据
批量更新点赞数:
java
@Override
public void updateLikeNumBatch() {
// 1. 获取昨天的日期
LocalDate localDate = LocalDate.now().minusDays(1);
String day = localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String key = "like:" + day;
// 2. 获取昨天的点赞数据
Map<String, Integer> cacheMap = redisCache.getCacheMap(key);
if (cacheMap == null || cacheMap.isEmpty()) {
return;
}
// 3. 过滤掉值为0的数据
Set<Map.Entry<String, Integer>> collect = cacheMap.entrySet().stream()
.filter(item -> !item.getValue().equals(0))
.collect(Collectors.toSet());
// 4. 计算分批次数(每100条一批)
int count = (collect.size() + 99) / 100;
CountDownLatch countDownLatch = new CountDownLatch(count);
// 5. 分批批量更新
List<UpdateLikeNumParam> list = new ArrayList<>();
for (Map.Entry<String, Integer> entry : cacheMap.entrySet()) {
if (entry.getValue().equals(0)) continue;
list.add(new UpdateLikeNumParam(Integer.valueOf(entry.getKey()), entry.getValue()));
if (list.size() == 100) {
List<UpdateLikeNumParam> tempList = list;
threadPoolExecutor.execute(() -> {
grassMapper.updateLikeNumBatch(tempList);
countDownLatch.countDown();
});
list = new ArrayList<>();
}
}
// 6. 处理剩余不足100条的数据
if (list.size() > 0) {
List<UpdateLikeNumParam> tempList = list;
threadPoolExecutor.execute(() -> {
grassMapper.updateLikeNumBatch(tempList);
countDownLatch.countDown();
});
}
// 7. 等待所有批次完成
countDownLatch.await();
// 8. 删除Redis中的昨天数据
redisCache.deleteObject(key);
}
批量更新 SQL GrassMapper.xml:
xml
java
<update id="updateLikeNumBatch">
UPDATE grass
SET like_num = CASE
<foreach collection="list" item="item">
WHEN id = #{item.grassId} THEN like_num + #{item.num}
</foreach>
END
WHERE id IN
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item.grassId}
</foreach>
</update>
6.2.6 点赞功能实现
点赞控制器 LikeInfoController.java:
java
运行
java
@RestController
@RequestMapping("/grass/like")
public class LikeInfoController {
@Resource
private LikeInfoService likeInfoService;
// 点赞/取消点赞(同一接口)
@GetMapping
public R like(Integer gid) {
likeInfoService.like(gid);
return R.ok();
}
}
点赞服务实现 LikeInfoServiceImpl.java:
java
@Service
public class LikeInfoServiceImpl implements LikeInfoService {
@Resource
private LikeInfoMapper likeInfoMapper;
@Resource
private RedisCache redisCache;
@Override
public void like(Integer gid) {
Integer uid = UserUtils.getUid();
// 1. 查询点赞记录
LikeInfo likeInfo = likeInfoMapper.selectOne(Wrappers.lambdaQuery(LikeInfo.class)
.eq(LikeInfo::getUid, uid)
.eq(LikeInfo::getGid, gid));
// 2. 构建Redis Key(按天存储)
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String key = "like:" + sdf.format(new Date());
// 3. 处理点赞逻辑
if (likeInfo == null) {
// 首次点赞
redisCache.incrCacheMapValue(key, gid.toString(), 1); // Redis点赞数+1
likeInfo = new LikeInfo();
likeInfo.setUid(uid);
likeInfo.setGid(gid);
likeInfoMapper.insert(likeInfo);
} else {
int flag;
if (likeInfo.getLikeFlag().equals(0)) {
// 之前取消了,现在重新点赞
flag = 1;
redisCache.incrCacheMapValue(key, gid.toString(), 1);
} else {
// 取消点赞
flag = 0;
redisCache.incrCacheMapValue(key, gid.toString(), -1);
}
likeInfoMapper.update(null, Wrappers.lambdaUpdate(LikeInfo.class)
.set(LikeInfo::getLikeFlag, flag)
.eq(LikeInfo::getId, likeInfo.getId()));
}
}
@Override
public List<Integer> likeListByUidAndGids(Integer uid, List<Integer> gidList) {
return likeInfoMapper.selectByUidAndGids(uid, gidList);
}
}
点赞流程图:
plaintext
用户点击点赞
↓
查询点赞记录(like_info表)
↓
├── 无记录(首次点赞)
│ ↓
│ Redis Hash自增+1
│ ↓
│ 插入点赞记录(likeFlag=1)
│
└── 有记录
↓
检查likeFlag
↓
├── likeFlag=0(已取消)
│ ↓
│ Redis Hash自增+1
│ ↓
│ 更新likeFlag=1
│
└── likeFlag=1(已点赞)
↓
Redis Hash自增-1
↓
更新likeFlag=0
6.2.7 定时任务
Spring 定时任务 GrassTask.java:
java
运行
java
// @Component // 已注释,使用XXL-JOB替代
public class GrassTask {
@Resource
private GrassService grassService;
// @Scheduled(cron = "0 30 0 * * ?") // 每天凌晨0:30执行
@Async("taskPool") // 异步执行
@MyLock(lockKey = "lock:likenum") // 分布式锁(防止集群重复执行)
@Scheduled(cron = "0/5 * * * * ?") // 测试:每5秒执行
public void updateLikeNumTask() {
grassService.updateLikeNumBatch();
}
}
XXL-JOB 定时任务 XxlJobGrassTask.java:
java
运行
java
@Component
public class XxlJobGrassTask {
@Resource
private GrassService grassService;
// 定时同步点赞数
@XxlJob("updateLikeNumHandler")
public void updateLikeNumTask() {
grassService.updateLikeNumBatch();
}
// 定时发布笔记
@XxlJob("updateEnableFlagHandler")
public void updateEnableFlagTask() {
// 从任务参数获取笔记ID
String jobParam = XxlJobHelper.getJobParam();
grassService.updateEnableFlag(Integer.valueOf(jobParam));
}
}
定时任务方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Spring @Scheduled | 简单易用 | 单机执行,集群需配合分布式锁 |
| XXL-JOB | 可视化管理、动态调整、自动路由 | 需额外部署调度中心 |
七、整体业务流程串联
7.1 用户注册流程
1. 用户输入手机号 → 点击发送验证码
2. 后端生成4位随机验证码 → 调用阿里云SMS发送 → 存入Redis(5分钟过期)
3. 用户填写注册信息 → 提交注册
4. 后端校验:用户名唯一性、手机号唯一性、验证码正确性
5. 处理邀请码:查询邀请人是否存在
6. 生成用户邀请码(4位随机字符串,最多重试3次)
7. 生成二维码图片(Hutool QrCodeUtil)→ 上传MinIO → 获取URL
8. 插入用户数据(含邀请码、二维码URL)
9. 异步给邀请人添加积分(线程池执行)
7.2 用户登录流程
1. 用户输入用户名密码 → 提交登录
2. 后端查询用户 → 校验密码
3. 生成JWT Token(包含用户ID)→ 返回前端
4. 前端存储Token → 后续请求携带Authorization Header
5. 拦截器解析Token → 存入ThreadLocal → 业务层通过UserUtils获取
7.3 发布笔记流程
1. 用户编写笔记(标题、内容、图片)→ 提交发布
2. 后端构建笔记实体 → 插入数据库(checkFlag=0待审核)
3. 异步任务1:调用百度内容审核API
- 合规:更新checkFlag=1
- 不合规:更新checkFlag=0
- 疑似:更新checkFlag=2(可扩展人工审核)
4. 异步任务2:发送RocketMQ消息
5. micro-user消费消息 → 查询积分规则 → 添加积分记录 → 更新用户积分
7.4 点赞流程
java
1. 用户点击点赞按钮
2. 后端查询点赞记录
3. 根据当前状态切换点赞/取消点赞
4. 更新Redis Hash(当天点赞增量)
5. 更新/插入点赞记录
6. 查询笔记时:总点赞数 = 数据库like_num + Redis增量
7. 每日凌晨定时任务:Redis增量批量同步到数据库
7.5 笔记列表查询流程
java
1. 前端请求笔记列表(带分页参数)
2. 分页查询已审核通过的笔记(checkFlag=1)
3. 提取所有作者ID → Feign远程调用micro-user获取用户信息
4. 查询当前用户的点赞记录
5. 从Redis获取当天点赞增量
6. 组装返回数据:笔记信息 + 作者信息 + 点赞状态 + 实时点赞数
八、技术亮点总结
8.1 架构层面
| 设计点 | 实现方式 | 作用 |
|---|---|---|
| 微服务拆分 | 按业务域划分 user、grass 服务 | 独立部署、独立扩展 |
| 公共模块抽取 | micro-common 下按功能细分 | 代码复用、即插即用 |
| 统一网关入口 | Spring Cloud Gateway | 路由转发、跨域处理 |
| 服务注册发现 | Nacos | 动态服务管理 |
| 远程调用 | OpenFeign + LoadBalancer | 声明式 HTTP 客户端 |
| 熔断降级 | Sentinel + FallbackFactory | 服务容错保护 |
8.2 数据层面
| 设计点 | 实现方式 | 作用 |
|---|---|---|
| 积分规则缓存 | Redis 缓存 24 小时 | 减少 DB 查询 |
| 点赞数分离 | 数据库存历史,Redis 存增量 | 高并发写入优化 |
| 定时批量同步 | XXL-JOB + 分批更新 | 错峰写入数据库 |
| 验证码存储 | Redis + 5 分钟过期 | 安全便捷 |
8.3 异步处理
| 场景 | 实现方式 | 作用 |
|---|---|---|
| 内容审核 | ThreadPoolExecutor | 不阻塞主流程 |
| 发布送积分 | RocketMQ | 服务解耦、削峰填谷 |
| 邀请送积分 | ThreadPoolExecutor | 异步处理 |
| 点赞同步 | XXL-JOB 定时任务 | 批量处理 |
8.4 安全与可靠性
| 设计点 | 实现方式 | 作用 |
|---|---|---|
| 用户认证 | JWT Token | 无状态认证 |
| 登录拦截 | 自定义拦截器 + @NoLogin 注解 | 灵活控制 |
| 用户上下文 | ThreadLocal | 线程安全 |
| 消息幂等 | Redis 记录已消费消息 ID | 防重复消费 |
| 分布式锁 | Redisson + 自定义注解 | 防重复执行 |
| 分布式事务 | Seata AT 模式 | 数据一致性 |
8.5 第三方服务集成
| 服务 | SDK / 组件 | 用途 |
|---|---|---|
| 短信服务 | 阿里云 SMS | 发送验证码 |
| 内容审核 | 百度 AI SDK | 文本敏感词过滤 |
| 对象存储 | MinIO | 存储图片、二维码 |
| 二维码生成 | Hutool QrCodeUtil | 生成邀请二维码 |
| 定时任务 | XXL-JOB | 分布式任务调度 |
九、配置清单汇总
9.1 中间件部署要求
| 组件 | 默认地址 | 说明 |
|---|---|---|
| Nacos | 127.0.0.1:8848 | 注册中心 + 配置中心 |
| MySQL | localhost:3306 | 数据库(micro2505 库) |
| Redis | 127.0.0.1:6379 | 缓存(database: 10) |
| MinIO | 127.0.0.1:9000 | 对象存储 |
| RocketMQ | 127.0.0.1:9876 | 消息队列 |
| XXL-JOB | 127.0.0.1:8080 | 任务调度中心 |
| Seata | Nacos 注册 | 分布式事务协调器 |
9.2 服务端口分配
| 服务 | 端口 | 说明 |
|---|---|---|
| micro-gateway | 9100 | API 网关 |
| micro-user | 9010 | 用户服务 |
| micro-grass | 9020 | 笔记服务 |
| XXL-JOB 执行器 | 9999 | micro-grass 内嵌 |
十、总结
本项目是一个完整的微服务实战项目,涵盖了 Spring Cloud Alibaba 生态的核心组件使用:服务治理:Nacos 服务注册发现、Gateway 网关路由、Feign 远程调用、Sentinel 熔断降级数据管理:MyBatis-Plus 持久层、Redis 缓存、Druid 连接池异步解耦:RocketMQ 消息队列、线程池异步任务分布式协调:Redisson 分布式锁、Seata 分布式事务、XXL-JOB 分布式调度第三方集成:阿里云短信、百度内容审核、MinIO 对象存储项目采用模块化设计,各个功能组件高内聚、低耦合,便于维护和扩展。通过实际业务场景(用户注册、笔记发布、点赞功能)串联各个技术点,是学习微服务架构的优秀实践案例