14 Комити 99bb2282c4 ... 93bcfead67

Аутор SHA1 Порука Датум
  yaodaoseng 93bcfead67 udpate пре 2 недеља
  yaodaoseng c15f11bc23 udpate пре 2 недеља
  yaodaoseng 50d970c580 udpate пре 2 недеља
  yaodaoseng 356c97b53f udpate пре 2 недеља
  yaodaoseng 4d8a6ca239 udpate пре 2 недеља
  yaodaoseng 7257735679 企业黑名单同步 пре 2 недеља
  yaodaoseng 7cc26db38a 企业黑名单同步 пре 2 недеља
  yaodaoseng f8197cfa4d 企业黑名单同步 пре 2 недеља
  yaodaoseng c5e216c54f 企业黑名单同步 пре 2 недеља
  yaodaoseng 812801d301 企业黑名单同步 пре 2 недеља
  yaodaoseng 7ddcafcec6 企业黑名单同步 пре 2 недеља
  yaodaoseng fa1961c5c1 企业黑名单 пре 2 недеља
  yaodaoseng b60b636269 企业黑名单 пре 2 недеља
  yaodaoseng 9c6b1397d0 企业黑名单 пре 2 недеља
26 измењених фајлова са 3104 додато и 22 уклоњено
  1. 78 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/dao/mapper/QywxCorpBlacklistUserMapper.java
  2. 89 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/job/BlacklistSyncJobHandler.java
  3. 107 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/model/po/QywxCorpBlacklistUser.java
  4. 223 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/model/po/QywxCorpBlacklistUserExample.java
  5. 185 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/model/qywx/RoomBlacklistResponse.java
  6. 65 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/CorpBlacklistService.java
  7. 220 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/impl/CorpBlacklistServiceImpl.java
  8. 9 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/impl/RiskUserHandleService.java
  9. 113 2
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/impl/RiskUserOperateService.java
  10. 1 1
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/qywx/Constant.java
  11. 34 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/BlacklistSyncService.java
  12. 209 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/cache/RedisSyncTimeCache.java
  13. 141 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/config/ApolloConfigService.java
  14. 143 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/config/SyncConfig.java
  15. 58 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/data/BlacklistDataService.java
  16. 173 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/data/impl/BlacklistDataServiceImpl.java
  17. 258 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/impl/BlacklistSyncServiceImpl.java
  18. 150 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/lock/RedisDistributedLockService.java
  19. 154 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/strategy/SyncDecision.java
  20. 189 0
      risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/strategy/SyncTimeStrategy.java
  21. 218 0
      risk-control-core/src/main/resources/mapper/QywxCorpBlacklistUserMapper.xml
  22. 203 0
      risk-control-server/src/main/java/com/tzld/piaoquan/risk/control/controller/BlacklistSyncController.java
  23. 72 0
      risk-control-server/src/main/java/com/tzld/piaoquan/risk/control/controller/CorpBlacklistController.java
  24. 1 5
      risk-control-server/src/main/java/com/tzld/piaoquan/risk/control/controller/ReceiveRiskInfoController.java
  25. 1 0
      risk-control-server/src/main/resources/application.yml
  26. 10 14
      risk-control-server/src/main/resources/logback-spring.xml

+ 78 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/dao/mapper/QywxCorpBlacklistUserMapper.java

@@ -0,0 +1,78 @@
+package com.tzld.piaoquan.risk.control.dao.mapper;
+
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser;
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUserExample;
+import java.util.Date;
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 企业微信客户主体黑名单用户数据访问接口
+ * 
+ * @author MyBatis Generator
+ * @since 1.0.0
+ */
+public interface QywxCorpBlacklistUserMapper {
+    
+    /**
+     * 根据查询条件统计记录数量
+     */
+    long countByExample(QywxCorpBlacklistUserExample example);
+
+    /**
+     * 根据查询条件批量删除记录
+     */
+    int deleteByExample(QywxCorpBlacklistUserExample example);
+
+    /**
+     * 根据主键删除单条记录
+     */
+    int deleteByPrimaryKey(Long id);
+
+    /**
+     * 插入完整记录
+     */
+    int insert(QywxCorpBlacklistUser record);
+
+    /**
+     * 选择性插入记录
+     */
+    int insertSelective(QywxCorpBlacklistUser record);
+
+    /**
+     * 根据查询条件查询记录列表
+     */
+    List<QywxCorpBlacklistUser> selectByExample(QywxCorpBlacklistUserExample example);
+
+    /**
+     * 根据主键查询单条记录
+     */
+    QywxCorpBlacklistUser selectByPrimaryKey(Long id);
+
+    /**
+     * 根据条件选择性更新记录
+     */
+    int updateByExampleSelective(@Param("record") QywxCorpBlacklistUser record, 
+                                @Param("example") QywxCorpBlacklistUserExample example);
+
+    /**
+     * 根据条件完整更新记录
+     */
+    int updateByExample(@Param("record") QywxCorpBlacklistUser record, 
+                       @Param("example") QywxCorpBlacklistUserExample example);
+
+    /**
+     * 根据主键选择性更新记录
+     */
+    int updateByPrimaryKeySelective(QywxCorpBlacklistUser record);
+
+    /**
+     * 根据主键完整更新记录
+     */
+    int updateByPrimaryKey(QywxCorpBlacklistUser record);
+
+    /**
+     * 根据vid和corpId查询拉黑时间
+     */
+    Date selectBlackTimeByVidAndCorpId(@Param("vid") Long vid, @Param("corpId") Long corpId);
+}

+ 89 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/job/BlacklistSyncJobHandler.java

@@ -0,0 +1,89 @@
+package com.tzld.piaoquan.risk.control.job;
+
+import com.tzld.piaoquan.risk.control.service.sync.BlacklistSyncService;
+import com.tzld.piaoquan.risk.control.service.sync.config.ApolloConfigService;
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.handler.annotation.XxlJob;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.UUID;
+
+/**
+ * 黑名单同步XXL-JOB任务处理器
+ * 
+ * 每234秒执行一次智能同步检查
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Component
+public class BlacklistSyncJobHandler {
+    
+    private static final Logger log = LoggerFactory.getLogger(BlacklistSyncJobHandler.class);
+    
+    @Autowired
+    private BlacklistSyncService blacklistSyncService;
+    
+    @Autowired
+    private ApolloConfigService apolloConfigService;
+    
+    /**
+     * 黑名单同步任务
+     * 
+     * XXL-JOB任务执行方法,每234秒执行一次
+     */
+    @XxlJob("blacklistSyncJob")
+    public ReturnT<String> execute(String params) throws Exception {
+        // 为XXL-JOB任务设置traceId
+        String traceId = "JOB-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
+        MDC.put("logTraceId", traceId);
+
+        try {
+            log.info("开始执行黑名单同步任务检查,traceId: {}, params: {}", traceId, params);
+
+            // 记录任务开始时间
+            long startTime = System.currentTimeMillis();
+            
+            // 记录配置信息
+            String configInfo = apolloConfigService.getConfigInfo();
+            log.info("当前配置信息: {}", configInfo);
+            log.info("XXL-JOB任务开始执行,配置信息: {}", configInfo);
+            Long value = null;
+            if (params != null) {
+                value = Long.valueOf(params);
+            }
+            // 执行智能同步检查
+            blacklistSyncService.intelligentSyncCheck(value);
+            
+            // 记录任务结束时间和耗时
+            long endTime = System.currentTimeMillis();
+            long duration = endTime - startTime;
+            
+            // 获取同步统计信息
+            String statistics = blacklistSyncService.getSyncStatistics();
+            
+            log.info("黑名单同步任务检查完成,耗时: {}ms", duration);
+            log.info("同步统计信息: {}", statistics);
+            
+            log.info("XXL-JOB任务执行完成,耗时: {}ms,统计信息: {}", duration, statistics);
+            return ReturnT.SUCCESS;
+            
+        } catch (Exception e) {
+            String errorMsg = "黑名单同步任务执行失败: " + e.getMessage();
+            log.info(errorMsg);
+            log.error("XXL-JOB任务执行异常", e);
+
+            // 重新抛出异常,让XXL-JOB记录任务失败
+            return ReturnT.FAIL;
+        } finally {
+            // 清理MDC
+            MDC.remove("logTraceId");
+        }
+    }
+    
+
+}

+ 107 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/model/po/QywxCorpBlacklistUser.java

@@ -0,0 +1,107 @@
+package com.tzld.piaoquan.risk.control.model.po;
+
+import java.util.Date;
+
+/**
+ * 企业微信客户主体黑名单用户实体类
+ * 
+ * 对应数据库表: qywx_corp_blacklist_user
+ * 
+ * @author 系统生成
+ * @since 1.0.0
+ */
+public class QywxCorpBlacklistUser {
+    
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 企业微信用户VID
+     */
+    private Long vid;
+
+    /**
+     * 企业ID
+     */
+    private Long corpId;
+
+    /**
+     * 拉黑时间
+     */
+    private Date blackTime;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getVid() {
+        return vid;
+    }
+
+    public void setVid(Long vid) {
+        this.vid = vid;
+    }
+
+    public Long getCorpId() {
+        return corpId;
+    }
+
+    public void setCorpId(Long corpId) {
+        this.corpId = corpId;
+    }
+
+    public Date getBlackTime() {
+        return blackTime;
+    }
+
+    public void setBlackTime(Date blackTime) {
+        this.blackTime = blackTime;
+    }
+
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    @Override
+    public String toString() {
+        return "QywxCorpBlacklistUser{" +
+                "id=" + id +
+                ", vid=" + vid +
+                ", corpId=" + corpId +
+                ", blackTime=" + blackTime +
+                ", createTime=" + createTime +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        QywxCorpBlacklistUser that = (QywxCorpBlacklistUser) obj;
+        return vid != null && vid.equals(that.vid) && corpId != null && corpId.equals(that.corpId);
+    }
+
+    @Override
+    public int hashCode() {
+        return vid != null && corpId != null ? (vid.toString() + corpId.toString()).hashCode() : 0;
+    }
+}

+ 223 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/model/po/QywxCorpBlacklistUserExample.java

@@ -0,0 +1,223 @@
+package com.tzld.piaoquan.risk.control.model.po;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企业微信客户主体黑名单用户查询条件构建类
+ * 
+ * @author MyBatis Generator
+ * @since 1.0.0
+ */
+public class QywxCorpBlacklistUserExample {
+    
+    protected String orderByClause;
+    protected boolean distinct;
+    protected List<Criteria> oredCriteria;
+
+    public QywxCorpBlacklistUserExample() {
+        oredCriteria = new ArrayList<>();
+    }
+
+    public void setOrderByClause(String orderByClause) {
+        this.orderByClause = orderByClause;
+    }
+
+    public String getOrderByClause() {
+        return orderByClause;
+    }
+
+    public void setDistinct(boolean distinct) {
+        this.distinct = distinct;
+    }
+
+    public boolean isDistinct() {
+        return distinct;
+    }
+
+    public List<Criteria> getOredCriteria() {
+        return oredCriteria;
+    }
+
+    public void or(Criteria criteria) {
+        oredCriteria.add(criteria);
+    }
+
+    public Criteria or() {
+        Criteria criteria = createCriteriaInternal();
+        oredCriteria.add(criteria);
+        return criteria;
+    }
+
+    public Criteria createCriteria() {
+        Criteria criteria = createCriteriaInternal();
+        if (oredCriteria.size() == 0) {
+            oredCriteria.add(criteria);
+        }
+        return criteria;
+    }
+
+    protected Criteria createCriteriaInternal() {
+        Criteria criteria = new Criteria();
+        return criteria;
+    }
+
+    public void clear() {
+        oredCriteria.clear();
+        orderByClause = null;
+        distinct = false;
+    }
+
+    protected abstract static class GeneratedCriteria {
+        protected List<Criterion> criteria;
+
+        protected GeneratedCriteria() {
+            super();
+            criteria = new ArrayList<>();
+        }
+
+        public boolean isValid() {
+            return criteria.size() > 0;
+        }
+
+        public List<Criterion> getAllCriteria() {
+            return criteria;
+        }
+
+        public List<Criterion> getCriteria() {
+            return criteria;
+        }
+
+        protected void addCriterion(String condition) {
+            if (condition == null) {
+                throw new RuntimeException("Value for condition cannot be null");
+            }
+            criteria.add(new Criterion(condition));
+        }
+
+        protected void addCriterion(String condition, Object value, String property) {
+            if (value == null) {
+                throw new RuntimeException("Value for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value));
+        }
+
+        protected void addCriterion(String condition, Object value1, Object value2, String property) {
+            if (value1 == null || value2 == null) {
+                throw new RuntimeException("Between values for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value1, value2));
+        }
+
+        public Criteria andIdEqualTo(Long value) {
+            addCriterion("id =", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andVidEqualTo(Long value) {
+            addCriterion("vid =", value, "vid");
+            return (Criteria) this;
+        }
+
+        public Criteria andCorpIdEqualTo(Long value) {
+            addCriterion("corp_id =", value, "corpId");
+            return (Criteria) this;
+        }
+
+        public Criteria andBlackTimeGreaterThanOrEqualTo(Date value) {
+            addCriterion("black_time >=", value, "blackTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andCreateTimeGreaterThanOrEqualTo(Date value) {
+            addCriterion("create_time >=", value, "createTime");
+            return (Criteria) this;
+        }
+    }
+
+    public static class Criteria extends GeneratedCriteria {
+        protected Criteria() {
+            super();
+        }
+    }
+
+    public static class Criterion {
+        private String condition;
+        private Object value;
+        private Object secondValue;
+        private boolean noValue;
+        private boolean singleValue;
+        private boolean betweenValue;
+        private boolean listValue;
+        private String typeHandler;
+
+        public String getCondition() {
+            return condition;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+
+        public Object getSecondValue() {
+            return secondValue;
+        }
+
+        public boolean isNoValue() {
+            return noValue;
+        }
+
+        public boolean isSingleValue() {
+            return singleValue;
+        }
+
+        public boolean isBetweenValue() {
+            return betweenValue;
+        }
+
+        public boolean isListValue() {
+            return listValue;
+        }
+
+        public String getTypeHandler() {
+            return typeHandler;
+        }
+
+        protected Criterion(String condition) {
+            super();
+            this.condition = condition;
+            this.typeHandler = null;
+            this.noValue = true;
+        }
+
+        protected Criterion(String condition, Object value, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.typeHandler = typeHandler;
+            if (value instanceof List<?>) {
+                this.listValue = true;
+            } else {
+                this.singleValue = true;
+            }
+        }
+
+        protected Criterion(String condition, Object value) {
+            this(condition, value, null);
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.secondValue = secondValue;
+            this.typeHandler = typeHandler;
+            this.betweenValue = true;
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue) {
+            this(condition, value, secondValue, null);
+        }
+    }
+}

+ 185 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/model/qywx/RoomBlacklistResponse.java

@@ -0,0 +1,185 @@
+package com.tzld.piaoquan.risk.control.model.qywx;
+
+import java.util.List;
+
+/**
+ * 企业微信群黑名单查询响应实体类
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+public class RoomBlacklistResponse {
+    
+    /**
+     * 错误码,0表示成功
+     */
+    private int errcode;
+    
+    /**
+     * 错误信息
+     */
+    private String errmsg;
+    
+    /**
+     * 响应数据
+     */
+    private Data data;
+    
+    public int getErrcode() {
+        return errcode;
+    }
+    
+    public void setErrcode(int errcode) {
+        this.errcode = errcode;
+    }
+    
+    public String getErrmsg() {
+        return errmsg;
+    }
+    
+    public void setErrmsg(String errmsg) {
+        this.errmsg = errmsg;
+    }
+    
+    public Data getData() {
+        return data;
+    }
+    
+    public void setData(Data data) {
+        this.data = data;
+    }
+    
+    /**
+     * 响应数据内容
+     */
+    public static class Data {
+        
+        /**
+         * 分页缓冲区
+         */
+        private String pageBuff;
+        
+        /**
+         *
+         */
+        private boolean is_reset;
+        
+        /**
+         * 是否查询完毕
+         */
+        private boolean is_end;
+        
+        /**
+         * 总数量
+         */
+        private int sum;
+        
+        /**
+         * 黑名单用户列表
+         */
+        private List<BlacklistItem> list;
+        
+        public String getPageBuff() {
+            return pageBuff;
+        }
+        
+        public void setPageBuff(String pageBuff) {
+            this.pageBuff = pageBuff;
+        }
+        
+        public boolean isIs_reset() {
+            return is_reset;
+        }
+        
+        public void setIs_reset(boolean is_reset) {
+            this.is_reset = is_reset;
+        }
+        
+        public boolean isIs_end() {
+            return is_end;
+        }
+        
+        public void setIs_end(boolean is_end) {
+            this.is_end = is_end;
+        }
+        
+        public int getSum() {
+            return sum;
+        }
+        
+        public void setSum(int sum) {
+            this.sum = sum;
+        }
+        
+        public List<BlacklistItem> getList() {
+            return list;
+        }
+        
+        public void setList(List<BlacklistItem> list) {
+            this.list = list;
+        }
+    }
+    
+    /**
+     * 黑名单用户项
+     */
+    public static class BlacklistItem {
+        
+        /**
+         * 黑名单用户VID
+         */
+        private Long blacklist_vid;
+        
+        /**
+         * 拉黑时间(时间戳10位)
+         */
+        private Long create_time;
+        
+        /**
+         * 谁拉黑的
+         */
+        private Long share_vid;
+        
+        public Long getBlacklist_vid() {
+            return blacklist_vid;
+        }
+        
+        public void setBlacklist_vid(Long blacklist_vid) {
+            this.blacklist_vid = blacklist_vid;
+        }
+        
+        public Long getCreate_time() {
+            return create_time;
+        }
+        
+        public void setCreate_time(Long create_time) {
+            this.create_time = create_time;
+        }
+        
+        public Long getShare_vid() {
+            return share_vid;
+        }
+        
+        public void setShare_vid(Long share_vid) {
+            this.share_vid = share_vid;
+        }
+        
+        @Override
+        public String toString() {
+            return "BlacklistItem{" +
+                    "blacklist_vid=" + blacklist_vid +
+                    ", create_time=" + create_time +
+                    ", share_vid=" + share_vid +
+                    '}';
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return "RoomBlacklistResponse{" +
+                "errcode=" + errcode +
+                ", errmsg='" + errmsg + '\'' +
+                ", data=" + data +
+                '}';
+    }
+}

+ 65 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/CorpBlacklistService.java

@@ -0,0 +1,65 @@
+package com.tzld.piaoquan.risk.control.service;
+
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企业微信客户主体黑名单管理服务接口
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+public interface CorpBlacklistService {
+    
+    /**
+     * 根据vid和corpId查询拉黑时间
+     * 
+     * @param vid 企业微信用户VID
+     * @param corpId 企业ID
+     * @return 拉黑时间,如果不存在则返回null
+     */
+    Date getBlackTimeByVidAndCorpId(Long vid, Long corpId);
+    
+    /**
+     * 添加用户到企业黑名单
+     * 
+     * @param vid 企业微信用户VID
+     * @param corpId 企业ID
+     * @throws RuntimeException 当用户已在该企业黑名单中时抛出
+     */
+    void addToCorpBlacklist(Long vid, Long corpId);
+    
+    /**
+     * 根据企业ID查询该企业的所有黑名单用户
+     * 
+     * @param corpId 企业ID
+     * @return 该企业的黑名单用户列表
+     */
+    List<QywxCorpBlacklistUser> getBlacklistByCorpId(Long corpId);
+    
+    /**
+     * 检查用户是否在指定企业的黑名单中
+     * 
+     * @param vid 企业微信用户VID
+     * @param corpId 企业ID
+     * @return true表示在黑名单中,false表示不在黑名单中
+     */
+    boolean isInCorpBlacklist(Long vid, Long corpId);
+    
+    /**
+     * 从企业黑名单中移除用户
+     * 
+     * @param vid 企业微信用户VID
+     * @param corpId 企业ID
+     * @return true表示移除成功,false表示用户不在黑名单中
+     */
+    boolean removeFromCorpBlacklist(Long vid, Long corpId);
+
+    /**
+     * 根据uuid查询corp_id
+     * @param uuid
+     * @return
+     */
+    Long getCorpIdByUuid(String uuid);
+}

+ 220 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/impl/CorpBlacklistServiceImpl.java

@@ -0,0 +1,220 @@
+package com.tzld.piaoquan.risk.control.service.impl;
+
+import com.tzld.piaoquan.risk.control.dao.mapper.QywxCorpBlacklistUserMapper;
+import com.tzld.piaoquan.risk.control.dao.mapper.UserBaseMapper;
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser;
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUserExample;
+import com.tzld.piaoquan.risk.control.model.po.UserBase;
+import com.tzld.piaoquan.risk.control.model.po.UserBaseExample;
+import com.tzld.piaoquan.risk.control.service.CorpBlacklistService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企业微信客户主体黑名单管理服务实现类
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Service
+public class CorpBlacklistServiceImpl implements CorpBlacklistService {
+    
+    private static final Logger log = LoggerFactory.getLogger(CorpBlacklistServiceImpl.class);
+    
+    @Autowired
+    private QywxCorpBlacklistUserMapper corpBlacklistUserMapper;
+
+    @Autowired
+    private UserBaseMapper userBaseMapper;
+    
+    /**
+     * 根据vid和corpId查询拉黑时间
+     */
+    @Override
+    public Date getBlackTimeByVidAndCorpId(Long vid, Long corpId) {
+        if (vid == null || corpId == null) {
+            log.warn("getBlackTimeByVidAndCorpId called with null parameters: vid={}, corpId={}", vid, corpId);
+            return null;
+        }
+        
+        try {
+            Date blackTime = corpBlacklistUserMapper.selectBlackTimeByVidAndCorpId(vid, corpId);
+            log.debug("Query black time for vid: {}, corpId: {}, result: {}", vid, corpId, blackTime);
+            return blackTime;
+        } catch (Exception e) {
+            log.error("Failed to query black time for vid: {}, corpId: {}", vid, corpId, e);
+            throw new RuntimeException("查询拉黑时间失败", e);
+        }
+    }
+    
+    /**
+     * 添加用户到企业黑名单
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void addToCorpBlacklist(Long vid, Long corpId) {
+        try {
+            if (vid == null || corpId == null) {
+                throw new IllegalArgumentException("用户VID和企业ID不能为空");
+            }
+
+            if (vid <= 0 || corpId <= 0) {
+                throw new IllegalArgumentException("用户VID和企业ID必须是有效的正数");
+            }
+            // 检查是否已存在
+            if (isInCorpBlacklist(vid, corpId)) {
+                throw new RuntimeException("用户已在该企业黑名单中,VID: " + vid + ", CorpId: " + corpId);
+            }
+            
+            // 创建黑名单记录
+            QywxCorpBlacklistUser record = new QywxCorpBlacklistUser();
+            record.setVid(vid);
+            record.setCorpId(corpId);
+            record.setBlackTime(new Date());
+            record.setCreateTime(new Date());
+            
+            int result = corpBlacklistUserMapper.insertSelective(record);
+            
+            if (result > 0) {
+                log.info("Successfully added user to corp blacklist, vid: {}, corpId: {}, id: {}", 
+                        vid, corpId, record.getId());
+            } else {
+                throw new RuntimeException("添加用户到企业黑名单失败,数据库操作无效");
+            }
+            
+        } catch (Exception e) {
+            log.error("Unexpected error while adding user to corp blacklist, vid: {}, corpId: {}", vid, corpId, e);
+        }
+    }
+    
+    /**
+     * 根据企业ID查询该企业的所有黑名单用户
+     */
+    @Override
+    public List<QywxCorpBlacklistUser> getBlacklistByCorpId(Long corpId) {
+        if (corpId == null) {
+            throw new IllegalArgumentException("企业ID不能为空");
+        }
+        
+        try {
+            QywxCorpBlacklistUserExample example = new QywxCorpBlacklistUserExample();
+            example.createCriteria().andCorpIdEqualTo(corpId);
+            example.setOrderByClause("black_time DESC");
+            
+            List<QywxCorpBlacklistUser> users = corpBlacklistUserMapper.selectByExample(example);
+            
+            log.info("Retrieved corp blacklist users for corpId: {}, count: {}", corpId, users.size());
+            
+            if (users.size() > 1000) {
+                log.warn("Large corp blacklist dataset retrieved: {} records for corpId: {}", users.size(), corpId);
+            }
+            
+            return users;
+            
+        } catch (Exception e) {
+            log.error("Failed to retrieve corp blacklist users for corpId: {}", corpId, e);
+            throw new RuntimeException("获取企业黑名单用户失败", e);
+        }
+    }
+    
+    /**
+     * 检查用户是否在指定企业的黑名单中
+     */
+    @Override
+    public boolean isInCorpBlacklist(Long vid, Long corpId) {
+        if (vid == null || corpId == null) {
+            log.warn("isInCorpBlacklist called with null parameters, returning false");
+            return false;
+        }
+        
+        try {
+            QywxCorpBlacklistUserExample example = new QywxCorpBlacklistUserExample();
+            example.createCriteria().andVidEqualTo(vid).andCorpIdEqualTo(corpId);
+            
+            long count = corpBlacklistUserMapper.countByExample(example);
+            
+            if (log.isDebugEnabled()) {
+                log.debug("Corp blacklist check for vid: {}, corpId: {}, result: {}", vid, corpId, count > 0);
+            }
+            
+            return count > 0;
+            
+        } catch (Exception e) {
+            log.error("Failed to check corp blacklist for vid: {}, corpId: {}", vid, corpId, e);
+            throw new RuntimeException("企业黑名单检查失败", e);
+        }
+    }
+    
+    /**
+     * 从企业黑名单中移除用户
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean removeFromCorpBlacklist(Long vid, Long corpId) {
+        if (vid == null || corpId == null) {
+            throw new IllegalArgumentException("用户VID和企业ID不能为空");
+        }
+        
+        try {
+            QywxCorpBlacklistUserExample example = new QywxCorpBlacklistUserExample();
+            example.createCriteria().andVidEqualTo(vid).andCorpIdEqualTo(corpId);
+            
+            int result = corpBlacklistUserMapper.deleteByExample(example);
+            
+            if (result > 0) {
+                log.info("Successfully removed user from corp blacklist, vid: {}, corpId: {}, deleted count: {}", 
+                        vid, corpId, result);
+                return true;
+            } else {
+                log.info("User not found in corp blacklist, no removal needed, vid: {}, corpId: {}", vid, corpId);
+                return false;
+            }
+            
+        } catch (Exception e) {
+            log.error("Failed to remove user from corp blacklist, vid: {}, corpId: {}", vid, corpId, e);
+            throw new RuntimeException("从企业黑名单移除用户失败", e);
+        }
+    }
+
+    /**
+     * 根据uuid查询corp_id
+     * @param uuid
+     * @return
+     */
+    @Override
+    public Long getCorpIdByUuid(String uuid) {
+        // 从qywx_user_base表根据uuid查询corp_id
+        if (uuid == null || uuid.trim().isEmpty()) {
+            log.warn("getCorpIdByUuid called with null or empty uuid");
+            return null;
+        }
+
+        try {
+            UserBaseExample example = new UserBaseExample();
+            example.createCriteria().andUuidEqualTo(uuid);
+
+            List<UserBase> users = userBaseMapper.selectByExample(example);
+
+            if (users.isEmpty()) {
+                log.warn("No user found for uuid: {}", uuid);
+                return null;
+            }
+
+            UserBase user = users.get(0);
+            Long corpId = user.getCorpId();
+
+            log.debug("Found corpId: {} for uuid: {}", corpId, uuid);
+            return corpId;
+
+        } catch (Exception e) {
+            log.error("Failed to get corpId for uuid: {}", uuid, e);
+            return null;
+        }
+    }
+}

+ 9 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/impl/RiskUserHandleService.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.TypeReference;
 import com.tzld.piaoquan.risk.control.config.QywxConfig;
 import com.tzld.piaoquan.risk.control.model.po.UserBase;
 import com.tzld.piaoquan.risk.control.model.qywx.*;
+import com.tzld.piaoquan.risk.control.service.CorpBlacklistService;
 import com.tzld.piaoquan.risk.control.util.HttpClientUtil;
 import com.tzld.piaoquan.risk.control.util.HttpPoolClient;
 import lombok.extern.slf4j.Slf4j;
@@ -28,6 +29,8 @@ public class RiskUserHandleService {
     @Autowired
     private RiskRuleConfigService riskRuleConfigService;
 
+    private CorpBlacklistService corpBlacklistService;
+
     @Value("${qywx.corpid}")
     private String corpid;
     @Value("${qywx.scorpid}")
@@ -40,6 +43,7 @@ public class RiskUserHandleService {
         log.info("handleRiskUser, riskUserInfo: {}", riskUserInfo);
         //根据名称找到人
         List<UserBase> staffList = findStaffByName(riskUserInfo);
+        log.info( "handleRiskUser, staffList: {}", staffList);
         if (staffList.isEmpty()) return 0;
         //根据群名匹配到:人-群:哪个员工哪个群
         Map<String, List<RoomListResponse.RoomInfo>> toBeOperate = matchUserAndRoom(staffList, riskUserInfo);
@@ -69,6 +73,11 @@ public class RiskUserHandleService {
                         if(ruleResult.isNormalCountEnough && !ruleResult.isAbnormalRatioHighEnough) {//正常进入用户满足需求,且异常率符合阈值
                             boolean success = riskUserOperateService.kick(staff,Long.parseLong(roomInfo.getRoomId()),externalVid);
                             if (success) {
+                                String uuid = staff.getUuid();
+                                // 加入黑名单
+                                // 根据 uuid 获取 corp_id
+                                Long corpId = corpBlacklistService.getCorpIdByUuid(uuid);
+                                corpBlacklistService.addToCorpBlacklist(externalVid, corpId);
                                 log.info("handleRiskUser to be kick user, vid: {},name: {},chatId: {},chatName:{} ", externalVid,riskUserInfo.getExternalNickname(),riskUserInfo.getChatId(),riskUserInfo.getGroupName());
                                 return 1;
                             }

+ 113 - 2
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/impl/RiskUserOperateService.java

@@ -2,11 +2,14 @@ package com.tzld.piaoquan.risk.control.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.tzld.piaoquan.risk.control.config.QywxConfig;
+import com.tzld.piaoquan.risk.control.dao.mapper.UserBaseMapper;
 import com.tzld.piaoquan.risk.control.dao.mapper.WorkWechatRoomInfoMapper;
 import com.tzld.piaoquan.risk.control.model.po.UserBase;
+import com.tzld.piaoquan.risk.control.model.po.UserBaseExample;
 import com.tzld.piaoquan.risk.control.model.po.WorkWechatRoomInfo;
 import com.tzld.piaoquan.risk.control.model.po.WorkWechatRoomInfoExample;
 import com.tzld.piaoquan.risk.control.model.qywx.*;
+import com.tzld.piaoquan.risk.control.service.CorpBlacklistService;
 import com.tzld.piaoquan.risk.control.util.HttpClientUtil;
 import com.tzld.piaoquan.risk.control.util.HttpPoolClient;
 import lombok.extern.slf4j.Slf4j;
@@ -26,6 +29,13 @@ public class RiskUserOperateService {
     private QywxConfig qywxConfig; // 注入配置类
     @Autowired
     private WorkWechatRoomInfoMapper workWechatRoomInfoMapper;
+
+    @Autowired
+    private CorpBlacklistService corpBlacklistService;
+
+    @Autowired
+    private UserBaseMapper userBaseMapper;
+
     @Value("${qywx.corpid}")
     private long corpid;
     private static final HttpPoolClient httpPoolClientDefault = HttpClientUtil.create(10000, 10000, 2000, 5000, 5, 10000);
@@ -104,11 +114,32 @@ public class RiskUserOperateService {
             if(message.getMemberList().isEmpty()) {
                 return false;
             }
+            boolean inBlacklist = false;
+
             long vid = message.getMemberList().get(0);
-            if(!BLACKLIST_VID.contains(vid)) {
-                LOGGER.info("quickKick, vid: {} is in blacklist, skip kick", vid);
+            // 根据 vid 判断是否在全局黑名单中
+            if(BLACKLIST_VID.contains(vid)) {
+                LOGGER.info("quickKick, vid: {} is in blacklist", vid);
+                inBlacklist = true;
+            }
+
+            // 根据 vid 判断是否在企业黑名单中
+            if ( !inBlacklist ) {
+                // 根据 uuid 查询 corp_id
+                Long corpId = corpBlacklistService.getCorpIdByUuid(uuid);
+                // 根据 corp_id 和 vid判断是否在企业黑名单中
+                if( corpBlacklistService.isInCorpBlacklist(vid, corpId)) {
+                    LOGGER.info("quickKick, vid: {} is in corp blacklist", vid);
+                    inBlacklist = true;
+                }
+            }
+
+            // 如果不在黑名单中,直接返回
+            if( !inBlacklist ) {
+                LOGGER.info("quickKick, vid: {} is not in any blacklist, skip kick", vid);
                 return false;
             }
+
             // 如果是群聊,直接踢出
             LOGGER.info("quickKick, message: {}, uuid: {},timestamp {}", message, uuid,System.currentTimeMillis());
             requestBody.put("blacklist_vid",Arrays.asList(message.getMemberList().get(0)));
@@ -118,6 +149,8 @@ public class RiskUserOperateService {
                 QwCommonResModel<RoomListResponse> roomInfo = QwCommonResModel.parseResponse(response.get(), RoomListResponse.class);
                 if (roomInfo.getErrcode() == 0) {
                     LOGGER.info("quick Kick external user {} successfully,timestamp {}", vid,System.currentTimeMillis());
+                    // 加入黑名单
+                    corpBlacklistService.addToCorpBlacklist( vid, corpid);
                     return true;
                 } else {
                     LOGGER.error("Failed to kick external user {} f: {},", vid, roomInfo.getErrmsg());
@@ -148,6 +181,14 @@ public class RiskUserOperateService {
             LOGGER.info("kick, vid: {} is in blacklist, skip normal kick", vid);
             return true;
         }
+        // 根据 vid 判断是否在企业黑名单中
+        // 根据 uuid 查询 corp_id
+        Long corpId = corpBlacklistService.getCorpIdByUuid(staff.getUuid());
+        // 根据 corp_id 和 vid判断是否在企业黑名单中
+        if( corpBlacklistService.isInCorpBlacklist(vid, corpId)) {
+            LOGGER.info("quickKick, vid: {} is in corp blacklist", vid);
+            return true;
+        }
         Map<String, Object> requestBody = new HashMap<>();
         requestBody.put("uuid", staff.getUuid());
         requestBody.put("oprType", 1);
@@ -218,4 +259,74 @@ public class RiskUserOperateService {
             log.info("forceCloseRoomSwitch, roomId: {}, autoRemoveUserSwitch set to 0", roomId);
         }
     }
+
+    /**
+     * 查询群黑名单
+     *
+     * 调用企业微信API获取指定群的黑名单用户列表
+     *
+     * @param uuid 企业微信用户UUID
+     * @return 黑名单查询响应结果
+     */
+    public RoomBlacklistResponse getRoomBlacklist(String uuid) {
+        if (uuid == null || uuid.trim().isEmpty()) {
+            LOGGER.error("getRoomBlacklist called with null or empty uuid");
+            return createErrorResponse(-1, "UUID不能为空");
+        }
+
+        try {
+            // 构建请求参数
+            Map<String, Object> requestBody = new HashMap<>();
+            requestBody.put("uuid", uuid);
+            requestBody.put("limit", 1000);
+
+            // 构建API URL
+            String url = qywxConfig.getDomain() + qywxConfig.getPath("get-room-black-list");
+
+            LOGGER.info("Calling getRoomBlacklist API, uuid: {}, limit: {}", uuid, 1000);
+
+            // 调用API
+            Optional<String> response = httpPoolClientDefault.postJson(url, JSON.toJSONString(requestBody.toString()));
+
+            if (response.isPresent()) {
+                String responseBody = response.get();
+                LOGGER.info("getRoomBlacklist API response: {}", responseBody);
+
+                // 解析响应
+                RoomBlacklistResponse result = JSON.parseObject(responseBody, RoomBlacklistResponse.class);
+
+                if (result != null && result.getErrcode() == 0) {
+                    LOGGER.info("Successfully retrieved room blacklist, uuid: {}, count: {}",
+                            uuid, result.getData() != null ? result.getData().getSum() : 0);
+                } else {
+                    LOGGER.warn("getRoomBlacklist API returned error, uuid: {}, errcode: {}, errmsg: {}",
+                            uuid,  result != null ? result.getErrcode() : -1,
+                            result != null ? result.getErrmsg() : "解析失败");
+                }
+
+                return result;
+            } else {
+                LOGGER.error("getRoomBlacklist API call failed, no response received, uuid: {}", uuid);
+                return createErrorResponse(-1, "API调用失败,未收到响应");
+            }
+
+        } catch (Exception e) {
+            LOGGER.error("Exception occurred while calling getRoomBlacklist API, uuid: {}", uuid, e);
+            return createErrorResponse(-1, "API调用异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 创建错误响应
+     *
+     * @param errcode 错误码
+     * @param errmsg 错误信息
+     * @return 错误响应对象
+     */
+    private RoomBlacklistResponse createErrorResponse(int errcode, String errmsg) {
+        RoomBlacklistResponse response = new RoomBlacklistResponse();
+        response.setErrcode(errcode);
+        response.setErrmsg(errmsg);
+        return response;
+    }
 }

+ 1 - 1
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/qywx/Constant.java

@@ -39,7 +39,7 @@ public class Constant {
 
 
     //登录成功
-    public static final int LOGIN_SUCCESS = 104001;
+    public static final int     LOGIN_SUCCESS = 104001;
     public static final int SECONDERY_VERIFY = 100012;
     public static final int LOGIN_OTHER_DEVICE = 100008;
     //企业切换

+ 34 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/BlacklistSyncService.java

@@ -0,0 +1,34 @@
+package com.tzld.piaoquan.risk.control.service.sync;
+
+/**
+ * 黑名单同步服务接口
+ * 
+ * 提供企业微信黑名单数据同步功能
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+public interface BlacklistSyncService {
+    
+    /**
+     * 智能同步检查
+     * 
+     * 遍历所有企业,基于时间间隔和随机概率进行智能同步决策
+     */
+    void intelligentSyncCheck(Long corpId);
+    
+    /**
+     * 强制同步指定企业
+     * 
+     * @param corpId 企业ID
+     * @return true表示同步成功,false表示同步失败
+     */
+    boolean forceSyncCorp(Long corpId);
+    
+    /**
+     * 获取同步统计信息
+     * 
+     * @return 同步统计信息
+     */
+    String getSyncStatistics();
+}

+ 209 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/cache/RedisSyncTimeCache.java

@@ -0,0 +1,209 @@
+package com.tzld.piaoquan.risk.control.service.sync.cache;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Redis同步时间缓存服务
+ * 
+ * 管理每个企业的最后同步时间
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Component
+public class RedisSyncTimeCache {
+    
+    private static final Logger log = LoggerFactory.getLogger(RedisSyncTimeCache.class);
+    
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+    
+    private static final String TIME_PREFIX = "blacklist_sync_time:";
+    
+    /**
+     * 获取企业最后同步时间
+     * 
+     * @param corpId 企业ID
+     * @return 最后同步时间戳,如果不存在返回null
+     */
+    public Long getLastSyncTime(Long corpId) {
+        if (corpId == null) {
+            log.warn("获取同步时间时企业ID为空");
+            return null;
+        }
+        
+        String key = TIME_PREFIX + corpId;
+        
+        try {
+            String timeStr = redisTemplate.opsForValue().get(key);
+            
+            if (timeStr != null) {
+                try {
+                    Long syncTime = Long.valueOf(timeStr);
+                    log.debug("获取企业{}同步时间: {}", corpId, syncTime);
+                    return syncTime;
+                } catch (NumberFormatException e) {
+                    log.warn("企业{}的同步时间格式错误: {}", corpId, timeStr);
+                    // 删除格式错误的数据
+                    redisTemplate.delete(key);
+                    return null;
+                }
+            }
+            
+            log.debug("企业{}未找到同步时间记录", corpId);
+            return null;
+            
+        } catch (Exception e) {
+            log.error("获取企业{}同步时间异常", corpId, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 更新企业最后同步时间
+     * 
+     * @param corpId 企业ID
+     */
+    public void updateLastSyncTime(Long corpId) {
+        updateLastSyncTime(corpId, System.currentTimeMillis());
+    }
+    
+    /**
+     * 更新企业最后同步时间
+     * 
+     * @param corpId 企业ID
+     * @param syncTime 同步时间戳
+     */
+    public void updateLastSyncTime(Long corpId, Long syncTime) {
+        if (corpId == null) {
+            log.warn("更新同步时间时企业ID为空");
+            return;
+        }
+        
+        if (syncTime == null) {
+            log.warn("更新企业{}同步时间时时间戳为空", corpId);
+            return;
+        }
+        
+        String key = TIME_PREFIX + corpId;
+        String currentTime = String.valueOf(syncTime);
+        
+        try {
+            redisTemplate.opsForValue().set(key, currentTime);
+            log.info("更新企业{}同步时间: {}", corpId, syncTime);
+        } catch (Exception e) {
+            log.error("更新企业{}同步时间异常", corpId, e);
+        }
+    }
+    
+    /**
+     * 删除企业同步时间记录
+     * 
+     * @param corpId 企业ID
+     */
+    public void removeLastSyncTime(Long corpId) {
+        if (corpId == null) {
+            log.warn("删除同步时间时企业ID为空");
+            return;
+        }
+        
+        String key = TIME_PREFIX + corpId;
+        
+        try {
+            Boolean result = redisTemplate.delete(key);
+            if (Boolean.TRUE.equals(result)) {
+                log.info("删除企业{}同步时间记录成功", corpId);
+            } else {
+                log.info("企业{}同步时间记录不存在", corpId);
+            }
+        } catch (Exception e) {
+            log.error("删除企业{}同步时间记录异常", corpId, e);
+        }
+    }
+    
+    /**
+     * 获取所有企业的同步时间信息
+     * 
+     * @return 企业ID和同步时间的映射
+     */
+    public Map<Long, Long> getAllSyncTimes() {
+        Map<Long, Long> result = new HashMap<>();
+        
+        try {
+            Set<String> keys = redisTemplate.keys(TIME_PREFIX + "*");
+            
+            if (keys != null && !keys.isEmpty()) {
+                log.debug("找到{}个企业同步时间记录", keys.size());
+                
+                for (String key : keys) {
+                    try {
+                        String corpIdStr = key.substring(TIME_PREFIX.length());
+                        Long corpId = Long.valueOf(corpIdStr);
+                        Long syncTime = getLastSyncTime(corpId);
+                        
+                        if (syncTime != null) {
+                            result.put(corpId, syncTime);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("解析企业ID失败: {}", key);
+                    }
+                }
+            } else {
+                log.debug("未找到任何企业同步时间记录");
+            }
+            
+        } catch (Exception e) {
+            log.error("获取所有企业同步时间异常", e);
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 批量更新企业同步时间
+     * 
+     * @param syncTimes 企业ID和同步时间的映射
+     */
+    public void batchUpdateSyncTimes(Map<Long, Long> syncTimes) {
+        if (syncTimes == null || syncTimes.isEmpty()) {
+            log.debug("批量更新同步时间:数据为空");
+            return;
+        }
+        
+        try {
+            for (Map.Entry<Long, Long> entry : syncTimes.entrySet()) {
+                updateLastSyncTime(entry.getKey(), entry.getValue());
+            }
+            log.info("批量更新{}个企业同步时间完成", syncTimes.size());
+        } catch (Exception e) {
+            log.error("批量更新企业同步时间异常", e);
+        }
+    }
+    
+    /**
+     * 清理所有同步时间记录
+     */
+    public void clearAllSyncTimes() {
+        try {
+            Set<String> keys = redisTemplate.keys(TIME_PREFIX + "*");
+            
+            if (keys != null && !keys.isEmpty()) {
+                Long deletedCount = redisTemplate.delete(keys);
+                log.info("清理同步时间记录完成,删除{}条记录", deletedCount);
+            } else {
+                log.info("没有找到需要清理的同步时间记录");
+            }
+            
+        } catch (Exception e) {
+            log.error("清理同步时间记录异常", e);
+        }
+    }
+}

+ 141 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/config/ApolloConfigService.java

@@ -0,0 +1,141 @@
+package com.tzld.piaoquan.risk.control.service.sync.config;
+
+import com.alibaba.fastjson.JSON;
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Apollo配置服务
+ *
+ * 从Apollo配置中心读取同步参数配置
+ *
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Component
+public class ApolloConfigService {
+
+    private static final Logger log = LoggerFactory.getLogger(ApolloConfigService.class);
+
+    @ApolloConfig
+    private Config config;
+
+    /**
+     * 配置项Key
+     */
+    private static final String SYNC_CONFIG_KEY = "blacklist.sync.config";
+
+    /**
+     * 默认配置JSON字符串
+     */
+    private static final String DEFAULT_CONFIG_JSON =
+        "{\"minIntervalHours\":2,\"maxIntervalHours\":6,\"syncProbability\":0.333}";
+
+    /**
+     * 获取同步配置参数(JSON格式)
+     *
+     * @return 同步配置对象
+     */
+    public SyncConfig getSyncConfig() {
+        try {
+            String configJson = config.getProperty(SYNC_CONFIG_KEY, DEFAULT_CONFIG_JSON);
+            log.debug("读取Apollo配置: {}", configJson);
+
+            SyncConfig syncConfig = JSON.parseObject(configJson, SyncConfig.class);
+
+            if (syncConfig == null) {
+                log.warn("解析同步配置为空,使用默认配置");
+                return getDefaultSyncConfig();
+            }
+
+            if (!syncConfig.isValid()) {
+                log.warn("同步配置参数无效: {}, 使用默认配置", syncConfig);
+                return getDefaultSyncConfig();
+            }
+
+            log.info("成功读取同步配置: {}", syncConfig.getDescription());
+            return syncConfig;
+
+        } catch (Exception e) {
+            log.error("解析同步配置失败,使用默认配置", e);
+            return getDefaultSyncConfig();
+        }
+    }
+
+    /**
+     * 获取默认同步配置
+     *
+     * @return 默认配置对象
+     */
+    private SyncConfig getDefaultSyncConfig() {
+        SyncConfig config = new SyncConfig();
+        config.setMinIntervalHours(2);
+        config.setMaxIntervalHours(6);
+        config.setSyncProbability(0.333);
+
+        log.info("使用默认同步配置: {}", config.getDescription());
+        return config;
+    }
+
+    /**
+     * 获取配置的JSON字符串(用于调试)
+     *
+     * @return 配置JSON字符串
+     */
+    public String getSyncConfigJson() {
+        return config.getProperty(SYNC_CONFIG_KEY, DEFAULT_CONFIG_JSON);
+    }
+
+    /**
+     * 检查配置是否存在
+     *
+     * @return true表示配置存在,false表示使用默认配置
+     */
+    public boolean hasCustomConfig() {
+        try {
+            String configValue = config.getProperty(SYNC_CONFIG_KEY, null);
+            return configValue != null && !DEFAULT_CONFIG_JSON.equals(configValue);
+        } catch (Exception e) {
+            log.error("检查配置存在性异常", e);
+            return false;
+        }
+    }
+
+    /**
+     * 验证配置格式是否正确
+     *
+     * @return true表示配置格式正确,false表示格式错误
+     */
+    public boolean validateConfig() {
+        try {
+            SyncConfig config = getSyncConfig();
+            return config.isValid();
+        } catch (Exception e) {
+            log.error("验证配置格式异常", e);
+            return false;
+        }
+    }
+
+    /**
+     * 获取配置详细信息(用于监控和调试)
+     *
+     * @return 配置详细信息
+     */
+    public String getConfigInfo() {
+        try {
+            SyncConfig syncConfig = getSyncConfig();
+            return String.format(
+                "配置来源: %s, 配置内容: %s, 配置状态: %s",
+                hasCustomConfig() ? "Apollo" : "默认",
+                syncConfig.toString(),
+                syncConfig.isValid() ? "有效" : "无效"
+            );
+        } catch (Exception e) {
+            log.error("获取配置信息异常", e);
+            return "配置信息获取失败: " + e.getMessage();
+        }
+    }
+}

+ 143 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/config/SyncConfig.java

@@ -0,0 +1,143 @@
+package com.tzld.piaoquan.risk.control.service.sync.config;
+
+/**
+ * 同步配置实体类
+ * 
+ * 用于存储从Apollo读取的同步参数配置
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+public class SyncConfig {
+    
+    /**
+     * 最小同步间隔(小时)
+     * 小于此时间间隔的企业不会被同步
+     */
+    private int minIntervalHours = 2;
+    
+    /**
+     * 最大同步间隔(小时)
+     * 大于此时间间隔的企业必须被同步
+     */
+    private int maxIntervalHours = 6;
+    
+    /**
+     * 同步概率(0-1之间)
+     * 在最小和最大间隔之间的企业,按此概率决定是否同步
+     */
+    private double syncProbability = 0.333; // 三分之一概率
+    
+    /**
+     * 默认构造函数
+     */
+    public SyncConfig() {
+    }
+    
+    /**
+     * 带参数构造函数
+     * 
+     * @param minIntervalHours 最小间隔小时数
+     * @param maxIntervalHours 最大间隔小时数
+     * @param syncProbability 同步概率
+     */
+    public SyncConfig(int minIntervalHours, int maxIntervalHours, double syncProbability) {
+        this.minIntervalHours = minIntervalHours;
+        this.maxIntervalHours = maxIntervalHours;
+        this.syncProbability = syncProbability;
+    }
+    
+    public int getMinIntervalHours() {
+        return minIntervalHours;
+    }
+    
+    public void setMinIntervalHours(int minIntervalHours) {
+        this.minIntervalHours = minIntervalHours;
+    }
+    
+    public int getMaxIntervalHours() {
+        return maxIntervalHours;
+    }
+    
+    public void setMaxIntervalHours(int maxIntervalHours) {
+        this.maxIntervalHours = maxIntervalHours;
+    }
+    
+    public double getSyncProbability() {
+        return syncProbability;
+    }
+    
+    public void setSyncProbability(double syncProbability) {
+        this.syncProbability = syncProbability;
+    }
+    
+    /**
+     * 获取最小间隔毫秒数
+     * 
+     * @return 最小间隔毫秒数
+     */
+    public long getMinIntervalMillis() {
+        return minIntervalHours * 3600L * 1000L;
+    }
+    
+    /**
+     * 获取最大间隔毫秒数
+     * 
+     * @return 最大间隔毫秒数
+     */
+    public long getMaxIntervalMillis() {
+        return maxIntervalHours * 3600L * 1000L;
+    }
+    
+    /**
+     * 验证配置参数是否有效
+     * 
+     * @return true表示配置有效,false表示配置无效
+     */
+    public boolean isValid() {
+        return minIntervalHours > 0 
+            && maxIntervalHours > minIntervalHours 
+            && syncProbability >= 0 
+            && syncProbability <= 1;
+    }
+    
+    /**
+     * 获取配置的描述信息
+     * 
+     * @return 配置描述
+     */
+    public String getDescription() {
+        return String.format("最小间隔: %d小时, 最大间隔: %d小时, 同步概率: %.1f%%", 
+                minIntervalHours, maxIntervalHours, syncProbability * 100);
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("SyncConfig{minInterval=%dh, maxInterval=%dh, probability=%.3f}", 
+                minIntervalHours, maxIntervalHours, syncProbability);
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        
+        SyncConfig that = (SyncConfig) obj;
+        return minIntervalHours == that.minIntervalHours
+            && maxIntervalHours == that.maxIntervalHours
+            && Double.compare(that.syncProbability, syncProbability) == 0;
+    }
+    
+    @Override
+    public int hashCode() {
+        int result = minIntervalHours;
+        result = 31 * result + maxIntervalHours;
+        long temp = Double.doubleToLongBits(syncProbability);
+        result = 31 * result + (int) (temp ^ (temp >>> 32));
+        return result;
+    }
+}

+ 58 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/data/BlacklistDataService.java

@@ -0,0 +1,58 @@
+package com.tzld.piaoquan.risk.control.service.sync.data;
+
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser;
+
+import java.util.List;
+
+/**
+ * 黑名单数据访问服务接口
+ * 
+ * 提供黑名单相关的数据库操作
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+public interface BlacklistDataService {
+    
+    /**
+     * 获取所有企业ID(去重)
+     * 
+     * @return 企业ID列表
+     */
+    List<Long> getAllCorpIds();
+    
+    /**
+     * 获取企业的登录用户UUID
+     * 
+     * @param corpId 企业ID
+     * @return 登录用户UUID,如果不存在返回null
+     */
+    String getLoginUserUuid(Long corpId);
+    
+    /**
+     * 清理企业黑名单数据
+     * 
+     * @param corpId 企业ID
+     * @return 删除的记录数
+     */
+    int clearCorpBlacklist(Long corpId);
+    
+    /**
+     * 批量插入黑名单数据
+     * 
+     * @param users 黑名单用户列表
+     * @return 插入的记录数
+     */
+    int batchInsertBlacklist(List<QywxCorpBlacklistUser> users);
+    
+    /**
+     * 根据企业ID查询黑名单用户数量
+     * 
+     * @param corpId 企业ID
+     * @return 黑名单用户数量
+     */
+    int getCorpBlacklistCount(Long corpId);
+
+
+
+}

+ 173 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/data/impl/BlacklistDataServiceImpl.java

@@ -0,0 +1,173 @@
+package com.tzld.piaoquan.risk.control.service.sync.data.impl;
+
+import com.tzld.piaoquan.risk.control.dao.mapper.QywxCorpBlacklistUserMapper;
+import com.tzld.piaoquan.risk.control.dao.mapper.UserBaseMapper;
+import com.tzld.piaoquan.risk.control.model.po.*;
+import com.tzld.piaoquan.risk.control.service.qywx.Constant;
+import com.tzld.piaoquan.risk.control.service.sync.data.BlacklistDataService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 黑名单数据访问服务实现类
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Service
+public class BlacklistDataServiceImpl implements BlacklistDataService {
+    
+    private static final Logger log = LoggerFactory.getLogger(BlacklistDataServiceImpl.class);
+    
+    @Autowired
+    private UserBaseMapper userBaseMapper;
+    
+    @Autowired
+    private QywxCorpBlacklistUserMapper corpBlacklistUserMapper;
+    
+    @Override
+    public List<Long> getAllCorpIds() {
+        try {
+            UserBaseExample example = new UserBaseExample();
+            // 只查询有效的企业ID(不为空且大于0)
+            example.createCriteria().andCorpIdIsNotNull();
+            
+            List<UserBase> users = userBaseMapper.selectByExample(example);
+            
+            // 去重企业ID
+            Set<Long> corpIdSet = new HashSet<>();
+            for (UserBase user : users) {
+                if (user.getCorpId() != null && user.getCorpId() > 0) {
+                    corpIdSet.add(user.getCorpId());
+                }
+            }
+            
+            List<Long> corpIds = new ArrayList<>(corpIdSet);
+            log.info("获取到{}个去重后的企业ID", corpIds.size());
+            
+            return corpIds;
+            
+        } catch (Exception e) {
+            log.error("获取所有企业ID异常", e);
+            return new ArrayList<>();
+        }
+    }
+    
+    @Override
+    public String getLoginUserUuid(Long corpId) {
+        if (corpId == null) {
+            log.warn("获取登录用户UUID时企业ID为空");
+            return null;
+        }
+        
+        try {
+            UserBaseExample example = new UserBaseExample();
+            example.createCriteria()
+                .andCorpIdEqualTo(corpId)
+                .andLoginStatusEqualTo(Constant.LOGIN_STATUS_LOGIN);
+            
+            List<UserBase> users = userBaseMapper.selectByExample(example);
+            
+            if (users.isEmpty()) {
+                log.warn("企业{}未找到登录用户", corpId);
+                return null;
+            }
+            
+            UserBase user = users.get(0);
+            String uuid = user.getUuid();
+            
+            log.debug("企业{}找到登录用户UUID: {}", corpId, uuid);
+            return uuid;
+            
+        } catch (Exception e) {
+            log.error("企业{}获取登录用户UUID异常", corpId, e);
+            return null;
+        }
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int clearCorpBlacklist(Long corpId) {
+        if (corpId == null) {
+            log.warn("清理企业黑名单时企业ID为空");
+            return 0;
+        }
+        
+        try {
+            QywxCorpBlacklistUserExample example = new QywxCorpBlacklistUserExample();
+            example.createCriteria().andCorpIdEqualTo(corpId);
+            
+            int deletedCount = corpBlacklistUserMapper.deleteByExample(example);
+            
+            log.info("企业{}清理黑名单完成,删除{}条记录", corpId, deletedCount);
+            return deletedCount;
+            
+        } catch (Exception e) {
+            log.error("企业{}清理黑名单异常", corpId, e);
+            throw new RuntimeException("清理企业黑名单失败", e);
+        }
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchInsertBlacklist(List<QywxCorpBlacklistUser> users) {
+        if (users == null || users.isEmpty()) {
+            log.debug("批量插入黑名单:数据为空");
+            return 0;
+        }
+        
+        try {
+            int insertedCount = 0;
+            
+            for (QywxCorpBlacklistUser user : users) {
+                if (user != null && user.getVid() != null && user.getCorpId() != null) {
+                    int result = corpBlacklistUserMapper.insertSelective(user);
+                    if (result > 0) {
+                        insertedCount++;
+                    }
+                } else {
+                    log.warn("跳过无效的黑名单用户记录: {}", user);
+                }
+            }
+            
+            log.info("批量插入黑名单完成,成功插入{}条记录", insertedCount);
+            return insertedCount;
+            
+        } catch (Exception e) {
+            log.error("批量插入黑名单异常", e);
+            throw new RuntimeException("批量插入黑名单失败", e);
+        }
+    }
+    
+    @Override
+    public int getCorpBlacklistCount(Long corpId) {
+        if (corpId == null) {
+            log.warn("获取企业黑名单数量时企业ID为空");
+            return 0;
+        }
+        
+        try {
+            QywxCorpBlacklistUserExample example = new QywxCorpBlacklistUserExample();
+            example.createCriteria().andCorpIdEqualTo(corpId);
+            
+            long count = corpBlacklistUserMapper.countByExample(example);
+            
+            log.debug("企业{}黑名单用户数量: {}", corpId, count);
+            return (int) count;
+            
+        } catch (Exception e) {
+            log.error("企业{}获取黑名单数量异常", corpId, e);
+            return 0;
+        }
+    }
+
+
+}

+ 258 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/impl/BlacklistSyncServiceImpl.java

@@ -0,0 +1,258 @@
+package com.tzld.piaoquan.risk.control.service.sync.impl;
+
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser;
+import com.tzld.piaoquan.risk.control.model.qywx.RoomBlacklistResponse;
+import com.tzld.piaoquan.risk.control.service.impl.RiskUserOperateService;
+import com.tzld.piaoquan.risk.control.service.sync.BlacklistSyncService;
+import com.tzld.piaoquan.risk.control.service.sync.data.BlacklistDataService;
+import com.tzld.piaoquan.risk.control.service.sync.lock.RedisDistributedLockService;
+import com.tzld.piaoquan.risk.control.service.sync.strategy.SyncDecision;
+import com.tzld.piaoquan.risk.control.service.sync.strategy.SyncTimeStrategy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 黑名单同步服务实现类
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Service
+public class BlacklistSyncServiceImpl implements BlacklistSyncService {
+    
+    private static final Logger log = LoggerFactory.getLogger(BlacklistSyncServiceImpl.class);
+    
+    @Autowired
+    private SyncTimeStrategy syncTimeStrategy;
+    
+    @Autowired
+    private BlacklistDataService blacklistDataService;
+    
+    @Autowired
+    private RiskUserOperateService riskUserOperateService;
+    
+    @Autowired
+    private RedisDistributedLockService lockService;
+
+    private static final int SLEEP_TIME = 1500; // 1.5秒
+    
+    @Override
+    public void intelligentSyncCheck(Long corpIdParam) {
+        // 1. 获取所有企业ID
+        List<Long> corpIds = new ArrayList<>();
+        if (corpIdParam == null) {
+            corpIds = blacklistDataService.getAllCorpIds();
+        }else {
+            corpIds.add(corpIdParam);
+        }
+        log.info("开始智能同步检查,共{}个企业", corpIds.size());
+        
+        int syncCount = 0;
+        int skipCount = 0;
+        int lockFailedCount = 0;
+        long startTime = System.currentTimeMillis();
+        
+        // 2. 遍历企业ID,进行智能决策
+        for (Long corpId : corpIds) {
+            try {
+                Thread.sleep(SLEEP_TIME);
+                // 3. 判断是否需要同步
+                SyncDecision decision = syncTimeStrategy.shouldSync(corpId);
+                
+                log.info("企业{}同步决策: {}, 原因: {}", corpId, 
+                        decision.isShouldSync() ? "执行同步" : "跳过同步", 
+                        decision.getReason());
+                
+                if (decision.isShouldSync()) {
+                    // 4. 尝试获取企业级分布式锁
+                    String lockKey = "corp_sync_" + corpId;
+                    if (lockService.tryLock(lockKey, 300)) { // 锁定5分钟
+                        try {
+                            log.info("企业{}获取分布式锁成功,开始执行同步", corpId);
+                            
+                            // 5. 执行同步
+                            boolean success = executeSyncForCorp(corpId);
+                            if (success) {
+                                // 6. 更新同步时间
+                                syncTimeStrategy.updateLastSyncTime(corpId);
+                                syncCount++;
+                                log.info("企业{}同步成功", corpId);
+                            } else {
+                                log.error("企业{}同步失败", corpId);
+                            }
+                            
+                        } finally {
+                            // 7. 释放企业级锁
+                            lockService.releaseLock(lockKey);
+                            log.debug("企业{}释放分布式锁", corpId);
+                        }
+                    } else {
+                        lockFailedCount++;
+                        log.info("企业{}获取分布式锁失败,其他实例正在同步该企业", corpId);
+                    }
+                } else {
+                    skipCount++;
+                }
+                
+            } catch (Exception e) {
+                log.error("企业{}同步检查异常", corpId, e);
+            }
+        }
+        
+        long endTime = System.currentTimeMillis();
+        long duration = endTime - startTime;
+        
+        log.info("智能同步检查完成,耗时{}ms,执行同步: {}个企业,跳过同步: {}个企业,锁冲突: {}个企业", 
+                duration, syncCount, skipCount, lockFailedCount);
+    }
+    
+    @Override
+    public boolean forceSyncCorp(Long corpId) {
+        if (corpId == null) {
+            log.warn("强制同步企业时企业ID为空");
+            return false;
+        }
+        
+        log.info("开始强制同步企业: {}", corpId);
+        
+        // 尝试获取企业级分布式锁
+        String lockKey = "corp_sync_" + corpId;
+        
+        if (lockService.tryLock(lockKey, 300)) { // 锁定5分钟
+            try {
+                log.info("企业{}获取分布式锁成功,开始强制同步", corpId);
+                
+                boolean success = executeSyncForCorp(corpId);
+                if (success) {
+                    // 更新同步时间
+                    syncTimeStrategy.updateLastSyncTime(corpId);
+                    log.info("企业{}强制同步成功", corpId);
+                    return true;
+                } else {
+                    log.error("企业{}强制同步失败", corpId);
+                    return false;
+                }
+                
+            } finally {
+                lockService.releaseLock(lockKey);
+                log.debug("企业{}释放分布式锁", corpId);
+            }
+        } else {
+            log.warn("企业{}获取分布式锁失败,其他实例正在同步该企业", corpId);
+            return false;
+        }
+    }
+    
+    /**
+     * 执行企业同步
+     * 
+     * @param corpId 企业ID
+     * @return true表示同步成功,false表示同步失败
+     */
+    private boolean executeSyncForCorp(Long corpId) {
+        try {
+            // 获取登录用户UUID
+            String uuid = blacklistDataService.getLoginUserUuid(corpId);
+            if (uuid == null) {
+                log.warn("企业{}未找到登录用户", corpId);
+                return false;
+            }
+            
+            log.info("企业{}开始同步,使用UUID: {}", corpId, uuid);
+            
+            // 调用API获取黑名单数据
+            RoomBlacklistResponse response = riskUserOperateService.getRoomBlacklist(uuid);
+            
+            if (response != null && response.getErrcode() == 0) {
+                // 清理旧数据
+                int deletedCount = blacklistDataService.clearCorpBlacklist(corpId);
+                log.debug("企业{}清理旧黑名单数据完成,删除{}条记录", corpId, deletedCount);
+                
+                // 插入新数据
+                if (response.getData() != null && response.getData().getList() != null) {
+                    List<QywxCorpBlacklistUser> users = convertToCorpBlacklistUsers(
+                            response.getData().getList(), corpId);
+                    
+                    if (!users.isEmpty()) {
+                        int insertedCount = blacklistDataService.batchInsertBlacklist(users);
+                        log.info("企业{}同步完成,更新{}条黑名单记录", corpId, insertedCount);
+                    } else {
+                        log.info("企业{}同步完成,黑名单为空", corpId);
+                    }
+                } else {
+                    log.info("企业{}同步完成,API返回数据为空", corpId);
+                }
+                
+                return true;
+            } else {
+                log.error("企业{}API调用失败,错误码: {}, 错误信息: {}", 
+                        corpId, response != null ? response.getErrcode() : -1,
+                        response != null ? response.getErrmsg() : "未知错误");
+                return false;
+            }
+            
+        } catch (Exception e) {
+            log.error("企业{}同步执行异常", corpId, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 转换API响应数据为数据库实体
+     * 
+     * @param items API响应的黑名单项列表
+     * @param corpId 企业ID
+     * @return 数据库实体列表
+     */
+    private List<QywxCorpBlacklistUser> convertToCorpBlacklistUsers(
+            List<RoomBlacklistResponse.BlacklistItem> items, Long corpId) {
+        
+        List<QywxCorpBlacklistUser> users = new ArrayList<>();
+        Date now = new Date();
+        
+        for (RoomBlacklistResponse.BlacklistItem item : items) {
+            if (item.getBlacklist_vid() != null) {
+                QywxCorpBlacklistUser user = new QywxCorpBlacklistUser();
+                user.setVid(item.getBlacklist_vid());
+                user.setCorpId(corpId);
+                // 判断时间戳长度,如果不是13位才补齐
+                long timestamp = item.getCreate_time();
+                if (String.valueOf(timestamp).length() == 10) {
+                    timestamp = timestamp * 1000; // 10位时间戳转13位
+                }
+                user.setBlackTime(new Date(timestamp));
+                user.setCreateTime(now);
+                users.add(user);
+            }
+        }
+        
+        return users;
+    }
+    
+    @Override
+    public String getSyncStatistics() {
+        try {
+            List<Long> corpIds = blacklistDataService.getAllCorpIds();
+            int totalCorps = corpIds.size();
+            int totalBlacklistUsers = 0;
+            
+            for (Long corpId : corpIds) {
+                totalBlacklistUsers += blacklistDataService.getCorpBlacklistCount(corpId);
+            }
+            
+            return String.format("同步统计: 企业总数=%d, 黑名单用户总数=%d", totalCorps, totalBlacklistUsers);
+            
+        } catch (Exception e) {
+            log.error("获取同步统计信息异常", e);
+            return "同步统计信息获取失败: " + e.getMessage();
+        }
+    }
+}

+ 150 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/lock/RedisDistributedLockService.java

@@ -0,0 +1,150 @@
+package com.tzld.piaoquan.risk.control.service.sync.lock;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis分布式锁服务
+ * 
+ * 使用Redis SETNX实现企业级分布式锁
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Component
+public class RedisDistributedLockService {
+    
+    private static final Logger log = LoggerFactory.getLogger(RedisDistributedLockService.class);
+
+    @Resource(name = "redisTemplate")
+    private RedisTemplate<String, String> redisTemplate;
+    
+    private static final String LOCK_PREFIX = "blacklist_sync_lock:";
+    private static final int DEFAULT_EXPIRE_TIME = 300; // 5分钟
+    
+    /**
+     * 锁值存储,用于释放锁时验证
+     */
+    private final ThreadLocal<String> lockValueHolder = new ThreadLocal<>();
+    
+    /**
+     * 尝试获取分布式锁
+     * 
+     * @param lockKey 锁的key
+     * @param expireTime 过期时间(秒)
+     * @return true表示获取成功,false表示获取失败
+     */
+    public boolean tryLock(String lockKey, int expireTime) {
+        String key = LOCK_PREFIX + lockKey;
+        String value = UUID.randomUUID().toString();
+        
+        try {
+            // 使用SET命令的NX和EX参数实现原子操作
+            Boolean result = redisTemplate.opsForValue()
+                .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
+                
+            if (Boolean.TRUE.equals(result)) {
+                // 将锁值存储到ThreadLocal,用于释放锁时验证
+                lockValueHolder.set(value);
+                log.info("成功获取分布式锁: {}, 过期时间: {}秒", key, expireTime);
+                return true;
+            } else {
+                log.debug("获取分布式锁失败: {}", key);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("获取分布式锁异常: {}", key, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 尝试获取分布式锁(使用默认过期时间)
+     * 
+     * @param lockKey 锁的key
+     * @return true表示获取成功,false表示获取失败
+     */
+    public boolean tryLock(String lockKey) {
+        return tryLock(lockKey, DEFAULT_EXPIRE_TIME);
+    }
+    
+    /**
+     * 释放分布式锁
+     * 
+     * @param lockKey 锁的key
+     */
+    public void releaseLock(String lockKey) {
+        String key = LOCK_PREFIX + lockKey;
+        String value = lockValueHolder.get();
+        
+        if (value != null) {
+            try {
+                // 使用Lua脚本确保原子性:只有锁的持有者才能释放锁
+                String luaScript = 
+                    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                    "return redis.call('del', KEYS[1]) " +
+                    "else return 0 end";
+                    
+                Long result = redisTemplate.execute(
+                    new DefaultRedisScript<>(luaScript, Long.class),
+                    Collections.singletonList(key),
+                    value
+                );
+                
+                if (result != null && result == 1) {
+                    log.info("成功释放分布式锁: {}", key);
+                } else {
+                    log.warn("释放分布式锁失败,可能已过期: {}", key);
+                }
+                
+            } catch (Exception e) {
+                log.error("释放分布式锁异常: {}", key, e);
+            } finally {
+                lockValueHolder.remove();
+            }
+        } else {
+            log.warn("尝试释放未持有的锁: {}", key);
+        }
+    }
+    
+    /**
+     * 检查锁是否存在
+     * 
+     * @param lockKey 锁的key
+     * @return true表示锁存在,false表示锁不存在
+     */
+    public boolean isLocked(String lockKey) {
+        String key = LOCK_PREFIX + lockKey;
+        try {
+            return Boolean.TRUE.equals(redisTemplate.hasKey(key));
+        } catch (Exception e) {
+            log.error("检查锁状态异常: {}", key, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 获取锁的剩余过期时间
+     * 
+     * @param lockKey 锁的key
+     * @return 剩余过期时间(秒),-1表示永不过期,-2表示key不存在
+     */
+    public long getLockTtl(String lockKey) {
+        String key = LOCK_PREFIX + lockKey;
+        try {
+            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            log.error("获取锁过期时间异常: {}", key, e);
+            return -2;
+        }
+    }
+}

+ 154 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/strategy/SyncDecision.java

@@ -0,0 +1,154 @@
+package com.tzld.piaoquan.risk.control.service.sync.strategy;
+
+/**
+ * 同步决策实体类
+ * 
+ * 封装同步决策的结果和原因
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+public class SyncDecision {
+    
+    /**
+     * 是否应该同步
+     */
+    private boolean shouldSync;
+    
+    /**
+     * 决策原因
+     */
+    private String reason;
+    
+    /**
+     * 时间差(毫秒)
+     */
+    private long timeDiff;
+    
+    /**
+     * 私有构造函数
+     * 
+     * @param shouldSync 是否应该同步
+     * @param reason 决策原因
+     * @param timeDiff 时间差(毫秒)
+     */
+    private SyncDecision(boolean shouldSync, String reason, long timeDiff) {
+        this.shouldSync = shouldSync;
+        this.reason = reason;
+        this.timeDiff = timeDiff;
+    }
+    
+    /**
+     * 创建同步决策(执行同步)
+     * 
+     * @param reason 决策原因
+     * @param timeDiff 时间差(毫秒)
+     * @return 同步决策对象
+     */
+    public static SyncDecision sync(String reason, long timeDiff) {
+        return new SyncDecision(true, reason, timeDiff);
+    }
+    
+    /**
+     * 创建跳过决策(跳过同步)
+     * 
+     * @param reason 决策原因
+     * @param timeDiff 时间差(毫秒)
+     * @return 同步决策对象
+     */
+    public static SyncDecision skip(String reason, long timeDiff) {
+        return new SyncDecision(false, reason, timeDiff);
+    }
+    
+    /**
+     * 创建首次同步决策
+     * 
+     * @param reason 决策原因
+     * @return 同步决策对象
+     */
+    public static SyncDecision firstSync(String reason) {
+        return new SyncDecision(true, reason, 0);
+    }
+    
+    public boolean isShouldSync() {
+        return shouldSync;
+    }
+    
+    public String getReason() {
+        return reason;
+    }
+    
+    public long getTimeDiff() {
+        return timeDiff;
+    }
+    
+    /**
+     * 获取时间差(小时)
+     * 
+     * @return 时间差小时数
+     */
+    public double getTimeDiffHours() {
+        return timeDiff / (3600.0 * 1000);
+    }
+    
+    /**
+     * 获取时间差(分钟)
+     * 
+     * @return 时间差分钟数
+     */
+    public double getTimeDiffMinutes() {
+        return timeDiff / (60.0 * 1000);
+    }
+    
+    /**
+     * 获取决策类型描述
+     * 
+     * @return 决策类型
+     */
+    public String getDecisionType() {
+        return shouldSync ? "执行同步" : "跳过同步";
+    }
+    
+    /**
+     * 获取详细的决策信息
+     * 
+     * @return 详细决策信息
+     */
+    public String getDetailedInfo() {
+        if (timeDiff == 0) {
+            return String.format("决策: %s, 原因: %s", getDecisionType(), reason);
+        } else {
+            return String.format("决策: %s, 原因: %s, 时间差: %.1f小时", 
+                    getDecisionType(), reason, getTimeDiffHours());
+        }
+    }
+    
+    @Override
+    public String toString() {
+        return String.format("SyncDecision{shouldSync=%s, reason='%s', timeDiff=%dms}", 
+                shouldSync, reason, timeDiff);
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        
+        SyncDecision that = (SyncDecision) obj;
+        return shouldSync == that.shouldSync
+            && timeDiff == that.timeDiff
+            && (reason != null ? reason.equals(that.reason) : that.reason == null);
+    }
+    
+    @Override
+    public int hashCode() {
+        int result = (shouldSync ? 1 : 0);
+        result = 31 * result + (reason != null ? reason.hashCode() : 0);
+        result = 31 * result + (int) (timeDiff ^ (timeDiff >>> 32));
+        return result;
+    }
+}

+ 189 - 0
risk-control-core/src/main/java/com/tzld/piaoquan/risk/control/service/sync/strategy/SyncTimeStrategy.java

@@ -0,0 +1,189 @@
+package com.tzld.piaoquan.risk.control.service.sync.strategy;
+
+import com.tzld.piaoquan.risk.control.service.sync.cache.RedisSyncTimeCache;
+import com.tzld.piaoquan.risk.control.service.sync.config.ApolloConfigService;
+import com.tzld.piaoquan.risk.control.service.sync.config.SyncConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 同步时间策略
+ * 
+ * 基于时间间隔和随机概率的智能同步决策
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@Component
+public class SyncTimeStrategy {
+    
+    private static final Logger log = LoggerFactory.getLogger(SyncTimeStrategy.class);
+    
+    @Autowired
+    private RedisSyncTimeCache syncTimeCache;
+    
+    @Autowired
+    private ApolloConfigService apolloConfigService;
+    
+    /**
+     * 判断企业是否需要同步
+     * 
+     * @param corpId 企业ID
+     * @return 同步决策结果
+     */
+    public SyncDecision shouldSync(Long corpId) {
+        if (corpId == null) {
+            log.warn("企业ID为空,跳过同步决策");
+            return SyncDecision.skip("企业ID为空", 0);
+        }
+        
+        try {
+            // 获取配置参数
+            SyncConfig config = apolloConfigService.getSyncConfig();
+            log.debug("企业{}使用同步配置: {}", corpId, config);
+            
+            // 获取上次同步时间
+            Long lastSyncTime = syncTimeCache.getLastSyncTime(corpId);
+            long currentTime = System.currentTimeMillis();
+            
+            if (lastSyncTime == null) {
+                // 首次同步,直接执行
+                log.info("企业{}首次同步", corpId);
+                return SyncDecision.firstSync("首次同步");
+            }
+            
+            long timeDiff = currentTime - lastSyncTime;
+            long minInterval = config.getMinIntervalMillis();
+            long maxInterval = config.getMaxIntervalMillis();
+            
+            // 转换为小时便于日志显示
+            double timeDiffHours = timeDiff / (3600.0 * 1000);
+            
+            log.debug("企业{}时间差分析: 当前间隔={}小时, 最小间隔={}小时, 最大间隔={}小时", 
+                    corpId, timeDiffHours, config.getMinIntervalHours(), config.getMaxIntervalHours());
+            
+            if (timeDiff < minInterval) {
+                // 小于最小间隔,不同步
+                String reason = String.format("未达到最小间隔时间(%.1f小时 < %d小时)", 
+                        timeDiffHours, config.getMinIntervalHours());
+                return SyncDecision.skip(reason, timeDiff);
+                
+            } else if (timeDiff > maxInterval) {
+                // 大于最大间隔,必须同步
+                String reason = String.format("超过最大间隔时间(%.1f小时 > %d小时)", 
+                        timeDiffHours, config.getMaxIntervalHours());
+                return SyncDecision.sync(reason, timeDiff);
+                
+            } else {
+                // 在区间内,随机决策
+                double random = Math.random();
+                
+                log.debug("企业{}随机决策: 随机数={:.3f}, 概率阈值={:.3f}", 
+                        corpId, random, config.getSyncProbability());
+                
+                if (random < config.getSyncProbability()) {
+                    String reason = String.format("随机概率命中(%.3f < %.3f, 间隔%.1f小时)", 
+                            random, config.getSyncProbability(), timeDiffHours);
+                    return SyncDecision.sync(reason, timeDiff);
+                } else {
+                    String reason = String.format("随机概率未命中(%.3f >= %.3f, 间隔%.1f小时)", 
+                            random, config.getSyncProbability(), timeDiffHours);
+                    return SyncDecision.skip(reason, timeDiff);
+                }
+            }
+            
+        } catch (Exception e) {
+            log.error("企业{}同步决策异常", corpId, e);
+            return SyncDecision.skip("决策异常: " + e.getMessage(), 0);
+        }
+    }
+    
+    /**
+     * 更新企业最后同步时间
+     * 
+     * @param corpId 企业ID
+     */
+    public void updateLastSyncTime(Long corpId) {
+        if (corpId == null) {
+            log.warn("更新同步时间时企业ID为空");
+            return;
+        }
+        
+        try {
+            syncTimeCache.updateLastSyncTime(corpId);
+            log.debug("企业{}同步时间已更新", corpId);
+        } catch (Exception e) {
+            log.error("企业{}更新同步时间异常", corpId, e);
+        }
+    }
+    
+    /**
+     * 获取企业上次同步时间
+     * 
+     * @param corpId 企业ID
+     * @return 上次同步时间戳,如果不存在返回null
+     */
+    public Long getLastSyncTime(Long corpId) {
+        if (corpId == null) {
+            log.warn("获取同步时间时企业ID为空");
+            return null;
+        }
+        
+        try {
+            return syncTimeCache.getLastSyncTime(corpId);
+        } catch (Exception e) {
+            log.error("企业{}获取同步时间异常", corpId, e);
+            return null;
+        }
+    }
+    
+    /**
+     * 删除企业同步时间记录
+     * 
+     * @param corpId 企业ID
+     */
+    public void removeLastSyncTime(Long corpId) {
+        if (corpId == null) {
+            log.warn("删除同步时间时企业ID为空");
+            return;
+        }
+        
+        try {
+            syncTimeCache.removeLastSyncTime(corpId);
+            log.info("企业{}同步时间记录已删除", corpId);
+        } catch (Exception e) {
+            log.error("企业{}删除同步时间异常", corpId, e);
+        }
+    }
+    
+    /**
+     * 获取企业同步状态信息
+     * 
+     * @param corpId 企业ID
+     * @return 同步状态信息
+     */
+    public String getSyncStatusInfo(Long corpId) {
+        if (corpId == null) {
+            return "企业ID为空";
+        }
+        
+        try {
+            Long lastSyncTime = getLastSyncTime(corpId);
+            if (lastSyncTime == null) {
+                return "未同步过";
+            }
+            
+            long timeDiff = System.currentTimeMillis() - lastSyncTime;
+            double timeDiffHours = timeDiff / (3600.0 * 1000);
+            
+            return String.format("上次同步: %.1f小时前",
+                    timeDiffHours);
+                    
+        } catch (Exception e) {
+            log.error("企业{}获取同步状态信息异常", corpId, e);
+            return "状态信息获取失败: " + e.getMessage();
+        }
+    }
+}

+ 218 - 0
risk-control-core/src/main/resources/mapper/QywxCorpBlacklistUserMapper.xml

@@ -0,0 +1,218 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<!-- 企业微信客户主体黑名单用户MyBatis映射文件 -->
+<mapper namespace="com.tzld.piaoquan.risk.control.dao.mapper.QywxCorpBlacklistUserMapper">
+  
+  <!-- 基础结果映射 -->
+  <resultMap id="BaseResultMap" type="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser">
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="vid" jdbcType="BIGINT" property="vid" />
+    <result column="corp_id" jdbcType="BIGINT" property="corpId" />
+    <result column="black_time" jdbcType="TIMESTAMP" property="blackTime" />
+    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
+  </resultMap>
+  
+  <!-- 动态WHERE条件构建 -->
+  <sql id="Example_Where_Clause">
+    <where>
+      <foreach collection="oredCriteria" item="criteria" separator="or">
+        <if test="criteria.valid">
+          <trim prefix="(" prefixOverrides="and" suffix=")">
+            <foreach collection="criteria.criteria" item="criterion">
+              <choose>
+                <when test="criterion.noValue">
+                  and ${criterion.condition}
+                </when>
+                <when test="criterion.singleValue">
+                  and ${criterion.condition} #{criterion.value}
+                </when>
+                <when test="criterion.betweenValue">
+                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
+                </when>
+                <when test="criterion.listValue">
+                  and ${criterion.condition}
+                  <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
+                    #{listItem}
+                  </foreach>
+                </when>
+              </choose>
+            </foreach>
+          </trim>
+        </if>
+      </foreach>
+    </where>
+  </sql>
+  
+  <!-- 基础字段列表 -->
+  <sql id="Base_Column_List">
+    id, vid, corp_id, black_time, create_time
+  </sql>
+  
+  <!-- 根据条件查询记录列表 -->
+  <select id="selectByExample" parameterType="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUserExample" resultMap="BaseResultMap">
+    select
+    <if test="distinct">
+      distinct
+    </if>
+    <include refid="Base_Column_List" />
+    from qywx_corp_blacklist_user
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+    <if test="orderByClause != null">
+      order by ${orderByClause}
+    </if>
+  </select>
+  
+  <!-- 根据主键查询单条记录 -->
+  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
+    select 
+    <include refid="Base_Column_List" />
+    from qywx_corp_blacklist_user
+    where id = #{id,jdbcType=BIGINT}
+  </select>
+  
+  <!-- 根据vid和corpId查询拉黑时间 -->
+  <select id="selectBlackTimeByVidAndCorpId" resultType="java.util.Date">
+    select black_time
+    from qywx_corp_blacklist_user
+    where vid = #{vid,jdbcType=BIGINT} and corp_id = #{corpId,jdbcType=BIGINT}
+  </select>
+  
+  <!-- 根据主键删除记录 -->
+  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
+    delete from qywx_corp_blacklist_user
+    where id = #{id,jdbcType=BIGINT}
+  </delete>
+  
+  <!-- 根据条件批量删除记录 -->
+  <delete id="deleteByExample" parameterType="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUserExample">
+    delete from qywx_corp_blacklist_user
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+  </delete>
+  
+  <!-- 插入完整记录 -->
+  <insert id="insert" parameterType="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser">
+    <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
+      SELECT LAST_INSERT_ID()
+    </selectKey>
+    insert into qywx_corp_blacklist_user (vid, corp_id, black_time, create_time)
+    values (#{vid,jdbcType=BIGINT}, #{corpId,jdbcType=BIGINT}, #{blackTime,jdbcType=TIMESTAMP}, #{createTime,jdbcType=TIMESTAMP})
+  </insert>
+  
+  <!-- 选择性插入记录 -->
+  <insert id="insertSelective" parameterType="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser">
+    <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
+      SELECT LAST_INSERT_ID()
+    </selectKey>
+    insert into qywx_corp_blacklist_user
+    <trim prefix="(" suffix=")" suffixOverrides=",">
+      <if test="vid != null">
+        vid,
+      </if>
+      <if test="corpId != null">
+        corp_id,
+      </if>
+      <if test="blackTime != null">
+        black_time,
+      </if>
+      <if test="createTime != null">
+        create_time,
+      </if>
+    </trim>
+    <trim prefix="values (" suffix=")" suffixOverrides=",">
+      <if test="vid != null">
+        #{vid,jdbcType=BIGINT},
+      </if>
+      <if test="corpId != null">
+        #{corpId,jdbcType=BIGINT},
+      </if>
+      <if test="blackTime != null">
+        #{blackTime,jdbcType=TIMESTAMP},
+      </if>
+      <if test="createTime != null">
+        #{createTime,jdbcType=TIMESTAMP},
+      </if>
+    </trim>
+  </insert>
+  
+  <!-- 根据条件统计记录数 -->
+  <select id="countByExample" parameterType="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUserExample" resultType="java.lang.Long">
+    select count(*) from qywx_corp_blacklist_user
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+  </select>
+  
+  <!-- 根据条件选择性更新记录 -->
+  <update id="updateByExampleSelective" parameterType="map">
+    update qywx_corp_blacklist_user
+    <set>
+      <if test="record.id != null">
+        id = #{record.id,jdbcType=BIGINT},
+      </if>
+      <if test="record.vid != null">
+        vid = #{record.vid,jdbcType=BIGINT},
+      </if>
+      <if test="record.corpId != null">
+        corp_id = #{record.corpId,jdbcType=BIGINT},
+      </if>
+      <if test="record.blackTime != null">
+        black_time = #{record.blackTime,jdbcType=TIMESTAMP},
+      </if>
+      <if test="record.createTime != null">
+        create_time = #{record.createTime,jdbcType=TIMESTAMP},
+      </if>
+    </set>
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+  </update>
+  
+  <!-- 根据条件完整更新记录 -->
+  <update id="updateByExample" parameterType="map">
+    update qywx_corp_blacklist_user
+    set id = #{record.id,jdbcType=BIGINT},
+      vid = #{record.vid,jdbcType=BIGINT},
+      corp_id = #{record.corpId,jdbcType=BIGINT},
+      black_time = #{record.blackTime,jdbcType=TIMESTAMP},
+      create_time = #{record.createTime,jdbcType=TIMESTAMP}
+    <if test="_parameter != null">
+      <include refid="Example_Where_Clause" />
+    </if>
+  </update>
+  
+  <!-- 根据主键选择性更新记录 -->
+  <update id="updateByPrimaryKeySelective" parameterType="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser">
+    update qywx_corp_blacklist_user
+    <set>
+      <if test="vid != null">
+        vid = #{vid,jdbcType=BIGINT},
+      </if>
+      <if test="corpId != null">
+        corp_id = #{corpId,jdbcType=BIGINT},
+      </if>
+      <if test="blackTime != null">
+        black_time = #{blackTime,jdbcType=TIMESTAMP},
+      </if>
+      <if test="createTime != null">
+        create_time = #{createTime,jdbcType=TIMESTAMP},
+      </if>
+    </set>
+    where id = #{id,jdbcType=BIGINT}
+  </update>
+  
+  <!-- 根据主键完整更新记录 -->
+  <update id="updateByPrimaryKey" parameterType="com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser">
+    update qywx_corp_blacklist_user
+    set vid = #{vid,jdbcType=BIGINT},
+      corp_id = #{corpId,jdbcType=BIGINT},
+      black_time = #{blackTime,jdbcType=TIMESTAMP},
+      create_time = #{createTime,jdbcType=TIMESTAMP}
+    where id = #{id,jdbcType=BIGINT}
+  </update>
+  
+</mapper>

+ 203 - 0
risk-control-server/src/main/java/com/tzld/piaoquan/risk/control/controller/BlacklistSyncController.java

@@ -0,0 +1,203 @@
+package com.tzld.piaoquan.risk.control.controller;
+
+import com.tzld.piaoquan.risk.control.common.base.CommonResponse;
+import com.tzld.piaoquan.risk.control.service.sync.BlacklistSyncService;
+import com.tzld.piaoquan.risk.control.service.sync.config.ApolloConfigService;
+import com.tzld.piaoquan.risk.control.service.sync.strategy.SyncTimeStrategy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 黑名单同步管理控制器
+ * 
+ * 提供同步任务的手动触发、监控和调试功能
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@RestController
+@RequestMapping("/sync")
+public class BlacklistSyncController {
+    
+    private static final Logger log = LoggerFactory.getLogger(BlacklistSyncController.class);
+    
+    @Autowired
+    private BlacklistSyncService blacklistSyncService;
+    
+    @Autowired
+    private ApolloConfigService apolloConfigService;
+    
+    @Autowired
+    private SyncTimeStrategy syncTimeStrategy;
+    
+    /**
+     * 手动触发智能同步检查
+     * 
+     * @return 执行结果
+     */
+    @PostMapping("/trigger")
+    public CommonResponse<String> triggerSync() {
+        try {
+            log.info("手动触发智能同步检查");
+            
+            long startTime = System.currentTimeMillis();
+            blacklistSyncService.intelligentSyncCheck(null);
+            long duration = System.currentTimeMillis() - startTime;
+            
+            String result = String.format("智能同步检查完成,耗时: %dms", duration);
+            log.info("手动触发同步完成: {}", result);
+            
+            return CommonResponse.success(result);
+            
+        } catch (Exception e) {
+            log.error("手动触发同步失败", e);
+            return CommonResponse.create(-1, "同步失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 强制同步指定企业
+     * 
+     * @param corpId 企业ID
+     * @return 执行结果
+     */
+    @PostMapping("/force/{corpId}")
+    public CommonResponse<String> forceSync(@PathVariable Long corpId) {
+        try {
+            log.info("手动强制同步企业: {}", corpId);
+            
+            if (corpId == null || corpId <= 0) {
+                return CommonResponse.create(-1, "企业ID无效");
+            }
+            
+            long startTime = System.currentTimeMillis();
+            boolean success = blacklistSyncService.forceSyncCorp(corpId);
+            long duration = System.currentTimeMillis() - startTime;
+            
+            if (success) {
+                String result = String.format("企业%d强制同步成功,耗时: %dms", corpId, duration);
+                log.info("手动强制同步成功: {}", result);
+                return CommonResponse.success(result);
+            } else {
+                String result = String.format("企业%d强制同步失败", corpId);
+                log.warn("手动强制同步失败: {}", result);
+                return CommonResponse.create(-1, result);
+            }
+            
+        } catch (Exception e) {
+            log.error("手动强制同步企业{}失败", corpId, e);
+            return CommonResponse.create(-1, "强制同步失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取同步统计信息
+     * 
+     * @return 统计信息
+     */
+    @GetMapping("/statistics")
+    public CommonResponse<String> getStatistics() {
+        try {
+            String statistics = blacklistSyncService.getSyncStatistics();
+            log.debug("获取同步统计信息: {}", statistics);
+            return CommonResponse.success(statistics);
+            
+        } catch (Exception e) {
+            log.error("获取同步统计信息失败", e);
+            return CommonResponse.create(-1, "获取统计信息失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取配置信息
+     * 
+     * @return 配置信息
+     */
+    @GetMapping("/config")
+    public CommonResponse<String> getConfig() {
+        try {
+            String configInfo = apolloConfigService.getConfigInfo();
+            log.debug("获取配置信息: {}", configInfo);
+            return CommonResponse.success(configInfo);
+            
+        } catch (Exception e) {
+            log.error("获取配置信息失败", e);
+            return CommonResponse.create(-1, "获取配置信息失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取企业同步状态
+     * 
+     * @param corpId 企业ID
+     * @return 同步状态信息
+     */
+    @GetMapping("/status/{corpId}")
+    public CommonResponse<String> getCorpSyncStatus(@PathVariable Long corpId) {
+        try {
+            if (corpId == null || corpId <= 0) {
+                return CommonResponse.create(-1, "企业ID无效");
+            }
+            
+            String statusInfo = syncTimeStrategy.getSyncStatusInfo(corpId);
+            log.debug("企业{}同步状态: {}", corpId, statusInfo);
+            return CommonResponse.success(statusInfo);
+            
+        } catch (Exception e) {
+            log.error("获取企业{}同步状态失败", corpId, e);
+            return CommonResponse.create(-1, "获取同步状态失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 删除企业同步时间记录
+     * 
+     * @param corpId 企业ID
+     * @return 执行结果
+     */
+    @DeleteMapping("/time/{corpId}")
+    public CommonResponse<String> removeCorpSyncTime(@PathVariable Long corpId) {
+        try {
+            if (corpId == null || corpId <= 0) {
+                return CommonResponse.create(-1, "企业ID无效");
+            }
+            
+            log.info("删除企业{}同步时间记录", corpId);
+            syncTimeStrategy.removeLastSyncTime(corpId);
+            
+            String result = String.format("企业%d同步时间记录已删除", corpId);
+            return CommonResponse.success(result);
+            
+        } catch (Exception e) {
+            log.error("删除企业{}同步时间记录失败", corpId, e);
+            return CommonResponse.create(-1, "删除同步时间记录失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 健康检查
+     * 
+     * @return 系统状态
+     */
+    @GetMapping("/health")
+    public CommonResponse<String> healthCheck() {
+        try {
+            // 检查配置是否正常
+            boolean configValid = apolloConfigService.validateConfig();
+            
+            // 获取基本统计信息
+            String statistics = blacklistSyncService.getSyncStatistics();
+            
+            String healthInfo = String.format("系统状态: 正常, 配置状态: %s, %s", 
+                    configValid ? "有效" : "无效", statistics);
+            
+            return CommonResponse.success(healthInfo);
+            
+        } catch (Exception e) {
+            log.error("健康检查失败", e);
+            return CommonResponse.create(-1, "系统异常: " + e.getMessage());
+        }
+    }
+}

+ 72 - 0
risk-control-server/src/main/java/com/tzld/piaoquan/risk/control/controller/CorpBlacklistController.java

@@ -0,0 +1,72 @@
+package com.tzld.piaoquan.risk.control.controller;
+
+import com.tzld.piaoquan.risk.control.common.base.CommonResponse;
+import com.tzld.piaoquan.risk.control.model.po.QywxCorpBlacklistUser;
+import com.tzld.piaoquan.risk.control.service.CorpBlacklistService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 企业微信客户主体黑名单管理控制器
+ * 
+ * @author 风控系统开发团队
+ * @since 1.0.0
+ */
+@RestController
+@RequestMapping("/corp-blacklist")
+public class CorpBlacklistController {
+    
+    private static final Logger log = LoggerFactory.getLogger(CorpBlacklistController.class);
+    
+    @Autowired
+    private CorpBlacklistService corpBlacklistService;
+    
+    /**
+     * 根据企业ID查询黑名单用户(全量,不分页)
+     * 
+     * @param corpId 企业ID
+     * @return 该企业的所有黑名单用户
+     */
+    @GetMapping("/corp/{corpId}")
+    public CommonResponse<List<QywxCorpBlacklistUser>> getBlacklistByCorpId(@PathVariable Long corpId) {
+        try {
+            log.info("Request to get corp blacklist users for corpId: {}", corpId);
+            
+            // 参数校验
+            if (corpId == null) {
+                log.warn("Get corp blacklist request with null corpId");
+                return CommonResponse.create(-1, "企业ID不能为空");
+            }
+            if (corpId <= 0) {
+                log.warn("Get corp blacklist request with invalid corpId: {}", corpId);
+                return CommonResponse.create(-1, "企业ID必须是有效的正数");
+            }
+            
+            // 调用业务服务获取企业黑名单用户
+            List<QywxCorpBlacklistUser> users = corpBlacklistService.getBlacklistByCorpId(corpId);
+            
+            log.info("Retrieved corp blacklist users for corpId: {}, count: {}", corpId, users.size());
+            
+            // 数据量警告
+            if (users.size() > 1000) {
+                log.warn("Large corp blacklist dataset returned: {} records for corpId: {}", users.size(), corpId);
+            }
+            
+            return CommonResponse.success(users);
+            
+        } catch (IllegalArgumentException e) {
+            log.warn("Parameter validation failed for get corp blacklist, corpId: {}", corpId, e);
+            return CommonResponse.create(-1, "参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("Failed to get corp blacklist users for corpId: {}", corpId, e);
+            return CommonResponse.create(-1, "获取企业黑名单用户失败,请稍后重试");
+        }
+    }
+}

+ 1 - 5
risk-control-server/src/main/java/com/tzld/piaoquan/risk/control/controller/ReceiveRiskInfoController.java

@@ -4,20 +4,16 @@ import com.alibaba.fastjson.JSON;
 import com.tzld.piaoquan.risk.control.common.annotation.UnAuth;
 import com.tzld.piaoquan.risk.control.common.base.CommonResponse;
 import com.tzld.piaoquan.risk.control.config.QywxConfig;
-import com.tzld.piaoquan.risk.control.model.qywx.QwLoginCheckCode;
 import com.tzld.piaoquan.risk.control.model.qywx.RiskUserDelResult;
 import com.tzld.piaoquan.risk.control.model.qywx.RiskUserInfo;
 import com.tzld.piaoquan.risk.control.service.impl.RiskUserHandleService;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.rocketmq.shaded.org.slf4j.MDC;
 import org.springframework.beans.factory.annotation.Autowired;
 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 java.util.UUID;
-
 @RestController
 @RequestMapping("/qw")
 @Slf4j
@@ -29,7 +25,7 @@ public class ReceiveRiskInfoController {
 
     /**
      * 接收风控用户信息
-     * @param userInfo 风控用户信息
+     * @param rawJson 风控用户信息
      * @return CommonResponse
      */
     @UnAuth

+ 1 - 0
risk-control-server/src/main/resources/application.yml

@@ -42,5 +42,6 @@ qywx:
     get-chatList: /wxwork/GetChatroomMembers
     get-roomMembers: /wxwork/GetRoomUserList
     kick-external: /wxwork/addOrSubRoomBlackList
+    get-room-black-list: /wxwork/getRoomBlackList
 getUserList:
   url: "https://api.piaoquantv.com/ad/platform/wechat/group/groupUserDetailData"  # 测试获取用户列表地址

+ 10 - 14
risk-control-server/src/main/resources/logback-spring.xml

@@ -48,7 +48,7 @@
         <file>${LOG_PATH}/debug.log</file>
         <!--日志文件输出格式-->
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] %logger{50} - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] [%X{requestMethod:-} %X{requestUri:-}] [%X{clientIp:-}] [%X{businessType:-}] [%X{markParam:-}] %logger{50} - %msg%n</pattern>
             <charset>UTF-8</charset> <!-- 设置字符集 -->
         </encoder>
         <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
@@ -88,14 +88,12 @@
         <!-- 可选参数 -->
 <!--        <topic>your-topic</topic>  &lt;!&ndash; 如需按Topic分类日志 &ndash;&gt;-->
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] %logger{50} - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] [%X{requestMethod:-} %X{requestUri:-}] [%X{clientIp:-}] [%X{businessType:-}] [%X{markParam:-}] %logger{50} - %msg%n</pattern>
             <charset>UTF-8</charset>
         </encoder>
-        <!-- 上传INFO级别日志 -->
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+        <!-- 上传INFO级别及以上的日志(包括ERROR) -->
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
             <level>INFO</level>
-            <onMatch>ACCEPT</onMatch>
-            <onMismatch>DENY</onMismatch>
         </filter>
     </appender>
     <appender name="LOG_HUB_ERROR" class="com.aliyun.openservices.log.logback.LoghubAppender">
@@ -117,7 +115,7 @@
         <file>${LOG_PATH}/info.log</file>
         <!--日志文件输出格式-->
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] %logger{50} - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] [%X{requestMethod:-} %X{requestUri:-}] [%X{clientIp:-}] [%X{businessType:-}] [%X{markParam:-}] %logger{50} - %msg%n</pattern>
             <charset>UTF-8</charset>
         </encoder>
         <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
@@ -130,11 +128,9 @@
             <!--日志文件保留天数-->
             <maxHistory>15</maxHistory>
         </rollingPolicy>
-        <!-- 此日志文件只记录info级别的 -->
-        <filter class="ch.qos.logback.classic.filter.LevelFilter">
-            <level>info</level>
-            <onMatch>ACCEPT</onMatch>
-            <onMismatch>DENY</onMismatch>
+        <!-- 此日志文件记录info级别及以上的日志(包括ERROR) -->
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
         </filter>
     </appender>
 
@@ -144,7 +140,7 @@
         <file>${LOG_PATH}/warn.log</file>
         <!--日志文件输出格式-->
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] %logger{50} - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] [%X{requestMethod:-} %X{requestUri:-}] [%X{clientIp:-}] [%X{businessType:-}] [%X{markParam:-}] %logger{50} - %msg%n</pattern>
             <charset>UTF-8</charset> <!-- 此处设置字符集 -->
         </encoder>
         <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
@@ -171,7 +167,7 @@
         <file>${LOG_PATH}/error.log</file>
         <!--日志文件输出格式-->
         <encoder>
-            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] %logger{50} - %msg%n</pattern>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{logTraceId}] [%X{requestMethod:-} %X{requestUri:-}] [%X{clientIp:-}] [%X{businessType:-}] [%X{markParam:-}] %logger{50} - %msg%n</pattern>
             <charset>UTF-8</charset> <!-- 此处设置字符集 -->
         </encoder>
         <!-- 日志记录器的滚动策略,按日期,按大小记录 -->