【无标题】

微服务项目完整解析

一、项目整体架构概述这是一个基于 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,防止内存泄漏
    }
}

工作流程:

  1. 拦截所有请求
  2. 判断方法是否有 @NoLogin 注解,有则跳过验证
  3. 从请求头获取 Authorization Token
  4. 解析 Token 获取用户 ID,存入 ThreadLocal
  5. 请求完成后清理 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 对象存储项目采用模块化设计,各个功能组件高内聚、低耦合,便于维护和扩展。通过实际业务场景(用户注册、笔记发布、点赞功能)串联各个技术点,是学习微服务架构的优秀实践案例


相关推荐
重整旗鼓~5 小时前
1.外卖项目介绍
spring boot
@ chen5 小时前
Spring事务 核心知识
java·后端·spring
aithinker5 小时前
使用QQ邮箱收发邮件遇到的坑 有些WIFI不支持ipv6
java
编程彩机5 小时前
互联网大厂Java面试:从微服务到分布式缓存的技术场景解析
redis·spring cloud·消息队列·微服务架构·openfeign·java面试·分布式缓存
星火开发设计5 小时前
C++ 预处理指令:#include、#define 与条件编译
java·开发语言·c++·学习·算法·知识
Hx_Ma166 小时前
SpringMVC返回值
java·开发语言·servlet
Yana.nice6 小时前
openssl将证书从p7b转换为crt格式
java·linux
独自破碎E6 小时前
【滑动窗口+字符计数数组】LCR_014_字符串的排列
android·java·开发语言
想逃离铁厂的老铁6 小时前
Day55 >> 并查集理论基础 + 107、寻找存在的路线
java·服务器