Explorar el Código

feat(seckill): 实现秒杀系统核心功能

- 新增秒杀控制器和相关注解
- 实现基于Redis的库存扣减和原子性操作- 添加Lua脚本支持热点数据分片
- 配置定时任务和系统时序图展示- 引入Redisson依赖实现分布式锁和信号量- 添加AOP切面支持秒杀防护和限流- 实现用户上下文管理和订单令牌生成
- 新增秒杀系统配置文件和灰度发布支持
wxd hace 2 meses
padre
commit
511f975996
Se han modificado 73 ficheros con 5854 adiciones y 58 borrados
  1. 57 0
      alien-entity/src/main/java/shop/alien/entity/second/SecurityRule.java
  2. 49 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/ScoreReq.java
  3. 17 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/ScoreResp.java
  4. 69 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/SecurityCtx.java
  5. 25 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/fact/AccountFact.java
  6. 19 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/fact/FactWrapper.java
  7. 24 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/fact/ItemFact.java
  8. 27 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/fact/LogisticsFact.java
  9. 22 0
      alien-entity/src/main/java/shop/alien/entity/second/vo/fact/TradeFact.java
  10. 10 0
      alien-entity/src/main/java/shop/alien/mapper/second/SecurityRuleMapper.java
  11. 5 0
      alien-gateway/pom.xml
  12. 36 0
      alien-gateway/src/main/java/shop/alien/gateway/config/BaseRedisService.java
  13. 17 0
      alien-gateway/src/main/java/shop/alien/gateway/config/GatewaySecurityConfig.java
  14. 312 0
      alien-gateway/src/main/java/shop/alien/gateway/config/GatewaySecurityProperties.java
  15. 9 1
      alien-gateway/src/main/java/shop/alien/gateway/config/JwtTokenFilter.java
  16. 73 0
      alien-gateway/src/main/java/shop/alien/gateway/config/RateLimiterConfig.java
  17. 236 0
      alien-gateway/src/main/java/shop/alien/gateway/config/RedisConfig.java
  18. 173 0
      alien-gateway/src/main/java/shop/alien/gateway/controller/BlackWhiteListController.java
  19. 150 0
      alien-gateway/src/main/java/shop/alien/gateway/controller/RateLimitController.java
  20. 77 0
      alien-gateway/src/main/java/shop/alien/gateway/entity/BlackWhiteListEntry.java
  21. 63 0
      alien-gateway/src/main/java/shop/alien/gateway/entity/RateLimitResponse.java
  22. 182 0
      alien-gateway/src/main/java/shop/alien/gateway/filter/ApiRateLimitFilter.java
  23. 191 0
      alien-gateway/src/main/java/shop/alien/gateway/filter/BlackWhiteListFilter.java
  24. 181 0
      alien-gateway/src/main/java/shop/alien/gateway/filter/GlobalRateLimitFilter.java
  25. 185 0
      alien-gateway/src/main/java/shop/alien/gateway/filter/IpRateLimitFilter.java
  26. 215 0
      alien-gateway/src/main/java/shop/alien/gateway/filter/UserRateLimitFilter.java
  27. 263 0
      alien-gateway/src/main/java/shop/alien/gateway/service/BlackWhiteListService.java
  28. 133 0
      alien-gateway/src/main/java/shop/alien/gateway/service/RateLimitService.java
  29. 135 0
      alien-gateway/src/main/java/shop/alien/gateway/util/IpUtils.java
  30. 284 0
      alien-gateway/src/main/java/shop/alien/gateway/util/JwtUtils.java
  31. 133 0
      alien-gateway/src/main/java/shop/alien/gateway/util/ResponseUtils.java
  32. 2 1
      alien-gateway/src/main/resources/logback-spring.xml
  33. 0 25
      alien-job/pom.xml
  34. 1 1
      alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java
  35. 213 2
      alien-job/src/main/resources/bootstrap.yml
  36. 12 0
      alien-second/pom.xml
  37. 2 2
      alien-second/src/main/java/shop/alien/second/AlienSecondApplication.java
  38. 97 0
      alien-second/src/main/java/shop/alien/second/config/RuleLoaderUtil.java
  39. 2 7
      alien-second/src/main/java/shop/alien/second/controller/SecondGoodsController.java
  40. 87 0
      alien-second/src/main/java/shop/alien/second/controller/SecurityController.java
  41. 201 0
      alien-second/src/main/java/shop/alien/second/seckill/SECKILL_SYSTEM_TEST_GUIDE.md
  42. 66 0
      alien-second/src/main/java/shop/alien/second/seckill/SEQUENCE_DIAGRAM.md
  43. 17 0
      alien-second/src/main/java/shop/alien/second/seckill/annotation/SeckillGuard.java
  44. 155 0
      alien-second/src/main/java/shop/alien/second/seckill/aspect/SeckillGuardAspect.java
  45. 37 0
      alien-second/src/main/java/shop/alien/second/seckill/aspect/tempfile_1760088147642.mermaid
  46. 53 0
      alien-second/src/main/java/shop/alien/second/seckill/aspect/tempfile_1760088171106.mermaid
  47. 13 0
      alien-second/src/main/java/shop/alien/second/seckill/config/SchedulingConfig.java
  48. 9 0
      alien-second/src/main/java/shop/alien/second/seckill/config/SeckillAspectConfig.java
  49. 33 0
      alien-second/src/main/java/shop/alien/second/seckill/config/SeckillMQConfig.java
  50. 41 0
      alien-second/src/main/java/shop/alien/second/seckill/controller/ExampleSeckillController.java
  51. 44 0
      alien-second/src/main/java/shop/alien/second/seckill/controller/SeckillController.java
  52. 58 0
      alien-second/src/main/java/shop/alien/second/seckill/controller/StockInitController.java
  53. 148 0
      alien-second/src/main/java/shop/alien/second/seckill/dao/RedisStockDao.java
  54. 15 0
      alien-second/src/main/java/shop/alien/second/seckill/dao/tempfile_1758866875150.mermaid
  55. 39 0
      alien-second/src/main/java/shop/alien/second/seckill/entity/SeckillOrderToken.java
  56. 48 0
      alien-second/src/main/java/shop/alien/second/seckill/job/StockSyncJob.java
  57. 72 0
      alien-second/src/main/java/shop/alien/second/seckill/mq/SeckillMessageConsumer.java
  58. 52 0
      alien-second/src/main/java/shop/alien/second/seckill/mq/SeckillMessageProducer.java
  59. 48 0
      alien-second/src/main/java/shop/alien/second/seckill/service/SeckillConfigService.java
  60. 194 0
      alien-second/src/main/java/shop/alien/second/seckill/service/StockInitializerService.java
  61. 101 0
      alien-second/src/main/java/shop/alien/second/seckill/service/StockSyncService.java
  62. 119 0
      alien-second/src/main/java/shop/alien/second/seckill/util/ShardingUtil.java
  63. 37 0
      alien-second/src/main/java/shop/alien/second/seckill/util/UserContext.java
  64. 7 0
      alien-second/src/main/java/shop/alien/second/service/SecondGoodsService.java
  65. 102 19
      alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java
  66. 137 0
      alien-second/src/main/java/shop/alien/second/service/impl/SecurityRuleServiceImpl.java
  67. 3 0
      alien-second/src/main/resources/application-seckill.yml
  68. 28 0
      alien-second/src/main/resources/lua/stockDecr.lua
  69. 178 0
      alien-second/src/main/resources/seckill_system.html
  70. 5 0
      alien-util/pom.xml
  71. 2 0
      alien-util/src/main/java/shop/alien/util/common/safe/ImageModerationUtil.java
  72. 2 0
      alien-util/src/main/java/shop/alien/util/common/safe/TextModerationUtil.java
  73. 2 0
      alien-util/src/main/java/shop/alien/util/common/safe/video/VideoModerationUtil.java

+ 57 - 0
alien-entity/src/main/java/shop/alien/entity/second/SecurityRule.java

@@ -0,0 +1,57 @@
+package shop.alien.entity.second;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 安全风控规则
+ */
+@Data
+@JsonInclude
+@TableName("t_security_rule")
+public class SecurityRule implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 启用状态常量 */
+    public static final int ENABLED_YES = 1;
+    public static final int ENABLED_NO  = 0;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @TableField("rule_name")
+    @ApiModelProperty(value = "规则名称")
+    private String ruleName;
+
+    @TableField("priority")
+    @ApiModelProperty(value = "优先级,数字越大越先匹配")
+    private Integer priority;
+
+    @TableField("when_expr")
+    @ApiModelProperty(value = "触发条件表达式(LHS)")
+    private String whenExpr;
+
+    @TableField("fact_type")
+    @ApiModelProperty(value = "事实类简称,如 AccountFact、ItemFact、LogisticsFact、TradeFact")
+    private String factType;
+
+    @TableField("then_expr")
+    @ApiModelProperty(value = "触发后执行动作(RHS)")
+    private String thenExpr;
+
+    @TableField("enabled")
+    @ApiModelProperty(value = "是否启用 1启用 0禁用")
+    private Integer enabled;
+
+    @TableField(value = "updated_at", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty(value = "最后一次更新时间")
+    private Date updatedAt;
+}

+ 49 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/ScoreReq.java

@@ -0,0 +1,49 @@
+package shop.alien.entity.second.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@JsonInclude
+@ApiModel(value = "规则引擎入参", description = "规则引擎入参")
+public class ScoreReq {
+    
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private long listingId;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private boolean highRiskCategory;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private int price;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private int marketPrice;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private boolean offsiteChat;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private boolean requestAdvancePayment;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private int historyViolation;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private double sellerScore;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private boolean realName;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private boolean noTracking;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private boolean cashOnDelivery;
+
+    @ApiModelProperty(value = "保存方式 0:草稿 1:发布")
+    private boolean remoteArea;
+
+}

+ 17 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/ScoreResp.java

@@ -0,0 +1,17 @@
+package shop.alien.entity.second.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@JsonInclude
+@ApiModel(value = "规则引擎出参", description = "规则引擎出参")
+@AllArgsConstructor
+public class ScoreResp {
+
+    @ApiModelProperty(value = "得分 0~100")
+    private double score;   // 0~100
+}

+ 69 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/SecurityCtx.java

@@ -0,0 +1,69 @@
+package shop.alien.entity.second.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+/**
+ * 规则引擎入参类,用于维护和计算商品、交易、账户、物流四个维度的评分。
+ * 提供加分、扣分方法以及总评分的加权计算。
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "规则引擎入参", description = "规则引擎入参")
+public class SecurityCtx {
+
+    /** 当前累计扣减分值 */
+    private int scoreDeduct;
+
+    /**
+     * 扣分方法,将指定的delta值累加到scoreDeduct。
+     * @param delta 要扣减的分值
+     */
+    public void deduct(int delta) {
+        this.scoreDeduct += delta;
+    }
+
+    private final long listingId;
+
+    private double itemScore = 0;
+
+    private double tradeScore = 0;
+
+    private double accountScore = 0;
+
+    private double logisticsScore = 0;
+
+    /**
+     * 计算总评分,按权重加权:商品35%、交易35%、账户20%、物流10%。
+     * @return 返回计算后的总评分
+     */
+    public double total() {
+        return itemScore * 0.35 + tradeScore * 0.35
+                + accountScore * 0.2  + logisticsScore * 0.1;
+    }
+
+    /**
+     * 为商品评分增加指定的分值。
+     * @param d 要增加的分值
+     */
+    public void addItem(double d)    { itemScore += d; }
+
+    /**
+     * 为交易评分增加指定的分值。
+     * @param d 要增加的分值
+     */
+    public void addTrade(double d)   { tradeScore += d; }
+
+    /**
+     * 为账户评分增加指定的分值。
+     * @param d 要增加的分值
+     */
+    public void addAccount(double d) { accountScore += d; }
+
+    /**
+     * 为物流评分增加指定的分值。
+     * @param d 要增加的分值
+     */
+    public void addLogistics(double d) { logisticsScore += d; }
+}

+ 25 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/fact/AccountFact.java

@@ -0,0 +1,25 @@
+package shop.alien.entity.second.vo.fact;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 账号维度事实类
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class AccountFact   {
+
+    @ApiModelProperty(value = "历史违规次数,>2 时扣减安全分")
+    private int historyViolation;
+
+    @ApiModelProperty(value = "卖家综合评分,<4.5 时扣减安全分")
+    private double sellerScore;
+
+    @ApiModelProperty(value = "是否完成实名认证,false 扣减安全分")
+    private boolean realName;
+
+}

+ 19 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/fact/FactWrapper.java

@@ -0,0 +1,19 @@
+package shop.alien.entity.second.vo.fact;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class FactWrapper {
+
+    /** 维度名称:AccountFact / ItemFact / ... */
+    private String factType;
+
+    /** 字段值 Map:key=字段名, value=字段值 */
+    private Map<String, Object> fields;
+}

+ 24 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/fact/ItemFact.java

@@ -0,0 +1,24 @@
+package shop.alien.entity.second.vo.fact;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 商品维度事实类
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ItemFact {
+
+    @ApiModelProperty(value = "品类目是否为高危类目,true=扣减安全分")
+    private boolean highRiskCategory;
+
+    @ApiModelProperty(value = "当前售价(分)")
+    private int price;
+
+    @ApiModelProperty(value = "平台市场价(分),与 price 比较判断“价格异常低”")
+    private int marketPrice;
+}

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/fact/LogisticsFact.java

@@ -0,0 +1,27 @@
+package shop.alien.entity.second.vo.fact;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 物流维度事实类
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class LogisticsFact {
+
+    @ApiModelProperty(value = "是否无物流跟踪号,true=扣减安全分")
+    private boolean noTracking;
+
+    @ApiModelProperty(value = "是否仅支持货到付款,true=扣减安全分")
+    private boolean cashOnDelivery;
+
+    @ApiModelProperty(value = "收货地址是否为偏远地区,true=扣减安全分")
+    private boolean remoteArea;
+
+
+
+}

+ 22 - 0
alien-entity/src/main/java/shop/alien/entity/second/vo/fact/TradeFact.java

@@ -0,0 +1,22 @@
+package shop.alien.entity.second.vo.fact;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 交易维度事实类
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TradeFact {
+
+    @ApiModelProperty(value = "是否引导买家站外沟通,true=扣减安全分")
+    private boolean offsiteChat;
+
+    @ApiModelProperty(value = "是否要求提前付款,true=扣减安全分")
+    private boolean requestAdvancePayment;
+
+}

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/second/SecurityRuleMapper.java

@@ -0,0 +1,10 @@
+package shop.alien.mapper.second;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.second.SecurityRule;
+
+/**
+ * 安全风控规则映射器
+ */
+public interface SecurityRuleMapper extends BaseMapper<SecurityRule> {
+}

+ 5 - 0
alien-gateway/pom.xml

@@ -180,6 +180,11 @@
         </dependency>
 
         <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-context</artifactId>
+        </dependency>
+
+        <dependency>
             <groupId>javax.xml.bind</groupId>
             <artifactId>jaxb-api</artifactId>
         </dependency>

+ 36 - 0
alien-gateway/src/main/java/shop/alien/gateway/config/BaseRedisService.java

@@ -144,6 +144,42 @@ public class BaseRedisService {
     }
 
     /**
+     * 使用key获取值(兼容方法)
+     *
+     * @param key 键
+     * @return 值
+     */
+    public Object get(String key) {
+        return stringRedisTemplate.opsForValue().get(key);
+    }
+
+    /**
+     * 设置值和过期时间
+     *
+     * @param key 键
+     * @param value 值
+     * @param timeout 过期时间
+     * @param unit 时间单位
+     */
+    public void set(String key, Object value, long timeout, TimeUnit unit) {
+        if (value instanceof String) {
+            stringRedisTemplate.opsForValue().set(key, (String) value, timeout, unit);
+        } else {
+            stringRedisTemplate.opsForValue().set(key, String.valueOf(value), timeout, unit);
+        }
+    }
+
+    /**
+     * 检查key是否存在
+     *
+     * @param key 键
+     * @return 是否存在
+     */
+    public boolean hasKey(String key) {
+        return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
+    }
+
+    /**
      * 使用key删除redis信息
      *
      * @param key 键

+ 17 - 0
alien-gateway/src/main/java/shop/alien/gateway/config/GatewaySecurityConfig.java

@@ -0,0 +1,17 @@
+package shop.alien.gateway.config;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Gateway安全配置启用类
+ * 启用GatewaySecurityProperties配置属性
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Configuration
+@EnableConfigurationProperties(GatewaySecurityProperties.class)
+public class GatewaySecurityConfig {
+}

+ 312 - 0
alien-gateway/src/main/java/shop/alien/gateway/config/GatewaySecurityProperties.java

@@ -0,0 +1,312 @@
+package shop.alien.gateway.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 网关防护配置类
+ * 支持动态配置限流策略和黑白名单
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Data
+@Component
+@RefreshScope
+@ConfigurationProperties(prefix = "gateway.security")
+@Primary
+public class GatewaySecurityProperties {
+
+    /**
+     * 是否启用安全防护
+     */
+    private boolean enabled = true;
+
+    /**
+     * 全局限流配置
+     */
+    private GlobalRateLimit globalRateLimit = new GlobalRateLimit();
+
+    /**
+     * IP限流配置
+     */
+    private IpRateLimit ipRateLimit = new IpRateLimit();
+
+    /**
+     * 用户限流配置
+     */
+    private UserRateLimit userRateLimit = new UserRateLimit();
+
+    /**
+     * 接口限流配置
+     */
+    private ApiRateLimit apiRateLimit = new ApiRateLimit();
+
+    /**
+     * 黑白名单配置
+     */
+    private BlackWhiteList blackWhiteList = new BlackWhiteList();
+
+    /**
+     * 监控配置
+     */
+    private Monitor monitor = new Monitor();
+
+    /**
+     * 全局限流配置类
+     * 用于配置基于路径和方法的全局限流策略
+     */
+    @Data
+    public static class GlobalRateLimit {
+        /**
+         * 是否启用全局限流
+         */
+        private boolean enabled = true;
+
+        /**
+         * 默认限流阈值(请求数)
+         */
+        private int defaultLimit = 10000;
+
+        /**
+         * 默认时间窗口(秒)
+         */
+        private int defaultWindow = 60;
+
+        /**
+         * 路径特定的限流配置
+         * key: 路径模式, value: 路径配置
+         */
+        private Map<String, PathConfig> pathConfigs;
+    }
+
+    /**
+     * IP限流配置类
+     * 用于配置基于IP维度的限流策略
+     */
+    @Data
+    public static class IpRateLimit {
+        /**
+         * 是否启用IP限流
+         */
+        private boolean enabled = true;
+
+        /**
+         * 默认IP限流阈值(请求数)
+         */
+        private int defaultLimit = 100;
+
+        /**
+         * 默认时间窗口(秒)
+         */
+        private int defaultWindow = 60;
+
+        /**
+         * 路径特定的IP限流配置
+         * key: 路径模式, value: 限流配置
+         */
+        private Map<String, RateLimitConfig> pathConfigs;
+
+        /**
+         * 特定IP的限流配置
+         * key: IP地址, value: 限流配置
+         */
+        private Map<String, RateLimitConfig> ipConfigs;
+    }
+
+    /**
+     * 用户限流配置类
+     * 用于配置基于用户维度的限流策略
+     */
+    @Data
+    public static class UserRateLimit {
+        /**
+         * 是否启用用户限流
+         */
+        private boolean enabled = true;
+
+        /**
+         * 默认用户限流阈值(请求数)
+         */
+        private int defaultLimit = 50;
+
+        /**
+         * 默认时间窗口(秒)
+         */
+        private int defaultWindow = 60;
+
+        /**
+         * 路径特定的用户限流配置
+         * key: 路径模式, value: 限流配置
+         */
+        private Map<String, RateLimitConfig> pathConfigs;
+
+        /**
+         * 特定用户的限流配置
+         * key: 用户ID, value: 限流配置
+         */
+        private Map<String, RateLimitConfig> userConfigs;
+    }
+
+    /**
+     * 限流配置类
+     * 用于配置具体的限流参数
+     */
+    @Data
+    public static class RateLimitConfig {
+        /**
+         * 是否启用限流
+         */
+        private boolean enabled = true;
+
+        /**
+         * 限流阈值(请求数)
+         */
+        private int limit;
+
+        /**
+         * 时间窗口(秒)
+         */
+        private int window;
+    }
+
+    /**
+     * 接口限流配置类
+     * 用于配置基于API接口维度的限流策略
+     */
+    @Data
+    public static class ApiRateLimit {
+        /**
+         * 是否启用接口限流
+         */
+        private boolean enabled = true;
+
+        /**
+         * 默认接口限流阈值(请求数)
+         */
+        private int defaultLimit = 1000;
+
+        /**
+         * 默认时间窗口(秒)
+         */
+        private int defaultWindow = 60;
+
+        /**
+         * 路径特定的接口限流配置
+         * key: 路径模式, value: 限流配置
+         */
+        private Map<String, RateLimitConfig> pathConfigs;
+
+        /**
+         * HTTP方法特定的限流配置
+         * key: HTTP方法, value: 限流配置
+         */
+        private Map<String, RateLimitConfig> methodConfigs;
+    }
+
+    /**
+     * 黑白名单配置类
+     * 用于配置黑白名单策略
+     */
+    @Data
+    public static class BlackWhiteList {
+        /**
+         * 是否启用黑白名单
+         */
+        private boolean enabled = true;
+
+        /**
+         * 是否启用白名单模式
+         * true: 只允许白名单IP访问, false: 黑名单IP禁止访问
+         */
+        private boolean whiteListMode = false;
+
+        /**
+         * 静态黑名单IP列表
+         */
+        private List<String> blackIps;
+
+        /**
+         * 静态白名单IP列表
+         */
+        private List<String> whiteIps;
+
+        /**
+         * 路径特定的黑名单配置
+         * key: 路径模式, value: IP列表
+         */
+        private Map<String, List<String>> pathBlackIps;
+
+        /**
+         * 路径特定的白名单配置
+         * key: 路径模式, value: IP列表
+         */
+        private Map<String, List<String>> pathWhiteIps;
+    }
+
+    /**
+     * 路径配置类
+     * 用于配置路径特定的限流参数
+     */
+    @Data
+    public static class PathConfig {
+        /**
+         * 是否启用路径配置
+         */
+        private boolean enabled = true;
+
+        /**
+         * 限流阈值(请求数)
+         */
+        private int limit;
+
+        /**
+         * 时间窗口(秒)
+         */
+        private int window;
+    }
+
+    /**
+     * 监控配置类
+     * 用于配置监控相关参数
+     */
+    @Data
+    public static class Monitor {
+        /**
+         * 是否启用监控
+         */
+        private boolean enabled = true;
+
+        /**
+         * 日志级别:DEBUG, INFO, WARN, ERROR
+         */
+        private String logLevel = "INFO";
+
+        /**
+         * 日志输出间隔(秒)
+         */
+        private int logInterval = 300;
+
+        /**
+         * 是否启用监控指标收集
+         */
+        private boolean metricsEnabled = true;
+
+        /**
+         * 是否启用告警
+         */
+        private boolean alertEnabled = true;
+
+        /**
+         * 排除监控的路径列表
+         */
+        private List<String> excludePaths;
+    }
+}

+ 9 - 1
alien-gateway/src/main/java/shop/alien/gateway/config/JwtTokenFilter.java

@@ -50,7 +50,9 @@ import static org.apache.commons.codec.language.bm.Languages.ANY;
 @Component
 public class JwtTokenFilter implements GlobalFilter, Ordered {
 
-    @Value("${jwt.skip-auth-urls}")
+    @Value("${jwt.skip-auth-urls:}")
+    private String skipAuthUrlsStr;
+    
     private String[] skipAuthUrls;
 
     @Autowired
@@ -71,6 +73,12 @@ public class JwtTokenFilter implements GlobalFilter, Ordered {
      */
     @Override
     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        // 初始化skipAuthUrls数组
+//        if (skipAuthUrls == null && skipAuthUrlsStr != null && !skipAuthUrlsStr.trim().isEmpty()) {
+//            skipAuthUrls = skipAuthUrlsStr.split(",");
+//        } else if (skipAuthUrls == null) {
+//            skipAuthUrls = new String[0];
+//        }
         String url = exchange.getRequest().getURI().getPath();
         log.info("JwtTokenFilter.filter?Path={}", url);
         if (Objects.equals(exchange.getRequest().getMethod(), HttpMethod.OPTIONS)) {

+ 73 - 0
alien-gateway/src/main/java/shop/alien/gateway/config/RateLimiterConfig.java

@@ -0,0 +1,73 @@
+//package shop.alien.gateway.config;
+//
+//import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.context.annotation.Primary;
+//import reactor.core.publisher.Mono;
+//import shop.alien.gateway.util.IpUtils;
+//
+///**
+// * 限流配置类
+// * 定义各种限流的Key解析器
+// *
+// * @author ssk
+// * @version 1.0
+// * @date 2025/01/21
+// */
+//@Configuration
+//public class RateLimiterConfig {
+//
+//    /**
+//     * IP限流的Key解析器
+//     */
+//    @Bean
+//    public KeyResolver ipKeyResolver() {
+//        return exchange -> Mono.just(IpUtils.getClientIp(exchange));
+//    }
+//
+//    /**
+//     * 用户限流的Key解析器
+//     */
+//    @Bean
+//    public KeyResolver userKeyResolver() {
+//        return exchange -> {
+//            // 从请求头中获取用户ID,如果没有则使用IP
+//            String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
+//            if (userId != null && !userId.trim().isEmpty()) {
+//                return Mono.just(userId);
+//            }
+//            // 如果没有用户ID,则使用IP作为备选
+//            return Mono.just(IpUtils.getClientIp(exchange));
+//        };
+//    }
+//
+//    /**
+//     * 接口限流的Key解析器
+//     */
+//    @Bean
+//    public KeyResolver apiKeyResolver() {
+//        return exchange -> {
+//            String path = exchange.getRequest().getURI().getPath();
+//            String method = exchange.getRequest().getMethod().name();
+//            return Mono.just(method + ":" + path);
+//        };
+//    }
+//
+//    /**
+//     * 全局限流的Key解析器(默认)
+//     */
+//    @Primary
+//    @Bean
+//    public KeyResolver globalKeyResolver() {
+//        return exchange -> Mono.just("global");
+//    }
+//
+//    /**
+//     * 主机限流的Key解析器
+//     */
+//    @Bean
+//    public KeyResolver hostKeyResolver() {
+//        return exchange -> Mono.just(exchange.getRequest().getURI().getHost());
+//    }
+//}

+ 236 - 0
alien-gateway/src/main/java/shop/alien/gateway/config/RedisConfig.java

@@ -0,0 +1,236 @@
+package shop.alien.gateway.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisClusterConfiguration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Redis配置类
+ * 支持集群和单机模式,配置Lua脚本
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Configuration
+public class RedisConfig {
+
+    @Value("${spring.redis.host:localhost}")
+    private String redisHost;
+
+    @Value("${spring.redis.port:6379}")
+    private int redisPort;
+
+    @Value("${spring.redis.password:}")
+    private String redisPassword;
+
+    @Value("${spring.redis.database:0}")
+    private int redisDatabase;
+
+    @Value("${spring.redis.cluster.nodes:}")
+    private String clusterNodes;
+
+    @Value("${spring.redis.cluster.enabled:false}")
+    private boolean clusterEnabled;
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
+                .commandTimeout(Duration.ofMillis(100))
+                .shutdownTimeout(Duration.ofSeconds(2))
+                .build();
+
+        if (clusterEnabled) {
+            // 集群模式
+            List<String> nodes = Arrays.asList(clusterNodes.split(","));
+            RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(nodes);
+            if (!redisPassword.isEmpty()) {
+                clusterConfig.setPassword(redisPassword);
+            }
+            return new LettuceConnectionFactory(clusterConfig, clientConfig);
+        } else {
+            // 单机模式
+            RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration();
+            standaloneConfig.setHostName(redisHost);
+            standaloneConfig.setPort(redisPort);
+            standaloneConfig.setDatabase(redisDatabase);
+            if (!redisPassword.isEmpty()) {
+                standaloneConfig.setPassword(redisPassword);
+            }
+            return new LettuceConnectionFactory(standaloneConfig, clientConfig);
+        }
+    }
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
+        ObjectMapper om = new ObjectMapper();
+        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        jackson2JsonRedisSerializer.setObjectMapper(om);
+
+        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+        // key采用String的序列化方式
+        template.setKeySerializer(stringRedisSerializer);
+        // hash的key也采用String的序列化方式
+        template.setHashKeySerializer(stringRedisSerializer);
+        // value序列化方式采用jackson
+        template.setValueSerializer(jackson2JsonRedisSerializer);
+        // hash的value序列化方式采用jackson
+        template.setHashValueSerializer(jackson2JsonRedisSerializer);
+        template.afterPropertiesSet();
+
+        return template;
+    }
+
+    @Bean
+    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
+        StringRedisTemplate template = new StringRedisTemplate();
+        template.setConnectionFactory(connectionFactory);
+        return template;
+    }
+
+    /**
+     * 全局限流Lua脚本
+     */
+    @Bean
+    public DefaultRedisScript<Long> globalRateLimitScript() {
+        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
+        script.setScriptText(
+            "local key = KEYS[1]\n" +
+            "local limit = tonumber(ARGV[1])\n" +
+            "local window = tonumber(ARGV[2])\n" +
+            "local current = tonumber(redis.call('get', key) or \"0\")\n" +
+            "if current + 1 > limit then\n" +
+            "    return 0\n" +
+            "else\n" +
+            "    redis.call('incr', key)\n" +
+            "    redis.call('expire', key, window)\n" +
+            "    return 1\n" +
+            "end"
+        );
+        script.setResultType(Long.class);
+        return script;
+    }
+
+    /**
+     * IP限流Lua脚本
+     */
+    @Bean
+    public DefaultRedisScript<Long> ipRateLimitScript() {
+        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
+        script.setScriptText(
+            "local key = KEYS[1]\n" +
+            "local limit = tonumber(ARGV[1])\n" +
+            "local window = tonumber(ARGV[2])\n" +
+            "local current = tonumber(redis.call('get', key) or \"0\")\n" +
+            "if current + 1 > limit then\n" +
+            "    return 0\n" +
+            "else\n" +
+            "    redis.call('incr', key)\n" +
+            "    redis.call('expire', key, window)\n" +
+            "    return 1\n" +
+            "end"
+        );
+        script.setResultType(Long.class);
+        return script;
+    }
+
+    /**
+     * 用户限流Lua脚本
+     */
+    @Bean
+    public DefaultRedisScript<Long> userRateLimitScript() {
+        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
+        script.setScriptText(
+            "local key = KEYS[1]\n" +
+            "local limit = tonumber(ARGV[1])\n" +
+            "local window = tonumber(ARGV[2])\n" +
+            "local current = tonumber(redis.call('get', key) or \"0\")\n" +
+            "if current + 1 > limit then\n" +
+            "    return 0\n" +
+            "else\n" +
+            "    redis.call('incr', key)\n" +
+            "    redis.call('expire', key, window)\n" +
+            "    return 1\n" +
+            "end"
+        );
+        script.setResultType(Long.class);
+        return script;
+    }
+
+    /**
+     * 接口限流Lua脚本
+     */
+    @Bean
+    public DefaultRedisScript<Long> apiRateLimitScript() {
+        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
+        script.setScriptText(
+            "local key = KEYS[1]\n" +
+            "local limit = tonumber(ARGV[1])\n" +
+            "local window = tonumber(ARGV[2])\n" +
+            "local current = tonumber(redis.call('get', key) or \"0\")\n" +
+            "if current + 1 > limit then\n" +
+            "    return 0\n" +
+            "else\n" +
+            "    redis.call('incr', key)\n" +
+            "    redis.call('expire', key, window)\n" +
+            "    return 1\n" +
+            "end"
+        );
+        script.setResultType(Long.class);
+        return script;
+    }
+
+    /**
+     * 综合限流Lua脚本(支持多种限流策略组合)
+     */
+    @Bean
+    public DefaultRedisScript<List> combinedRateLimitScript() {
+        DefaultRedisScript<List> script = new DefaultRedisScript<>();
+        script.setScriptText(
+            "local results = {}\n" +
+            "for i = 1, #KEYS do\n" +
+            "    if KEYS[i] ~= \"\" then\n" +
+            "        local key = KEYS[i]\n" +
+            "        local limit = tonumber(ARGV[i])\n" +
+            "        local window = tonumber(ARGV[#ARGV])\n" +
+            "        local current = tonumber(redis.call('get', key) or \"0\")\n" +
+            "        if current + 1 > limit then\n" +
+            "            results[i] = 0\n" +
+            "        else\n" +
+            "            redis.call('incr', key)\n" +
+            "            redis.call('expire', key, window)\n" +
+            "            results[i] = 1\n" +
+            "        end\n" +
+            "    else\n" +
+            "        results[i] = 1\n" +
+            "    end\n" +
+            "end\n" +
+            "return results"
+        );
+        script.setResultType(List.class);
+        return script;
+    }
+}

+ 173 - 0
alien-gateway/src/main/java/shop/alien/gateway/controller/BlackWhiteListController.java

@@ -0,0 +1,173 @@
+package shop.alien.gateway.controller;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+import shop.alien.gateway.util.ResponseUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 黑白名单管理控制器
+ * 提供动态管理黑白名单的接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@RestController
+@RequestMapping("/gateway/admin/black-white-list")
+public class BlackWhiteListController {
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    /**
+     * 添加IP到黑名单
+     */
+    @PostMapping("/blacklist/add")
+    public Map<String, Object> addToBlackList(@RequestParam String ip, 
+                                              @RequestParam(required = false) Long expireSeconds) {
+        try {
+            String key = "gateway:blacklist:" + ip;
+            if (expireSeconds != null && expireSeconds > 0) {
+                redisTemplate.opsForValue().set(key, "1", expireSeconds, TimeUnit.SECONDS);
+            } else {
+                redisTemplate.opsForValue().set(key, "1");
+            }
+            
+            log.info("添加IP到黑名单成功 - IP: {}, 过期时间: {}", ip, expireSeconds);
+            return ResponseUtils.buildSuccessResponse("添加成功");
+        } catch (Exception e) {
+            log.error("添加IP到黑名单失败 - IP: {}", ip, e);
+            return ResponseUtils.buildErrorResponse(500, "添加失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 从黑名单中移除IP
+     */
+    @DeleteMapping("/blacklist/remove")
+    public Map<String, Object> removeFromBlackList(@RequestParam String ip) {
+        try {
+            String key = "gateway:blacklist:" + ip;
+            Boolean result = redisTemplate.delete(key);
+            
+            log.info("从黑名单中移除IP成功 - IP: {}, 结果: {}", ip, result);
+            return ResponseUtils.buildSuccessResponse("移除成功");
+        } catch (Exception e) {
+            log.error("从黑名单中移除IP失败 - IP: {}", ip, e);
+            return ResponseUtils.buildErrorResponse(500, "移除失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 查询IP是否在黑名单中
+     */
+    @GetMapping("/blacklist/check")
+    public Map<String, Object> checkBlackList(@RequestParam String ip) {
+        try {
+            String key = "gateway:blacklist:" + ip;
+            Boolean exists = redisTemplate.hasKey(key);
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("ip", ip);
+            data.put("inBlacklist", exists != null && exists);
+            
+            return ResponseUtils.buildSuccessResponse(data);
+        } catch (Exception e) {
+            log.error("查询IP黑名单状态失败 - IP: {}", ip, e);
+            return ResponseUtils.buildErrorResponse(500, "查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 添加IP到白名单
+     */
+    @PostMapping("/whitelist/add")
+    public Map<String, Object> addToWhiteList(@RequestParam String ip, 
+                                              @RequestParam(required = false) Long expireSeconds) {
+        try {
+            String key = "gateway:whitelist:" + ip;
+            if (expireSeconds != null && expireSeconds > 0) {
+                redisTemplate.opsForValue().set(key, "1", expireSeconds, TimeUnit.SECONDS);
+            } else {
+                redisTemplate.opsForValue().set(key, "1");
+            }
+            
+            log.info("添加IP到白名单成功 - IP: {}, 过期时间: {}", ip, expireSeconds);
+            return ResponseUtils.buildSuccessResponse("添加成功");
+        } catch (Exception e) {
+            log.error("添加IP到白名单失败 - IP: {}", ip, e);
+            return ResponseUtils.buildErrorResponse(500, "添加失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 从白名单中移除IP
+     */
+    @DeleteMapping("/whitelist/remove")
+    public Map<String, Object> removeFromWhiteList(@RequestParam String ip) {
+        try {
+            String key = "gateway:whitelist:" + ip;
+            Boolean result = redisTemplate.delete(key);
+            
+            log.info("从白名单中移除IP成功 - IP: {}, 结果: {}", ip, result);
+            return ResponseUtils.buildSuccessResponse("移除成功");
+        } catch (Exception e) {
+            log.error("从白名单中移除IP失败 - IP: {}", ip, e);
+            return ResponseUtils.buildErrorResponse(500, "移除失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 查询IP是否在白名单中
+     */
+    @GetMapping("/whitelist/check")
+    public Map<String, Object> checkWhiteList(@RequestParam String ip) {
+        try {
+            String key = "gateway:whitelist:" + ip;
+            Boolean exists = redisTemplate.hasKey(key);
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("ip", ip);
+            data.put("inWhitelist", exists != null && exists);
+            
+            return ResponseUtils.buildSuccessResponse(data);
+        } catch (Exception e) {
+            log.error("查询IP白名单状态失败 - IP: {}", ip, e);
+            return ResponseUtils.buildErrorResponse(500, "查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取黑白名单统计信息
+     */
+    @GetMapping("/statistics")
+    public Map<String, Object> getStatistics() {
+        try {
+            // 这里可以实现更复杂的统计逻辑
+            // 目前返回基础信息
+            Map<String, Object> data = new HashMap<>();
+            data.put("blacklistEnabled", securityProperties.getBlackWhiteList().isEnabled());
+            data.put("whitelistMode", securityProperties.getBlackWhiteList().isWhiteListMode());
+            data.put("staticBlackIps", securityProperties.getBlackWhiteList().getBlackIps() != null ? 
+                    securityProperties.getBlackWhiteList().getBlackIps().size() : 0);
+            data.put("staticWhiteIps", securityProperties.getBlackWhiteList().getWhiteIps() != null ? 
+                    securityProperties.getBlackWhiteList().getWhiteIps().size() : 0);
+            
+            return ResponseUtils.buildSuccessResponse(data);
+        } catch (Exception e) {
+            log.error("获取黑白名单统计信息失败", e);
+            return ResponseUtils.buildErrorResponse(500, "获取统计信息失败: " + e.getMessage());
+        }
+    }
+}

+ 150 - 0
alien-gateway/src/main/java/shop/alien/gateway/controller/RateLimitController.java

@@ -0,0 +1,150 @@
+package shop.alien.gateway.controller;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+import shop.alien.gateway.util.ResponseUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 限流管理控制器
+ * 提供限流相关管理接口
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@RestController
+@RequestMapping("/gateway/admin/rate-limit")
+public class RateLimitController {
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    /**
+     * 获取限流统计信息
+     */
+    @GetMapping("/statistics")
+    public Map<String, Object> getStatistics() {
+        try {
+            Map<String, Object> data = new HashMap<>();
+            
+            // 全局限流统计
+            data.put("globalRateLimitEnabled", securityProperties.getGlobalRateLimit().isEnabled());
+            data.put("globalRateLimitDefaultLimit", securityProperties.getGlobalRateLimit().getDefaultLimit());
+            data.put("globalRateLimitDefaultWindow", securityProperties.getGlobalRateLimit().getDefaultWindow());
+            
+            // IP限流统计
+            data.put("ipRateLimitEnabled", securityProperties.getIpRateLimit().isEnabled());
+            data.put("ipRateLimitDefaultLimit", securityProperties.getIpRateLimit().getDefaultLimit());
+            data.put("ipRateLimitDefaultWindow", securityProperties.getIpRateLimit().getDefaultWindow());
+            
+            // 用户限流统计
+            data.put("userRateLimitEnabled", securityProperties.getUserRateLimit().isEnabled());
+            data.put("userRateLimitDefaultLimit", securityProperties.getUserRateLimit().getDefaultLimit());
+            data.put("userRateLimitDefaultWindow", securityProperties.getUserRateLimit().getDefaultWindow());
+            
+            // 接口限流统计
+            data.put("apiRateLimitEnabled", securityProperties.getApiRateLimit().isEnabled());
+            data.put("apiRateLimitDefaultLimit", securityProperties.getApiRateLimit().getDefaultLimit());
+            data.put("apiRateLimitDefaultWindow", securityProperties.getApiRateLimit().getDefaultWindow());
+            
+            return ResponseUtils.buildSuccessResponse(data);
+        } catch (Exception e) {
+            log.error("获取限流统计信息失败", e);
+            return ResponseUtils.buildErrorResponse(500, "获取统计信息失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 清理限流缓存
+     */
+    @DeleteMapping("/cache/clear")
+    public Map<String, Object> clearRateLimitCache(@RequestParam(required = false) String type,
+                                                   @RequestParam(required = false) String key) {
+        try {
+            String pattern = null;
+            
+            if ("global".equals(type)) {
+                pattern = "gateway:global:limit:*";
+            } else if ("ip".equals(type)) {
+                pattern = "gateway:ip:limit:*";
+            } else if ("user".equals(type)) {
+                pattern = "gateway:user:limit:*";
+            } else if ("api".equals(type)) {
+                pattern = "gateway:api:limit:*";
+            } else if (key != null) {
+                // 清理指定的key
+                Boolean result = redisTemplate.delete(key);
+                log.info("清理限流缓存成功 - Key: {}, 结果: {}", key, result);
+                return ResponseUtils.buildSuccessResponse("清理成功");
+            }
+            
+            if (pattern != null) {
+                Set<String> keys = redisTemplate.keys(pattern);
+                if (keys != null && !keys.isEmpty()) {
+                    Long deletedCount = redisTemplate.delete(keys);
+                    log.info("清理限流缓存成功 - Pattern: {}, 清理数量: {}", pattern, deletedCount);
+                    return ResponseUtils.buildSuccessResponse("清理成功,共清理 " + deletedCount + " 个key");
+                }
+            }
+            
+            return ResponseUtils.buildSuccessResponse("没有需要清理的缓存");
+        } catch (Exception e) {
+            log.error("清理限流缓存失败", e);
+            return ResponseUtils.buildErrorResponse(500, "清理缓存失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取限流配置详情
+     */
+    @GetMapping("/config")
+    public Map<String, Object> getRateLimitConfig() {
+        try {
+            Map<String, Object> data = new HashMap<>();
+            data.put("globalRateLimit", securityProperties.getGlobalRateLimit());
+            data.put("ipRateLimit", securityProperties.getIpRateLimit());
+            data.put("userRateLimit", securityProperties.getUserRateLimit());
+            data.put("apiRateLimit", securityProperties.getApiRateLimit());
+            
+            return ResponseUtils.buildSuccessResponse(data);
+        } catch (Exception e) {
+            log.error("获取限流配置详情失败", e);
+            return ResponseUtils.buildErrorResponse(500, "获取配置失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 检查指定key的限流状态
+     */
+    @GetMapping("/check")
+    public Map<String, Object> checkRateLimitStatus(@RequestParam String key) {
+        try {
+            // 获取当前计数
+            Object count = redisTemplate.opsForValue().get(key);
+            // 获取过期时间
+            Long expire = redisTemplate.getExpire(key);
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("key", key);
+            data.put("currentCount", count);
+            data.put("expireSeconds", expire);
+            data.put("hasKey", redisTemplate.hasKey(key));
+            
+            return ResponseUtils.buildSuccessResponse(data);
+        } catch (Exception e) {
+            log.error("检查限流状态失败 - Key: {}", key, e);
+            return ResponseUtils.buildErrorResponse(500, "检查状态失败: " + e.getMessage());
+        }
+    }
+}

+ 77 - 0
alien-gateway/src/main/java/shop/alien/gateway/entity/BlackWhiteListEntry.java

@@ -0,0 +1,77 @@
+package shop.alien.gateway.entity;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 黑白名单实体
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Data
+public class BlackWhiteListEntry implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * IP地址
+     */
+    private String ip;
+
+    /**
+     * 类型:BLACKLIST, WHITELIST
+     */
+    private String type;
+
+    /**
+     * 原因
+     */
+    private String reason;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 过期时间(null表示永不过期)
+     */
+    private Date expireTime;
+
+    /**
+     * 操作者
+     */
+    private String operator;
+
+    /**
+     * 是否启用
+     */
+    private boolean enabled;
+
+    /**
+     * 路径模式(可选)
+     */
+    private String pathPattern;
+
+    public BlackWhiteListEntry() {}
+
+    public BlackWhiteListEntry(String ip, String type, String reason, String operator) {
+        this.ip = ip;
+        this.type = type;
+        this.reason = reason;
+        this.operator = operator;
+        this.createTime = new Date();
+        this.enabled = true;
+    }
+
+    public boolean isExpired() {
+        if (expireTime == null) {
+            return false;
+        }
+        return new Date().after(expireTime);
+    }
+}

+ 63 - 0
alien-gateway/src/main/java/shop/alien/gateway/entity/RateLimitResponse.java

@@ -0,0 +1,63 @@
+package shop.alien.gateway.entity;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 限流响应实体
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Data
+public class RateLimitResponse implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 是否触发限流
+     */
+    private boolean limited;
+
+    /**
+     * 当前计数
+     */
+    private long currentCount;
+
+    /**
+     * 限制阈值
+     */
+    private long limit;
+
+    /**
+     * 剩余时间(秒)
+     */
+    private long remainingSeconds;
+
+    /**
+     * 消息
+     */
+    private String message;
+
+    /**
+     * 限流类型
+     */
+    private String rateLimitType;
+
+    /**
+     * 限流key
+     */
+    private String rateLimitKey;
+
+    public RateLimitResponse() {}
+
+    public RateLimitResponse(boolean limited, long currentCount, long limit, long remainingSeconds, String message) {
+        this.limited = limited;
+        this.currentCount = currentCount;
+        this.limit = limit;
+        this.remainingSeconds = remainingSeconds;
+        this.message = message;
+    }
+}

+ 182 - 0
alien-gateway/src/main/java/shop/alien/gateway/filter/ApiRateLimitFilter.java

@@ -0,0 +1,182 @@
+package shop.alien.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+import shop.alien.gateway.util.IpUtils;
+import shop.alien.gateway.util.ResponseUtils;
+
+import java.util.Collections;
+
+/**
+ * 全局过滤器 - 接口限流
+ * 基于API接口维度进行限流,可以针对不同的接口设置不同的限流策略
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Component
+public class ApiRateLimitFilter implements GlobalFilter, Ordered {
+
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private DefaultRedisScript<Long> apiRateLimitScript;
+
+    /**
+     * 过滤器核心方法,实现基于API接口的限流逻辑
+     * @param exchange 服务交换对象,包含请求和响应信息
+     * @param chain 过滤器链,用于传递请求到下一个过滤器
+     * @return Mono<Void> 异步响应结果
+     */
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        // 检查接口限流功能是否启用,如果未启用则直接放行
+        if (!securityProperties.getApiRateLimit().isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 获取请求路径和HTTP方法,用于构建限流key
+        String path = exchange.getRequest().getURI().getPath();
+        String method = exchange.getRequest().getMethod().name();
+
+        // 获取当前接口的限流配置
+        GatewaySecurityProperties.RateLimitConfig config = getRateLimitConfig(path, method);
+        // 如果配置不存在或未启用,则直接放行
+        if (config == null || !config.isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 构建限流key,用于在Redis中记录该接口的访问次数
+        String rateLimitKey = buildRateLimitKey(path, method);
+        
+        // 记录调试日志,显示接口限流检查信息
+        log.debug("接口限流检查 - Path: {}, Method: {}, Key: {}", path, method, rateLimitKey);
+
+        try {
+            // 执行Lua脚本进行接口限流检查,Lua脚本保证了原子性
+            Long result = redisTemplate.execute(
+                apiRateLimitScript,
+                Collections.singletonList(rateLimitKey),
+                String.valueOf(config.getLimit()),
+                String.valueOf(config.getWindow())
+            );
+
+            // 如果返回结果为null或0,说明触发了限流
+            if (result == null || result == 0L) {
+                // 记录警告日志,显示接口限流触发信息
+                log.warn("接口限流触发 - Path: {}, Method: {}", path, method);
+                // 返回限流响应
+                return handleRateLimit(exchange, "接口请求过于频繁,请稍后再试");
+            }
+
+            // 记录调试日志,显示接口限流通过信息
+            log.debug("接口限流通过 - Path: {}, Method: {}", path, method);
+            // 接口限流检查通过,传递请求到下一个过滤器
+            return chain.filter(exchange);
+
+        } catch (Exception e) {
+            // 记录错误日志,显示接口限流执行异常
+            log.error("接口限流执行异常 - Path: {}, Method: {}", path, method, e);
+            // 限流失效时,允许请求通过(降级策略)
+            return chain.filter(exchange);
+        }
+    }
+
+    /**
+     * 获取接口限流配置
+     * @param path 请求路径
+     * @param method HTTP方法
+     * @return GatewaySecurityProperties.RateLimitConfig 限流配置
+     */
+    private GatewaySecurityProperties.RateLimitConfig getRateLimitConfig(String path, String method) {
+        // 检查路径特定的配置
+        if (securityProperties.getApiRateLimit().getPathConfigs() != null) {
+            // 遍历所有路径特定的配置
+            for (String pattern : securityProperties.getApiRateLimit().getPathConfigs().keySet()) {
+                // 如果路径匹配,则返回对应的配置
+                if (pathMatches(path, pattern)) {
+                    return securityProperties.getApiRateLimit().getPathConfigs().get(pattern);
+                }
+            }
+        }
+
+        // 检查方法特定的配置
+        if (securityProperties.getApiRateLimit().getMethodConfigs() != null) {
+            // 获取特定HTTP方法的配置
+            GatewaySecurityProperties.RateLimitConfig config = securityProperties.getApiRateLimit().getMethodConfigs().get(method);
+            if (config != null) {
+                return config;
+            }
+        }
+
+        // 使用默认配置
+        GatewaySecurityProperties.RateLimitConfig defaultConfig = new GatewaySecurityProperties.RateLimitConfig();
+        defaultConfig.setEnabled(true);
+        defaultConfig.setLimit(securityProperties.getApiRateLimit().getDefaultLimit());
+        defaultConfig.setWindow(securityProperties.getApiRateLimit().getDefaultWindow());
+        return defaultConfig;
+    }
+
+    /**
+     * 构建限流key
+     * @param path 请求路径
+     * @param method HTTP方法
+     * @return String 限流key
+     */
+    private String buildRateLimitKey(String path, String method) {
+        // 构建格式化的限流key,包含方法和路径信息
+        return String.format("gateway:api:limit:%s:%s", method, path);
+    }
+
+    /**
+     * 路径匹配检查(支持通配符)
+     * @param path 请求路径
+     * @param pattern 路径模式
+     * @return boolean 是否匹配
+     */
+    private boolean pathMatches(String path, String pattern) {
+        // 如果模式包含通配符,则转换为正则表达式进行匹配
+        if (pattern.contains("*")) {
+            String regex = pattern.replace("*", ".*");
+            return path.matches(regex);
+        }
+        // 否则直接比较路径是否相等
+        return path.equals(pattern);
+    }
+
+    /**
+     * 处理限流响应
+     * @param exchange 服务交换对象
+     * @param message 限流提示信息
+     * @return Mono<Void> 异步响应结果
+     */
+    private Mono<Void> handleRateLimit(ServerWebExchange exchange, String message) {
+        // 返回429 Too Many Requests错误响应
+        return ResponseUtils.writeErrorResponse(exchange, HttpStatus.TOO_MANY_REQUESTS, 429, message);
+    }
+
+    /**
+     * 获取过滤器执行顺序
+     * @return int 执行顺序,数值越小优先级越高
+     */
+    @Override
+    public int getOrder() {
+        return -60; // 在用户限流之后执行
+    }
+}

+ 191 - 0
alien-gateway/src/main/java/shop/alien/gateway/filter/BlackWhiteListFilter.java

@@ -0,0 +1,191 @@
+package shop.alien.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+import shop.alien.gateway.util.IpUtils;
+import shop.alien.gateway.util.ResponseUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 全局过滤器 - 黑白名单
+ * 在请求处理的最早阶段进行IP黑白名单检查
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Component
+public class BlackWhiteListFilter implements GlobalFilter, Ordered {
+
+    @Qualifier("gateway.security-shop.alien.gateway.config.GatewaySecurityProperties")
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    /**
+     * 过滤器核心方法,处理每个进入网关的请求
+     * @param exchange 服务交换对象,包含请求和响应信息
+     * @param chain 过滤器链,用于传递请求到下一个过滤器
+     * @return Mono<Void> 异步响应结果
+     */
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        // 检查黑白名单功能是否启用,如果未启用则直接放行
+        if (!securityProperties.getBlackWhiteList().isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 获取客户端IP地址,用于黑白名单检查
+        String ip = IpUtils.getClientIp(exchange);
+        // 获取请求路径,用于路径特定的黑白名单检查
+        String path = exchange.getRequest().getURI().getPath();
+
+        // 记录调试日志,显示当前请求的IP和路径
+        log.debug("黑白名单检查 - IP: {}, Path: {}", ip, path);
+
+        // 检查IP是否在黑名单中,如果在黑名单中则拒绝请求
+        if (isInBlackList(ip, path)) {
+            // 记录警告日志,显示被黑名单拦截的请求
+            log.warn("IP在黑名单中被拦截 - IP: {}, Path: {}", ip, path);
+            // 返回拒绝响应
+            return handleReject(exchange, "访问被拒绝 - IP在黑名单中");
+        }
+
+        // 如果启用了白名单模式,则检查IP是否在白名单中
+        if (securityProperties.getBlackWhiteList().isWhiteListMode()) {
+            // 检查IP是否在白名单中,如果不在白名单中则拒绝请求
+            if (!isInWhiteList(ip, path)) {
+                // 记录警告日志,显示被白名单拦截的请求
+                log.warn("IP不在白名单中被拦截 - IP: {}, Path: {}", ip, path);
+                // 返回拒绝响应
+                return handleReject(exchange, "访问被拒绝 - IP不在白名单中");
+            }
+        }
+
+        // 如果通过黑白名单检查,则传递请求到下一个过滤器
+        return chain.filter(exchange);
+    }
+
+    /**
+     * 检查IP是否在黑名单中
+     * @param ip 客户端IP地址
+     * @param path 请求路径
+     * @return boolean 是否在黑名单中
+     */
+    private boolean isInBlackList(String ip, String path) {
+        // 检查配置文件中定义的静态黑名单IP列表
+        if (securityProperties.getBlackWhiteList().getBlackIps() != null) {
+            // 如果当前IP在静态黑名单中,则返回true
+            if (securityProperties.getBlackWhiteList().getBlackIps().contains(ip)) {
+                return true;
+            }
+        }
+
+        // 检查路径特定的黑名单配置
+        if (securityProperties.getBlackWhiteList().getPathBlackIps() != null) {
+            // 遍历所有路径特定的黑名单配置
+            for (Map.Entry<String, List<String>> entry : securityProperties.getBlackWhiteList().getPathBlackIps().entrySet()) {
+                String pattern = entry.getKey();
+                // 检查当前路径是否匹配配置的路径模式,并且IP在该路径的黑名单中
+                if (pathMatches(path, pattern) && entry.getValue().contains(ip)) {
+                    return true;
+                }
+            }
+        }
+
+        // 检查Redis中的动态黑名单
+        String blackKey = "gateway:blacklist:" + ip;
+        Boolean exists = redisTemplate.hasKey(blackKey);
+        // 如果Redis中存在该IP的黑名单记录,则返回true
+        return exists != null && exists;
+    }
+
+    /**
+     * 检查IP是否在白名单中
+     * @param ip 客户端IP地址
+     * @param path 请求路径
+     * @return boolean 是否在白名单中
+     */
+    private boolean isInWhiteList(String ip, String path) {
+        // 检查配置文件中定义的静态白名单IP列表
+        if (securityProperties.getBlackWhiteList().getWhiteIps() != null) {
+            // 如果当前IP在静态白名单中,则返回true
+            if (securityProperties.getBlackWhiteList().getWhiteIps().contains(ip)) {
+                return true;
+            }
+        }
+
+        // 检查路径特定的白名单配置
+        if (securityProperties.getBlackWhiteList().getPathWhiteIps() != null) {
+            // 遍历所有路径特定的白名单配置
+            for (Map.Entry<String, List<String>> entry : securityProperties.getBlackWhiteList().getPathWhiteIps().entrySet()) {
+                String pattern = entry.getKey();
+                // 检查当前路径是否匹配配置的路径模式,并且IP在该路径的白名单中
+                if (pathMatches(path, pattern) && entry.getValue().contains(ip)) {
+                    return true;
+                }
+            }
+        }
+
+        // 检查Redis中的动态白名单
+        String whiteKey = "gateway:whitelist:" + ip;
+        Boolean exists = redisTemplate.hasKey(whiteKey);
+        // 如果Redis中存在该IP的白名单记录,则返回true
+        return exists != null && exists;
+    }
+
+    /**
+     * 路径匹配检查(支持通配符)
+     * @param path 请求路径
+     * @param pattern 路径模式
+     * @return boolean 是否匹配
+     */
+    private boolean pathMatches(String path, String pattern) {
+        // 简单的通配符匹配,如果模式包含*,则替换为正则表达式
+        if (pattern.contains("*")) {
+            String regex = pattern.replace("*", ".*");
+            return path.matches(regex);
+        }
+        // 如果不包含通配符,则直接比较路径是否相等
+        return path.equals(pattern);
+    }
+
+    /**
+     * 处理被拒绝的请求
+     * @param exchange 服务交换对象
+     * @param message 拒绝原因消息
+     * @return Mono<Void> 异步响应结果
+     */
+    private Mono<Void> handleReject(ServerWebExchange exchange, String message) {
+        // 使用ResponseUtils工具类返回403 Forbidden错误响应
+        return ResponseUtils.writeErrorResponse(exchange, HttpStatus.FORBIDDEN, 403, message);
+    }
+
+    /**
+     * 获取过滤器执行顺序
+     * @return int 执行顺序,数值越小优先级越高
+     */
+    @Override
+    public int getOrder() {
+        return -100; // 最高优先级,最先执行黑白名单检查
+    }
+}

+ 181 - 0
alien-gateway/src/main/java/shop/alien/gateway/filter/GlobalRateLimitFilter.java

@@ -0,0 +1,181 @@
+package shop.alien.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+import shop.alien.gateway.util.IpUtils;
+import shop.alien.gateway.util.ResponseUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 全局过滤器 - 全局限流
+ * 基于Redis Lua脚本实现高性能限流
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Component
+public class GlobalRateLimitFilter implements GlobalFilter, Ordered {
+
+    @Qualifier("gateway.security-shop.alien.gateway.config.GatewaySecurityProperties")
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private DefaultRedisScript<Long> globalRateLimitScript;
+
+    /**
+     * 过滤器核心方法,实现全局限流逻辑
+     * @param exchange 服务交换对象,包含请求和响应信息
+     * @param chain 过滤器链,用于传递请求到下一个过滤器
+     * @return Mono<Void> 异步响应结果
+     */
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        // 检查全局限流功能是否启用,如果未启用则直接放行
+        if (!securityProperties.getGlobalRateLimit().isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 获取请求路径和HTTP方法,用于构建限流key
+        String path = exchange.getRequest().getURI().getPath();
+        String method = exchange.getRequest().getMethod().name();
+        
+        // 获取当前路径的限流配置
+        GatewaySecurityProperties.PathConfig config = getPathConfig(path);
+        // 如果配置不存在或未启用,则直接放行
+        if (config == null || !config.isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 构建限流key,用于在Redis中记录访问次数
+        String rateLimitKey = buildRateLimitKey(path, method);
+        
+        // 记录调试日志,显示限流检查信息
+        log.debug("全局限流检查 - Path: {}, Method: {}, Key: {}", path, method, rateLimitKey);
+
+        try {
+            // 执行Lua脚本进行限流检查,Lua脚本保证了原子性
+            Long result = redisTemplate.execute(
+                globalRateLimitScript,
+                Collections.singletonList(rateLimitKey),
+                String.valueOf(config.getLimit()),
+                String.valueOf(config.getWindow())
+            );
+
+            // 如果返回结果为null或0,说明触发了限流
+            if (result == null || result == 0L) {
+                // 记录警告日志,显示限流触发信息
+                log.warn("全局限流触发 - Path: {}, Method: {}", path, method);
+                // 返回限流响应
+                return handleRateLimit(exchange, "系统繁忙,请稍后再试");
+            }
+
+            // 记录调试日志,显示限流通过信息
+            log.debug("全局限流通过 - Path: {}, Method: {}", path, method);
+            // 限流检查通过,传递请求到下一个过滤器
+            return chain.filter(exchange);
+
+        } catch (Exception e) {
+            // 记录错误日志,显示限流执行异常
+            log.error("全局限流执行异常 - Path: {}, Method: {}", path, method, e);
+            // 限流失效时,允许请求通过(降级策略)
+            return chain.filter(exchange);
+        }
+    }
+
+    /**
+     * 获取路径限流配置
+     * @param path 请求路径
+     * @return GatewaySecurityProperties.PathConfig 路径配置
+     */
+    private GatewaySecurityProperties.PathConfig getPathConfig(String path) {
+        // 检查是否有路径特定的配置
+        if (securityProperties.getGlobalRateLimit().getPathConfigs() != null) {
+            // 精确匹配路径配置
+            GatewaySecurityProperties.PathConfig config = securityProperties.getGlobalRateLimit().getPathConfigs().get(path);
+            if (config != null) {
+                return config;
+            }
+
+            // 通配符匹配路径配置
+            for (String pattern : securityProperties.getGlobalRateLimit().getPathConfigs().keySet()) {
+                if (pathMatches(path, pattern)) {
+                    return securityProperties.getGlobalRateLimit().getPathConfigs().get(pattern);
+                }
+            }
+        }
+
+        // 使用默认配置
+        GatewaySecurityProperties.PathConfig defaultConfig = new GatewaySecurityProperties.PathConfig();
+        defaultConfig.setEnabled(true);
+        defaultConfig.setLimit(securityProperties.getGlobalRateLimit().getDefaultLimit());
+        defaultConfig.setWindow(securityProperties.getGlobalRateLimit().getDefaultWindow());
+        return defaultConfig;
+    }
+
+    /**
+     * 构建限流key
+     * @param path 请求路径
+     * @param method HTTP方法
+     * @return String 限流key
+     */
+    private String buildRateLimitKey(String path, String method) {
+        // 构建格式化的限流key,包含方法和路径信息
+        return String.format("gateway:global:limit:%s:%s", method, path);
+    }
+
+    /**
+     * 路径匹配检查(支持通配符)
+     * @param path 请求路径
+     * @param pattern 路径模式
+     * @return boolean 是否匹配
+     */
+    private boolean pathMatches(String path, String pattern) {
+        // 如果模式包含通配符,则转换为正则表达式进行匹配
+        if (pattern.contains("*")) {
+            String regex = pattern.replace("*", ".*");
+            return path.matches(regex);
+        }
+        // 否则直接比较路径是否相等
+        return path.equals(pattern);
+    }
+
+    /**
+     * 处理限流响应
+     * @param exchange 服务交换对象
+     * @param message 限流提示信息
+     * @return Mono<Void> 异步响应结果
+     */
+    private Mono<Void> handleRateLimit(ServerWebExchange exchange, String message) {
+        // 返回429 Too Many Requests错误响应
+        return ResponseUtils.writeErrorResponse(exchange, HttpStatus.TOO_MANY_REQUESTS, 429, message);
+    }
+
+    /**
+     * 获取过滤器执行顺序
+     * @return int 执行顺序,数值越小优先级越高
+     */
+    @Override
+    public int getOrder() {
+        return -90; // 高优先级,在黑白名单之后执行
+    }
+}

+ 185 - 0
alien-gateway/src/main/java/shop/alien/gateway/filter/IpRateLimitFilter.java

@@ -0,0 +1,185 @@
+package shop.alien.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+import shop.alien.gateway.util.IpUtils;
+import shop.alien.gateway.util.ResponseUtils;
+
+import java.util.Collections;
+
+/**
+ * 全局过滤器 - IP限流
+ * 基于IP维度进行限流,防止单个IP恶意请求
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Component
+public class IpRateLimitFilter implements GlobalFilter, Ordered {
+
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private DefaultRedisScript<Long> ipRateLimitScript;
+
+    /**
+     * 过滤器核心方法,实现基于IP的限流逻辑
+     * @param exchange 服务交换对象,包含请求和响应信息
+     * @param chain 过滤器链,用于传递请求到下一个过滤器
+     * @return Mono<Void> 异步响应结果
+     */
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        // 检查IP限流功能是否启用,如果未启用则直接放行
+        if (!securityProperties.getIpRateLimit().isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 获取客户端IP地址,用于IP维度限流
+        String ip = IpUtils.getClientIp(exchange);
+        // 获取请求路径和HTTP方法,用于构建限流key
+        String path = exchange.getRequest().getURI().getPath();
+        String method = exchange.getRequest().getMethod().name();
+
+        // 获取当前IP的限流配置
+        GatewaySecurityProperties.RateLimitConfig config = getRateLimitConfig(ip, path);
+        // 如果配置不存在或未启用,则直接放行
+        if (config == null || !config.isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 构建限流key,用于在Redis中记录该IP的访问次数
+        String rateLimitKey = buildRateLimitKey(ip, path, method);
+        
+        // 记录调试日志,显示IP限流检查信息
+        log.debug("IP限流检查 - IP: {}, Path: {}, Method: {}, Key: {}", ip, path, method, rateLimitKey);
+
+        try {
+            // 执行Lua脚本进行IP限流检查,Lua脚本保证了原子性
+            Long result = redisTemplate.execute(
+                ipRateLimitScript,
+                Collections.singletonList(rateLimitKey),
+                String.valueOf(config.getLimit()),
+                String.valueOf(config.getWindow())
+            );
+
+            // 如果返回结果为null或0,说明触发了限流
+            if (result == null || result == 0L) {
+                // 记录警告日志,显示IP限流触发信息
+                log.warn("IP限流触发 - IP: {}, Path: {}, Method: {}", ip, path, method);
+                // 返回限流响应
+                return handleRateLimit(exchange, "请求过于频繁,请稍后再试");
+            }
+
+            // 记录调试日志,显示IP限流通过信息
+            log.debug("IP限流通过 - IP: {}, Path: {}, Method: {}", ip, path, method);
+            // IP限流检查通过,传递请求到下一个过滤器
+            return chain.filter(exchange);
+
+        } catch (Exception e) {
+            // 记录错误日志,显示IP限流执行异常
+            log.error("IP限流执行异常 - IP: {}, Path: {}, Method: {}", ip, path, method, e);
+            // 限流失效时,允许请求通过(降级策略)
+            return chain.filter(exchange);
+        }
+    }
+
+    /**
+     * 获取IP限流配置
+     * @param ip 客户端IP地址
+     * @param path 请求路径
+     * @return GatewaySecurityProperties.RateLimitConfig 限流配置
+     */
+    private GatewaySecurityProperties.RateLimitConfig getRateLimitConfig(String ip, String path) {
+        // 检查是否有IP特定的配置
+        if (securityProperties.getIpRateLimit().getIpConfigs() != null) {
+            // 获取特定IP的配置
+            GatewaySecurityProperties.RateLimitConfig config = securityProperties.getIpRateLimit().getIpConfigs().get(ip);
+            if (config != null) {
+                return config;
+            }
+        }
+
+        // 检查路径特定的配置
+        if (securityProperties.getIpRateLimit().getPathConfigs() != null) {
+            // 遍历所有路径特定的配置
+            for (String pattern : securityProperties.getIpRateLimit().getPathConfigs().keySet()) {
+                // 如果路径匹配,则返回对应的配置
+                if (pathMatches(path, pattern)) {
+                    return securityProperties.getIpRateLimit().getPathConfigs().get(pattern);
+                }
+            }
+        }
+
+        // 使用默认配置
+        GatewaySecurityProperties.RateLimitConfig defaultConfig = new GatewaySecurityProperties.RateLimitConfig();
+        defaultConfig.setEnabled(true);
+        defaultConfig.setLimit(securityProperties.getIpRateLimit().getDefaultLimit());
+        defaultConfig.setWindow(securityProperties.getIpRateLimit().getDefaultWindow());
+        return defaultConfig;
+    }
+
+    /**
+     * 构建限流key
+     * @param ip 客户端IP地址
+     * @param path 请求路径
+     * @param method HTTP方法
+     * @return String 限流key
+     */
+    private String buildRateLimitKey(String ip, String path, String method) {
+        // 构建格式化的限流key,包含IP、方法和路径信息
+        return String.format("gateway:ip:limit:%s:%s:%s", ip, method, path);
+    }
+
+    /**
+     * 路径匹配检查(支持通配符)
+     * @param path 请求路径
+     * @param pattern 路径模式
+     * @return boolean 是否匹配
+     */
+    private boolean pathMatches(String path, String pattern) {
+        // 如果模式包含通配符,则转换为正则表达式进行匹配
+        if (pattern.contains("*")) {
+            String regex = pattern.replace("*", ".*");
+            return path.matches(regex);
+        }
+        // 否则直接比较路径是否相等
+        return path.equals(pattern);
+    }
+
+    /**
+     * 处理限流响应
+     * @param exchange 服务交换对象
+     * @param message 限流提示信息
+     * @return Mono<Void> 异步响应结果
+     */
+    private Mono<Void> handleRateLimit(ServerWebExchange exchange, String message) {
+        // 返回429 Too Many Requests错误响应
+        return ResponseUtils.writeErrorResponse(exchange, HttpStatus.TOO_MANY_REQUESTS, 429, message);
+    }
+
+    /**
+     * 获取过滤器执行顺序
+     * @return int 执行顺序,数值越小优先级越高
+     */
+    @Override
+    public int getOrder() {
+        return -80; // 在全局限流之后执行
+    }
+}

+ 215 - 0
alien-gateway/src/main/java/shop/alien/gateway/filter/UserRateLimitFilter.java

@@ -0,0 +1,215 @@
+package shop.alien.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.core.Ordered;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+import shop.alien.gateway.util.IpUtils;
+import shop.alien.gateway.util.JwtUtils;
+import shop.alien.gateway.util.ResponseUtils;
+
+import java.util.Collections;
+
+/**
+ * 全局过滤器 - 用户限流
+ * 基于用户维度进行限流,需要用户已认证
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Component
+public class UserRateLimitFilter implements GlobalFilter, Ordered {
+
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private DefaultRedisScript<Long> userRateLimitScript;
+
+    @Autowired
+    private JwtUtils jwtUtils;
+
+    /**
+     * 过滤器核心方法,实现基于用户的限流逻辑
+     * @param exchange 服务交换对象,包含请求和响应信息
+     * @param chain 过滤器链,用于传递请求到下一个过滤器
+     * @return Mono<Void> 异步响应结果
+     */
+    @Override
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+        // 检查用户限流功能是否启用,如果未启用则直接放行
+        if (!securityProperties.getUserRateLimit().isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 获取用户信息,用于用户维度限流
+        String userId = getUserId(exchange);
+        // 如果用户未认证,则跳过用户限流
+        if (userId == null) {
+            return chain.filter(exchange);
+        }
+
+        // 获取请求路径和HTTP方法,用于构建限流key
+        String path = exchange.getRequest().getURI().getPath();
+        String method = exchange.getRequest().getMethod().name();
+
+        // 获取当前用户的限流配置
+        GatewaySecurityProperties.RateLimitConfig config = getRateLimitConfig(userId, path);
+        // 如果配置不存在或未启用,则直接放行
+        if (config == null || !config.isEnabled()) {
+            return chain.filter(exchange);
+        }
+
+        // 构建限流key,用于在Redis中记录该用户的访问次数
+        String rateLimitKey = buildRateLimitKey(userId, path, method);
+        
+        // 记录调试日志,显示用户限流检查信息
+        log.debug("用户限流检查 - UserId: {}, Path: {}, Method: {}, Key: {}", userId, path, method, rateLimitKey);
+
+        try {
+            // 执行Lua脚本进行用户限流检查,Lua脚本保证了原子性
+            Long result = redisTemplate.execute(
+                userRateLimitScript,
+                Collections.singletonList(rateLimitKey),
+                String.valueOf(config.getLimit()),
+                String.valueOf(config.getWindow())
+            );
+
+            // 如果返回结果为null或0,说明触发了限流
+            if (result == null || result == 0L) {
+                // 记录警告日志,显示用户限流触发信息
+                log.warn("用户限流触发 - UserId: {}, Path: {}, Method: {}", userId, path, method);
+                // 返回限流响应
+                return handleRateLimit(exchange, "请求过于频繁,请稍后再试");
+            }
+
+            // 记录调试日志,显示用户限流通过信息
+            log.debug("用户限流通过 - UserId: {}, Path: {}, Method: {}", userId, path, method);
+            // 用户限流检查通过,传递请求到下一个过滤器
+            return chain.filter(exchange);
+
+        } catch (Exception e) {
+            // 记录错误日志,显示用户限流执行异常
+            log.error("用户限流执行异常 - UserId: {}, Path: {}, Method: {}", userId, path, method, e);
+            // 限流失效时,允许请求通过(降级策略)
+            return chain.filter(exchange);
+        }
+    }
+
+    /**
+     * 获取用户ID
+     * @param exchange 服务交换对象
+     * @return String 用户ID
+     */
+    private String getUserId(ServerWebExchange exchange) {
+        // 从JWT token中获取用户ID
+        String token = jwtUtils.getTokenFromRequest(exchange.getRequest());
+        if (token != null) {
+            try {
+                // 从token中解析用户ID
+                return jwtUtils.getUserIdFromToken(token);
+            } catch (Exception e) {
+                // 记录调试日志,显示token解析失败
+                log.debug("从token获取用户ID失败", e);
+            }
+        }
+        // 如果无法获取用户ID,则返回null
+        return null;
+    }
+
+    /**
+     * 获取用户限流配置
+     * @param userId 用户ID
+     * @param path 请求路径
+     * @return GatewaySecurityProperties.RateLimitConfig 限流配置
+     */
+    private GatewaySecurityProperties.RateLimitConfig getRateLimitConfig(String userId, String path) {
+        // 检查是否有用户特定的配置
+        if (securityProperties.getUserRateLimit().getUserConfigs() != null) {
+            // 获取特定用户的配置
+            GatewaySecurityProperties.RateLimitConfig config = securityProperties.getUserRateLimit().getUserConfigs().get(userId);
+            if (config != null) {
+                return config;
+            }
+        }
+
+        // 检查路径特定的配置
+        if (securityProperties.getUserRateLimit().getPathConfigs() != null) {
+            // 遍历所有路径特定的配置
+            for (String pattern : securityProperties.getUserRateLimit().getPathConfigs().keySet()) {
+                // 如果路径匹配,则返回对应的配置
+                if (pathMatches(path, pattern)) {
+                    return securityProperties.getUserRateLimit().getPathConfigs().get(pattern);
+                }
+            }
+        }
+
+        // 使用默认配置
+        GatewaySecurityProperties.RateLimitConfig defaultConfig = new GatewaySecurityProperties.RateLimitConfig();
+        defaultConfig.setEnabled(true);
+        defaultConfig.setLimit(securityProperties.getUserRateLimit().getDefaultLimit());
+        defaultConfig.setWindow(securityProperties.getUserRateLimit().getDefaultWindow());
+        return defaultConfig;
+    }
+
+    /**
+     * 构建限流key
+     * @param userId 用户ID
+     * @param path 请求路径
+     * @param method HTTP方法
+     * @return String 限流key
+     */
+    private String buildRateLimitKey(String userId, String path, String method) {
+        // 构建格式化的限流key,包含用户ID、方法和路径信息
+        return String.format("gateway:user:limit:%s:%s:%s", userId, method, path);
+    }
+
+    /**
+     * 路径匹配检查(支持通配符)
+     * @param path 请求路径
+     * @param pattern 路径模式
+     * @return boolean 是否匹配
+     */
+    private boolean pathMatches(String path, String pattern) {
+        // 如果模式包含通配符,则转换为正则表达式进行匹配
+        if (pattern.contains("*")) {
+            String regex = pattern.replace("*", ".*");
+            return path.matches(regex);
+        }
+        // 否则直接比较路径是否相等
+        return path.equals(pattern);
+    }
+
+    /**
+     * 处理限流响应
+     * @param exchange 服务交换对象
+     * @param message 限流提示信息
+     * @return Mono<Void> 异步响应结果
+     */
+    private Mono<Void> handleRateLimit(ServerWebExchange exchange, String message) {
+        // 返回429 Too Many Requests错误响应
+        return ResponseUtils.writeErrorResponse(exchange, HttpStatus.TOO_MANY_REQUESTS, 429, message);
+    }
+
+    /**
+     * 获取过滤器执行顺序
+     * @return int 执行顺序,数值越小优先级越高
+     */
+    @Override
+    public int getOrder() {
+        return -70; // 在IP限流之后执行
+    }
+}

+ 263 - 0
alien-gateway/src/main/java/shop/alien/gateway/service/BlackWhiteListService.java

@@ -0,0 +1,263 @@
+package shop.alien.gateway.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 黑白名单服务
+ * 提供黑白名单相关的业务逻辑处理
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Service
+public class BlackWhiteListService {
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    private static final String BLACKLIST_PREFIX = "gateway:blacklist:";
+    private static final String WHITELIST_PREFIX = "gateway:whitelist:";
+
+    /**
+     * 添加IP到黑名单
+     */
+    public boolean addToBlackList(String ip, Long expireSeconds) {
+        try {
+            String key = BLACKLIST_PREFIX + ip;
+            if (expireSeconds != null && expireSeconds > 0) {
+                redisTemplate.opsForValue().set(key, "1", expireSeconds, TimeUnit.SECONDS);
+            } else {
+                redisTemplate.opsForValue().set(key, "1");
+            }
+            
+            log.info("添加IP到黑名单成功 - IP: {}, 过期时间: {}", ip, expireSeconds);
+            return true;
+        } catch (Exception e) {
+            log.error("添加IP到黑名单失败 - IP: {}", ip, e);
+            return false;
+        }
+    }
+
+    /**
+     * 从黑名单中移除IP
+     */
+    public boolean removeFromBlackList(String ip) {
+        try {
+            String key = BLACKLIST_PREFIX + ip;
+            Boolean result = redisTemplate.delete(key);
+            
+            log.info("从黑名单中移除IP成功 - IP: {}, 结果: {}", ip, result);
+            return result != null && result;
+        } catch (Exception e) {
+            log.error("从黑名单中移除IP失败 - IP: {}", ip, e);
+            return false;
+        }
+    }
+
+    /**
+     * 检查IP是否在黑名单中
+     */
+    public boolean isInBlackList(String ip) {
+        try {
+            // 检查静态黑名单
+            if (securityProperties.getBlackWhiteList().getBlackIps() != null) {
+                if (securityProperties.getBlackWhiteList().getBlackIps().contains(ip)) {
+                    return true;
+                }
+            }
+
+            // 检查动态黑名单(Redis)
+            String key = BLACKLIST_PREFIX + ip;
+            Boolean exists = redisTemplate.hasKey(key);
+            return exists != null && exists;
+        } catch (Exception e) {
+            log.error("检查IP是否在黑名单中失败 - IP: {}", ip, e);
+            return false;
+        }
+    }
+
+    /**
+     * 添加IP到白名单
+     */
+    public boolean addToWhiteList(String ip, Long expireSeconds) {
+        try {
+            String key = WHITELIST_PREFIX + ip;
+            if (expireSeconds != null && expireSeconds > 0) {
+                redisTemplate.opsForValue().set(key, "1", expireSeconds, TimeUnit.SECONDS);
+            } else {
+                redisTemplate.opsForValue().set(key, "1");
+            }
+            
+            log.info("添加IP到白名单成功 - IP: {}, 过期时间: {}", ip, expireSeconds);
+            return true;
+        } catch (Exception e) {
+            log.error("添加IP到白名单失败 - IP: {}", ip, e);
+            return false;
+        }
+    }
+
+    /**
+     * 从白名单中移除IP
+     */
+    public boolean removeFromWhiteList(String ip) {
+        try {
+            String key = WHITELIST_PREFIX + ip;
+            Boolean result = redisTemplate.delete(key);
+            
+            log.info("从白名单中移除IP成功 - IP: {}, 结果: {}", ip, result);
+            return result != null && result;
+        } catch (Exception e) {
+            log.error("从白名单中移除IP失败 - IP: {}", ip, e);
+            return false;
+        }
+    }
+
+    /**
+     * 检查IP是否在白名单中
+     */
+    public boolean isInWhiteList(String ip) {
+        try {
+            // 检查静态白名单
+            if (securityProperties.getBlackWhiteList().getWhiteIps() != null) {
+                if (securityProperties.getBlackWhiteList().getWhiteIps().contains(ip)) {
+                    return true;
+                }
+            }
+
+            // 检查动态白名单(Redis)
+            String key = WHITELIST_PREFIX + ip;
+            Boolean exists = redisTemplate.hasKey(key);
+            return exists != null && exists;
+        } catch (Exception e) {
+            log.error("检查IP是否在白名单中失败 - IP: {}", ip, e);
+            return false;
+        }
+    }
+
+    /**
+     * 获取黑名单列表
+     */
+    public List<String> getBlackList() {
+        try {
+            Set<String> keys = redisTemplate.keys(BLACKLIST_PREFIX + "*");
+            List<String> blackList = new ArrayList<>();
+            
+            if (keys != null) {
+                for (String key : keys) {
+                    String ip = key.substring(BLACKLIST_PREFIX.length());
+                    blackList.add(ip);
+                }
+            }
+            
+            return blackList;
+        } catch (Exception e) {
+            log.error("获取黑名单列表失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 获取白名单列表
+     */
+    public List<String> getWhiteList() {
+        try {
+            Set<String> keys = redisTemplate.keys(WHITELIST_PREFIX + "*");
+            List<String> whiteList = new ArrayList<>();
+            
+            if (keys != null) {
+                for (String key : keys) {
+                    String ip = key.substring(WHITELIST_PREFIX.length());
+                    whiteList.add(ip);
+                }
+            }
+            
+            return whiteList;
+        } catch (Exception e) {
+            log.error("获取白名单列表失败", e);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * 批量添加IP到黑名单
+     */
+    public Map<String, Boolean> batchAddToBlackList(List<String> ips, Long expireSeconds) {
+        Map<String, Boolean> results = new HashMap<>();
+        for (String ip : ips) {
+            results.put(ip, addToBlackList(ip, expireSeconds));
+        }
+        return results;
+    }
+
+    /**
+     * 批量添加IP到白名单
+     */
+    public Map<String, Boolean> batchAddToWhiteList(List<String> ips, Long expireSeconds) {
+        Map<String, Boolean> results = new HashMap<>();
+        for (String ip : ips) {
+            results.put(ip, addToWhiteList(ip, expireSeconds));
+        }
+        return results;
+    }
+
+    /**
+     * 获取IP的详细信息
+     */
+    public Map<String, Object> getIpInfo(String ip) {
+        Map<String, Object> info = new HashMap<>();
+        
+        // 检查是否在静态黑名单中
+        boolean inStaticBlackList = securityProperties.getBlackWhiteList().getBlackIps() != null &&
+                securityProperties.getBlackWhiteList().getBlackIps().contains(ip);
+        
+        // 检查是否在动态黑名单中
+        boolean inDynamicBlackList = isInBlackList(ip) && !inStaticBlackList;
+        
+        // 检查是否在静态白名单中
+        boolean inStaticWhiteList = securityProperties.getBlackWhiteList().getWhiteIps() != null &&
+                securityProperties.getBlackWhiteList().getWhiteIps().contains(ip);
+        
+        // 检查是否在动态白名单中
+        boolean inDynamicWhiteList = isInWhiteList(ip) && !inStaticWhiteList;
+        
+        // 获取黑名单过期时间
+        Long blackListExpire = null;
+        if (inDynamicBlackList) {
+            String blackKey = BLACKLIST_PREFIX + ip;
+            blackListExpire = redisTemplate.getExpire(blackKey);
+        }
+        
+        // 获取白名单过期时间
+        Long whiteListExpire = null;
+        if (inDynamicWhiteList) {
+            String whiteKey = WHITELIST_PREFIX + ip;
+            whiteListExpire = redisTemplate.getExpire(whiteKey);
+        }
+        
+        info.put("ip", ip);
+        info.put("inStaticBlackList", inStaticBlackList);
+        info.put("inDynamicBlackList", inDynamicBlackList);
+        info.put("inStaticWhiteList", inStaticWhiteList);
+        info.put("inDynamicWhiteList", inDynamicWhiteList);
+        info.put("blackListExpireSeconds", blackListExpire);
+        info.put("whiteListExpireSeconds", whiteListExpire);
+        
+        return info;
+    }
+}

+ 133 - 0
alien-gateway/src/main/java/shop/alien/gateway/service/RateLimitService.java

@@ -0,0 +1,133 @@
+package shop.alien.gateway.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import shop.alien.gateway.config.GatewaySecurityProperties;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 限流服务
+ * 提供限流相关的业务逻辑处理
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Service
+public class RateLimitService {
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private GatewaySecurityProperties securityProperties;
+
+    /**
+     * 检查是否触发限流
+     */
+    public boolean isRateLimited(String key, int limit, int windowSeconds) {
+        try {
+            Long current = redisTemplate.opsForValue().increment(key);
+            if (current == null) {
+                return false;
+            }
+
+            // 如果是第一次访问,设置过期时间
+            if (current == 1) {
+                redisTemplate.expire(key, windowSeconds, TimeUnit.SECONDS);
+            }
+
+            return current > limit;
+        } catch (Exception e) {
+            log.error("检查限流失败 - Key: {}", key, e);
+            // 出现异常时,允许通过(降级策略)
+            return false;
+        }
+    }
+
+    /**
+     * 获取当前访问次数
+     */
+    public long getCurrentCount(String key) {
+        try {
+            Object count = redisTemplate.opsForValue().get(key);
+            return count != null ? Long.parseLong(count.toString()) : 0;
+        } catch (Exception e) {
+            log.error("获取当前访问次数失败 - Key: {}", key, e);
+            return 0;
+        }
+    }
+
+    /**
+     * 清理限流缓存
+     */
+    public boolean clearRateLimitCache(String key) {
+        try {
+            Boolean result = redisTemplate.delete(key);
+            return result != null && result;
+        } catch (Exception e) {
+            log.error("清理限流缓存失败 - Key: {}", key, e);
+            return false;
+        }
+    }
+
+    /**
+     * 批量清理限流缓存
+     */
+    public long clearRateLimitCacheByPattern(String pattern) {
+        try {
+            Set<String> keys = redisTemplate.keys(pattern);
+            if (keys != null && !keys.isEmpty()) {
+                Long deletedCount = redisTemplate.delete(keys);
+                return deletedCount != null ? deletedCount : 0;
+            }
+            return 0;
+        } catch (Exception e) {
+            log.error("批量清理限流缓存失败 - Pattern: {}", pattern, e);
+            return 0;
+        }
+    }
+
+    /**
+     * 获取限流key的过期时间
+     */
+    public long getKeyExpireTime(String key) {
+        try {
+            Long expire = redisTemplate.getExpire(key);
+            return expire != null ? expire : -1;
+        } catch (Exception e) {
+            log.error("获取key过期时间失败 - Key: {}", key, e);
+            return -1;
+        }
+    }
+
+    /**
+     * 检查key是否存在
+     */
+    public boolean hasKey(String key) {
+        try {
+            Boolean exists = redisTemplate.hasKey(key);
+            return exists != null && exists;
+        } catch (Exception e) {
+            log.error("检查key是否存在失败 - Key: {}", key, e);
+            return false;
+        }
+    }
+
+    /**
+     * 获取所有匹配的key
+     */
+    public Set<String> getKeysByPattern(String pattern) {
+        try {
+            return redisTemplate.keys(pattern);
+        } catch (Exception e) {
+            log.error("获取匹配的key失败 - Pattern: {}", pattern, e);
+            return null;
+        }
+    }
+}

+ 135 - 0
alien-gateway/src/main/java/shop/alien/gateway/util/IpUtils.java

@@ -0,0 +1,135 @@
+package shop.alien.gateway.util;
+
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * IP工具类
+ * 用于获取客户端真实IP地址
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+public class IpUtils {
+
+    /**
+     * 常见的包含客户端真实IP的HTTP头名称数组
+     * 按优先级排序,优先从代理服务器设置的头中获取真实IP
+     */
+    private static final String[] IP_HEADER_NAMES = {
+        "X-Forwarded-For",      // 经过代理或负载均衡器时记录的客户端IP链
+        "X-Real-IP",            // Nginx等代理服务器设置的真实客户端IP
+        "Proxy-Client-IP",      // 传统代理服务器设置的客户端IP
+        "WL-Proxy-Client-IP",   // WebLogic代理设置的客户端IP
+        "HTTP_CLIENT_IP",       // 某些HTTP客户端设置的IP
+        "HTTP_X_FORWARDED_FOR"  // 另一种X-Forwarded-For格式
+    };
+
+    /**
+     * 获取客户端IP地址
+     * 优先从HTTP头中获取,如果获取不到则从连接信息中获取
+     * @param exchange 服务交换对象,包含请求信息
+     * @return String 客户端IP地址
+     */
+    public static String getClientIp(ServerWebExchange exchange) {
+        ServerHttpRequest request = exchange.getRequest();
+        
+        // 优先从请求头中获取客户端IP
+        for (String headerName : IP_HEADER_NAMES) {
+            // 从HTTP头中获取IP地址
+            String ip = request.getHeaders().getFirst(headerName);
+            // 检查IP地址是否有效且不为unknown
+            if (ip != null && ip.length() > 0 && !"unknown".equalsIgnoreCase(ip)) {
+                // 处理多个IP的情况(如X-Forwarded-For可能包含多个IP),取第一个
+                if (ip.contains(",")) {
+                    ip = ip.split(",")[0].trim();
+                }
+                // 返回获取到的有效IP地址
+                return ip;
+            }
+        }
+
+        // 如果从HTTP头中无法获取有效IP,则从远程地址获取
+        if (request.getRemoteAddress() != null) {
+            // 返回连接的远程地址
+            return request.getRemoteAddress().getAddress().getHostAddress();
+        }
+
+        // 如果无法获取任何IP地址,则返回unknown
+        return "unknown";
+    }
+
+    /**
+     * 检查IP是否在指定范围内(CIDR格式)
+     * @param ip 待检查的IP地址
+     * @param cidr CIDR格式的网络地址(如192.168.1.0/24)
+     * @return boolean 是否在指定范围内
+     */
+    public static boolean isIpInRange(String ip, String cidr) {
+        // 检查输入参数是否为空
+        if (ip == null || cidr == null) {
+            return false;
+        }
+
+        try {
+            // 解析CIDR格式的网络地址
+            String[] parts = cidr.split("/");
+            // 网络地址部分
+            String network = parts[0];
+            // 前缀长度部分,默认为32(即单个IP)
+            int prefixLength = parts.length > 1 ? Integer.parseInt(parts[1]) : 32;
+
+            // 将IP地址转换为long型进行比较
+            long ipLong = ipToLong(ip);
+            long networkLong = ipToLong(network);
+            // 计算子网掩码
+            long mask = -1L << (32 - prefixLength);
+
+            // 检查IP是否在指定网络范围内
+            return (ipLong & mask) == (networkLong & mask);
+        } catch (Exception e) {
+            // 发生异常时返回false
+            return false;
+        }
+    }
+
+    /**
+     * IP地址转long型
+     * 将点分十进制IP地址转换为long型数值
+     * @param ip 点分十进制IP地址
+     * @return long IP地址对应的long型数值
+     */
+    private static long ipToLong(String ip) {
+        // 按点分割IP地址
+        String[] parts = ip.split("\\.");
+        long result = 0;
+        // 将每个部分转换为数值并组合成long型
+        for (int i = 0; i < 4; i++) {
+            result = result << 8 | Integer.parseInt(parts[i]);
+        }
+        return result;
+    }
+
+    /**
+     * 检查IP是否为内网IP
+     * @param ip 待检查的IP地址
+     * @return boolean 是否为内网IP
+     */
+    public static boolean isInternalIp(String ip) {
+        // 检查输入参数是否为空
+        if (ip == null || ip.length() == 0) {
+            return false;
+        }
+
+        // 检查是否为本地IP
+        if ("127.0.0.1".equals(ip) || "localhost".equalsIgnoreCase(ip)) {
+            return true;
+        }
+
+        // 检查常见的内网IP段
+        return isIpInRange(ip, "10.0.0.0/8") ||      // A类私有地址
+               isIpInRange(ip, "172.16.0.0/12") ||    // B类私有地址
+               isIpInRange(ip, "192.168.0.0/16");     // C类私有地址
+    }
+}

+ 284 - 0
alien-gateway/src/main/java/shop/alien/gateway/util/JwtUtils.java

@@ -0,0 +1,284 @@
+package shop.alien.gateway.util;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import shop.alien.gateway.config.BaseRedisService;
+
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * JWT工具类
+ * 用于处理JWT token的生成、解析和验证
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+@Component
+public class JwtUtils {
+
+    /**
+     * JWT签名密钥
+     */
+    @Value("${jwt.secret:defaultSecretKey}")
+    private String secret;
+
+    /**
+     * JWT过期时间(秒)
+     */
+    @Value("${jwt.expiration:86400}")
+    private Long expiration;
+
+    /**
+     * JWT请求头名称
+     */
+    @Value("${jwt.header:Authorization}")
+    private String header;
+
+    /**
+     * JWT前缀
+     */
+    @Value("${jwt.prefix:Bearer }")
+    private String prefix;
+
+    @Autowired
+    private BaseRedisService baseRedisService;
+
+    /**
+     * 从请求中获取token
+     * @param request 服务器HTTP请求对象
+     * @return String JWT token字符串
+     */
+    public String getTokenFromRequest(ServerHttpRequest request) {
+        // 从请求头中获取Bearer token
+        String bearerToken = request.getHeaders().getFirst(header);
+        // 检查token是否存在且以指定前缀开头
+        if (bearerToken != null && bearerToken.startsWith(prefix)) {
+            // 返回去除前缀后的token
+            return bearerToken.substring(prefix.length());
+        }
+        // 如果无法获取token则返回null
+        return null;
+    }
+
+    /**
+     * 生成token
+     * @param userId 用户ID
+     * @param phone 用户手机号
+     * @param userType 用户类型
+     * @return String 生成的JWT token
+     */
+    public String generateToken(String userId, String phone, String userType) {
+        // 获取当前时间
+        Date now = new Date();
+        // 计算过期时间
+        Date expiryDate = new Date(now.getTime() + expiration * 1000);
+
+        // 构建JWT token
+        return Jwts.builder()
+                .setSubject(userId)                    // 设置用户ID为主题
+                .claim("phone", phone)                 // 添加手机号声明
+                .claim("userType", userType)           // 添加用户类型声明
+                .setIssuedAt(now)                      // 设置签发时间
+                .setExpiration(expiryDate)             // 设置过期时间
+                .signWith(SignatureAlgorithm.HS512, secret)  // 使用HS512算法签名
+                .compact();
+    }
+
+    /**
+     * 从token中获取用户ID
+     * @param token JWT token
+     * @return String 用户ID
+     */
+    public String getUserIdFromToken(String token) {
+        // 从token中解析声明
+        Claims claims = getClaimsFromToken(token);
+        // 返回主题(用户ID)
+        return claims != null ? claims.getSubject() : null;
+    }
+
+    /**
+     * 从token中获取手机号
+     * @param token JWT token
+     * @return String 手机号
+     */
+    public String getPhoneFromToken(String token) {
+        // 从token中解析声明
+        Claims claims = getClaimsFromToken(token);
+        // 返回手机号声明
+        return claims != null ? (String) claims.get("phone") : null;
+    }
+
+    /**
+     * 从token中获取用户类型
+     * @param token JWT token
+     * @return String 用户类型
+     */
+    public String getUserTypeFromToken(String token) {
+        // 从token中解析声明
+        Claims claims = getClaimsFromToken(token);
+        // 返回用户类型声明
+        return claims != null ? (String) claims.get("userType") : null;
+    }
+
+    /**
+     * 获取token的过期时间
+     * @param token JWT token
+     * @return Date 过期时间
+     */
+    public Date getExpirationDateFromToken(String token) {
+        // 从token中解析声明
+        Claims claims = getClaimsFromToken(token);
+        // 返回过期时间
+        return claims != null ? claims.getExpiration() : null;
+    }
+
+    /**
+     * 验证token是否有效
+     * @param token JWT token
+     * @return boolean token是否有效
+     */
+    public boolean validateToken(String token) {
+        try {
+            // 检查token格式
+            if (token == null || token.trim().isEmpty()) {
+                return false;
+            }
+
+            // 解析token声明
+            Claims claims = getClaimsFromToken(token);
+            if (claims == null) {
+                return false;
+            }
+
+            // 检查过期时间
+            Date expiration = claims.getExpiration();
+            if (expiration == null || expiration.before(new Date())) {
+                return false;
+            }
+
+            // 检查Redis中是否存在(防止token被删除)
+            String userId = claims.getSubject();
+            String redisKey = "user:token:" + userId;
+            String redisToken = (String) baseRedisService.get(redisKey);
+            
+            // 验证Redis中的token是否匹配
+            if (redisToken == null || !redisToken.equals(token)) {
+                return false;
+            }
+
+            // 所有验证通过,token有效
+            return true;
+
+        } catch (Exception e) {
+            // 记录验证失败日志
+            log.error("验证token失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 将token存入Redis
+     * @param userId 用户ID
+     * @param token JWT token
+     */
+    public void saveTokenToRedis(String userId, String token) {
+        try {
+            // 构建Redis key
+            String redisKey = "user:token:" + userId;
+            // 将token存入Redis并设置过期时间
+            baseRedisService.set(redisKey, token, expiration, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            // 记录保存失败日志
+            log.error("保存token到Redis失败", e);
+        }
+    }
+
+    /**
+     * 从Redis中删除token
+     * @param userId 用户ID
+     */
+    public void removeTokenFromRedis(String userId) {
+        try {
+            // 构建Redis key
+            String redisKey = "user:token:" + userId;
+            // 从Redis中删除token
+            baseRedisService.delete(redisKey);
+        } catch (Exception e) {
+            // 记录删除失败日志
+            log.error("从Redis中删除token失败", e);
+        }
+    }
+
+    /**
+     * 检查用户是否被禁用
+     * @param userId 用户ID
+     * @return boolean 用户是否被禁用
+     */
+    public boolean isUserDisabled(String userId) {
+        try {
+            // 构建Redis key
+            String redisKey = "user:disabled:" + userId;
+            // 检查Redis中是否存在用户禁用标记
+            return baseRedisService.hasKey(redisKey);
+        } catch (Exception e) {
+            // 记录检查失败日志
+            log.error("检查用户禁用状态失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 检查用户是否异地登录
+     * @param userId 用户ID
+     * @param currentToken 当前token
+     * @return boolean 是否异地登录
+     */
+    public boolean isUserLoginFromOtherPlace(String userId, String currentToken) {
+        try {
+            // 构建Redis key
+            String redisKey = "user:token:" + userId;
+            // 从Redis中获取token
+            String redisToken = (String) baseRedisService.get(redisKey);
+            
+            // 检查Redis中的token是否与当前token一致
+            if (redisToken != null && !redisToken.equals(currentToken)) {
+                return true;
+            }
+            
+            return false;
+        } catch (Exception e) {
+            // 记录检查失败日志
+            log.error("检查用户异地登录失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 从token中获取声明
+     * @param token JWT token
+     * @return Claims 声明对象
+     */
+    private Claims getClaimsFromToken(String token) {
+        try {
+            // 使用密钥解析token并获取声明
+            return Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        } catch (Exception e) {
+            // 记录解析失败日志
+            log.error("解析token失败", e);
+            return null;
+        }
+    }
+}

+ 133 - 0
alien-gateway/src/main/java/shop/alien/gateway/util/ResponseUtils.java

@@ -0,0 +1,133 @@
+package shop.alien.gateway.util;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 响应工具类
+ * 用于统一处理响应数据
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/21
+ */
+@Slf4j
+public class ResponseUtils {
+
+    /**
+     * 写入错误响应
+     * @param exchange 服务交换对象
+     * @param httpStatus HTTP状态码
+     * @param code 业务状态码
+     * @param message 错误消息
+     * @return Mono<Void> 异步响应结果
+     */
+    public static Mono<Void> writeErrorResponse(ServerWebExchange exchange, HttpStatus httpStatus, int code, String message) {
+        // 调用通用响应写入方法
+        return writeResponse(exchange, httpStatus, code, message, null);
+    }
+
+    /**
+     * 写入成功响应
+     * @param exchange 服务交换对象
+     * @param data 响应数据
+     * @return Mono<Void> 异步响应结果
+     */
+    public static Mono<Void> writeSuccessResponse(ServerWebExchange exchange, Object data) {
+        // 调用通用响应写入方法,使用200状态码和success消息
+        return writeResponse(exchange, HttpStatus.OK, 200, "success", data);
+    }
+
+    /**
+     * 写入响应数据
+     * @param exchange 服务交换对象
+     * @param httpStatus HTTP状态码
+     * @param code 业务状态码
+     * @param message 响应消息
+     * @param data 响应数据
+     * @return Mono<Void> 异步响应结果
+     */
+    public static Mono<Void> writeResponse(ServerWebExchange exchange, HttpStatus httpStatus, int code, String message, Object data) {
+        try {
+            // 构建响应数据
+            Map<String, Object> responseData = new HashMap<>();
+            responseData.put("code", code);                // 业务状态码
+            responseData.put("message", message);          // 响应消息
+            responseData.put("data", data);                // 响应数据
+            responseData.put("timestamp", System.currentTimeMillis());  // 时间戳
+
+            // 将响应数据转换为JSON字符串
+            String jsonResponse = JSON.toJSONString(responseData);
+
+            // 设置响应状态码
+            exchange.getResponse().setStatusCode(httpStatus);
+            
+            // 设置响应头
+            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
+            // 设置内容长度
+            exchange.getResponse().getHeaders().set("Content-Length", String.valueOf(jsonResponse.getBytes(StandardCharsets.UTF_8).length));
+
+            // 获取数据缓冲工厂
+            DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
+            // 创建数据缓冲区
+            DataBuffer buffer = bufferFactory.wrap(jsonResponse.getBytes(StandardCharsets.UTF_8));
+
+            // 写入响应体
+            return exchange.getResponse().writeWith(Mono.just(buffer));
+
+        } catch (Exception e) {
+            // 记录写入响应数据异常日志
+            log.error("写入响应数据异常", e);
+            // 如果写入失败,返回空的Mono
+            return Mono.empty();
+        }
+    }
+
+    /**
+     * 构建标准响应对象
+     * @param code 业务状态码
+     * @param message 响应消息
+     * @param data 响应数据
+     * @return Map<String, Object> 标准响应对象
+     */
+    public static Map<String, Object> buildResponse(int code, String message, Object data) {
+        // 创建响应Map
+        Map<String, Object> response = new HashMap<>();
+        response.put("code", code);                    // 设置业务状态码
+        response.put("message", message);              // 设置响应消息
+        response.put("data", data);                    // 设置响应数据
+        response.put("timestamp", System.currentTimeMillis());  // 设置时间戳
+        return response;
+    }
+
+    /**
+     * 构建成功响应
+     * @param data 响应数据
+     * @return Map<String, Object> 成功响应对象
+     */
+    public static Map<String, Object> buildSuccessResponse(Object data) {
+        // 调用通用响应构建方法,使用200状态码和success消息
+        return buildResponse(200, "success", data);
+    }
+
+    /**
+     * 构建错误响应
+     * @param code 业务状态码
+     * @param message 错误消息
+     * @return Map<String, Object> 错误响应对象
+     */
+    public static Map<String, Object> buildErrorResponse(int code, String message) {
+        // 调用通用响应构建方法,数据部分为null
+        return buildResponse(code, message, null);
+    }
+}

+ 2 - 1
alien-gateway/src/main/resources/logback-spring.xml

@@ -12,7 +12,8 @@
     <!-- 定义全局参数常量 -->
     <property name="log.level" value="debug"/>
     <property name="log.maxHistory" value="30"/><!-- 30表示30个 -->
-    <springProperty scope="context" name="logging.path" source="logging.path"/>
+    <property name="logging.path" value="./logs"/>
+<!--    <springProperty scope="context" name="logging.path" source="logging.path"/>-->
     <!--输出文件前缀-->
     <property name="FILENAME" value="xiaokuihua_gateway"/>
 

+ 0 - 25
alien-job/pom.xml

@@ -74,16 +74,6 @@
         </dependency>
 
         <dependency>
-            <groupId>com.alibaba.cloud</groupId>
-            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
-        </dependency>
-
-        <dependency>
-            <groupId>com.alibaba.cloud</groupId>
-            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
-        </dependency>
-
-        <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-boot-starter</artifactId>
         </dependency>
@@ -187,19 +177,4 @@
             </plugin>
         </plugins>
     </build>
-    <profiles>
-        <profile>
-            <id>test</id>
-            <properties>
-                <profiles.active>test</profiles.active>
-                <nacos.server-addr>192.168.2.252:8848</nacos.server-addr>
-                <nacos.namespace>0e1e2d77-56e8-422c-8317-6f71d7285e59</nacos.namespace>
-                <nacos.username>nacos</nacos.username>
-                <nacos.password>ngfriend198092</nacos.password>
-            </properties>
-            <activation>
-                <activeByDefault>true</activeByDefault>
-            </activation>
-        </profile>
-    </profiles>
 </project>

+ 1 - 1
alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java

@@ -5,7 +5,7 @@ import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 
-@FeignClient(url = "${feign.alienStore.url}", name = "alien-store")
+@FeignClient(url = "http://localhost:8888", name = "alien-store")
 public interface AlienStoreFeign {
 
     @GetMapping("/websocket/sendMsgToClientByPhoneId")

+ 213 - 2
alien-job/src/main/resources/bootstrap.yml

@@ -1,3 +1,214 @@
+server:
+  port: 8089
+
 spring:
-  profiles:
-    active: dev
+  application:
+    name: alien-job
+
+  # JSON配置
+  jackson:
+    default-property-inclusion: non_null
+    date-format: yyyy-MM-dd HH:mm:ss
+    time-zone: GMT+8
+
+  redis:
+    host: 192.168.2.252
+    port: 6379
+    database: 1
+    # 密码
+    password: Wady@1213
+    timeout: 10s
+    lettuce:
+      pool:
+        min-idle: 0
+        max-idle: 8
+        max-active: 8
+        max-wait: -1ms
+  web:
+    resources:
+      # 上传静态资源目录
+      #static-locations: /usr/local/alien/file
+      # 上传app目录
+      app-locations: /usr/local/alien/file/app
+      static-locations: /usr/local/alien/file
+      url: http://devfile.ailien.shop/
+      # excel表格
+      excel-path: /usr/local/alien/file/excel/
+      # 回执单模板名称
+      excel-clearing-receipt: clearing_receipt.xlsx
+      #生成的文件地址
+      excel-generate-path: /generate/
+
+  datasource:
+    druid:
+      driver-class-name: com.mysql.cj.jdbc.Driver
+      url: jdbc:mysql://192.168.2.252:3306/alien?serverTimezone=Asia/Shanghai&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false
+      username: root
+      password: Wady@1213
+
+      #连接初始化数量
+      initial-size: 1
+      #连接池中的最小空闲连接数
+      min-idle: 1
+      #配置连接池同时能维持的最大连接数
+      max-active: 20
+      #获取连接等待超时时间,单位是毫秒
+      #      max-wait: 60000
+      #间隔多久进行一次检测,检测需要关闭的空闲连接
+      time-between-eviction-runs-millis: 60000
+      #一个连接在池中最小生存的时间,单位是毫秒
+      min-evictable-idle-time-millis: 300000
+      #一个连接在池中最大生存的时间,单位是毫秒
+      max-evictable-idle-time-millis: 900000
+      #Druid用来测试连接是否可用的SQL语句
+      validation-query: SELECT 'x' FROM DUAL
+      #申请连接的时候检测
+      test-while-idle: true
+      #申请连接时执行validationQuery检测连接是否有效
+      test-on-borrow: true
+      #归还连接时执行validationQuery检测连接是否有效
+      test-on-return: false
+      #是否缓存preparedStatement
+      pool-prepared-statements: false
+      #最大缓存
+      max-pool-prepared-statement-per-connection-size: 20
+      #监控统计用的filter:stat
+      filters: stat
+      #通过connectProperties属性来打开mergeSql功能;慢SQL记录
+      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=10000
+      stat-view-servlet:
+        #是否开启druid管理界面
+        enabled: true
+      #是否启用StatFilter默认值false,用于采集 web-jdbc 关联监控的数据。
+      web-stat-filter:
+        enabled: true
+        #排除一些静态资源,以提高效率
+        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
+        #需要监控的 url
+        url-pattern: "/*"
+
+mybatis-plus:
+  # xml文件路径
+  #  mapper-locations: classpath:/mapper/**/*.xml,/mapper/*.xml
+  mapper-locations: mapper/*.xml
+  #实体扫描,多个package用逗号或者分号分隔
+  typeAliasesPackage: shop.alien.entity
+  global-config:
+    #数据库相关配置
+    db-config:
+      #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
+      id-type: AUTO
+      logic-delete-field: isDel
+      logic-delete-value: 1
+      logic-not-delete-value: 0
+    banner: false
+  #原生配置
+  configuration:
+    #数据库字段驼峰下划线转换
+    map-underscore-to-camel-case: true
+    cache-enabled: false
+    call-setters-on-nulls: true
+    jdbc-type-for-null: 'null'
+    # 打印sql
+    # sql只打印到控制台
+    #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+    # sql 打印到日志和控制台
+    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
+
+# 分页
+pagehelper:
+  helperDialect: mysql
+  reasonable: true
+  supportMethodsArguments: true
+  params: count=countSql
+
+logging:
+  config: classpath:logback-spring.xml
+  level:
+    shop:
+      alien:
+        mapper: debug
+
+    com:
+      baomidou:
+        mybatisplus: debug
+
+xxl:
+  job:
+    accessToken: default_token
+    admin:
+      addresses: http://192.168.2.251:8800/xxl-job-admin
+
+    executor:
+      appname: alien-xxl-job-executor-lixiaobai
+      address:
+      ip:
+      port: 8899
+      logpath: /javaLog/alienJobLog/xxl-job/jobhandler
+      logretentiondays: 30
+
+
+#阿里云短信
+ali:
+  aes:
+    #对称加密算法密钥
+    encryptKey: '4lSSA6Fi+4BG1qhakxdncw=='
+
+  #短信
+  sms:
+    endPoint: 'dysmsapi.aliyuncs.com'
+    accessKeyId: 'LTAI5tNeG9kUFWTYukrtVQxU'
+    accessKeySecret: 'vIWYJQWzCRcfH3rgnH5cuq05KEOwgw'
+    signName: '爱丽恩严大连商务科技'
+    templateCode: 'SMS_483575093'
+
+  yundun:
+    accessKeyID: LTAI5tReZWshffH78oQJPzZG
+    secret: Gi30OLYAWunaDbwsv5qqdZ3PzbDyGP
+    regionId: cn-beijing
+    textEndpoint: green-cip.cn-beijing.aliyuncs.com
+    imgEndpoint: green-cip.cn-beijing.aliyuncs.com
+    videoEndpoint: green-cip.cn-shanghai.aliyuncs.com
+    videoRegionId: cn-shanghai
+
+  oss:
+    endPoint: oss-cn-beijing.aliyuncs.com
+    bucketName: alien-volume
+    accessKeyId: LTAI5tG6wSYrSgN3Cwek17Du
+    accessKeySecret: PQDPdTtDy0v848p0Bl9vT5yHmpt79H
+
+#支付宝app
+app:
+  #商家端
+  business:
+    appId: 2021004191696239
+    #app公钥
+    appPublicKey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqIxlCU8fx+mDYF6XEqtl7u/8ylH8TLLUcPx0wn00m/V/LT1vbd70QU4oyaPRslCmfj6mscadR2DeDK5Flpp67DDvPERU3UxMCxMNbKuv6MPsxedh5PnZkLGQ5NGlImv/xtJ6A9kFbSu62y51TL96/R7gwzwdxr5nph+qNfUr40THvHhvcqVV695YSktgcHOrg6pfpeOIAuTpdUs5gPEt7RuGhSz2x3+bOC2c1hdwc4azqcJ5Tz45DxTXG5PWxcSybqDg3Rh5rISzhLgipQYdoGoAHTWgJUmoqBlRMVhiRHxUKYs8Gng/4Ksvyq+EXREcswe34tmUHvBTmbGM6aDtvQIDAQAB'
+    #app私钥
+    appPrivateKey: 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCojGUJTx/H6YNgXpcSq2Xu7/zKUfxMstRw/HTCfTSb9X8tPW9t3vRBTijJo9GyUKZ+Pqaxxp1HYN4MrkWWmnrsMO88RFTdTEwLEw1sq6/ow+zF52Hk+dmQsZDk0aUia//G0noD2QVtK7rbLnVMv3r9HuDDPB3GvmemH6o19SvjRMe8eG9ypVXr3lhKS2Bwc6uDql+l44gC5Ol1SzmA8S3tG4aFLPbHf5s4LZzWF3BzhrOpwnlPPjkPFNcbk9bFxLJuoODdGHmshLOEuCKlBh2gagAdNaAlSaioGVExWGJEfFQpizwaeD/gqy/Kr4RdERyzB7fi2ZQe8FOZsYzpoO29AgMBAAECggEAarRHGree9R7eb1oy5jzgUX5ZWXTB94SXfdg1yxMdBUdMdkWYj8RAej+TDuTJUsxAO1hKi+2jkJbAvOJvZ8Lw4yqvqkXQPDGbSr+LFiszOHchJMExdpa+qB3iDxqyIrKgwSxq1pjEGLNAnpYw6J+oqyqrPK6IY1QTplYRmgtW949+Wa+0nkH8rKczbX4VZhT1u3OAXSj+0IoJLm+6AM73Q3U+Zu95WSKiX9rKN7IBFjHZJm+1NLDQMroilcLvgbJ1Q7Tez+0r0yOdeFEnq3ZIuEs95bRLCyew36QOVK8qSMPAPtkoRRWH+iCgWJ6nTZK+HphL+6WN0Llm8eVWFcNnuQKBgQD44L0o9da2lBp98UcmZRp7+dAIiLySwzThKcMfcIlLahFddFpwVzZHIWl4nN3YT12aPmT/aN31tQ9IO2t+L2iZglPgbzZIcZOJhp9Xott7onp3yMrB0Jxs/rEAlRuR3CHY0ONnrkPMIMAFCdRRpffuHcqqhJavBBs9ThLFw3e33wKBgQCtXysAtAa9j3IC6x3aImKzhAVJJyOzR8DBWP1FPVS4R9o0752Fjwa7FAgi4B04FTw+QOWUbdw9pzupjlgetRM1oCkAcvF1dT2+i7kOjjq/hM2lE63PY9nLNWDGbl8mt15J62w4GA61efH9AAUrA3KaEj5Or5Y+q04sITeWmG994wKBgH++o3ltzlsC5CI3AUVE6fYuwMhQnzJZvj/AtuXhVqd99xHYEj/NlA+op8GACyLnge0VD7RFuIBRAloaHc+0N+VrjvAJRXyjx430qr9J2Yy5VlE7oE8ha9lKVcItxuv0aO8oMUFbonMZcFZluDR8kykG46royYtl+TFbbKlT7pRTAoGABCfq3Ppzyul8XyuJrKf0W79HbSh+B5cpvLjIYXeF52D5jxlLWl9CVwu04jkjGiwQJKHjnvo/NE8k8/yQijPnAPsU4Ijg1uL3lvC8f+fS6rJhR5rAKN+I4gEmj7snlsMAhJ8bGgEhoSIKNflhJlXP0nerfLFi1Z+/oPku3vJKgecCgYEAy5UkL8X1OGMIanrm53Bx4N0kSv1D2RlR52LGwCfkglaSH1sVrlXiDIyr55bieiyUQ4J5uknta/hGXnJM9w8w8+HM1gmbO5vXyTUQLEt4k9gBJECY82ZrxxtTKXDprryxKciM9Zs46cDsuEQWbpr8Ps4NIeI9F6LfClbDJiQBdCY='
+
+    win:
+      #应用公钥证书文件路径
+      appCertPath: 'E:/Cache/business/appCertPublicKey_2021004191696239.crt'
+      #支付宝公钥证书文件路径
+      alipayPublicCertPath: 'E:/Cache/business/alipayCertPublicKey_RSA2.crt'
+      #支付宝根证书文件路径
+      alipayRootCertPath: 'E:/Cache/business/alipayRootCert.crt'
+
+    linux:
+      #应用公钥证书文件路径
+      appCertPath: '/usr/local/alien/aliPayCert/appCertPublicKey_2021004191696239.crt'
+      #支付宝公钥证书文件路径
+      alipayPublicCertPath: '/usr/local/alien/aliPayCert/alipayCertPublicKey_RSA2.crt'
+      #支付宝根证书文件路径
+      alipayRootCertPath: '/usr/local/alien/aliPayCert/alipayRootCert.crt'
+
+  #用户端
+  user:
+    appId: 2021004192631044
+    #app公钥
+    appPublicKey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgSlnroP58xYTdsFxY9BuMJVJHEkp2K+DzV9EOk5hCq91E33rHLxizYRTkIGQ+Fw4+Qkrjcj330+eVI7ZABkdqF2YQah0hPbWSnyJtdJuECTgb8ZSoYTInA1aoKOkifBNy6/yeOARW+Sus3uLDt+svtQJuJ04sFO8YLQOQRomLfztdRsUsdnBUoEufb7hTeAnpJD/4lj2zsRGhKzi/bqSq4iRDg2sr6FJurFaROqXND9R8/DLQNuJQfj0bCaf2u2bNAVtodFzS/HWeXWeGH03JU9NQByBUB5KHZd3qUQaYmkpSNAEOJyt8e6weNFk2mEYhVlLC8nx7gj0NvAoEh7s7QIDAQAB'
+    #app私钥
+    appPrivateKey: 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCBKWeug/nzFhN2wXFj0G4wlUkcSSnYr4PNX0Q6TmEKr3UTfescvGLNhFOQgZD4XDj5CSuNyPffT55UjtkAGR2oXZhBqHSE9tZKfIm10m4QJOBvxlKhhMicDVqgo6SJ8E3Lr/J44BFb5K6ze4sO36y+1Am4nTiwU7xgtA5BGiYt/O11GxSx2cFSgS59vuFN4CekkP/iWPbOxEaErOL9upKriJEODayvoUm6sVpE6pc0P1Hz8MtA24lB+PRsJp/a7Zs0BW2h0XNL8dZ5dZ4YfTclT01AHIFQHkodl3epRBpiaSlI0AQ4nK3x7rB40WTaYRiFWUsLyfHuCPQ28CgSHuztAgMBAAECggEABHzQymJxe72nnXepHeOvAGcfbCkqmAm5FZiawT/Z5UolUjEMyynMcQGI+btWsxpZLGXiKoHyAsM8aAmyhmtY+XGPCbD1o20EEwBytGuEQXFWjuugEVaQtcNNA8nLvLMpsMsDLKn0UHIIk9nWAgVJdjPdxplHz/CI2f9Hg4GKFAzpXwV1c574+rSDIkpa2NDFI2lqwtbew7/1ETaCiBXSbqw4X/tk+2wgUdT8u4uA/Ts5bVZvvHm54U9ZdTLs2rHzIk2O9VbKB3waHT1WkQzrPF8+JrPvcYjsK7ROxi07N1e5kv4TuxLfK3rXvmoUI8TvidSLhmq57VXhU9iNZBEHwQKBgQD4GR58WhOotPHcNI5oKRkJSektNSz/pXFLjh3oF/y98kOuFfeH903Z+JQ/5fgjzQjKra3HqjsVDlTjhCSZtNc/QpYafMf6TGzX+41QNnjlxxeZgYa2CCPOAlOw8d1a6YV0v8mjjnWoh2+vaYaqBKZmm2uid9zyklbg8I0DZiYaUQKBgQCFRogto52N6i5QSMwPGrPJOabKqkpA2EtbEgnkrpybN5DFQ+OqwWujQVkwWNlufHAhWdb2OCJ+bb2dCwrmX8kHB/SF+VSlYGsRdDAONF9aVGHqnaJB1yvfwp4ArNUb0vORJun22jRCpmtN0dA7lS/PnwqgCZw8LNWkAFuVewSl3QKBgF+PdVGadHYH9BzIVY3DPrR4NotGLyXs+J2MiPJrwtr65Jy6M1S4qdDqUVfnYVOQ1vESQpxkckti8MmNjXdy03G3WQ+svm4nX8k1SDH8OUbwD6P94wBcVEY2WTwPfR5WUiQs9yK7bZBTUm5zK/5yuhFNjgDPQFhii3oqzXXgs45xAoGADjZ4XU2ehrzblYo94Lp7Q9FXHTPN2V53os2oqm+ImfDyCmd0Bwi7ftyAM2y2O1cf6h6XkwhnfU4cs3uy/OBoIszRYdw2D7DZmjHm0gz5wjtPeeK3pAfbUPCDQxUrYN09RlR9DOs2OYlf1huy9jexspWGX4zG0ZACdIvpOGa3Fy0CgYEA8waJ9ciOG50VjX/f/vYRKQUQTFQKhvZ0xcFnA8iT30FNfBs7XOhiBaXF25mul34hHhAxVuUIlEmOqyEkLO/ZWioOtWy//gRZIFL8qHoCaM8lS8xwT/xOz8nfDiu2hpISEXAquU2PNdP4yGiLg4qyafrvdctRw4TtpHEOrtYYyt8='
+    #支付宝公钥
+    aliPayPublicKey: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqEK+9FHCA4GX/veehln6mFaYdpfAA8knkF8rRMX1RGunTZcIGs65yPvmGuyEhNZH3hajbGIMt4AKrdi9NO+1kSCSNc14IKYhpQFq6NIHxQHYP2+0aRCPy3JPpCBSowjWuzounh2uaEOImatykCSRiuicFymlwb8cv75vlOp6Pq6Gu+Ep/NIeHbhqbE9YtScmaRBvwpn0tz9M1yN9AVDuZD6VxSfjObtgMDQqHSUJJmmRuo4/DYGX0rkvtzhQcJbU9LTA1QkBWKgriww/4ZyAMg7q02FQ0dh81h9DxC0ugPhSo55FouMx7wrgN/GPYrdcEyOojpzgSYZC3uRnWuqZpQIDAQAB'

+ 12 - 0
alien-second/pom.xml

@@ -113,6 +113,18 @@
             <groupId>com.github.pagehelper</groupId>
             <artifactId>pagehelper-spring-boot-starter</artifactId>
         </dependency>
+        <!-- Redisson依赖 -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <version>3.17.0</version>
+        </dependency>
+
+        <!-- AspectJ依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
 
         <!-- mybatis-plus代码生成器 Start -->
         <dependency>

+ 2 - 2
alien-second/src/main/java/shop/alien/second/AlienSecondApplication.java

@@ -11,8 +11,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
 //@ComponentScan("shop.alien.second.*")
 @EnableFeignClients(basePackages = {"shop.alien.second.feign"})
 @ComponentScan({
-        "shop.alien.second.*",
-        "shop.alien.util.*",
+        "shop.alien.second",
+        "shop.alien.util",
         "shop.alien.config.http",
         "shop.alien.config.databases",
         "shop.alien.config.feign",

+ 97 - 0
alien-second/src/main/java/shop/alien/second/config/RuleLoaderUtil.java

@@ -0,0 +1,97 @@
+package shop.alien.second.config;
+
+import lombok.extern.slf4j.Slf4j;
+//import org.drools.core.io.impl.ByteArrayResource;
+//import org.kie.api.KieBase;
+//import org.kie.api.io.ResourceType;
+//import org.kie.internal.utils.KieHelper;
+import shop.alien.entity.second.SecurityRule;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * 纯工具类:把 MySQL 规则列表 → KieBase
+ * 无 Spring 依赖,方便单元测试复用
+ */
+@Slf4j
+public final class RuleLoaderUtil {
+
+    private RuleLoaderUtil() {}
+
+    /**
+     * 根据规则列表编译 KieBase
+     * @param rules 来自 MySQL 的启用规则
+     * @return 可直接使用的 KieBase
+     */
+//    public static KieBase loadKieBase(List<SecurityRule> rules) {
+//        if (rules == null || rules.isEmpty()) {
+//            log.warn("规则为空,返回空 KieBase");
+//            return new KieHelper().build(); // 空规则
+//        }
+//
+//        // 1. 拼接 DRL 字符串
+//        String drl = buildDrlV2(rules);
+//
+//        // 2. KieHelper 直接内存编译,无临时文件
+//        KieHelper helper = new KieHelper();
+//        helper.addResource(new ByteArrayResource(drl.getBytes(StandardCharsets.UTF_8)), ResourceType.DRL);
+//        KieBase kieBase = helper.build();
+//
+//        log.info("规则编译成功,共 {} 条", rules.size());
+//        return kieBase;
+//    }
+
+    /**
+     * 把规则列表拼成标准 DRL
+     */
+    private static String buildDrl(List<SecurityRule> rules) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("package shop.alien.second.config\n");
+        sb.append("dialect \"mvel\"\n\n");
+
+        // 导入所有可能的事实类
+        sb.append("import shop.alien.entity.second.vo.fact.AccountFact;\n");
+        sb.append("import shop.alien.entity.second.vo.fact.ItemFact;\n");
+        sb.append("import shop.alien.entity.second.vo.fact.LogisticsFact;\n");
+        sb.append("import shop.alien.entity.second.vo.fact.TradeFact;\n");
+        sb.append("import shop.alien.entity.second.vo.SecurityCtx;\n");
+        sb.append("global SecurityCtx ctx;\n\n");
+
+        for (SecurityRule r : rules) {
+            sb.append("rule \"").append(r.getRuleName()).append("\"\n");
+            sb.append("salience ").append(r.getPriority()).append("\n");
+            sb.append("when\n");
+            // 根据 factType 动态指定事实类
+            sb.append("    $o : ").append(r.getFactType())
+                    .append("( ").append(r.getWhenExpr()).append(" )\n");
+            sb.append("then\n");
+            sb.append("    ").append(r.getThenExpr()).append("\n");
+            sb.append("end\n\n");
+        }
+        return sb.toString();
+    }
+
+    private static String buildDrlV2(List<SecurityRule> rules) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("package shop.alien.second.config\n");
+        sb.append("dialect \"mvel\"\n\n");
+        sb.append("import shop.alien.entity.second.vo.fact.FactWrapper;\n");
+        sb.append("import shop.alien.entity.second.vo.SecurityCtx;\n");
+        sb.append("global SecurityCtx ctx;\n\n");
+
+        for (SecurityRule r : rules) {
+            sb.append("rule \"").append(r.getRuleName()).append("\"\n");
+            sb.append("salience ").append(r.getPriority()).append("\n");
+            sb.append("when\n");
+            // 匹配对应维度的 FactWrapper
+            sb.append("    $f : FactWrapper(factType == \"")
+                    .append(r.getFactType()).append("\", ")
+                    .append(r.getWhenExpr()).append(")\n");
+            sb.append("then\n");
+            sb.append("    ").append(r.getThenExpr()).append("\n");
+            sb.append("end\n\n");
+        }
+        return sb.toString();
+    }
+}

+ 2 - 7
alien-second/src/main/java/shop/alien/second/controller/SecondGoodsController.java

@@ -117,13 +117,8 @@ public class SecondGoodsController {
     @NoRepeatSubmit(expireTime = 3, message = "请勿重复提交下架商品请求")
     public R<Void> shelveSecondGoods(@ApiParam("下架二手商品") @RequestBody SecondGoodsVo secondGoods) {
         log.info("SecondGoodsController.shelveSecondGoods?secondGoods={}", secondGoods.toString());
-        // 修改商品状态为4 - 已下架
-        secondGoods.setGoodsStatus(4);
-        boolean updateResult = secondGoodsService.updateById(secondGoods);
-        if (updateResult) {
-            // 获取最新的商品信息并记录操作历史
-            SecondGoods updatedGoods = secondGoodsService.getById(secondGoods.getId());
-            secondGoodsService.recordGoodsOperation(updatedGoods,"下架商品");
+        boolean shelveResult = secondGoodsService.shelveSecondGoods(secondGoods);
+        if (shelveResult) {
             return R.success("下架成功");
         } else {
             return R.fail("下架失败");

+ 87 - 0
alien-second/src/main/java/shop/alien/second/controller/SecurityController.java

@@ -0,0 +1,87 @@
+//package shop.alien.second.controller;
+//
+//import cn.hutool.crypto.digest.MD5;
+//import com.alibaba.nacos.common.utils.MD5Utils;
+//import io.swagger.annotations.Api;
+//import io.swagger.annotations.ApiSort;
+//import lombok.RequiredArgsConstructor;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.web.bind.annotation.*;
+//import shop.alien.entity.result.R;
+//import shop.alien.entity.second.vo.*;
+//import shop.alien.entity.second.vo.fact.*;
+//import shop.alien.second.service.impl.SecurityRuleServiceImpl;
+//
+//import java.util.HashMap;
+//import java.util.List;
+//import java.util.Map;
+//import java.util.stream.Collectors;
+//
+//@Slf4j
+//@Api(tags = {"二手平台-商品管理"})
+//@ApiSort(1)
+//@CrossOrigin
+//@RestController
+//@RequestMapping("/security")
+//@RequiredArgsConstructor
+//public class SecurityController {
+//
+//
+//    private final SecurityRuleServiceImpl service;
+//
+//    @PostMapping("/evaluate")
+//    public R<Double> evaluate(@RequestBody ScoreReq req) {
+//        SecurityCtx ctx = new SecurityCtx(req.getListingId());
+//
+//        // 构造各维度事实对象(实际业务从 req / DB / 外部接口填充)
+//        AccountFact account = new AccountFact(req.getHistoryViolation(), req.getSellerScore(), req.isRealName());
+//        ItemFact item = new ItemFact(req.isHighRiskCategory(), req.getPrice(), req.getMarketPrice());
+//        LogisticsFact logistics = new LogisticsFact(req.isNoTracking(), req.isCashOnDelivery(), req.isRemoteArea());
+//        TradeFact trade = new TradeFact(req.isOffsiteChat(), req.isRequestAdvancePayment());
+//
+//        double deduct = service.evaluate(ctx, account, item, logistics, trade);
+//        return R.data(deduct); // 返回扣减分值
+//    }
+//    @PostMapping("/score/dynamic")
+//    public R<Integer> scoreDynamic(@RequestBody Map<String, Map<String, Object>> input) {
+//        SecurityCtx ctx = new SecurityCtx(9527L);
+//
+//        // 将 Map 转为 FactWrapper 列表
+//        List<FactWrapper> facts = input.entrySet()
+//                .stream()
+//                .filter(e -> e.getKey() != null && e.getValue() != null)
+//                .map(e -> new FactWrapper(
+//                        e.getKey().trim(),
+//                        new HashMap<>(e.getValue())
+//                ))
+//                .collect(Collectors.toList());
+//
+//        int deduct = service.score(ctx, facts);
+//        return R.data(deduct); // 返回扣减分值
+//    }
+//
+//
+//    /**
+//     * 商品安全评分接口
+//     */
+//    @PostMapping("/score")
+//    public ScoreResp score(@RequestBody ScoreReq req) {
+//        double score = service.score(
+//                req.getListingId(),
+//                new ItemFact(req.isHighRiskCategory(), req.getPrice(), req.getMarketPrice()),
+//                new TradeFact(req.isOffsiteChat(), req.isRequestAdvancePayment()),
+//                new AccountFact(req.getHistoryViolation(), req.getSellerScore(), req.isRealName()),
+//                new LogisticsFact(req.isNoTracking(), req.isCashOnDelivery(), req.isRemoteArea())
+//        );
+//        return new ScoreResp(score);
+//    }
+//
+//    /**
+//     * 后台强制刷新规则
+//     */
+//    @PostMapping("/admin/refresh")
+//    public String refresh() {
+//        service.forceRefresh();
+//        return "OK";
+//    }
+//}

+ 201 - 0
alien-second/src/main/java/shop/alien/second/seckill/SECKILL_SYSTEM_TEST_GUIDE.md

@@ -0,0 +1,201 @@
+# 秒杀系统测试流程和接口调用流程
+
+## 一、系统架构概述
+
+该秒杀系统基于Spring Boot开发,采用Redis作为库存存储,通过Redisson实现分布式锁和信号量控制,并使用注解切面的方式实现秒杀流程控制。系统还集成了消息队列用于异步处理秒杀成功后的业务逻辑,并通过定时任务同步库存数据。
+
+## 二、核心组件
+
+1. **SeckillGuardAspect** - 秒杀切面,处理整个秒杀流程控制
+2. **RedisStockDao** - Redis库存操作DAO,通过Lua脚本保证原子性操作
+3. **SeckillConfigService** - 秒杀配置服务,控制灰度发布
+4. **StockInitializerService** - 库存初始化服务
+5. **StockSyncService** - 库存同步服务
+6. **SeckillMessageProducer/Consumer** - 消息生产者和消费者
+7. **StockSyncJob** - 库存同步定时任务
+
+## 三、接口调用流程
+
+根据分析代码,秒杀系统的完整流程如下:
+
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant EC as ExampleSeckillController
+    participant SGA as SeckillGuardAspect
+    participant RC as Redisson Client
+    participant RSD as RedisStockDao
+    participant LU as Lua Script
+    participant SMP as SeckillMessageProducer
+    participant SIS as StockInitializerService
+    participant SSS as StockSyncService
+    participant SMC as SeckillMessageConsumer
+
+    U->>EC: 发起秒杀请求 /api/seckill/item/{itemId}
+    EC->>SGA: @SeckillGuard注解触发切面
+    SGA->>SGA: 检查是否启用新秒杀逻辑和灰度用户
+    alt 未启用新逻辑或非灰度用户
+        SGA->>EC: 降级到旧逻辑,直接执行业务方法
+        EC-->>U: 返回业务结果
+    else 启用新逻辑且为灰度用户
+        SGA->>RC: 用户限流检查 (RAtomicLong)
+        RC-->>SGA: 返回限流检查结果
+        alt 超过用户限制
+            SGA-->>U: 抛出用户限流异常
+        else 未超过用户限制
+            SGA->>RC: 获取信号量许可 (RSemaphore)
+            RC-->>SGA: 返回许可获取结果
+            alt 未获取到许可(系统繁忙)
+                SGA-->>U: 抛出系统繁忙异常
+            else 获取到许可
+                SGA->>RSD: 检查库存是否存在
+                RSD-->>SGA: 返回库存存在状态
+                alt 库存不存在
+                    SGA->>SIS: 初始化库存
+                    SIS-->>SGA: 返回初始化结果
+                end
+                SGA->>RSD: 执行库存扣减
+                RSD->>LU: 执行Lua脚本
+                LU-->>RSD: 返回剩余库存
+                RSD-->>SGA: 返回扣减结果
+                alt 库存不足
+                    SGA->>RC: 释放信号量许可
+                    SGA-->>U: 抛出库存不足异常
+                else 库存充足
+                    SGA->>SGA: 创建订单令牌并设置到上下文
+                    SGA->>EC: 执行业务逻辑(proceed)
+                    EC->>EC: 业务方法执行
+                    EC-->>SGA: 返回业务执行结果
+                    alt 业务执行成功
+                        SGA->>SMP: 发送秒杀成功消息
+                        SGA->>SMP: 发送库存回滚消息
+                        SGA->>SSS: 记录库存变化
+                        SGA->>RC: 释放信号量许可
+                        SGA-->>U: 返回业务结果
+                        SMP->>SMC: 消费秒杀成功消息
+                        SMP->>SMC: 消费库存回滚消息
+                    else 业务执行失败
+                        SGA->>RSD: 回滚库存
+                        SGA->>RC: 释放信号量许可
+                        SGA-->>U: 抛出业务异常
+                    end
+                end
+            end
+        end
+    end
+```
+
+## 四、详细流程说明
+
+### 1. 预处理阶段
+- 检查是否启用新秒杀逻辑或是否为灰度用户
+- 如果未启用新逻辑或当前用户不是灰度用户,则直接执行原方法(降级到旧逻辑)
+
+### 2. 用户限流
+- 使用Redis的RAtomicLong实现用户维度限流
+- 构造用户限流键:`seckill:userLimit:{活动ID}:{用户ID}`
+- 如果用户访问次数超过限制(默认为1次),抛出用户限流异常
+
+### 3. 分布式排队
+- 使用Redis的RSemaphore实现分布式信号量控制并发
+- 获取信号量许可,超时时间为200毫秒
+- 如果未能获取到许可,抛出系统繁忙异常
+
+### 4. 库存检查与初始化
+- 检查Redis中是否存在库存
+- 如果不存在,则调用库存初始化服务初始化库存
+
+### 5. 库存扣减
+- 使用Lua脚本保证库存扣减和订单令牌生成的原子性操作
+- Lua脚本同时完成扣减库存和生成订单令牌两个操作
+- 如果库存不足,释放信号量许可并抛出库存不足异常
+
+### 6. 业务处理
+- 创建订单令牌并设置到用户上下文中
+- 执行被拦截的业务方法(如创建订单等)
+
+### 7. 后续处理
+- 如果业务执行成功:
+  - 发送秒杀成功消息到消息队列
+  - 发送15分钟后检查订单状态的消息(用于回滚库存)
+  - 记录库存变化
+  - 释放信号量许可
+- 如果业务执行失败:
+  - 回滚已扣减的库存
+  - 释放信号量许可
+  - 重新抛出异常
+
+## 五、测试流程
+
+### 1. 环境准备
+- 启动Redis服务
+- 确保应用能够正常连接Redis
+- 确保配置文件中秒杀相关配置正确
+
+### 2. 初始化测试数据
+- 调用库存初始化接口初始化商品库存:
+  ```
+  POST /seckill/stock/init/{itemId}?initialStock=100
+  ```
+- 或者调用批量初始化接口:
+  ```
+  POST /seckill/stock/init/batch?itemIds=1001,1002,1003
+  ```
+
+### 3. 设置用户信息
+- 调用接口设置当前用户ID(测试用):
+  ```
+  POST /api/seckill/user/{userId}
+  ```
+
+### 4. 执行秒杀测试
+- 调用秒杀接口:
+  ```
+  POST /api/seckill/item/{itemId}
+  ```
+
+### 5. 并发测试
+- 使用压测工具模拟多个用户同时进行秒杀
+- 验证系统在高并发情况下的正确性和稳定性
+- 检查限流和排队机制是否正常工作
+
+### 6. 异常测试
+- 测试库存不足情况下的处理
+- 测试用户限流功能
+- 测试系统繁忙情况下的处理
+- 测试业务执行失败时的回滚机制
+
+### 7. 异步处理测试
+- 验证消息队列是否正常接收和处理秒杀成功消息
+- 验证库存回滚消息是否正常处理
+
+### 8. 定时任务测试
+- 验证库存同步定时任务是否正常执行
+- 检查Redis中的库存变化是否正确同步到数据库
+
+## 六、关键配置
+
+### 1. 秒杀配置
+```yaml
+seckill:
+  cubeEnabled: true  # 是否启用新秒杀逻辑
+  grayTail: "0,1"    # 灰度用户尾号
+```
+
+### 2. 注解配置
+```java
+@SeckillGuard(activityId = "#{#itemId}", maxWait = 5000, userLimit = 1)
+```
+- activityId: 活动ID
+- maxWait: 最大允许等待人数,超过直接拒绝(默认5000)
+- userLimit: 每用户抢购上限(默认1)
+
+## 七、系统特点
+
+1. **高并发支持**:通过Redis和Lua脚本保证操作原子性,避免超卖
+2. **限流保护**:实现用户维度限流和系统维度排队机制
+3. **灰度发布**:支持配置灰度用户,可逐步放量
+4. **异步处理**:通过消息队列异步处理秒杀成功后的业务逻辑
+5. **数据一致性**:通过定时任务同步Redis和数据库中的库存数据
+6. **热点数据分片**:通过分片机制避免单点热点问题
+7. **自动回滚**:在业务执行失败时自动回滚库存和信号量

+ 66 - 0
alien-second/src/main/java/shop/alien/second/seckill/SEQUENCE_DIAGRAM.md

@@ -0,0 +1,66 @@
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant EC as ExampleSeckillController
+    participant SGA as SeckillGuardAspect
+    participant RC as Redisson Client
+    participant RSD as RedisStockDao
+    participant LU as Lua Script
+    participant SMP as SeckillMessageProducer
+    participant SIS as StockInitializerService
+    participant SSS as StockSyncService
+    participant SMC as SeckillMessageConsumer
+
+    U->>EC: 发起秒杀请求 /api/seckill/item/{itemId}
+    EC->>SGA: @SeckillGuard注解触发切面
+    SGA->>SGA: 检查是否启用新秒杀逻辑和灰度用户
+    alt 未启用新逻辑或非灰度用户
+        SGA->>EC: 降级到旧逻辑,直接执行业务方法
+        EC-->>U: 返回业务结果
+    else 启用新逻辑且为灰度用户
+        SGA->>RC: 用户限流检查 (RAtomicLong)
+        RC-->>SGA: 返回限流检查结果
+        alt 超过用户限制
+            SGA-->>U: 抛出用户限流异常
+        else 未超过用户限制
+            SGA->>RC: 获取信号量许可 (RSemaphore)
+            RC-->>SGA: 返回许可获取结果
+            alt 未获取到许可(系统繁忙)
+                SGA-->>U: 抛出系统繁忙异常
+            else 获取到许可
+                SGA->>RSD: 检查库存是否存在
+                RSD-->>SGA: 返回库存存在状态
+                alt 库存不存在
+                    SGA->>SIS: 初始化库存
+                    SIS-->>SGA: 返回初始化结果
+                end
+                SGA->>RSD: 执行库存扣减
+                RSD->>LU: 执行Lua脚本
+                LU-->>RSD: 返回剩余库存
+                RSD-->>SGA: 返回扣减结果
+                alt 库存不足
+                    SGA->>RC: 释放信号量许可
+                    SGA-->>U: 抛出库存不足异常
+                else 库存充足
+                    SGA->>SGA: 创建订单令牌并设置到上下文
+                    SGA->>EC: 执行业务逻辑(proceed)
+                    EC->>EC: 业务方法执行
+                    EC-->>SGA: 返回业务执行结果
+                    alt 业务执行成功
+                        SGA->>SMP: 发送秒杀成功消息
+                        SGA->>SMP: 发送库存回滚消息
+                        SGA->>SSS: 记录库存变化
+                        SGA->>RC: 释放信号量许可
+                        SGA-->>U: 返回业务结果
+                        SMP->>SMC: 消费秒杀成功消息
+                        SMP->>SMC: 消费库存回滚消息
+                    else 业务执行失败
+                        SGA->>RSD: 回滚库存
+                        SGA->>RC: 释放信号量许可
+                        SGA-->>U: 抛出业务异常
+                    end
+                end
+            end
+        end
+    end
+```

+ 17 - 0
alien-second/src/main/java/shop/alien/second/seckill/annotation/SeckillGuard.java

@@ -0,0 +1,17 @@
+package shop.alien.second.seckill.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SeckillGuard {
+    /** 活动ID */
+    String activityId();
+    /** 最大允许等待人数,超过直接拒绝 */
+    int maxWait() default 5000;
+    /** 每用户抢购上限 */
+    int userLimit() default 1;
+}

+ 155 - 0
alien-second/src/main/java/shop/alien/second/seckill/aspect/SeckillGuardAspect.java

@@ -0,0 +1,155 @@
+package shop.alien.second.seckill.aspect;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.redisson.api.RAtomicLong;
+import org.redisson.api.RSemaphore;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Component;
+import shop.alien.second.seckill.annotation.SeckillGuard;
+import shop.alien.second.seckill.dao.RedisStockDao;
+import shop.alien.second.seckill.entity.SeckillOrderToken;
+import shop.alien.second.seckill.mq.SeckillMessageProducer;
+import shop.alien.second.seckill.service.SeckillConfigService;
+import shop.alien.second.seckill.service.StockInitializerService;
+import shop.alien.second.seckill.service.StockSyncService;
+import shop.alien.second.seckill.util.UserContext;
+
+import java.time.Duration;
+import java.util.UUID;
+
+/**
+ * 秒杀保护切面类
+ * 实现了完整的秒杀流程保护机制,包括限流、排队、库存扣减等核心功能
+ * 集成消息队列实现异步处理
+ */
+@Aspect
+@Component
+public class SeckillGuardAspect {
+
+    // 注入库存DAO,用于操作Redis库存
+    private final RedisStockDao stockDao;
+    // 注入Redisson客户端,用于实现分布式锁和信号量
+    private final RedissonClient redisson;
+    // 注入秒杀配置服务,用于灰度发布控制
+    private final SeckillConfigService configService;
+    // 注入库存同步服务,用于异步同步库存到数据库
+    private final StockSyncService stockSyncService;
+    // 注入消息生产者,用于发送消息到消息队列
+    private final SeckillMessageProducer messageProducer;
+    // 注入库存初始化服务,用于初始化库存
+    private final StockInitializerService stockInitializerService;
+
+    // 构造函数注入依赖
+    public SeckillGuardAspect(RedisStockDao stockDao, RedissonClient redisson, 
+                              SeckillConfigService configService, StockSyncService stockSyncService,
+                              SeckillMessageProducer messageProducer, 
+                              StockInitializerService stockInitializerService) {
+        this.stockDao = stockDao;
+        this.redisson = redisson;
+        this.configService = configService;
+        this.stockSyncService = stockSyncService;
+        this.messageProducer = messageProducer;
+        this.stockInitializerService = stockInitializerService;
+    }
+
+    /**
+     * 环绕通知处理方法
+     * 在被@SeckillGuard注解标记的方法执行前后进行增强处理
+     *
+     * @param pjp 连接点对象,代表被拦截的方法
+     * @param guard 秒杀保护注解实例,包含配置信息
+     * @return 原方法执行结果
+     * @throws Throwable 可能抛出的异常
+     */
+    @Around("@annotation(guard)")
+    public Object around(ProceedingJoinPoint pjp, SeckillGuard guard) throws Throwable {
+        // 从注解中获取活动ID,并转为Long类型
+        Long itemId = Long.valueOf(guard.activityId());
+        // 从用户上下文中获取当前用户ID
+        Long userId = UserContext.getUserId();
+
+        // 检查是否启用新秒杀逻辑或是否为灰度用户
+        // 如果未启用新逻辑或当前用户不是灰度用户,则直接执行原方法(降级到旧逻辑)
+        if (!configService.isCubeEnabled() || !configService.isGrayUser(userId)) {
+            // 降级到旧逻辑
+            return pjp.proceed();
+        }
+
+        // 1. 用户维度限流 - 防止单个用户频繁请求秒杀接口
+        // 构造用户限流键,格式为 seckill:userLimit:{活动ID}:{用户ID}
+        String userKey = "seckill:userLimit:" + itemId + ":" + userId;
+        // 获取Redis中的原子计数器,用于记录用户访问次数
+        RAtomicLong userCounter = redisson.getAtomicLong(userKey);
+        // 增加计数器并检查是否超过用户限制
+        if (userCounter.incrementAndGet() > guard.userLimit()) {
+            // 如果超过限制,抛出用户限流异常
+            throw new RuntimeException("User limit exceeded");
+        }
+        // 设置计数器过期时间为30分钟,防止内存泄漏
+        userCounter.expire(Duration.ofMinutes(30));
+
+        // 2. 分布式排队锁(信号量) - 控制同时参与秒杀的人数
+        // 获取指定活动的信号量,用于控制并发
+        RSemaphore semaphore = redisson.getSemaphore("seckill:sem:" + itemId);
+        // 设置信号量的最大许可数(即最多允许多少人同时参与秒杀)
+        semaphore.trySetPermits(guard.maxWait());
+        // 尝试获取信号量许可,超时时间为200毫秒
+        boolean acquired = semaphore.tryAcquire(200, java.util.concurrent.TimeUnit.MILLISECONDS);
+        // 如果未能获取到许可,说明系统繁忙
+        if (!acquired) {
+            // 抛出系统繁忙异常
+            throw new RuntimeException("System busy");
+        }
+
+        // 3. 检查并初始化库存 - 确保Redis中有库存数据
+        if (!stockDao.existsStock(itemId)) {
+            // 如果Redis中没有库存数据,则初始化库存(使用基于Redisson的分布式锁V3版本)
+            stockInitializerService.checkAndInitializeStockV3(itemId);
+        }
+
+        // 4. 扣库存 - 使用Lua脚本保证原子性操作
+        // 生成唯一的订单号
+        String orderNo = UUID.randomUUID().toString().replace("-", "");
+        // 调用库存DAO扣减库存,返回剩余库存数量
+        Long left = stockDao.decrStock(itemId, 1, orderNo);
+        // 如果返回值小于0,表示库存不足
+        if (left < 0) {
+            // 恢复之前获取的信号量许可
+            semaphore.release();
+            // 抛出库存不足异常
+            throw new RuntimeException("Stock shortage");
+        }
+
+        // 5. 封装订单 Token 透传给下游 - 将订单信息传递给业务方法
+        // 创建秒杀订单令牌对象,包含订单号、商品ID和用户ID
+        SeckillOrderToken token = new SeckillOrderToken(orderNo, itemId, userId);
+        // 将订单令牌设置到用户上下文中,供业务方法使用
+        UserContext.setOrderToken(token);
+
+        try {
+            // 执行被拦截的业务方法(如创建订单等)
+            Object result = pjp.proceed();
+            
+            // 秒杀成功后,发送消息到消息队列进行异步处理
+            messageProducer.sendSeckillSuccessMessage(token);
+            
+            // 同时发送15分钟后检查订单状态的消息,用于回滚库存
+            messageProducer.sendRollbackStockMessage(token);
+            
+            // 记录库存变化,用于批量同步
+            stockSyncService.recordStockChange(itemId, -1);
+            
+            return result;
+        } catch (Exception e) {
+            // 6. 业务异常回滚:恢复库存 + 信号量
+            // 如果业务执行失败,则需要回滚已扣减的库存
+            stockDao.incrStock(itemId, 1);
+            // 同时释放信号量许可
+            semaphore.release();
+            // 重新抛出异常
+            throw e;
+        }
+    }
+}

+ 37 - 0
alien-second/src/main/java/shop/alien/second/seckill/aspect/tempfile_1760088147642.mermaid

@@ -0,0 +1,37 @@
+flowchart TD
+    A[管理员登录] --> B{身份验证}
+    B -- 失败 --> C[登录失败]
+    B -- 成功 --> D[进入管理后台]
+    D --> E[选择管理功能]
+    E --> F[商户审核]
+    E --> G[活动审核]
+    E --> H[对账结算]
+    E --> I[数据统计分析]
+    
+    F --> J[查看商户申请]
+    J --> K{审核商户信息}
+    K -- 通过 --> L[激活商户账户]
+    K -- 不通过 --> M[拒绝商户申请]
+    
+    G --> N[查看活动申请]
+    N --> O{审核活动内容}
+    O -- 通过 --> P[发布活动]
+    O -- 不通过 --> Q[拒绝活动申请]
+    
+    H --> R[生成结算清单]
+    R --> S[处理提现申请]
+    
+    I --> T[生成统计数据]
+    T --> U[生成可视化报表]
+    
+    L --> V[通知商户]
+    M --> V
+    P --> V
+    Q --> V
+    S --> V
+    U --> V
+    V --> W[返回功能选择]
+    W --> E
+    
+    style A fill:#FFE4B5,stroke:#333
+    style V fill:#98FB98,stroke:#333

+ 53 - 0
alien-second/src/main/java/shop/alien/second/seckill/aspect/tempfile_1760088171106.mermaid

@@ -0,0 +1,53 @@
+graph TD
+    subgraph "Actors"
+        A[用户]
+        B[商户]
+        C[平台管理员]
+        D[系统]
+    end
+
+    subgraph "用户功能"
+        A1[登录/注册]
+        A2[浏览店铺]
+        A3[收藏店铺]
+        A4[关注用户]
+        A5[发布动态]
+        A6[下单购买]
+        A7[评价店铺]
+        A8[消息互动]
+        A9[拉黑用户]
+    end
+
+    subgraph "商户功能"
+        B1[商户登录]
+        B2[管理店铺信息]
+        B3[发布优惠券]
+        B4[处理订单]
+        B5[查看统计数据]
+        B6[参与活动]
+    end
+
+    subgraph "平台管理员功能"
+        C1[审核商户]
+        C2[审核活动]
+        C3[对账结算]
+        C4[数据统计]
+        C5[生成报表]
+    end
+
+    subgraph "系统功能"
+        D1[支付处理]
+        D2[消息通知]
+        D3[内容审核]
+        D4[数据存储]
+    end
+
+    A --- A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 & A9
+    B --- B1 & B2 & B3 & B4 & B5 & B6
+    C --- C1 & C2 & C3 & C4 & C5
+    D --- D1 & D2 & D3 & D4
+
+    style A fill:#FFE4B5,stroke:#333
+    style B fill:#E6E6FA,stroke:#333
+    style C fill:#E0FFFF,stroke:#333
+    style D fill:#F0FFF0,stroke:#333

+ 13 - 0
alien-second/src/main/java/shop/alien/second/seckill/config/SchedulingConfig.java

@@ -0,0 +1,13 @@
+package shop.alien.second.seckill.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 定时任务配置类
+ * 启用Spring的定时任务功能
+ */
+@Configuration
+@EnableScheduling
+public class SchedulingConfig {
+}

+ 9 - 0
alien-second/src/main/java/shop/alien/second/seckill/config/SeckillAspectConfig.java

@@ -0,0 +1,9 @@
+package shop.alien.second.seckill.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+@Configuration
+@EnableAspectJAutoProxy
+public class SeckillAspectConfig {
+}

+ 33 - 0
alien-second/src/main/java/shop/alien/second/seckill/config/SeckillMQConfig.java

@@ -0,0 +1,33 @@
+package shop.alien.second.seckill.config;
+
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 秒杀消息队列配置类
+ * 用于配置消息队列相关的Bean
+ * 在实际项目中,这里应该配置RocketMQ相关的Bean
+ */
+@Configuration
+public class SeckillMQConfig {
+    
+    // 在实际项目中,这里应该配置RocketMQ的Producer和Consumer
+    
+    /*
+    @Bean
+    public RocketMQTemplate rocketMQTemplate() {
+        RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
+        // 配置RocketMQ相关参数
+        return rocketMQTemplate;
+    }
+    
+    @Bean
+    public DefaultMQPushConsumer defaultMQPushConsumer() throws MQClientException {
+        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("seckill_consumer_group");
+        consumer.setNamesrvAddr("127.0.0.1:9876");
+        consumer.subscribe("seckill-success", "*");
+        consumer.subscribe("seckill-rollback", "*");
+        // 配置其他参数
+        return consumer;
+    }
+    */
+}

+ 41 - 0
alien-second/src/main/java/shop/alien/second/seckill/controller/ExampleSeckillController.java

@@ -0,0 +1,41 @@
+package shop.alien.second.seckill.controller;
+
+import org.springframework.web.bind.annotation.*;
+import shop.alien.second.seckill.annotation.SeckillGuard;
+import shop.alien.second.seckill.entity.SeckillOrderToken;
+import shop.alien.second.seckill.util.UserContext;
+
+@RestController
+@RequestMapping("/api/seckill")
+public class ExampleSeckillController {
+
+    /**
+     * 秒杀接口示例
+     * 
+     * @param itemId 商品ID
+     * @return 订单号
+     */
+    @PostMapping("/item/{itemId}")
+    @SeckillGuard(activityId = "#{#itemId}", maxWait = 5000, userLimit = 1)
+    public String seckill(@PathVariable Long itemId) {
+        // 获取秒杀订单信息
+        SeckillOrderToken token = UserContext.getOrderToken();
+        
+        // 这里可以调用业务服务创建订单
+        // orderService.createOrder(token);
+        
+        return "秒杀成功,订单号: " + token.getOrderNo();
+    }
+    
+    /**
+     * 设置用户ID(仅用于测试)
+     * 
+     * @param userId 用户ID
+     * @return 成功信息
+     */
+    @PostMapping("/user/{userId}")
+    public String setUser(@PathVariable Long userId) {
+        UserContext.setUserId(userId);
+        return "用户ID设置成功: " + userId;
+    }
+}

+ 44 - 0
alien-second/src/main/java/shop/alien/second/seckill/controller/SeckillController.java

@@ -0,0 +1,44 @@
+package shop.alien.second.seckill.controller;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.second.seckill.annotation.SeckillGuard;
+import shop.alien.second.seckill.entity.SeckillOrderToken;
+import shop.alien.second.seckill.util.UserContext;
+
+@RestController
+@RequestMapping("/seckill")
+public class SeckillController {
+
+    @PostMapping("/order")
+    @SeckillGuard(activityId = "10086", maxWait = 3000, userLimit = 2)
+    public String createOrder(@RequestBody CreateOrderCmd cmd) {
+        // 此处已经是"库存扣减成功、拿到订单号"的安全区域
+        SeckillOrderToken token = UserContext.getOrderToken();
+        // orderAppService.create(token, cmd);
+        return "Order created: " + token.getOrderNo();
+    }
+
+    public static class CreateOrderCmd {
+        private Long itemId;
+        private Integer quantity;
+
+        public Long getItemId() {
+            return itemId;
+        }
+
+        public void setItemId(Long itemId) {
+            this.itemId = itemId;
+        }
+
+        public Integer getQuantity() {
+            return quantity;
+        }
+
+        public void setQuantity(Integer quantity) {
+            this.quantity = quantity;
+        }
+    }
+}

+ 58 - 0
alien-second/src/main/java/shop/alien/second/seckill/controller/StockInitController.java

@@ -0,0 +1,58 @@
+package shop.alien.second.seckill.controller;
+
+import org.springframework.web.bind.annotation.*;
+import shop.alien.second.seckill.service.StockInitializerService;
+
+/**
+ * 库存初始化控制器
+ * 用于手动初始化商品库存
+ */
+@RestController
+@RequestMapping("/seckill/stock")
+public class StockInitController {
+    
+    private final StockInitializerService stockInitializerService;
+    
+    public StockInitController(StockInitializerService stockInitializerService) {
+        this.stockInitializerService = stockInitializerService;
+    }
+    
+    /**
+     * 初始化单个商品库存
+     * 
+     * @param itemId 商品ID
+     * @param initialStock 初始库存数量
+     * @return 初始化结果
+     */
+    @PostMapping("/init/{itemId}")
+    public String initializeStock(@PathVariable Long itemId, 
+                                  @RequestParam(defaultValue = "100") Long initialStock) {
+        try {
+            stockInitializerService.initializeStock(itemId, initialStock);
+            return "商品 " + itemId + " 库存初始化成功,初始库存: " + initialStock;
+        } catch (Exception e) {
+            return "商品 " + itemId + " 库存初始化失败: " + e.getMessage();
+        }
+    }
+    
+    /**
+     * 批量初始化商品库存
+     * 
+     * @param itemIds 商品ID列表,用逗号分隔
+     * @return 初始化结果
+     */
+    @PostMapping("/init/batch")
+    public String batchInitializeStock(@RequestParam String itemIds) {
+        try {
+            String[] itemIdStrs = itemIds.split(",");
+            Long[] itemIdArray = new Long[itemIdStrs.length];
+            for (int i = 0; i < itemIdStrs.length; i++) {
+                itemIdArray[i] = Long.valueOf(itemIdStrs[i]);
+            }
+            stockInitializerService.batchInitializeStock(itemIdArray);
+            return "批量初始化库存成功";
+        } catch (Exception e) {
+            return "批量初始化库存失败: " + e.getMessage();
+        }
+    }
+}

+ 148 - 0
alien-second/src/main/java/shop/alien/second/seckill/dao/RedisStockDao.java

@@ -0,0 +1,148 @@
+package shop.alien.second.seckill.dao;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.ReturnType;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StreamUtils;
+import shop.alien.second.seckill.util.ShardingUtil;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis库存操作DAO类
+ * 用于实现库存的增减操作,基于Redis和Lua脚本保证操作的原子性
+ * 支持热点数据分片,避免单点热点问题
+ */
+@Component
+public class RedisStockDao {
+    // Lua脚本SHA值在Redis中的存储键名
+    private static final String STOCK_LUA_SHA_KEY = "seckill:stock:lua:sha";
+
+    // 注入StringRedisTemplate,用于操作Redis
+    @Resource
+    private StringRedisTemplate rt;
+
+    // 存储Lua脚本的SHA值
+    private String luaSha;
+
+    /**
+     * 初始化方法,在Bean创建后自动执行
+     * 主要用于预加载Lua脚本到Redis,避免每次执行时重复上传脚本
+     */
+    @PostConstruct
+    public void init() {
+        try {
+            // 从classpath中读取Lua脚本文件内容
+            // 预加载脚本,避免每次上传
+            String script = StreamUtils.copyToString(
+                    new ClassPathResource("lua/stockDecr.lua").getInputStream(),
+                    StandardCharsets.UTF_8);
+            // 将Lua脚本加载到Redis中,返回脚本的SHA值
+            luaSha = rt.getConnectionFactory().getConnection()
+                    .scriptLoad(script.getBytes(StandardCharsets.UTF_8));
+            // 将SHA值存储到Redis中,便于后续使用
+            rt.opsForValue().set(STOCK_LUA_SHA_KEY, luaSha);
+        } catch (Exception e) {
+            // 如果加载失败,抛出运行时异常
+            throw new RuntimeException("Failed to load lua script", e);
+        }
+    }
+
+    /**
+     * 扣减库存方法
+     * 使用Lua脚本保证库存扣减和订单令牌生成的原子性操作
+     * 支持热点数据分片,避免单点热点问题
+     *
+     * @param itemId 商品ID
+     * @param quantity 扣减数量
+     * @param orderNo 订单号
+     * @return 剩余库存数量,-1表示库存不足
+     */
+    public Long decrStock(Long itemId, Integer quantity, String orderNo) {
+        // 使用分片键替代原来的键,避免热点问题
+        List<String> keys = Arrays.asList(
+                ShardingUtil.getStockShardingKey(itemId),          // 库存分片键
+                ShardingUtil.getOrderTokenShardingKey(itemId));    // 订单令牌分片键
+        // 执行Lua脚本
+        return rt.execute(
+                // Redis回调函数,用于执行Lua脚本
+                (RedisConnection conn) -> conn.evalSha(luaSha,
+                        ReturnType.INTEGER,             // 返回值类型为整数
+                        2,                              // 键的数量
+                        keys.get(0).getBytes(StandardCharsets.UTF_8),   // 库存键
+                        keys.get(1).getBytes(StandardCharsets.UTF_8),   // 订单令牌键
+                        quantity.toString().getBytes(StandardCharsets.UTF_8),  // 扣减数量
+                        orderNo.getBytes(StandardCharsets.UTF_8),       // 订单号
+                        "900".getBytes(StandardCharsets.UTF_8)),        // 过期时间(秒)
+                true);
+    }
+
+    /**
+     * 增加库存(用于回滚)
+     * 当秒杀失败时,需要将之前扣减的库存加回去
+     * 支持热点数据分片,避免单点热点问题
+     *
+     * @param itemId 商品ID
+     * @param quantity 增加数量
+     * @return 增加后的库存数量
+     */
+    public Long incrStock(Long itemId, Integer quantity) {
+        // 使用分片键替代原来的键,避免热点问题
+        String key = ShardingUtil.getStockShardingKey(itemId);
+        // 使用Redis的increment命令增加库存
+        return rt.opsForValue().increment(key, quantity);
+    }
+    
+    /**
+     * 获取库存
+     * 
+     * @param itemId 商品ID
+     * @return 库存数量
+     */
+    public String getStock(Long itemId) {
+        String key = ShardingUtil.getStockShardingKey(itemId);
+        return rt.opsForValue().get(key);
+    }
+    
+    /**
+     * 设置库存
+     * 
+     * @param itemId 商品ID
+     * @param stock 库存数量
+     */
+    public void setStock(Long itemId, Long stock) {
+        String key = ShardingUtil.getStockShardingKey(itemId);
+        rt.opsForValue().set(key, stock.toString());
+    }
+    
+    /**
+     * 检查库存是否存在
+     * 
+     * @param itemId 商品ID
+     * @return 是否存在库存
+     */
+    public boolean existsStock(Long itemId) {
+        String key = ShardingUtil.getStockShardingKey(itemId);
+        return Boolean.TRUE.equals(rt.hasKey(key));
+    }
+
+    /**
+     * 原子性设置库存(仅在键不存在时)
+     * 
+     * @param key 键
+     * @param value 值
+     * @param timeout 过期时间
+     * @param unit 时间单位
+     * @return 是否设置成功
+     */
+    public boolean setStockIfAbsent(String key, String value, long timeout, TimeUnit unit) {
+        return Boolean.TRUE.equals(rt.opsForValue().setIfAbsent(key, value, timeout, unit));
+    }
+}

+ 15 - 0
alien-second/src/main/java/shop/alien/second/seckill/dao/tempfile_1758866875150.mermaid

@@ -0,0 +1,15 @@
+graph TD
+    A[用户点击秒杀] --> B{系统是否启用新逻辑?}
+    B -->|否| C[直接执行原业务逻辑]
+    B -->|是| D{是否灰度用户?}
+    D -->|否| C
+    D -->|是| E[用户限流检查]
+    E --> F[获取排队许可]
+    F --> G[执行库存扣减]
+    G --> H{库存是否充足?}
+    H -->|否| I[释放排队许可, 返回库存不足]
+    H -->|是| J[创建订单令牌]
+    J --> K[执行业务逻辑]
+    K --> L{业务是否成功?}
+    L -->|是| M[秒杀成功]
+    L -->|否| N[回滚库存和许可

+ 39 - 0
alien-second/src/main/java/shop/alien/second/seckill/entity/SeckillOrderToken.java

@@ -0,0 +1,39 @@
+package shop.alien.second.seckill.entity;
+
+public class SeckillOrderToken {
+    private String orderNo;
+    private Long itemId;
+    private Long userId;
+
+    public SeckillOrderToken() {}
+
+    public SeckillOrderToken(String orderNo, Long itemId, Long userId) {
+        this.orderNo = orderNo;
+        this.itemId = itemId;
+        this.userId = userId;
+    }
+
+    public String getOrderNo() {
+        return orderNo;
+    }
+
+    public void setOrderNo(String orderNo) {
+        this.orderNo = orderNo;
+    }
+
+    public Long getItemId() {
+        return itemId;
+    }
+
+    public void setItemId(Long itemId) {
+        this.itemId = itemId;
+    }
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+}

+ 48 - 0
alien-second/src/main/java/shop/alien/second/seckill/job/StockSyncJob.java

@@ -0,0 +1,48 @@
+package shop.alien.second.seckill.job;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import shop.alien.second.seckill.service.StockSyncService;
+
+/**
+ * 库存同步定时任务
+ * 定期将Redis中的库存变化同步到数据库
+ */
+@Component
+public class StockSyncJob {
+    
+    private final StockSyncService stockSyncService;
+    
+    public StockSyncJob(StockSyncService stockSyncService) {
+        this.stockSyncService = stockSyncService;
+    }
+    
+    /**
+     * 每分钟执行一次库存同步
+     * 将Redis中的库存变化批量同步到数据库
+     */
+    @Scheduled(cron = "0 * * * * ?")
+    public void syncStock() {
+        try {
+            // 执行批量库存同步
+            stockSyncService.batchSyncStockToDatabase();
+            System.out.println("定时任务:库存同步完成");
+        } catch (Exception e) {
+            System.err.println("定时任务:库存同步失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 每小时执行一次深度库存校验
+     * 对比Redis和数据库中的库存数据,确保一致性
+     */
+    @Scheduled(cron = "0 0 * * * ?")
+    public void validateStock() {
+        System.out.println("定时任务:执行库存数据校验");
+        // 实际项目中应该实现库存数据校验逻辑
+        // 1. 从Redis获取当前库存
+        // 2. 从数据库获取当前库存
+        // 3. 对比两者数据是否一致
+        // 4. 如不一致,记录日志并触发告警
+    }
+}

+ 72 - 0
alien-second/src/main/java/shop/alien/second/seckill/mq/SeckillMessageConsumer.java

@@ -0,0 +1,72 @@
+package shop.alien.second.seckill.mq;
+
+import org.springframework.stereotype.Component;
+import shop.alien.second.seckill.entity.SeckillOrderToken;
+
+import javax.annotation.PostConstruct;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 秒杀消息消费者
+ * 用于处理秒杀成功消息和库存回滚消息
+ * 在实际项目中,这里应该集成RocketMQ或其他消息队列的消费者
+ */
+@Component
+public class SeckillMessageConsumer {
+    
+    // 模拟消息队列的接收
+    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+    
+    @PostConstruct
+    public void init() {
+        // 模拟定时处理消息
+        scheduler.scheduleAtFixedRate(this::processMessages, 0, 1, TimeUnit.SECONDS);
+    }
+    
+    /**
+     * 处理消息
+     * 模拟从消息队列接收消息并处理
+     */
+    private void processMessages() {
+        // 模拟处理秒杀成功消息
+        // 在实际项目中,这里应该是RocketMQ的@RocketMQMessageListener注解方法
+        System.out.println("处理秒杀成功消息...");
+        
+        // 模拟处理库存回滚消息(15分钟未支付的订单)
+        System.out.println("检查是否有超时未支付订单需要回滚库存...");
+    }
+    
+    /**
+     * 处理秒杀成功消息
+     * 
+     * @param token 秒杀订单令牌
+     */
+    public void handleSeckillSuccess(SeckillOrderToken token) {
+        System.out.println("处理秒杀成功消息: 订单号=" + token.getOrderNo() + 
+                          ", 商品ID=" + token.getItemId() + 
+                          ", 用户ID=" + token.getUserId());
+        
+        // 实际项目中应该在这里执行以下操作:
+        // 1. 创建订单记录到数据库
+        // 2. 更新用户购买记录
+        // 3. 发送订单确认通知
+    }
+    
+    /**
+     * 处理库存回滚消息
+     * 
+     * @param token 秒杀订单令牌
+     */
+    public void handleRollbackStock(SeckillOrderToken token) {
+        System.out.println("处理库存回滚消息: 订单号=" + token.getOrderNo() + 
+                          ", 商品ID=" + token.getItemId() + 
+                          ", 用户ID=" + token.getUserId());
+        
+        // 实际项目中应该在这里执行以下操作:
+        // 1. 检查订单状态是否已支付
+        // 2. 如果未支付,回滚库存
+        // 3. 更新订单状态为已取消
+    }
+}

+ 52 - 0
alien-second/src/main/java/shop/alien/second/seckill/mq/SeckillMessageProducer.java

@@ -0,0 +1,52 @@
+package shop.alien.second.seckill.mq;
+
+import org.springframework.stereotype.Component;
+import shop.alien.second.seckill.entity.SeckillOrderToken;
+
+/**
+ * 秒杀消息生产者
+ * 用于将秒杀成功事件发送到消息队列,实现异步处理
+ */
+@Component
+public class SeckillMessageProducer {
+    
+    /**
+     * 发送秒杀成功消息
+     * 在实际项目中,这里应该集成RocketMQ或其他消息队列
+     * 
+     * @param token 秒杀订单令牌
+     */
+    public void sendSeckillSuccessMessage(SeckillOrderToken token) {
+        // 模拟发送消息到消息队列
+        System.out.println("发送秒杀成功消息到MQ: " + token.getOrderNo());
+        
+        // 实际项目中应该是这样的代码:
+        /*
+        Message<SeckillOrderToken> message = MessageBuilder
+            .withPayload(token)
+            .setHeader("topic", "seckill-success")
+            .build();
+        rocketMQTemplate.send("seckill-success", message);
+        */
+    }
+    
+    /**
+     * 发送库存回滚消息
+     * 当订单超时未支付时,发送消息回滚库存
+     * 
+     * @param token 秒杀订单令牌
+     */
+    public void sendRollbackStockMessage(SeckillOrderToken token) {
+        // 模拟发送库存回滚消息
+        System.out.println("发送库存回滚消息到MQ: " + token.getOrderNo());
+        
+        // 实际项目中应该是这样的代码:
+        /*
+        Message<SeckillOrderToken> message = MessageBuilder
+            .withPayload(token)
+            .setHeader("topic", "seckill-rollback")
+            .build();
+        rocketMQTemplate.send("seckill-rollback", message);
+        */
+    }
+}

+ 48 - 0
alien-second/src/main/java/shop/alien/second/seckill/service/SeckillConfigService.java

@@ -0,0 +1,48 @@
+package shop.alien.second.seckill.service;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+@ConfigurationProperties(prefix = "seckill")
+public class SeckillConfigService {
+    /** 是否走新秒杀逻辑 */
+    private boolean cubeEnabled = true;
+    /** 灰度用户尾号 */
+    private String grayTail = "0,1";
+
+    public boolean isCubeEnabled() {
+        return cubeEnabled;
+    }
+
+    public void setCubeEnabled(boolean cubeEnabled) {
+        this.cubeEnabled = cubeEnabled;
+    }
+
+    public String getGrayTail() {
+        return grayTail;
+    }
+
+    public void setGrayTail(String grayTail) {
+        this.grayTail = grayTail;
+    }
+    
+    public boolean isGrayUser(Long userId) {
+        if (userId == null) {
+            return false;
+        }
+        
+        String userIdStr = String.valueOf(userId);
+        String lastDigit = userIdStr.substring(userIdStr.length() - 1);
+        
+        List<String> grayTails = Arrays.stream(grayTail.split(","))
+                .map(String::trim)
+                .collect(Collectors.toList());
+        
+        return grayTails.contains(lastDigit);
+    }
+}

+ 194 - 0
alien-second/src/main/java/shop/alien/second/seckill/service/StockInitializerService.java

@@ -0,0 +1,194 @@
+package shop.alien.second.seckill.service;
+
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Service;
+import shop.alien.second.seckill.dao.RedisStockDao;
+import shop.alien.second.seckill.util.ShardingUtil;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 库存初始化服务
+ * 用于将数据库中的实际库存初始化到Redis中
+ */
+@Service
+public class StockInitializerService {
+    
+    private final RedisStockDao stockDao;
+    private final RedissonClient redissonClient;
+    
+    public StockInitializerService(RedisStockDao stockDao, RedissonClient redissonClient) {
+        this.stockDao = stockDao;
+        this.redissonClient = redissonClient;
+    }
+    
+    /**
+     * 初始化商品库存
+     * 从数据库加载实际库存并设置到Redis中
+     * 
+     * @param itemId 商品ID
+     * @param initialStock 初始库存数量
+     */
+    public void initializeStock(Long itemId, Long initialStock) {
+        System.out.println("初始化商品 " + itemId + " 的库存到Redis: " + initialStock);
+        
+        // 在实际项目中,这里应该从数据库查询商品的实际库存
+        // Long actualStock = productStockMapper.selectStockByItemId(itemId);
+        // stockDao.setStock(itemId, actualStock);
+        
+        // 为了演示,我们直接使用传入的初始库存
+        stockDao.setStock(itemId, initialStock);
+    }
+    
+    /**
+     * 批量初始化商品库存
+     * 
+     * @param itemIds 商品ID列表
+     */
+    public void batchInitializeStock(Long[] itemIds) {
+        System.out.println("批量初始化商品库存到Redis");
+        
+        // 在实际项目中,这里应该从数据库批量查询商品的实际库存
+        // List<ProductStock> stocks = productStockMapper.selectStockByItemIds(itemIds);
+        // for (ProductStock stock : stocks) {
+        //     stockDao.setStock(stock.getItemId(), stock.getStock());
+        // }
+        
+        // 为了演示,我们为每个商品设置一个默认库存
+        for (Long itemId : itemIds) {
+            stockDao.setStock(itemId, 100L); // 默认100个库存
+        }
+    }
+    
+    /**
+     * 检查并初始化库存
+     * 如果Redis中没有库存数据,则从数据库加载
+     * 
+     * @param itemId 商品ID
+     * @return 当前库存数量
+     * 存在竞态问题,如 并发访问,可能多次重复初始化库存
+     * 数据一致性问题 分布式情况下,多个服务同时初始化,可能读取的库存数量可能不一致
+     */
+    public Long checkAndInitializeStock(Long itemId) {
+        // 先从Redis获取库存
+        String stockStr = stockDao.getStock(itemId);
+        
+        // 如果Redis中没有库存数据,则从数据库加载
+        if (stockStr == null) {
+            System.out.println("Redis中没有商品 " + itemId + " 的库存数据,从数据库加载");
+            
+            // 在实际项目中,这里应该从数据库查询商品的实际库存
+            // Long actualStock = productStockMapper.selectStockByItemId(itemId);
+            Long actualStock = 100L; // 模拟从数据库获取的库存
+            
+            // 将库存设置到Redis中
+            stockDao.setStock(itemId, actualStock);
+            
+            return actualStock;
+        }
+        
+        return Long.valueOf(stockStr);
+    }
+
+    /**
+     * 检查并初始化库存 V2版本
+     * 使用Redis的SETNX原子操作避免竞态条件
+     * 
+     * @param itemId 商品ID
+     * @return 当前库存数量
+     */
+    public Long checkAndInitializeStockV2(Long itemId) {
+        // 先从Redis获取库存
+        String stockStr = stockDao.getStock(itemId);
+        
+        // 如果Redis中没有库存数据,则从数据库加载
+        if (stockStr == null) {
+            // 使用Redis的SETNX原子操作避免竞态条件
+            String key = ShardingUtil.getStockShardingKey(itemId);
+            // 在实际项目中,这里应该从数据库查询商品的实际库存
+            // Long actualStock = productStockMapper.selectStockByItemId(itemId);
+            Long actualStock = 100L; // 模拟从数据库获取的库存(实际项目中应从数据库查询)
+            
+            // 使用SET命令的NX选项确保只有在键不存在时才设置
+            // 设置过期时间防止死锁
+            boolean success = stockDao.setStockIfAbsent(key, actualStock.toString(), 10, TimeUnit.SECONDS);
+            if (success) {
+                // 设置成功,说明当前线程完成了初始化
+                return actualStock;
+            } else {
+                // 设置失败,说明其他线程已经初始化,直接重新获取
+                stockStr = stockDao.getStock(itemId);
+                if (stockStr != null) {
+                    return Long.valueOf(stockStr);
+                }
+                // 如果还是获取不到,可能是初始化过程中出现了问题,再次尝试初始化
+                return checkAndInitializeStock(itemId);
+            }
+        }
+        
+        return Long.valueOf(stockStr);
+    }
+    
+    /**
+     * 检查并初始化库存 V3版本
+     * 使用Redisson分布式锁避免竞态条件
+     * 
+     * @param itemId 商品ID
+     * @return 当前库存数量
+     */
+    public Long checkAndInitializeStockV3(Long itemId) {
+        // 先从Redis获取库存
+        String stockStr = stockDao.getStock(itemId);
+        
+        // 如果Redis中没有库存数据,则从数据库加载
+        if (stockStr == null) {
+            // 使用分布式锁确保同一时间只有一个线程初始化库存
+            String lockKey = "seckill:stock:init:lock:" + itemId;
+            RLock lock = redissonClient.getLock(lockKey);
+            
+            try {
+                // 尝试获取锁,使用更合理的超时参数
+                // 等待时间根据系统负载动态调整,持有锁时间足够完成初始化操作
+                long waitTime = Math.min(100 + itemId % 100, 1000); // 100-1000ms动态等待时间
+                long leaseTime = 5000; // 5秒锁租期,由Redisson看门狗自动续期
+                
+                if (lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS)) {
+                    // 双重检查,确保其他线程没有已经初始化
+                    stockStr = stockDao.getStock(itemId);
+                    if (stockStr != null) {
+                        return Long.valueOf(stockStr);
+                    }
+                    
+                    // 从数据库加载库存
+                    System.out.println("Redis中没有商品 " + itemId + " 的库存数据,从数据库加载");
+                    // 实际项目中应该从数据库查询商品的实际库存
+                    Long actualStock = 100L; // 模拟从数据库获取的库存
+                    
+                    // 将库存设置到Redis中
+                    stockDao.setStock(itemId, actualStock);
+                    return actualStock;
+                } else {
+                    // 获取锁失败,直接重新检查
+                    stockStr = stockDao.getStock(itemId);
+                    if (stockStr != null) {
+                        return Long.valueOf(stockStr);
+                    }
+                    // 如果还是获取不到,再次尝试初始化
+                    return checkAndInitializeStock(itemId);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("初始化库存被中断", e);
+            } finally {
+                // Redisson会自动通过看门狗机制续期,无需手动处理解锁
+                // 仅在当前线程持有锁的情况下才解锁
+                if (lock.isHeldByCurrentThread()) {
+                    lock.unlock();
+                }
+            }
+        }
+        
+        return Long.valueOf(stockStr);
+    }
+}

+ 101 - 0
alien-second/src/main/java/shop/alien/second/seckill/service/StockSyncService.java

@@ -0,0 +1,101 @@
+package shop.alien.second.seckill.service;
+
+import org.springframework.stereotype.Service;
+import shop.alien.second.seckill.dao.RedisStockDao;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 库存同步服务
+ * 用于将Redis中的库存变化同步到数据库中
+ * 采用缓冲+批量同步的策略,提高同步效率
+ */
+@Service
+public class StockSyncService {
+    
+    private final RedisStockDao stockDao;
+    
+    // 模拟存储库存变化的缓冲区
+    private final ConcurrentHashMap<Long, AtomicInteger> stockChanges = new ConcurrentHashMap<>();
+    
+    public StockSyncService(RedisStockDao stockDao) {
+        this.stockDao = stockDao;
+    }
+    
+    /**
+     * 记录库存变化
+     * 将库存变化记录到缓冲区中,等待批量同步
+     * 
+     * @param itemId 商品ID
+     * @param quantity 变化数量(正数表示增加,负数表示减少)
+     */
+    public void recordStockChange(Long itemId, Integer quantity) {
+        stockChanges.computeIfAbsent(itemId, k -> new AtomicInteger(0))
+                   .addAndGet(quantity);
+    }
+    
+    /**
+     * 同步库存到数据库
+     * 这个方法通常由定时任务或消息队列触发
+     * 
+     * @param itemId 商品ID
+     * @param quantity 变化数量(正数表示增加,负数表示减少)
+     */
+    public void syncStockToDatabase(Long itemId, Integer quantity) {
+        // 实际项目中应该连接数据库执行更新操作
+        System.out.println("同步商品 " + itemId + " 的库存变化 " + quantity + " 到数据库");
+        
+        // 模拟数据库操作
+        try {
+            // 这里应该执行数据库更新操作
+            // update product_stock set stock = stock + ? where item_id = ?
+            
+            // 同时记录日志
+            // insert into stock_sync_log (item_id, change_quantity, sync_time) values (?, ?, now())
+        } catch (Exception e) {
+            System.err.println("同步库存到数据库失败: " + e.getMessage());
+            // 实际项目中应该将失败的记录保存到失败队列中,以便重试
+        }
+    }
+    
+    /**
+     * 批量同步库存到数据库
+     * 用于定时任务批量处理库存变化
+     */
+    public void batchSyncStockToDatabase() {
+        System.out.println("开始批量同步库存到数据库");
+        
+        // 实际项目中应该从缓冲区获取所有变化的记录,批量更新数据库
+        try {
+            // 模拟批量同步过程
+            for (Long itemId : stockChanges.keySet()) {
+                AtomicInteger change = stockChanges.get(itemId);
+                if (change != null && change.get() != 0) {
+                    int quantity = change.getAndSet(0); // 获取并清零
+                    if (quantity != 0) {
+                        System.out.println("批量同步商品 " + itemId + " 的库存变化 " + quantity + " 到数据库");
+                        // 实际项目中应该执行数据库批量更新操作
+                        syncStockToDatabase(itemId, quantity);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("批量同步库存到数据库失败: " + e.getMessage());
+            // 实际项目中应该记录失败的日志,以便后续重试
+        }
+        
+        System.out.println("批量同步库存到数据库完成");
+    }
+    
+    /**
+     * 获取当前记录的库存变化
+     * 
+     * @param itemId 商品ID
+     * @return 库存变化数量
+     */
+    public int getStockChange(Long itemId) {
+        AtomicInteger change = stockChanges.get(itemId);
+        return change != null ? change.get() : 0;
+    }
+}

+ 119 - 0
alien-second/src/main/java/shop/alien/second/seckill/util/ShardingUtil.java

@@ -0,0 +1,119 @@
+package shop.alien.second.seckill.util;
+
+/**
+ * 分片工具类
+ * 用于实现热点数据的分片处理,避免单点热点问题
+ */
+public class ShardingUtil {
+    
+    // 分片总数
+    private static final int SHARDING_COUNT = 128;
+    // 随机因子,用于优化分片分布
+    private static final long RANDOM_FACTOR = 0x9e3779b97f4a7c15L; // 来自Knuth的乘法哈希
+    // 基础分片数
+    private static final int BASE_SHARDING_COUNT = 16;
+    // 最大分片数
+    private static final int MAX_SHARDING_COUNT = 128;
+    // 分片增长阈值
+    private static final int SHARDING_GROWTH_THRESHOLD = 1000;
+
+    /**
+     * 根据商品ID计算分片索引
+     * 
+     * @param itemId 商品ID
+     * @return 分片索引
+     */
+    public static int getShardIndex(Long itemId, long totalItems) {
+        if (itemId == null) {
+            return 0;
+        }
+        // 根据商品总数动态调整分片数
+        int dynamicShardingCount = calculateDynamicShardingCount(totalItems);
+
+        // 使用商品ID对分片总数取模,得到分片索引
+//        return (int) (Math.abs(itemId) % SHARDING_COUNT);
+        // 使用异或操作和乘法哈希优化分片分布
+        long hashedId = Math.abs(itemId) ^ RANDOM_FACTOR;
+        hashedId = hashedId * RANDOM_FACTOR;
+        return (int) ((hashedId >> 32) % dynamicShardingCount);
+
+    }
+
+
+    /**
+     * 根据商品总数计算动态分片数
+     *
+     * @param totalItems 商品总数
+     * @return 动态分片数
+     */
+    private static int calculateDynamicShardingCount(long totalItems) {
+        if (totalItems < SHARDING_GROWTH_THRESHOLD) {
+            return BASE_SHARDING_COUNT;
+        }
+
+        // 根据商品数量动态计算分片数,最大不超过MAX_SHARDING_COUNT
+        int calculatedCount = (int) Math.min(
+                BASE_SHARDING_COUNT * (totalItems / SHARDING_GROWTH_THRESHOLD),
+                MAX_SHARDING_COUNT
+        );
+
+        // 确保分片数是2的幂次方,提高Redis性能
+        return roundUpToPowerOfTwo(calculatedCount);
+    }
+
+    /**
+     * 向上取整到最近的2的幂次方
+     *
+     * @param value 值
+     * @return 最近的2的幂次方
+     */
+    private static int roundUpToPowerOfTwo(int value) {
+        if (value <= 1) return 1;
+        int power = 1;
+        while (power < value) {
+            power <<= 1;
+        }
+        return Math.min(power, MAX_SHARDING_COUNT);
+    }
+
+    // 兼容旧方法
+    public static int getShardIndex(Long itemId) {
+        return getShardIndex(itemId, MAX_SHARDING_COUNT);
+    }
+
+
+    /**
+     * 获取分片键
+     * 
+     * @param itemId 商品ID
+     * @param prefix 键前缀
+     * @return 分片键
+     */
+    public static String getShardingKey(Long itemId, String prefix) {
+        if (itemId == null) {
+            return prefix + ":0";
+        }
+        // 构造分片键:前缀:分片索引:商品ID
+        return prefix + ":" + getShardIndex(itemId) + ":" + itemId;
+    }
+    
+    /**
+     * 获取库存分片键
+     * 
+     * @param itemId 商品ID
+     * @return 库存分片键
+     */
+    public static String getStockShardingKey(Long itemId) {
+        return getShardingKey(itemId, "seckill:stock");
+    }
+    
+    /**
+     * 获取订单令牌分片键
+     * 
+     * @param itemId 商品ID
+     * @return 订单令牌分片键
+     */
+    public static String getOrderTokenShardingKey(Long itemId) {
+        return getShardingKey(itemId, "seckill:orderToken");
+    }
+}

+ 37 - 0
alien-second/src/main/java/shop/alien/second/seckill/util/UserContext.java

@@ -0,0 +1,37 @@
+package shop.alien.second.seckill.util;
+
+import shop.alien.second.seckill.entity.SeckillOrderToken;
+
+public class UserContext {
+    private static final ThreadLocal<Long> userIdHolder = new ThreadLocal<>();
+    private static final ThreadLocal<SeckillOrderToken> orderTokenHolder = new ThreadLocal<>();
+
+    public static void setUserId(Long userId) {
+        userIdHolder.set(userId);
+    }
+
+    public static Long getUserId() {
+        return userIdHolder.get();
+    }
+
+    public static void clearUserId() {
+        userIdHolder.remove();
+    }
+
+    public static void setOrderToken(SeckillOrderToken token) {
+        orderTokenHolder.set(token);
+    }
+
+    public static SeckillOrderToken getOrderToken() {
+        return orderTokenHolder.get();
+    }
+
+    public static void clearOrderToken() {
+        orderTokenHolder.remove();
+    }
+
+    public static void clearAll() {
+        userIdHolder.remove();
+        orderTokenHolder.remove();
+    }
+}

+ 7 - 0
alien-second/src/main/java/shop/alien/second/service/SecondGoodsService.java

@@ -189,6 +189,13 @@ public interface SecondGoodsService extends IService<SecondGoods> {
     SecondGoodsRecordDetailVo getAdminGoodsRecordDetail(Integer recordId);
 
     /**
+     * 下架商品
+     * @param secondGoods 商品信息
+     * @return 是否下架成功
+     */
+    boolean shelveSecondGoods(SecondGoodsVo secondGoods);
+
+    /**
      * 处理商品信息(管理后台使用)
      * @param goodsId 商品ID
      * @return 处理后的商品详情信息

+ 102 - 19
alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java

@@ -18,6 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 import shop.alien.entity.SecondVideoTask;
 import shop.alien.entity.second.*;
@@ -634,27 +635,40 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
      * 创建商品审核记录
      * @param goods 商品信息
      */
+//    @Transactional(rollbackFor = Exception.class)
     private void approveAndListGoods(SecondGoods goods) {
-        // 如果所有审核都通过,设置为上架状态
-        goods.setGoodsStatus(SecondGoodsStatusEnum.LISTED.getCode()); // 上架
-        goods.setFailedReason("");
-        goods.setReleaseTime(new Date()); // 上架时间
-        updateById(goods);
-        // 插入审核记录
-        createGoodsAudit(goods, "", Constants.AuditStatus.PASSED);
-        // 发送审核成功消息
-        sendMessage(goods);
-        // 上架 记录商品操作历史
-        String operationName = "";
-        QueryWrapper<SecondGoodsRecord> queryWrapper = new QueryWrapper<>();
-        queryWrapper.eq("goods_id", goods.getId());
-        List<SecondGoodsRecord> recordList = secondGoodsRecordMapper.selectList(queryWrapper);
-        if (CollectionUtil.isNotEmpty(recordList)){
-            operationName = "重新发布";
-        }else {
-            operationName = "首次发布";
+        boolean isNotified = false;
+        try {
+            // 如果所有审核都通过,设置为上架状态
+            goods.setGoodsStatus(SecondGoodsStatusEnum.LISTED.getCode()); // 上架
+            goods.setFailedReason("");
+            goods.setReleaseTime(new Date()); // 上架时间
+            updateById(goods);
+            // 插入审核记录
+            createGoodsAudit(goods, "", Constants.AuditStatus.PASSED);
+            // 发送审核成功消息
+            sendMessage(goods);
+            isNotified = true; // 标记通知已发送
+            // 上架 记录商品操作历史
+            String operationName = "";
+            QueryWrapper<SecondGoodsRecord> queryWrapper = new QueryWrapper<>();
+            queryWrapper.eq("goods_id", goods.getId());
+            List<SecondGoodsRecord> recordList = secondGoodsRecordMapper.selectList(queryWrapper);
+            if (CollectionUtil.isNotEmpty(recordList)){
+                operationName = "重新发布";
+            }else {
+                operationName = "首次发布";
+            }
+            recordGoodsOperation(goods, operationName);
+        } catch (Exception e) {
+            log.error("商品上架过程中发生异常,执行回滚", e);
+            // 如果通知已发送但后续操作失败,需要补偿
+            if (isNotified) {
+                // 发送补偿消息,比如撤销通知或标记为异常状态
+//                sendCompensationMessage(goods);
+            }
+            throw e;
         }
-        recordGoodsOperation(goods, operationName);
     }
 
     /**
@@ -1765,4 +1779,73 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
         
         return result;
     }
+
+    /**
+     * 下架商品
+     * @param secondGoods 商品信息
+     * @return 是否下架成功
+     */
+    @Override
+    public boolean shelveSecondGoods(SecondGoodsVo secondGoods) {
+        log.info("SecondGoodsServiceImpl.shelveSecondGoods?secondGoods={}", secondGoods.toString());
+        // 修改商品状态为4 - 已下架
+        secondGoods.setGoodsStatus(4);
+        boolean updateResult = updateById(secondGoods);
+        if (updateResult) {
+            // 获取最新的商品信息并记录操作历史
+            SecondGoods updatedGoods = getById(secondGoods.getId());
+            recordGoodsOperation(updatedGoods, "下架商品");
+            // 发送系统通知
+            sendShelveMessage(updatedGoods);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+
+    /**
+     * 发送商品下架消息
+     * @param goods 商品信息
+     */
+    private void sendShelveMessage(SecondGoods goods) {
+        try {
+            // 根据 goods.getUserId() 获取用户信息
+            LifeUser lifeUser = lifeUserMapper.selectById(goods.getUserId());
+            String phone = lifeUser.getUserPhone();
+            // 调取feign接口 发送消息 life_notice表
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setSenderId("system");
+            lifeNotice.setReceiverId("user_"+ phone);
+            lifeNotice.setBusinessId(goods.getId());
+            lifeNotice.setTitle("商品下架通知");
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("goodsId", goods.getId());
+//            jsonObject.put("status", "true");
+            jsonObject.put("message", "您的商品,商品标题:"+ goods.getTitle() + "已下架");
+            lifeNotice.setContext(jsonObject.toJSONString());
+            lifeNotice.setNoticeType(Constants.Notice.SYSTEM_NOTICE); // 系统通知
+            lifeNotice.setIsRead(0);
+            lifeNoticeMapper.insert(lifeNotice);
+            sendShelveNotice("user_"+ phone, lifeNotice);
+        } catch (Exception e) {
+            log.error("发送消息通知失败,goods: {}", goods, e);
+        }
+    }
+
+    private void sendShelveNotice(String receiverId, LifeNotice lifeNotice) {
+        try {
+            WebSocketVo webSocketVo = new WebSocketVo();
+            webSocketVo.setSenderId("system");
+            webSocketVo.setReceiverId(receiverId);
+            webSocketVo.setCategory("notice");
+            webSocketVo.setNoticeType("1");
+            webSocketVo.setIsRead(0);
+            webSocketVo.setText(JSONObject.from(lifeNotice).toJSONString());
+            alienStoreFeign.sendMsgToClientByPhoneId(receiverId, JSONObject.from(webSocketVo).toJSONString());
+        } catch (Exception e) {
+            log.error("发送消息通知失败,receiverId: {}", receiverId, e);
+        }
+    }
+
 }

+ 137 - 0
alien-second/src/main/java/shop/alien/second/service/impl/SecurityRuleServiceImpl.java

@@ -0,0 +1,137 @@
+//package shop.alien.second.service.impl;
+//
+//import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+//import lombok.RequiredArgsConstructor;
+//import lombok.extern.slf4j.Slf4j;
+//import org.kie.api.KieBase;
+//import org.kie.api.runtime.KieSession;
+//import org.springframework.scheduling.annotation.Scheduled;
+//import org.springframework.stereotype.Service;
+//import shop.alien.entity.second.SecurityRule;
+//import shop.alien.entity.second.vo.SecurityCtx;
+//import shop.alien.entity.second.vo.fact.*;
+//import shop.alien.mapper.second.SecurityRuleMapper;
+//import shop.alien.second.config.RuleLoaderUtil;
+//
+//import java.util.List;
+//
+///**
+// * 负责:
+// * 1. 定时拉取 MySQL → KieBase(缓存)
+// * 2. 对外暴露评分接口
+// */
+//@Slf4j
+//@Service
+//@RequiredArgsConstructor
+//public class SecurityRuleServiceImpl {
+//
+//    private final SecurityRuleMapper ruleMapper;
+//
+//    // 本地缓存 KieBase,volatile 保证可见性
+//    private volatile KieBase kieBase;
+//
+//
+//    /**
+//     * 对外暴露的评分接口
+//     */
+//    public double evaluate(SecurityCtx ctx,
+//                        AccountFact account,
+//                        ItemFact item,
+//                        LogisticsFact logistics,
+//                        TradeFact trade) {
+//
+//        // 1. 从数据库拉取启用的规则
+//        QueryWrapper<SecurityRule> query = new QueryWrapper<>();
+//        query.eq("enabled", true);
+//        List<SecurityRule> rules = ruleMapper.selectList(query);
+//
+//        // 2. 动态编译 KieBase
+//        KieBase kieBase = RuleLoaderUtil.loadKieBase(rules);
+//
+//        // 3. 创建会话并设置全局变量
+//        KieSession kieSession = kieBase.newKieSession();
+//        kieSession.setGlobal("ctx", ctx);
+//
+//        // 4. 插入所有维度的事实
+//        kieSession.insert(account);
+//        kieSession.insert(item);
+//        kieSession.insert(logistics);
+//        kieSession.insert(trade);
+//
+//        // 5. 触发规则
+//        kieSession.fireAllRules();
+//        kieSession.dispose();
+//
+//        // 6. 返回本次扣减总分
+//        return ctx.total();
+//    }
+//
+//    public int score(SecurityCtx ctx, List<FactWrapper> facts) {
+//        // 1. 从数据库拉取启用的规则
+//        QueryWrapper<SecurityRule> query = new QueryWrapper<>();
+//        query.eq("enabled", true);
+//        List<SecurityRule> rules = ruleMapper.selectList(query);
+//        KieBase kieBase = RuleLoaderUtil.loadKieBase(rules);
+//
+//        KieSession ks = kieBase.newKieSession();
+//        ks.setGlobal("ctx", ctx);
+//        facts.forEach(ks::insert); // 动态插入任意维度
+//        ks.fireAllRules();
+//        ks.dispose();
+//        return ctx.getScoreDeduct();
+//    }
+//
+//
+//    /* ===================== 对外评分 API ===================== */
+//
+//    /**
+//     * 计算安全分
+//     * @param listingId 商品 id,仅用于日志
+//     * @param facts 可变长度事实对象(ItemFact、TradeFact...)
+//     * @return 0-100,越高越安全
+//     */
+//    public double score(long listingId, Object... facts) {
+//        if (kieBase == null) {
+//            refresh(); // 第一次兜底
+//        }
+//        SecurityCtx ctx = new SecurityCtx(listingId);
+//        KieSession session = kieBase.newKieSession();
+//        try {
+//            session.setGlobal("ctx", ctx);
+//            for (Object f : facts) session.insert(f);
+//            session.fireAllRules();
+//            return ctx.total();
+//        } finally {
+//            session.dispose();
+//        }
+//    }
+//
+//    /* ===================== 规则热刷新 ===================== */
+//
+//    /**
+//     * 每 30 秒增量刷新规则;无变更则跳过
+//     * 结果缓存在本地内存,方法结束即释放 KieBase 旧引用
+//     */
+//    @Scheduled(fixedDelay = 30_000)
+//    public void refresh() {
+//        List<SecurityRule> rules = ruleMapper.selectList(
+//                new QueryWrapper<SecurityRule>()
+//                        .eq("enabled", true)
+//                        .orderByDesc("priority")
+//        );
+//        if (rules.isEmpty()) {
+//            log.warn("MySQL 无可用安全规则");
+//            return;
+//        }
+//        KieBase newBase = RuleLoaderUtil.loadKieBase(rules);
+//        this.kieBase = newBase;  // 原子替换
+//        log.info("安全规则热刷新完成,共 {} 条", rules.size());
+//    }
+//
+//    /**
+//     * 管理后台手动触发
+//     */
+//    public void forceRefresh() {
+//        refresh();
+//    }
+//}

+ 3 - 0
alien-second/src/main/resources/application-seckill.yml

@@ -0,0 +1,3 @@
+seckill:
+  cubeEnabled: true
+  grayTail: "0,1,2,3,4,5,6,7,8,9"

+ 28 - 0
alien-second/src/main/resources/lua/stockDecr.lua

@@ -0,0 +1,28 @@
+--  秒杀库存扣减Lua脚本
+--  通过Lua脚本保证库存扣减与订单令牌生成的原子性操作
+--  支持热点数据分片,避免单点热点问题
+--
+--  KEYS[1] 库存 key          seckill:stock:{shardIndex}:{itemId}
+--  KEYS[2] 订单令牌 key       seckill:orderToken:{shardIndex}:{itemId}
+--  ARGV[1] 本次扣减数量       1
+--  ARGV[2] 订单号            210618000123
+--  ARGV[3] 过期时间(s)       900(15 min)
+
+-- 获取当前库存数量
+local stock = redis.call('GET', KEYS[1])
+
+-- 检查库存是否存在,以及库存是否充足
+if not stock or tonumber(stock) < tonumber(ARGV[1]) then
+    -- 库存不足,返回-1表示失败
+    return -1
+end
+
+-- 扣减库存:将库存键对应的值减去指定数量
+redis.call('DECRBY', KEYS[1], ARGV[1])
+
+-- 生成订单令牌:将订单号存储到订单令牌键中,并设置过期时间
+-- 这样可以防止重复下单,同时在过期后自动清理
+redis.call('SETEX', KEYS[2] .. ':' .. ARGV[2], ARGV[3], ARGV[2])
+
+-- 返回扣减后的库存数量
+return tonumber(redis.call('GET', KEYS[1]))

+ 178 - 0
alien-second/src/main/resources/seckill_system.html

@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8"/>
+    <title>秒杀系统时序图</title>
+    <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
+    <style>
+        html,body{margin:0;height:100%;background:#f5f5f5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;}
+        .wrap{display:flex;flex-direction:column;height:100%;}
+        .toolbar{flex:0 0 50px;display:flex;align-items:center;justify-content:center;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.08);}
+        .toolbar button{margin:0 6px;padding:6px 14px;border:0;border-radius:4px;background:#2196f3;color:#fff;cursor:pointer;}
+        .viewport{flex:1;overflow:hidden;position:relative;background:#fafafa;}
+        #mermaid-svg{position:absolute;top:0;left:0;cursor:grab;transition:none;}
+        #mermaid-svg.grabbing{cursor:grabbing;}
+    </style>
+</head>
+<body>
+<div class="wrap">
+    <div class="toolbar">
+        <button onclick="zoom(0.9)">缩小</button>
+        <button onclick="zoom(1.1)">放大</button>
+        <button onclick="reset()">重置</button>
+        <button onclick="toggleDrag()">拖拽开关</button>
+    </div>
+    <div class="viewport" id="viewport">
+        <div id="mermaid-svg"></div>
+    </div>
+</div>
+
+<script>
+    /* ---------- 1. 渲染时序图 ---------- */
+    mermaid.initialize({
+        startOnLoad: false,
+        theme: 'default',
+        sequence: { diagramMarginX: 50, diagramMarginY: 10, actorMargin: 50, width: 150, height: 65 }
+    });
+    const code = `
+sequenceDiagram
+    participant U as 用户
+    participant SC as SeckillController
+    participant SGA as SeckillGuardAspect
+    participant UC as UserContext
+    participant RS as RedissonClient
+    participant RSD as RedisStockDao
+    participant LU as Lua脚本
+    participant SMP as SeckillMessageProducer
+    participant SSS as StockSyncService
+    participant SIS as StockInitializerService
+    participant SJ as StockSyncJob
+    participant SMC as SeckillMessageConsumer
+
+    U->>SC: 发起秒杀请求
+    SC->>SGA: 调用被@SeckillGuard注解的方法
+    SGA->>SGA: 检查系统配置和灰度规则
+    alt 非灰度用户或未启用新逻辑
+        SGA->>SC: 降级到旧逻辑
+        SC-->>U: 返回旧逻辑处理结果
+    else 灰度用户且启用新逻辑
+        SGA->>RS: 用户限流检查(RAtomicLong.incrementAndGet)
+        RS-->>SGA: 返回用户访问次数
+        alt 超过用户限制
+            SGA-->>U: 抛出用户限流异常
+        else 未超过用户限制
+            SGA->>RS: 获取信号量许可(RSemaphore.tryAcquire)
+            RS-->>SGA: 返回许可获取结果
+            alt 未获取到许可(系统繁忙)
+                SGA-->>U: 抛出系统繁忙异常
+            else 获取到许可
+                SGA->>RSD: 检查库存是否存在(existsStock)
+                RSD-->>SGA: 返回库存存在状态
+                alt 库存不存在
+                    SGA->>SIS: 初始化库存(checkAndInitializeStock)
+                    SIS-->>SGA: 返回初始化结果
+                end
+                SGA->>UC: 生成订单号(UUID)
+                SGA->>RSD: 执行库存扣减(decrStock)
+                RSD->>LU: 执行Lua脚本
+                LU-->>RSD: 返回剩余库存
+                RSD-->>SGA: 返回扣减结果
+                alt 库存不足
+                    SGA->>RS: 释放信号量许可
+                    SGA-->>U: 抛出库存不足异常
+                else 库存充足
+                    SGA->>UC: 设置订单Token(setOrderToken)
+                    SGA->>SC: 执行业务逻辑(proceed)
+                    SC-->>SGA: 返回业务执行结果
+                    alt 业务执行成功
+                        SGA->>SMP: 发送秒杀成功消息
+                        SGA->>SMP: 发送库存回滚消息
+                        SGA->>SSS: 记录库存变化
+                        SGA->>RS: 释放信号量许可
+                        SGA-->>U: 返回秒杀成功结果
+                    else 业务执行失败
+                        SGA->>RSD: 回滚库存(incrStock)
+                        SGA->>RS: 释放信号量许可
+                        SGA-->>U: 抛出业务异常
+                    end
+                end
+            end
+        end
+    end
+`;
+    mermaid.render('mermaid-svg', code).then(({ svg }) => {
+        document.getElementById('mermaid-svg').innerHTML = svg;
+        reset();          // 初始化居中
+    });
+
+    /* ---------- 2. 无溢出缩放 + 丝滑拖拽 ---------- */
+    let scale = 1, translateX = 0, translateY = 0;
+    let dragOn = false, dragging = false, startX, startY;
+
+    const svgRoot = () => document.querySelector('#mermaid-svg svg');
+    const viewport = document.getElementById('viewport');
+
+    function applyTransform() {
+        const el = svgRoot();
+        if (!el) return;
+        el.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
+        el.style.transformOrigin = '0 0';
+    }
+    function zoom(factor, anchorX, anchorY) {
+        const el = svgRoot();
+        if (!el) return;
+        const rect = el.getBoundingClientRect();
+        anchorX = anchorX ?? rect.left + rect.width / 2;
+        anchorY = anchorY ?? rect.top + rect.height / 2;
+
+        const newScale = Math.max(0.3, Math.min(5, scale * factor));
+        const dx = (anchorX - rect.left) * (1 - newScale / scale);
+        const dy = (anchorY - rect.top) * (1 - newScale / scale);
+        scale = newScale;
+        translateX += dx;
+        translateY += dy;
+        applyTransform();
+    }
+    function reset() {
+        scale = 1; translateX = 0; translateY = 0;
+        const el = svgRoot();
+        if (!el) return;
+        // 居中
+        const vRect = viewport.getBoundingClientRect();
+        const sRect = el.getBoundingClientRect();
+        translateX = (vRect.width - sRect.width) / 2;
+        translateY = 20;
+        applyTransform();
+    }
+    function toggleDrag() {
+        dragOn = !dragOn;
+        viewport.style.cursor = dragOn ? 'grab' : 'default';
+    }
+
+    /* 滚轮缩放以鼠标位置为锚点 */
+    viewport.addEventListener('wheel', e => {
+        e.preventDefault();
+        zoom(e.deltaY < 0 ? 1.1 : 0.9, e.clientX, e.clientY);
+    });
+
+    /* 拖拽 */
+    viewport.addEventListener('mousedown', e => {
+        if (!dragOn) return;
+        dragging = true;
+        startX = e.clientX - translateX;
+        startY = e.clientY - translateY;
+        svgRoot()?.classList.add('grabbing');
+    });
+    window.addEventListener('mousemove', e => {
+        if (!dragging) return;
+        translateX = e.clientX - startX;
+        translateY = e.clientY - startY;
+        applyTransform();
+    });
+    window.addEventListener('mouseup', () => {
+        dragging = false;
+        svgRoot()?.classList.remove('grabbing');
+    });
+</script>
+</body>
+</html>

+ 5 - 0
alien-util/pom.xml

@@ -323,6 +323,11 @@
             <version>3.17.4</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-context</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 2 - 0
alien-util/src/main/java/shop/alien/util/common/safe/ImageModerationUtil.java

@@ -8,6 +8,7 @@ import com.aliyun.teautil.models.RuntimeOptions;
 import com.google.common.collect.Lists;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
@@ -18,6 +19,7 @@ import java.util.UUID;
 
 @Slf4j
 @Component
+@RefreshScope
 public class ImageModerationUtil {
 
     @Value("${ali.yundun.accessKeyID}")

+ 2 - 0
alien-util/src/main/java/shop/alien/util/common/safe/TextModerationUtil.java

@@ -9,6 +9,7 @@ import com.aliyun.teautil.models.RuntimeOptions;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.google.common.collect.Lists;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.stereotype.Component;
 
 import java.util.List;
@@ -16,6 +17,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @Component
+@RefreshScope
 public class TextModerationUtil {
 
 

+ 2 - 0
alien-util/src/main/java/shop/alien/util/common/safe/video/VideoModerationUtil.java

@@ -7,12 +7,14 @@ import com.aliyun.green20220302.models.*;
 import com.aliyun.teaopenapi.models.Config;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.PostConstruct;
 
 @Slf4j
 @Component
+@RefreshScope
 public class VideoModerationUtil {
 
     @Value("${ali.yundun.accessKeyID}")