Forráskód Böngészése

云平台代码提交

fuyuchuan 2 napja
szülő
commit
776e034e58
24 módosított fájl, 658 hozzáadás és 21 törlés
  1. 10 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java
  2. 6 14
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  3. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql
  4. 70 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SysUser.java
  5. 15 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/SysUserMapper.java
  6. 22 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncPublishService.java
  7. 24 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java
  8. 5 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java
  9. 20 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java
  10. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java
  11. 2 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java
  12. 119 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncPublishServiceImpl.java
  13. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java
  14. 2 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java
  15. 2 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java
  16. 13 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPullServiceImpl.java
  17. 14 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java
  18. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserSyncPublishListener.java
  19. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudSyncEventPublisher.java
  20. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplier.java
  21. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplierRegistry.java
  22. 50 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncPayloadSupport.java
  23. 78 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/impl/UserSyncDataApplier.java
  24. 18 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/ManualSyncRequest.java

+ 10 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java

@@ -8,6 +8,8 @@ import com.usky.issue.service.constant.CloudIntegrationConstants;
 import com.usky.issue.service.vo.CloudConnectionTestResponse;
 import com.usky.issue.service.vo.SyncPacket;
 import com.usky.issue.service.vo.SyncResponse;
+import com.usky.issue.service.util.CredentialMaskUtil;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -28,6 +30,7 @@ import java.util.List;
  * @author fyc
  * @date 2026-06-08
  */
+@Slf4j
 @RestController
 @RequestMapping("/cloud/integration")
 public class CloudIntegrationApiController {
@@ -72,7 +75,11 @@ public class CloudIntegrationApiController {
             @RequestHeader(value = CloudIntegrationConstants.HEADER_TENANT_ID, required = false) String tenantIdHeader,
             @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey) {
         Integer tenantId = parseTenantId(tenantIdHeader);
+        log.info("[连接测试-云端] 收到请求 tenantIdHeader={}, tenantId={}, apiKey={}",
+                tenantIdHeader, tenantId, CredentialMaskUtil.mask(apiKey));
+
         if (tenantId != null && !cloudAuthService.validateToken(apiKey, tenantId)) {
+            log.warn("[连接测试-云端] 认证失败 tenantId={}, apiKey={}", tenantId, CredentialMaskUtil.mask(apiKey));
             CloudConnectionTestResponse response = new CloudConnectionTestResponse();
             response.setSuccess(false);
             response.setConnectionStatus(2);
@@ -83,9 +90,12 @@ public class CloudIntegrationApiController {
             return ApiResult.success(response);
         }
         CloudConnectionTestResponse response = cloudConfigService.validateTenantConnection(tenantId);
+        log.info("[连接测试-云端] 租户校验 success={}, message={}", response.isSuccess(), response.getMessage());
         response.setNetworkReachable(true);
         response.setAuthValid(tenantId == null || cloudAuthService.validateToken(apiKey, tenantId));
         response.setTenantExists(response.isSuccess());
+        log.info("[连接测试-云端] 返回 success={}, authValid={}, tenantExists={}, message={}",
+                response.isSuccess(), response.getAuthValid(), response.getTenantExists(), response.getMessage());
         return ApiResult.success(response);
     }
 

+ 6 - 14
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java

@@ -2,17 +2,14 @@ package com.usky.issue.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.security.utils.SecurityUtils;
+import com.usky.issue.service.vo.ManualSyncRequest;
 import com.usky.issue.service.vo.SyncStatusResponse;
 import com.usky.issue.service.vo.SyncTaskResponse;
 import com.usky.issue.service.CloudSyncService;
 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.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
-import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
 import java.util.List;
 
 /**
@@ -33,18 +30,13 @@ public class CloudSyncController {
         return ApiResult.success(cloudSyncService.listSyncStatus());
     }
 
-    @PostMapping("/sync/{type}")
-    public ApiResult<SyncTaskResponse> manualSync(@PathVariable String type, HttpServletRequest request) {
-        return ApiResult.success(cloudSyncService.triggerManualSync(type));
+    @PostMapping("/sync")
+    public ApiResult<SyncTaskResponse> manualSync(@RequestBody ManualSyncRequest request) {
+        return ApiResult.success(cloudSyncService.triggerManualSync(request.getType()));
     }
 
     @GetMapping("/sync/task/{taskId}")
     public ApiResult<SyncTaskResponse> taskProgress(@PathVariable Long taskId) {
         return ApiResult.success(cloudSyncService.getTaskProgress(taskId));
     }
-
-    // private String resolveOperator(HttpServletRequest request) {
-    //     String user = request.getHeader("X-User-Name");
-    //     return user != null ? user : "system";
-    // }
 }

+ 25 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql

@@ -0,0 +1,25 @@
+CREATE TABLE `sys_user` (
+  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
+  `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
+  `user_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户账号',
+  `nick_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称',
+  `user_type` varchar(2) COLLATE utf8mb4_unicode_ci DEFAULT '00' COMMENT '用户类型(00系统用户, 01 租户管理员)',
+  `email` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '用户邮箱',
+  `phonenumber` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '手机号码',
+  `sex` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
+  `full_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '姓名',
+  `avatar` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '头像地址',
+  `password` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '密码',
+  `status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
+  `del_flag` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
+  `login_ip` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '最后登录IP',
+  `login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
+  `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+  `tenant_id` int(11) DEFAULT NULL COMMENT '租户ID',
+  `address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
+  PRIMARY KEY (`user_id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1825 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息表';

+ 70 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SysUser.java

@@ -0,0 +1,70 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 用户信息表
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Data
+@TableName("sys_user")
+public class SysUser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "user_id", type = IdType.AUTO)
+    private Long userId;
+
+    private Long deptId;
+
+    private String userName;
+
+    private String nickName;
+
+    /** 用户类型(00系统用户,01租户管理员) */
+    private String userType;
+
+    private String email;
+
+    private String phonenumber;
+
+    private String sex;
+
+    private String fullName;
+
+    private String avatar;
+
+    private String password;
+
+    /** 帐号状态(0正常 1停用) */
+    private String status;
+
+    /** 删除标志(0代表存在 2代表删除) */
+    private String delFlag;
+
+    private String loginIp;
+
+    private LocalDateTime loginDate;
+
+    private String createBy;
+
+    private LocalDateTime createTime;
+
+    private String updateBy;
+
+    private LocalDateTime updateTime;
+
+    private String remark;
+
+    private Integer tenantId;
+
+    private String address;
+}

+ 15 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/SysUserMapper.java

@@ -0,0 +1,15 @@
+package com.usky.issue.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.SysUser;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 用户信息 Mapper
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Repository
+public interface SysUserMapper extends CrudMapper<SysUser> {
+}

+ 22 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncPublishService.java

@@ -0,0 +1,22 @@
+package com.usky.issue.service;
+
+import com.usky.issue.domain.SysUser;
+
+/**
+ * 云端下发队列发布(云→本地)
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public interface CloudSyncPublishService {
+
+    /**
+     * 将单条用户变更加入下发队列
+     */
+    void enqueueUser(SysUser user, String operation);
+
+    /**
+     * 首次拉取时从 sys_user 回填下发队列(仅当前租户)
+     */
+    void backfillUsersIfNeeded(Integer tenantId, Long lastVersion);
+}

+ 24 - 2
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.usky.issue.domain.IssueCloudConfig;
 import com.usky.issue.service.constant.CloudIntegrationConstants;
 import com.usky.issue.service.util.AesGcmCipher;
+import com.usky.issue.service.util.CredentialMaskUtil;
 import com.usky.issue.service.util.HmacSignUtil;
 import com.usky.issue.service.vo.CloudConnectionTestResult;
 import com.usky.issue.service.vo.SyncPacket;
@@ -64,11 +65,14 @@ public class CloudPlatformClient {
 
         String testUrl = buildTestConnectionUrl(cloudAddress);
         long start = System.currentTimeMillis();
+        log.info("[连接测试-HTTP] 准备请求 url={}, tenantId={}, credentialKey={}, hasToken={}",
+                testUrl, tenantId, CredentialMaskUtil.mask(credentialKey), StringUtils.hasText(token));
 
         try {
             HttpHeaders headers = buildAuthHeaders(tenantId, credentialKey, token);
             ResponseEntity<String> response = restTemplate.exchange(
                     testUrl, HttpMethod.POST, new HttpEntity<>(headers), String.class);
+            log.info("[连接测试-HTTP] 响应 status={}, body={}", response.getStatusCodeValue(), response.getBody());
             return parseConnectionTestResult(response.getBody(), System.currentTimeMillis() - start);
         } catch (ResourceAccessException ex) {
             log.error("云平台网络不可达: url={}, error={}", testUrl, ex.getMessage());
@@ -187,17 +191,19 @@ public class CloudPlatformClient {
 
     private CloudConnectionTestResult parseConnectionTestResult(String body, long costTime) {
         if (!StringUtils.hasText(body)) {
+            log.warn("[连接测试-HTTP] 云端返回空响应");
             return CloudConnectionTestResult.failure(true, "云端返回空响应");
         }
         try {
             JsonNode json = OBJECT_MAPPER.readTree(body);
             JsonNode dataNode = json.has("data") ? json.get("data") : json;
             if (dataNode == null || dataNode.isNull()) {
+                log.warn("[连接测试-HTTP] 云端返回空 data, rawBody={}", body);
                 return CloudConnectionTestResult.failure(true, "云端返回空数据");
             }
             CloudConnectionTestResult result = new CloudConnectionTestResult();
             result.setSuccess(dataNode.path("success").asBoolean(false));
-            result.setMessage(dataNode.path("message").asText(null));
+            result.setMessage(resolveResponseMessage(json, dataNode));
             result.setNetworkReachable(dataNode.has("networkReachable")
                     ? dataNode.get("networkReachable").asBoolean() : true);
             result.setAuthValid(dataNode.has("authValid")
@@ -209,14 +215,30 @@ public class CloudPlatformClient {
                 result.setMessage(result.isSuccess()
                         ? CloudIntegrationConstants.MSG_CONNECTION_SUCCESS
                         : "连接测试未通过");
+                log.warn("[连接测试-HTTP] 响应缺少 message,已使用默认文案, parsedSuccess={}, rawBody={}",
+                        result.isSuccess(), body);
             }
+            log.info("[连接测试-HTTP] 解析结果 success={}, authValid={}, tenantExists={}, message={}",
+                    result.isSuccess(), result.isAuthValid(), result.isTenantExists(), result.getMessage());
             return result;
         } catch (Exception e) {
-            log.error("连接测试响应解析失败: {}", body, e);
+            log.error("[连接测试-HTTP] 响应解析失败, rawBody={}", body, e);
             return CloudConnectionTestResult.failure(true, "响应解析失败");
         }
     }
 
+    private String resolveResponseMessage(JsonNode root, JsonNode dataNode) {
+        String message = dataNode.path("message").asText(null);
+        if (StringUtils.hasText(message)) {
+            return message;
+        }
+        message = root.path("msg").asText(null);
+        if (StringUtils.hasText(message)) {
+            return message;
+        }
+        return dataNode.path("errorMessage").asText(null);
+    }
+
     private String buildTestConnectionUrl(String cloudAddress) {
         return normalizeCloudBaseUrl(cloudAddress) + URL_END;
     }

+ 5 - 1
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java

@@ -30,8 +30,12 @@ public enum SyncTypeEnum {
     }
 
     public static SyncTypeEnum fromCode(String code) {
+        if (code == null) {
+            throw new IllegalArgumentException("未知同步类型: null");
+        }
+        String normalized = code.trim();
         for (SyncTypeEnum type : values()) {
-            if (type.code.equalsIgnoreCase(code)) {
+            if (type.code.equalsIgnoreCase(normalized)) {
                 return type;
             }
         }

+ 20 - 2
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java

@@ -6,6 +6,7 @@ import com.usky.issue.domain.IssueCloudConfig;
 import com.usky.issue.mapper.IssueCloudConfigMapper;
 import com.usky.issue.service.CloudAuthService;
 import com.usky.issue.service.util.AesGcmCipher;
+import com.usky.issue.service.util.CredentialMaskUtil;
 import com.usky.issue.service.util.HmacSignUtil;
 import com.usky.issue.service.vo.SyncPacket;
 import lombok.extern.slf4j.Slf4j;
@@ -32,14 +33,27 @@ public class CloudAuthServiceImpl implements CloudAuthService {
 
     @Override
     public boolean validateToken(String apiKey, Integer tenantId) {
-        if (!StringUtils.hasText(apiKey) || tenantId == null) {
+        if (!StringUtils.hasText(apiKey)) {
+            log.warn("[连接测试-认证] apiKey 为空 tenantId={}", tenantId);
+            return false;
+        }
+        if (tenantId == null) {
+            log.warn("[连接测试-认证] tenantId 为空");
             return false;
         }
         IssueCloudConfig config = findActiveConfig(tenantId);
         if (config == null) {
+            log.warn("[连接测试-认证] 云端无启用配置 tenantId={}", tenantId);
             return false;
         }
-        return matchCredential(apiKey, config);
+        boolean matched = matchCredential(apiKey, config);
+        if (!matched) {
+            log.warn("[连接测试-认证] apiKey 与云端配置不匹配 tenantId={}, configId={}, apiKey={}",
+                    tenantId, config.getId(), maskApiKey(apiKey));
+        } else {
+            log.info("[连接测试-认证] 通过 tenantId={}, configId={}", tenantId, config.getId());
+        }
+        return matched;
     }
 
     @Override
@@ -88,4 +102,8 @@ public class CloudAuthServiceImpl implements CloudAuthService {
             return false;
         }
     }
+
+    private String maskApiKey(String apiKey) {
+        return CredentialMaskUtil.mask(apiKey);
+    }
 }

+ 23 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java

@@ -1,5 +1,6 @@
 package com.usky.issue.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
@@ -27,6 +28,7 @@ import com.usky.issue.service.vo.CloudConnectionTestRequest;
 import com.usky.issue.service.vo.CloudConnectionTestResponse;
 import com.usky.issue.service.vo.CloudConnectionTestResult;
 import com.usky.issue.service.vo.CloudDisableResponse;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -41,6 +43,8 @@ import java.util.List;
  * @author fyc
  * @date 2026-05-21
  */
+@DS("usky-cloud")
+@Slf4j
 @Service
 public class CloudConfigServiceImpl implements CloudConfigService {
 
@@ -119,24 +123,35 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         String operator = CloudEntitySupport.resolveOperator();
         String requestIp = CloudEntitySupport.resolveRequestIp();
         Integer tenantId = request.getTenantId();
+        log.info("[连接测试] 开始 tenantId={}, cloudAddress={}, credentialKey={}",
+                tenantId, request.getCloudAddress(), CredentialMaskUtil.mask(request.getCredentialKey()));
+
         Integer loginTenantId = SecurityUtils.getTenantId();
         if (loginTenantId != null && !tenantId.equals(loginTenantId)) {
+            log.warn("[连接测试] 租户不一致 requestTenantId={}, loginTenantId={}", tenantId, loginTenantId);
             throw new BusinessException("输入租户ID与登录租户不一致,请确认后重试!");
         }
 
         IssueCloudConfig config = findConfigByTenantId(tenantId);
         if (config == null) {
+            log.warn("[连接测试] 配置不存在 tenantId={}", tenantId);
             throw new BusinessException("云平台配置不存在,请先保存配置");
         }
         if (config.getStatus() != null && config.getStatus() == 0) {
+            log.warn("[连接测试] 集成已禁用 tenantId={}, configId={}", tenantId, config.getId());
             throw new BusinessException("云平台集成已禁用");
         }
         validateCredentialKey(config, request.getCredentialKey());
+        log.info("[连接测试] 本地密钥校验通过 configId={}", config.getId());
 
         CloudConnectionTestResult result = cloudPlatformClient.testConnection(
                 request.getCloudAddress(), request.getTenantId(), request.getCredentialKey(),
                 SecurityUtils.getToken());
 
+        log.info("[连接测试] 远程结果 success={}, networkReachable={}, authValid={}, tenantExists={}, message={}, costTime={}ms",
+                result.isSuccess(), result.isNetworkReachable(), result.isAuthValid(),
+                result.isTenantExists(), result.getMessage(), result.getCostTime());
+
         config.setConnectionStatus(result.isSuccess() ? 1 : 2);
         config.setLastTestTime(LocalDateTime.now());
         CloudEntitySupport.fillOnUpdate(config);
@@ -150,11 +165,15 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         try {
             String storedKey = aesGcmCipher.decrypt(config.getCredentialKey());
             if (!storedKey.equals(requestKey)) {
+                log.warn("[连接测试] 密钥不一致 tenantId={}, configId={}, requestKey={}, storedKey={}",
+                        config.getTenantId(), config.getId(),
+                        CredentialMaskUtil.mask(requestKey), CredentialMaskUtil.mask(storedKey));
                 throw new BusinessException("凭证密钥与已保存配置不一致");
             }
         } catch (BusinessException ex) {
             throw ex;
         } catch (Exception ex) {
+            log.error("[连接测试] 密钥解密失败 tenantId={}, configId={}", config.getTenantId(), config.getId(), ex);
             throw new BusinessException("凭证密钥校验失败");
         }
     }
@@ -163,6 +182,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
     public CloudConnectionTestResponse validateTenantConnection(Integer tenantId) {
         CloudConnectionTestResponse response = new CloudConnectionTestResponse();
         if (tenantId == null) {
+            log.warn("[连接测试-租户校验] tenantId 为空");
             response.setSuccess(false);
             response.setConnectionStatus(2);
             response.setMessage("租户ID不能为空");
@@ -174,6 +194,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
 
         IssueCloudConfig config = findConfigByTenantId(tenantId);
         if (config == null) {
+            log.warn("[连接测试-租户校验] 配置不存在 tenantId={}", tenantId);
             response.setSuccess(false);
             response.setConnectionStatus(2);
             response.setMessage(CloudIntegrationConstants.MSG_TENANT_CONFIG_MISSING);
@@ -183,6 +204,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
             return response;
         }
         if (config.getStatus() != null && config.getStatus() == 0) {
+            log.warn("[连接测试-租户校验] 配置已禁用 tenantId={}, configId={}", tenantId, config.getId());
             response.setSuccess(false);
             response.setConnectionStatus(2);
             response.setMessage("租户配置已禁用");
@@ -198,6 +220,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         response.setNetworkReachable(true);
         response.setAuthValid(true);
         response.setTenantExists(true);
+        log.info("[连接测试-租户校验] 通过 tenantId={}, configId={}", tenantId, config.getId());
         return response;
     }
 

+ 2 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java

@@ -1,5 +1,6 @@
 package com.usky.issue.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.usky.issue.domain.IssueCloudConfig;
 import com.usky.issue.domain.IssueOperationLog;
 import com.usky.issue.mapper.IssueCloudConfigMapper;
@@ -24,6 +25,7 @@ public class CloudOperationLogServiceImpl implements CloudOperationLogService {
     @Autowired
     private IssueCloudConfigMapper configMapper;
 
+    @DS("usky-cloud")
     @Override
     public void log(Long configId, String operationType, String detail, String operator, String requestIp) {
         IssueOperationLog log = new IssueOperationLog();

+ 119 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncPublishServiceImpl.java

@@ -0,0 +1,119 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.issue.domain.IssueSyncDownQueue;
+import com.usky.issue.domain.SysUser;
+import com.usky.issue.mapper.IssueSyncDownQueueMapper;
+import com.usky.issue.mapper.SysUserMapper;
+import com.usky.issue.service.CloudSyncPublishService;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.sync.SyncPayloadSupport;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * 云端下发队列发布实现
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Slf4j
+@Service
+public class CloudSyncPublishServiceImpl implements CloudSyncPublishService {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Autowired
+    private IssueSyncDownQueueMapper downQueueMapper;
+    @Autowired
+    private SysUserMapper sysUserMapper;
+
+    @DS("usky-cloud")
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void enqueueUser(SysUser user, String operation) {
+        if (user == null || user.getUserId() == null) {
+            throw new BusinessException("用户信息不完整,无法入队");
+        }
+        if (user.getTenantId() == null) {
+            throw new BusinessException("用户缺少租户ID,无法入队 userId=" + user.getUserId());
+        }
+        String op = StringUtils.hasText(operation) ? operation : "UPDATE";
+        String tableName = SyncTypeEnum.USER.getCode();
+        String dataKey = SyncPayloadSupport.buildUserDataKey(user.getUserId());
+        long syncVersion = System.currentTimeMillis();
+
+        Integer pending = downQueueMapper.selectCount(new LambdaQueryWrapper<IssueSyncDownQueue>()
+                .eq(IssueSyncDownQueue::getTenantId, user.getTenantId())
+                .eq(IssueSyncDownQueue::getTableName, tableName)
+                .eq(IssueSyncDownQueue::getStatus, 0)
+                .eq(IssueSyncDownQueue::getIsDeleted, 0)
+                .like(IssueSyncDownQueue::getPayload, "\"dataKey\":\"" + dataKey + "\""));
+        if (pending != null && pending > 0) {
+            log.debug("用户下发队列已存在待同步记录 dataKey={}", dataKey);
+            return;
+        }
+
+        try {
+            String businessPayload = OBJECT_MAPPER.writeValueAsString(user);
+            String wrappedPayload = SyncPayloadSupport.wrapBusinessPayload(
+                    user.getTenantId(), tableName, dataKey, businessPayload, syncVersion);
+
+            IssueSyncDownQueue queue = new IssueSyncDownQueue();
+            queue.setTenantId(user.getTenantId());
+            queue.setTableName(tableName);
+            queue.setOperation(op);
+            queue.setPayload(wrappedPayload);
+            queue.setSyncVersion(syncVersion);
+            queue.setStatus(0);
+            CloudEntitySupport.fillOnInsert(queue);
+            downQueueMapper.insert(queue);
+            log.info("用户已入下发队列 userId={}, tenantId={}, operation={}",
+                    user.getUserId(), user.getTenantId(), op);
+        } catch (Exception e) {
+            throw new BusinessException("用户入下发队列失败: " + e.getMessage());
+        }
+    }
+
+    @DS("usky-cloud")
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void backfillUsersIfNeeded(Integer tenantId, Long lastVersion) {
+        if (tenantId == null) {
+            return;
+        }
+        if (lastVersion != null && lastVersion > 0) {
+            return;
+        }
+        String tableName = SyncTypeEnum.USER.getCode();
+        Integer pending = downQueueMapper.selectCount(new LambdaQueryWrapper<IssueSyncDownQueue>()
+                .eq(IssueSyncDownQueue::getTenantId, tenantId)
+                .eq(IssueSyncDownQueue::getTableName, tableName)
+                .eq(IssueSyncDownQueue::getStatus, 0)
+                .eq(IssueSyncDownQueue::getIsDeleted, 0));
+        if (pending != null && pending > 0) {
+            return;
+        }
+
+        List<SysUser> users = sysUserMapper.selectList(new LambdaQueryWrapper<SysUser>()
+                .eq(SysUser::getTenantId, tenantId)
+                .eq(SysUser::getDelFlag, "0"));
+        if (users.isEmpty()) {
+            log.debug("租户{}无用户数据可回填", tenantId);
+            return;
+        }
+        log.info("开始回填用户下发队列 tenantId={}, count={}", tenantId, users.size());
+        for (SysUser user : users) {
+            enqueueUser(user, "INSERT");
+        }
+    }
+}

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java

@@ -1,5 +1,6 @@
 package com.usky.issue.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -8,9 +9,13 @@ import com.usky.issue.domain.IssueSyncDownQueue;
 import com.usky.issue.mapper.IssueSyncDataMapper;
 import com.usky.issue.mapper.IssueSyncDownQueueMapper;
 import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudSyncPublishService;
 import com.usky.issue.service.CloudSyncReceiveService;
 import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.enums.SyncTypeEnum;
 import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.sync.SyncDataApplierRegistry;
+import com.usky.issue.service.sync.SyncPayloadSupport;
 import com.usky.issue.service.vo.SyncPacket;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -41,6 +46,10 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
     private IssueSyncDownQueueMapper downQueueMapper;
     @Autowired
     private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudSyncPublishService cloudSyncPublishService;
+    @Autowired
+    private SyncDataApplierRegistry syncDataApplierRegistry;
 
     @Override
     public boolean validateTenant(String tenantId) {
@@ -73,6 +82,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                     incoming.setSyncDirection("LOCAL");
                     CloudEntitySupport.fillOnInsert(incoming);
                     syncDataMapper.insert(incoming);
+                    applyToBusinessTable(incoming, "UPDATE");
                     count++;
                 } else if (incoming.getSyncVersion() >= exist.getSyncVersion()) {
                     incoming.setId(exist.getId());
@@ -80,6 +90,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                     incoming.setSyncDirection("LOCAL");
                     CloudEntitySupport.fillOnUpdate(incoming);
                     syncDataMapper.updateById(incoming);
+                    applyToBusinessTable(incoming, "UPDATE");
                     count++;
                 }
             } catch (Exception e) {
@@ -89,12 +100,16 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
         return count;
     }
 
+    @DS("usky-cloud")
     @Override
     public SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion) {
         if (lastVersion == null) {
             lastVersion = 0L;
         }
         Integer tenant = Integer.valueOf(tenantId);
+        if (SyncTypeEnum.USER.getCode().equalsIgnoreCase(tableName)) {
+            cloudSyncPublishService.backfillUsersIfNeeded(tenant, lastVersion);
+        }
         List<IssueSyncDownQueue> queueList = downQueueMapper.selectList(new LambdaQueryWrapper<IssueSyncDownQueue>()
                 .eq(IssueSyncDownQueue::getTenantId, tenant)
                 .eq(IssueSyncDownQueue::getTableName, tableName)
@@ -159,4 +174,17 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
         data.setSyncVersion(version == null ? System.currentTimeMillis() : Long.valueOf(String.valueOf(version)));
         return data;
     }
+
+    private void applyToBusinessTable(IssueSyncData incoming, String operation) {
+        if (!StringUtils.hasText(incoming.getPayload()) || incoming.getTenantId() == null) {
+            return;
+        }
+        try {
+            String businessPayload = SyncPayloadSupport.resolveBusinessPayload(incoming.getPayload());
+            syncDataApplierRegistry.apply(incoming.getTenantId(), incoming.getTableName(), operation, businessPayload);
+        } catch (Exception e) {
+            log.error("云端应用同步数据到业务表失败 tableName={}, tenantId={}",
+                    incoming.getTableName(), incoming.getTenantId(), e);
+        }
+    }
 }

+ 2 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java

@@ -1,6 +1,7 @@
 package com.usky.issue.service.impl;
 
 import com.alibaba.fastjson.JSON;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.usky.common.core.exception.BusinessException;
 import com.usky.common.redis.core.RedisHelper;
@@ -38,6 +39,7 @@ import java.util.stream.Collectors;
  * @author fyc
  * @date 2026-05-21
  */
+@DS("usky-cloud")
 @Slf4j
 @Service
 public class CloudSyncServiceImpl implements CloudSyncService {

+ 2 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java

@@ -1,5 +1,6 @@
 package com.usky.issue.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.usky.issue.domain.IssueSyncDetail;
 import com.usky.issue.domain.IssueSyncStatus;
@@ -30,6 +31,7 @@ import java.time.LocalDateTime;
  * @author fyc
  * @date 2026-05-21
  */
+@DS("usky-cloud")
 @Service
 public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
 

+ 13 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPullServiceImpl.java

@@ -13,12 +13,14 @@ import com.usky.issue.service.LocalPullService;
 import com.usky.issue.service.client.CloudPlatformClient;
 import com.usky.issue.service.enums.SyncDirection;
 import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.sync.SyncDataApplierRegistry;
 import com.usky.issue.service.vo.SyncPacket;
 import com.usky.issue.service.vo.SyncResponse;
 import com.usky.issue.service.vo.SyncResult;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
 import java.util.ArrayList;
@@ -43,6 +45,8 @@ public class LocalPullServiceImpl implements LocalPullService {
     private IssueSyncCursorMapper cursorMapper;
     @Autowired
     private CloudPlatformClient cloudPlatformClient;
+    @Autowired
+    private SyncDataApplierRegistry syncDataApplierRegistry;
 
     @Override
     public SyncResult pullAndApply(String tableName, IssueCloudConfig config) {
@@ -133,6 +137,15 @@ public class LocalPullServiceImpl implements LocalPullService {
             CloudEntitySupport.fillOnUpdate(data);
             syncDataMapper.updateById(data);
         }
+        applyToBusinessTable(queue, data);
+    }
+
+    private void applyToBusinessTable(IssueSyncDownQueue queue, IssueSyncData data) throws Exception {
+        if (!StringUtils.hasText(data.getPayload())) {
+            return;
+        }
+        String operation = StringUtils.hasText(queue.getOperation()) ? queue.getOperation() : "UPDATE";
+        syncDataApplierRegistry.apply(queue.getTenantId(), queue.getTableName(), operation, data.getPayload());
     }
 
     private void ackCloud(List<Long> queueIds, IssueCloudConfig config) {

+ 14 - 2
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java

@@ -1,5 +1,6 @@
 package com.usky.issue.service.listener;
 
+import com.usky.issue.domain.SysUser;
 import org.springframework.context.ApplicationEvent;
 
 /**
@@ -10,7 +11,18 @@ import org.springframework.context.ApplicationEvent;
  */
 public class CloudUserChangedEvent extends ApplicationEvent {
 
-    public CloudUserChangedEvent(Object source) {
-        super(source);
+    private final String operation;
+
+    public CloudUserChangedEvent(SysUser user, String operation) {
+        super(user);
+        this.operation = operation != null ? operation : "UPDATE";
+    }
+
+    public SysUser getUser() {
+        return (SysUser) getSource();
+    }
+
+    public String getOperation() {
+        return operation;
     }
 }

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserSyncPublishListener.java

@@ -0,0 +1,28 @@
+package com.usky.issue.service.listener;
+
+import com.usky.issue.service.CloudSyncPublishService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户变更时写入云端下发队列
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Slf4j
+@Component
+public class CloudUserSyncPublishListener {
+
+    @Autowired
+    private CloudSyncPublishService cloudSyncPublishService;
+
+    @EventListener
+    public void onUserChanged(CloudUserChangedEvent event) {
+        log.info("收到用户变更事件 userId={}, operation={}",
+                event.getUser().getUserId(), event.getOperation());
+        cloudSyncPublishService.enqueueUser(event.getUser(), event.getOperation());
+    }
+}

+ 32 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudSyncEventPublisher.java

@@ -0,0 +1,32 @@
+package com.usky.issue.service.support;
+
+import com.usky.issue.domain.SysUser;
+import com.usky.issue.service.listener.CloudUserChangedEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+/**
+ * 云平台同步事件发布(供用户/组织模块在数据变更后调用)
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Component
+public class CloudSyncEventPublisher {
+
+    @Autowired
+    private ApplicationEventPublisher eventPublisher;
+
+    public void notifyUserCreated(SysUser user) {
+        eventPublisher.publishEvent(new CloudUserChangedEvent(user, "INSERT"));
+    }
+
+    public void notifyUserUpdated(SysUser user) {
+        eventPublisher.publishEvent(new CloudUserChangedEvent(user, "UPDATE"));
+    }
+
+    public void notifyUserDeleted(SysUser user) {
+        eventPublisher.publishEvent(new CloudUserChangedEvent(user, "DELETE"));
+    }
+}

+ 24 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplier.java

@@ -0,0 +1,24 @@
+package com.usky.issue.service.sync;
+
+/**
+ * 同步数据应用到本地业务表
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public interface SyncDataApplier {
+
+    /**
+     * 是否支持该同步类型(如 USER)
+     */
+    boolean supports(String tableName);
+
+    /**
+     * 将 payload 应用到业务表
+     *
+     * @param tenantId        期望租户(来自同步队列/配置)
+     * @param operation       INSERT / UPDATE / DELETE
+     * @param businessPayload 业务 JSON(如 SysUser)
+     */
+    void apply(Integer tenantId, String operation, String businessPayload) throws Exception;
+}

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplierRegistry.java

@@ -0,0 +1,28 @@
+package com.usky.issue.service.sync;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 同步数据应用器路由
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Component
+public class SyncDataApplierRegistry {
+
+    @Autowired
+    private List<SyncDataApplier> appliers;
+
+    public void apply(Integer tenantId, String tableName, String operation, String businessPayload) throws Exception {
+        for (SyncDataApplier applier : appliers) {
+            if (applier.supports(tableName)) {
+                applier.apply(tenantId, operation, businessPayload);
+                return;
+            }
+        }
+    }
+}

+ 50 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncPayloadSupport.java

@@ -0,0 +1,50 @@
+package com.usky.issue.service.sync;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.issue.domain.IssueSyncData;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import org.springframework.util.StringUtils;
+
+/**
+ * 同步 payload 解析
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public final class SyncPayloadSupport {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private SyncPayloadSupport() {
+    }
+
+    public static String buildUserDataKey(Long userId) {
+        return SyncTypeEnum.USER.getCode() + "-" + userId;
+    }
+
+    public static String resolveBusinessPayload(String rawPayload) throws Exception {
+        if (!StringUtils.hasText(rawPayload)) {
+            return rawPayload;
+        }
+        try {
+            IssueSyncData wrapper = OBJECT_MAPPER.readValue(rawPayload, IssueSyncData.class);
+            if (StringUtils.hasText(wrapper.getPayload())) {
+                return wrapper.getPayload();
+            }
+        } catch (Exception ignored) {
+            // 非 IssueSyncData 包装,按业务 JSON 原样处理
+        }
+        return rawPayload;
+    }
+
+    public static String wrapBusinessPayload(Integer tenantId, String tableName, String dataKey,
+                                             String businessPayload, Long syncVersion) throws Exception {
+        IssueSyncData syncData = new IssueSyncData();
+        syncData.setTenantId(tenantId);
+        syncData.setTableName(tableName);
+        syncData.setDataKey(dataKey);
+        syncData.setPayload(businessPayload);
+        syncData.setSyncVersion(syncVersion);
+        return OBJECT_MAPPER.writeValueAsString(syncData);
+    }
+}

+ 78 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/impl/UserSyncDataApplier.java

@@ -0,0 +1,78 @@
+package com.usky.issue.service.sync.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.issue.domain.SysUser;
+import com.usky.issue.mapper.SysUserMapper;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.sync.SyncDataApplier;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户同步应用到 sys_user
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Slf4j
+@Component
+public class UserSyncDataApplier implements SyncDataApplier {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Autowired
+    private SysUserMapper sysUserMapper;
+
+    @Override
+    public boolean supports(String tableName) {
+        return SyncTypeEnum.USER.getCode().equalsIgnoreCase(tableName);
+    }
+
+    @Override
+    public void apply(Integer tenantId, String operation, String businessPayload) throws Exception {
+        if (tenantId == null) {
+            throw new BusinessException("用户同步缺少租户ID");
+        }
+        SysUser user = OBJECT_MAPPER.readValue(businessPayload, SysUser.class);
+        if (user.getUserId() == null) {
+            throw new BusinessException("用户同步缺少 userId");
+        }
+        if (user.getTenantId() != null && !tenantId.equals(user.getTenantId())) {
+            throw new BusinessException("用户租户不匹配,拒绝同步 userId=" + user.getUserId());
+        }
+        user.setTenantId(tenantId);
+
+        if ("DELETE".equalsIgnoreCase(operation)) {
+            softDeleteUser(user.getUserId(), tenantId);
+            return;
+        }
+
+        if ("2".equals(user.getDelFlag())) {
+            softDeleteUser(user.getUserId(), tenantId);
+            return;
+        }
+
+        SysUser exist = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
+                .eq(SysUser::getUserId, user.getUserId())
+                .eq(SysUser::getTenantId, tenantId));
+        if (exist == null) {
+            sysUserMapper.insert(user);
+            log.debug("用户同步插入 userId={}, tenantId={}", user.getUserId(), tenantId);
+        } else {
+            sysUserMapper.updateById(user);
+            log.debug("用户同步更新 userId={}, tenantId={}", user.getUserId(), tenantId);
+        }
+    }
+
+    private void softDeleteUser(Long userId, Integer tenantId) {
+        sysUserMapper.update(null, new LambdaUpdateWrapper<SysUser>()
+                .eq(SysUser::getUserId, userId)
+                .eq(SysUser::getTenantId, tenantId)
+                .set(SysUser::getDelFlag, "2"));
+        log.debug("用户同步软删除 userId={}, tenantId={}", userId, tenantId);
+    }
+}

+ 18 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/ManualSyncRequest.java

@@ -0,0 +1,18 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 手动同步请求
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Data
+public class ManualSyncRequest {
+
+    @NotBlank(message = "同步类型不能为空")
+    private String type;
+}