【无标题】

微服务项目完整解析

一、项目整体架构概述这是一个基于 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 对象存储项目采用模块化设计,各个功能组件高内聚、低耦合,便于维护和扩展。通过实际业务场景(用户注册、笔记发布、点赞功能)串联各个技术点,是学习微服务架构的优秀实践案例


相关推荐
咩咩啃树皮8 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚8 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白9 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司10 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地11 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415911 小时前
TCP超时重传机制是为了解决什么问题?
java
莫逸风14 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
隔窗听雨眠14 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生
z1234567898614 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112315 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python