| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
ccb20f3141 | feat: 去掉异常返回轻微缺陷;对null值做异常处理 | 6 days ago |
|
|
595d0382e3 | Merge branch 'yanyuan' of https://git.ht-atia.cn/qinyanlei/inspect into yanyuan | 6 days ago |
|
|
19334d72a6 | feat: eqpbookChannel表新增useMode字段用于区分摄像头设备类型 | 1 week ago |
|
|
519b6dd15e | feat: 当登录ip值为空或*时,允许任意ip进行登录 | 1 week ago |
|
|
678e553263 | feat: 部门树和菜单树新增status字段,表示启用/禁用 | 2 weeks ago |
|
|
11e7545f1d | feat: 新增中恒token登录后的“动环”相关角色同步 | 2 weeks ago |
|
|
260c7ac160 | fix:修复三方登录异常;同步三方aollowIp设为*;对null值做处理 | 2 weeks ago |
|
|
15e028094a | feat: 定时同步中恒用户相关信息,并实现中恒token登录 | 2 weeks ago |
| @ -0,0 +1,24 @@ | |||||
| package com.inspect.admin.feign; | |||||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||||
| import com.inspect.admin.feign.factory.RemoteExtAuthFallbackFactory; | |||||
| import com.inspect.base.core.constant.SecurityConstants; | |||||
| import com.inspect.base.core.domain.Response; | |||||
| import org.springframework.cloud.openfeign.FeignClient; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RequestHeader; | |||||
| /** | |||||
| * 调用 inspect-main 内部接口,由 inspect-main 向外部盐源平台换取登录用户。 | |||||
| */ | |||||
| @FeignClient( | |||||
| contextId = "remoteExtAuthService", | |||||
| value = "inspect-main", | |||||
| fallbackFactory = RemoteExtAuthFallbackFactory.class | |||||
| ) | |||||
| public interface RemoteExtAuthService { | |||||
| @GetMapping("/inner/ext_auth/loginUser") | |||||
| Response<JoinbrightUser> getLoginUser(@RequestHeader("joinbright-token") String joinbrightToken, | |||||
| @RequestHeader(SecurityConstants.FROM_SOURCE) String source); | |||||
| } | |||||
| @ -0,0 +1,30 @@ | |||||
| package com.inspect.admin.feign; | |||||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||||
| import com.inspect.admin.feign.factory.RemoteExtUserFallbackFactory; | |||||
| import com.inspect.base.core.constant.SecurityConstants; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import org.springframework.cloud.openfeign.FeignClient; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestBody; | |||||
| import org.springframework.web.bind.annotation.RequestHeader; | |||||
| import org.springframework.web.bind.annotation.RequestParam; | |||||
| /** | |||||
| * 调用 dliip-system 的内部接口,为外部登录用户自动建号 / 绑定本地账号。 | |||||
| */ | |||||
| @FeignClient( | |||||
| contextId = "remoteExtUserService", | |||||
| value = "inspect-management", | |||||
| fallbackFactory = RemoteExtUserFallbackFactory.class | |||||
| ) | |||||
| public interface RemoteExtUserService { | |||||
| @PostMapping("/inner/ext_user/ensure") | |||||
| AjaxResult ensureUser(@RequestBody JoinbrightUser user, | |||||
| @RequestHeader(SecurityConstants.FROM_SOURCE) String source); | |||||
| @PostMapping("/inner/ext_user/syncRoles") | |||||
| AjaxResult syncRoles(@RequestParam("username") String username, | |||||
| @RequestHeader(SecurityConstants.FROM_SOURCE) String source); | |||||
| } | |||||
| @ -0,0 +1,37 @@ | |||||
| package com.inspect.admin.feign.domain; | |||||
| import lombok.Data; | |||||
| import java.io.Serializable; | |||||
| /** | |||||
| * 内部接口 /inner/ext_auth/loginUser 返回的外部登录用户信息。 | |||||
| * 与 inspect-main-api 的 ExtLoginUser 按 JSON 字段约定保持一致。 | |||||
| */ | |||||
| @Data | |||||
| public class JoinbrightUser implements Serializable { | |||||
| private static final long serialVersionUID = 1L; | |||||
| /** 用户id(对应本地 sys_user.ext_id) */ | |||||
| private String id; | |||||
| /** 账号 */ | |||||
| private String username; | |||||
| /** 用户名 */ | |||||
| private String realName; | |||||
| /** 账号状态编码,0201401 为激活 */ | |||||
| private String accountStatus; | |||||
| /** 账号状态文本,如“激活” */ | |||||
| private String accountStatusText; | |||||
| /** 所属部门id(对应本地 sys_dept.ext_id) */ | |||||
| private String deptId; | |||||
| /** 所属部门名称 */ | |||||
| private String deptName; | |||||
| /** 手机号 */ | |||||
| private String mobilePhone; | |||||
| /** 邮箱 */ | |||||
| private String email; | |||||
| /** 所属场站id */ | |||||
| private String boosterStationId; | |||||
| /** 所属场站名称 */ | |||||
| private String boosterStationName; | |||||
| } | |||||
| @ -0,0 +1,26 @@ | |||||
| package com.inspect.admin.feign.factory; | |||||
| import com.inspect.admin.feign.RemoteExtAuthService; | |||||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||||
| import com.inspect.base.core.domain.Response; | |||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import org.springframework.cloud.openfeign.FallbackFactory; | |||||
| import org.springframework.stereotype.Component; | |||||
| @Component | |||||
| public class RemoteExtAuthFallbackFactory implements FallbackFactory<RemoteExtAuthService> { | |||||
| private static final Logger log = LoggerFactory.getLogger(RemoteExtAuthFallbackFactory.class); | |||||
| @Override | |||||
| public RemoteExtAuthService create(final Throwable throwable) { | |||||
| log.error("[JOINBRIGHT] inspect-main 内部服务调用失败:{}", throwable.getMessage()); | |||||
| return new RemoteExtAuthService() { | |||||
| @Override | |||||
| public Response<JoinbrightUser> getLoginUser(String joinbrightToken, String source) { | |||||
| return Response.fail(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部认证服务暂不可用,请稍后重试"); | |||||
| } | |||||
| }; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,31 @@ | |||||
| package com.inspect.admin.feign.factory; | |||||
| import com.inspect.admin.feign.RemoteExtUserService; | |||||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import org.springframework.cloud.openfeign.FallbackFactory; | |||||
| import org.springframework.stereotype.Component; | |||||
| @Component | |||||
| public class RemoteExtUserFallbackFactory implements FallbackFactory<RemoteExtUserService> { | |||||
| private static final Logger log = LoggerFactory.getLogger(RemoteExtUserFallbackFactory.class); | |||||
| @Override | |||||
| public RemoteExtUserService create(final Throwable throwable) { | |||||
| log.error("[JOINBRIGHT] inspect-management 用户建号服务调用失败:{}", throwable.getMessage()); | |||||
| return new RemoteExtUserService() { | |||||
| @Override | |||||
| public AjaxResult ensureUser(JoinbrightUser user, String source) { | |||||
| return AjaxResult.error(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部账号同步服务暂不可用,请稍后重试"); | |||||
| } | |||||
| @Override | |||||
| public AjaxResult syncRoles(String username, String source) { | |||||
| return AjaxResult.error(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部角色同步服务暂不可用"); | |||||
| } | |||||
| }; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,152 @@ | |||||
| package com.inspect.admin.service; | |||||
| import com.inspect.admin.feign.RemoteExtAuthService; | |||||
| import com.inspect.admin.feign.RemoteExtUserService; | |||||
| import com.inspect.admin.feign.domain.JoinbrightUser; | |||||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||||
| import com.inspect.base.core.constant.SecurityConstants; | |||||
| import com.inspect.base.core.domain.Response; | |||||
| import com.inspect.base.core.exception.ServiceException; | |||||
| import com.inspect.base.core.utils.StringUtils; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import com.inspect.common.security.service.TokenService; | |||||
| import com.inspect.system.base.model.LoginUser; | |||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import org.springframework.stereotype.Service; | |||||
| import javax.annotation.Resource; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import java.util.Map; | |||||
| /** | |||||
| * joinbright 页面集成单点登录: | |||||
| * 1. 前端在 header(优先)或 query 传入 joinbright-token; | |||||
| * 2. 经 inspect-main 内部接口向外部平台 /ext_auth/getLoginUser 换取用户信息; | |||||
| * 3. 外部认证失败或未返回用户名:返回 4001,前端回登录页,不再降级 guest/htjc; | |||||
| * 4. 平台已有该账号:直接签发平台 token;平台没有:按外部数据自动建号(密码 abcd1234*、角色 common); | |||||
| * 5. 同名本地账号按外部数据绑定并覆盖,内置账号 admin/guest/htjc 受保护,返回 4009。 | |||||
| */ | |||||
| @Service | |||||
| public class JoinbrightLoginService { | |||||
| private static final Logger log = LoggerFactory.getLogger(JoinbrightLoginService.class); | |||||
| private static final String JOINBRIGHT_TOKEN = "joinbright-token"; | |||||
| private static final String MSG_TOKEN_INVALID = "登录凭证无效,请重新登录"; | |||||
| private static final String MSG_SERVICE_UNAVAILABLE = "外部认证服务暂不可用,请稍后重试"; | |||||
| @Resource | |||||
| private RemoteExtAuthService remoteExtAuthService; | |||||
| @Resource | |||||
| private RemoteExtUserService remoteExtUserService; | |||||
| @Resource | |||||
| private SysLoginService sysLoginService; | |||||
| @Resource | |||||
| private TokenService tokenService; | |||||
| public Map<String, Object> login(HttpServletRequest request) { | |||||
| String joinbrightToken = request.getParameter(JOINBRIGHT_TOKEN); | |||||
| if (StringUtils.isEmpty(joinbrightToken)) { | |||||
| throw new ServiceException(MSG_TOKEN_INVALID, ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| JoinbrightUser externalUser = getExternalUser(joinbrightToken); | |||||
| ensurePlatformUser(externalUser); | |||||
| syncExternalRoles(externalUser.getUsername()); | |||||
| LoginUser loginUser = loadPlatformUser(externalUser.getUsername()); | |||||
| return tokenService.createToken(loginUser); | |||||
| } | |||||
| /** | |||||
| * 调内部接口换取外部用户信息;任何失败都直接抛业务异常,由前端按错误码回登录页。 | |||||
| */ | |||||
| private JoinbrightUser getExternalUser(String joinbrightToken) { | |||||
| Response<JoinbrightUser> response; | |||||
| try { | |||||
| response = remoteExtAuthService.getLoginUser(joinbrightToken, SecurityConstants.INNER); | |||||
| } catch (Exception e) { | |||||
| log.error("[JOINBRIGHT] 调用外部认证服务异常", e); | |||||
| throw new ServiceException(MSG_SERVICE_UNAVAILABLE, ExtAuthConstants.SERVICE_UNAVAILABLE); | |||||
| } | |||||
| if (response == null) { | |||||
| log.warn("[JOINBRIGHT] 外部认证服务无响应"); | |||||
| throw new ServiceException(MSG_SERVICE_UNAVAILABLE, ExtAuthConstants.SERVICE_UNAVAILABLE); | |||||
| } | |||||
| if (response.getCode() == ExtAuthConstants.SERVICE_UNAVAILABLE) { | |||||
| throw new ServiceException(defaultMessage(response.getMsg(), MSG_SERVICE_UNAVAILABLE), | |||||
| ExtAuthConstants.SERVICE_UNAVAILABLE); | |||||
| } | |||||
| if (response.getCode() != Response.SUCCESS) { | |||||
| log.warn("[JOINBRIGHT] 外部认证失败: code={}, msg={}", response.getCode(), response.getMsg()); | |||||
| throw new ServiceException(defaultMessage(response.getMsg(), MSG_TOKEN_INVALID), | |||||
| ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| JoinbrightUser user = response.getData(); | |||||
| if (user == null || StringUtils.isEmpty(user.getUsername())) { | |||||
| log.warn("[JOINBRIGHT] 外部认证未返回用户名"); | |||||
| throw new ServiceException("未获取到外部用户信息,请重新登录", ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| return user; | |||||
| } | |||||
| /** | |||||
| * 平台用户不存在时自动建号,存在时由 dliip-system 按 ext_id 更新/绑定。 | |||||
| */ | |||||
| private void ensurePlatformUser(JoinbrightUser externalUser) { | |||||
| AjaxResult result; | |||||
| try { | |||||
| result = remoteExtUserService.ensureUser(externalUser, SecurityConstants.INNER); | |||||
| } catch (Exception e) { | |||||
| log.error("[JOINBRIGHT] 调用用户建号服务异常, username={}", externalUser.getUsername(), e); | |||||
| throw new ServiceException("外部账号同步失败,请稍后重试", ExtAuthConstants.SERVICE_UNAVAILABLE); | |||||
| } | |||||
| if (result == null) { | |||||
| throw new ServiceException("外部账号同步失败,请稍后重试", ExtAuthConstants.SERVICE_UNAVAILABLE); | |||||
| } | |||||
| int code = readCode(result); | |||||
| if (code != Response.SUCCESS) { | |||||
| String message = defaultMessage((String) result.get(AjaxResult.MSG_TAG), "外部账号同步失败,请稍后重试"); | |||||
| log.warn("[JOINBRIGHT] 用户建号失败: code={}, msg={}", code, message); | |||||
| throw new ServiceException(message, code); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 登录时同步外部角色:失败只记录日志,不阻断登录(fail-open)。 | |||||
| */ | |||||
| private void syncExternalRoles(String username) { | |||||
| try { | |||||
| AjaxResult result = remoteExtUserService.syncRoles(username, SecurityConstants.INNER); | |||||
| if (result == null || readCode(result) != Response.SUCCESS) { | |||||
| log.warn("[JOINBRIGHT] 外部角色同步未成功,按现有角色放行, username={}, result={}", | |||||
| username, result); | |||||
| return; | |||||
| } | |||||
| log.info("[JOINBRIGHT] 外部角色同步完成, username={}, msg={}", | |||||
| username, result.get(AjaxResult.MSG_TAG)); | |||||
| } catch (Exception e) { | |||||
| log.warn("[JOINBRIGHT] 外部角色同步异常,按现有角色放行, username={}", username, e); | |||||
| } | |||||
| } | |||||
| private LoginUser loadPlatformUser(String username) { | |||||
| try { | |||||
| return sysLoginService.getLoginUserByUsername(username); | |||||
| } catch (ServiceException e) { | |||||
| String message = e.getMessage(); | |||||
| boolean accountState = message != null | |||||
| && (message.contains("停用") || message.contains("删除")); | |||||
| throw new ServiceException(message, | |||||
| accountState ? ExtAuthConstants.USER_DISABLED : ExtAuthConstants.SERVICE_UNAVAILABLE); | |||||
| } | |||||
| } | |||||
| private int readCode(AjaxResult result) { | |||||
| Object value = result.get(AjaxResult.CODE_TAG); | |||||
| return value instanceof Number ? ((Number) value).intValue() : Response.FAIL; | |||||
| } | |||||
| private String defaultMessage(String message, String defaultMessage) { | |||||
| return StringUtils.isNotEmpty(message) ? message : defaultMessage; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,16 @@ | |||||
| package com.inspect.base.core.constant; | |||||
| /** | |||||
| * 外部平台(joinbright)集成错误码。 | |||||
| * 由 dliip-auth 的 joinbrightLogin 统一返回给前端,便于前端区分“回登录页”与“服务异常”。 | |||||
| */ | |||||
| public class ExtAuthConstants { | |||||
| /** joinbright-token 缺失/无效,或外部平台未返回用户名 */ | |||||
| public static final int TOKEN_INVALID = 4001; | |||||
| /** 平台账号已停用或已删除 */ | |||||
| public static final int USER_DISABLED = 4003; | |||||
| /** 同名账号冲突(受保护账号不允许被外部账号绑定) */ | |||||
| public static final int USER_CONFLICT = 4009; | |||||
| /** 外部认证 / 用户同步服务不可用 */ | |||||
| public static final int SERVICE_UNAVAILABLE = 5003; | |||||
| } | |||||
| @ -0,0 +1,22 @@ | |||||
| package com.inspect.base.core.enums; | |||||
| public enum UserType { | |||||
| SYSTEM("0", "系统用户"), | |||||
| EXTERNAL("1", "外部用户"); | |||||
| private final String code; | |||||
| private final String info; | |||||
| private UserType(String code, String info) { | |||||
| this.code = code; | |||||
| this.info = info; | |||||
| } | |||||
| public String getCode() { | |||||
| return this.code; | |||||
| } | |||||
| public String getInfo() { | |||||
| return this.info; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| package com.inspect.job.client; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import org.springframework.cloud.openfeign.FeignClient; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestHeader; | |||||
| /** | |||||
| * 外部平台组织同步客户端(调用 inspect-management 的内部接口)。 | |||||
| * 定时任务线程没有请求上下文,必须显式传 from-source=inner。 | |||||
| */ | |||||
| @FeignClient(name = "inspect-management", contextId = "ext-org-sync") | |||||
| public interface ExtOrgSyncClient { | |||||
| @PostMapping("/inner/ext_sync/all") | |||||
| AjaxResult syncAll(@RequestHeader("from-source") String source); | |||||
| @PostMapping("/inner/ext_sync/dept") | |||||
| AjaxResult syncDept(@RequestHeader("from-source") String source); | |||||
| @PostMapping("/inner/ext_sync/user") | |||||
| AjaxResult syncUser(@RequestHeader("from-source") String source); | |||||
| } | |||||
| @ -0,0 +1,63 @@ | |||||
| package com.inspect.job.task; | |||||
| import com.inspect.base.core.constant.SecurityConstants; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import com.inspect.job.client.ExtOrgSyncClient; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.stereotype.Component; | |||||
| import java.util.function.Function; | |||||
| /** | |||||
| * 外部平台(盐源智慧感知平台)部门 / 用户同步定时任务。 | |||||
| * <p> | |||||
| * 在“定时任务”中配置调用: | |||||
| * <ul> | |||||
| * <li>{@code extOrgSyncTask.syncAll()}:部门 + 用户全量同步(建议每日凌晨);</li> | |||||
| * <li>{@code extOrgSyncTask.syncUser()}:部门 + 用户同步;</li> | |||||
| * <li>{@code extOrgSyncTask.syncDept()}:仅部门同步。</li> | |||||
| * </ul> | |||||
| * 角色同步暂缓。调用失败直接抛出异常,由 sys_job_log 记录失败原因。 | |||||
| */ | |||||
| @Slf4j | |||||
| @Component("extOrgSyncTask") | |||||
| public class ExtOrgSyncTask { | |||||
| private final ExtOrgSyncClient extOrgSyncClient; | |||||
| public ExtOrgSyncTask(ExtOrgSyncClient extOrgSyncClient) { | |||||
| this.extOrgSyncClient = extOrgSyncClient; | |||||
| } | |||||
| public void syncAll() { | |||||
| invoke("外部组织全量同步", extOrgSyncClient::syncAll); | |||||
| } | |||||
| public void syncDept() { | |||||
| invoke("外部部门同步", extOrgSyncClient::syncDept); | |||||
| } | |||||
| public void syncUser() { | |||||
| invoke("外部用户同步", extOrgSyncClient::syncUser); | |||||
| } | |||||
| private void invoke(String action, Function<String, AjaxResult> call) { | |||||
| long start = System.currentTimeMillis(); | |||||
| log.info("[EXT-SYNC] {} 开始", action); | |||||
| AjaxResult result; | |||||
| try { | |||||
| result = call.apply(SecurityConstants.INNER); | |||||
| } catch (Exception e) { | |||||
| log.error("[EXT-SYNC] {} 调用异常", action, e); | |||||
| throw new RuntimeException(action + "调用异常: " + e.getMessage(), e); | |||||
| } | |||||
| Object code = result == null ? null : result.get(AjaxResult.CODE_TAG); | |||||
| Object msg = result == null ? null : result.get(AjaxResult.MSG_TAG); | |||||
| long elapsed = System.currentTimeMillis() - start; | |||||
| if (result == null || !(code instanceof Number) || ((Number) code).intValue() != 200) { | |||||
| log.error("[EXT-SYNC] {} 失败, code={}, msg={}, 耗时={}ms", action, code, msg, elapsed); | |||||
| throw new RuntimeException(action + "失败: " + (msg == null ? "无响应" : msg)); | |||||
| } | |||||
| log.info("[EXT-SYNC] {} 完成, msg={}, 耗时={}ms", action, msg, elapsed); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,144 @@ | |||||
| package com.inspect.api.controller; | |||||
| import com.inspect.api.domain.ApiResult; | |||||
| import com.inspect.api.domain.ExtDept; | |||||
| import com.inspect.api.domain.ExtLoginUser; | |||||
| import com.inspect.api.domain.ExtRole; | |||||
| import com.inspect.api.domain.ExtUser; | |||||
| import com.inspect.api.service.ExtAuthService; | |||||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||||
| import com.inspect.base.core.constant.SecurityConstants; | |||||
| import com.inspect.base.core.domain.Response; | |||||
| import com.inspect.base.core.exception.InnerAuthException; | |||||
| import com.inspect.base.core.utils.StringUtils; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RequestHeader; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RequestParam; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import javax.servlet.http.HttpServletRequest; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 内部接口:供 dliip-auth 的 joinbrightLogin 单点登录、以及 inspect-management 的组织同步调用。 | |||||
| * 不通过网关对外暴露,仅允许携带 from-source=inner 的服务间 Feign 调用。 | |||||
| */ | |||||
| @Slf4j | |||||
| @RestController | |||||
| @RequestMapping("/inner/ext_auth") | |||||
| public class ExtAuthInternalController { | |||||
| private final ExtAuthService extAuthService; | |||||
| public ExtAuthInternalController(ExtAuthService extAuthService) { | |||||
| this.extAuthService = extAuthService; | |||||
| } | |||||
| @GetMapping("/loginUser") | |||||
| public Response<ExtLoginUser> getLoginUser( | |||||
| @RequestHeader(value = "joinbright-token", required = false) String joinbrightToken, | |||||
| HttpServletRequest request) { | |||||
| requireInner(request); | |||||
| if (StringUtils.isEmpty(joinbrightToken)) { | |||||
| return Response.fail(ExtAuthConstants.TOKEN_INVALID, "joinbright-token不能为空"); | |||||
| } | |||||
| try { | |||||
| ApiResult<ExtLoginUser> result = extAuthService.getLoginUser(joinbrightToken); | |||||
| if (result != null && result.isSuccess()) { | |||||
| if (result.getData() == null || StringUtils.isEmpty(result.getData().getUsername())) { | |||||
| log.warn("[JOINBRIGHT] getLoginUser 未返回有效用户名, code={}", result.getCode()); | |||||
| return Response.fail(ExtAuthConstants.TOKEN_INVALID, "未获取到外部用户信息,请重新登录"); | |||||
| } | |||||
| return Response.ok(result.getData()); | |||||
| } | |||||
| String hintMessage = result == null ? null : result.getHintMessage(); | |||||
| log.warn("[JOINBRIGHT] getLoginUser 外部认证失败: {}", hintMessage); | |||||
| return Response.fail(ExtAuthConstants.TOKEN_INVALID, | |||||
| StringUtils.isNotEmpty(hintMessage) ? hintMessage : "外部认证失败,请重新登录"); | |||||
| } catch (Exception e) { | |||||
| log.error("[JOINBRIGHT] getLoginUser 调用异常", e); | |||||
| return Response.fail(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部认证服务暂不可用,请稍后重试"); | |||||
| } | |||||
| } | |||||
| /** 组织同步:全部根部门 */ | |||||
| @GetMapping("/getRootDept") | |||||
| public Response<List<ExtDept>> getRootDept(HttpServletRequest request) { | |||||
| requireInner(request); | |||||
| try { | |||||
| ApiResult<List<ExtDept>> result = extAuthService.getRootDept(); | |||||
| return toResponse(result, "获取根部门失败"); | |||||
| } catch (Exception e) { | |||||
| log.error("[JOINBRIGHT] getRootDept 调用异常", e); | |||||
| return Response.fail("外部组织接口异常: " + e.getMessage()); | |||||
| } | |||||
| } | |||||
| /** 组织同步:直属下级部门 */ | |||||
| @GetMapping("/getDeptBySuperId") | |||||
| public Response<List<ExtDept>> getDeptBySuperId(@RequestParam("superId") String superId, | |||||
| HttpServletRequest request) { | |||||
| requireInner(request); | |||||
| if (StringUtils.isEmpty(superId)) { | |||||
| return Response.fail("superId不能为空"); | |||||
| } | |||||
| try { | |||||
| ApiResult<List<ExtDept>> result = extAuthService.getDeptBySuperId(superId); | |||||
| return toResponse(result, "获取下级部门失败"); | |||||
| } catch (Exception e) { | |||||
| log.error("[JOINBRIGHT] getDeptBySuperId 调用异常, superId={}", superId, e); | |||||
| return Response.fail("外部组织接口异常: " + e.getMessage()); | |||||
| } | |||||
| } | |||||
| /** 组织同步:部门下的用户 */ | |||||
| @GetMapping("/getUserByDeptId") | |||||
| public Response<List<ExtUser>> getUserByDeptId(@RequestParam("deptId") String deptId, | |||||
| HttpServletRequest request) { | |||||
| requireInner(request); | |||||
| if (StringUtils.isEmpty(deptId)) { | |||||
| return Response.fail("deptId不能为空"); | |||||
| } | |||||
| try { | |||||
| ApiResult<List<ExtUser>> result = extAuthService.getUserByDeptId(deptId); | |||||
| return toResponse(result, "获取部门用户失败"); | |||||
| } catch (Exception e) { | |||||
| log.error("[JOINBRIGHT] getUserByDeptId 调用异常, deptId={}", deptId, e); | |||||
| return Response.fail("外部组织接口异常: " + e.getMessage()); | |||||
| } | |||||
| } | |||||
| /** 组织同步:用户拥有的角色 */ | |||||
| @GetMapping("/getRoleByUser") | |||||
| public Response<List<ExtRole>> getRoleByUser(@RequestParam("username") String username, | |||||
| HttpServletRequest request) { | |||||
| requireInner(request); | |||||
| if (StringUtils.isEmpty(username)) { | |||||
| return Response.fail("username不能为空"); | |||||
| } | |||||
| try { | |||||
| ApiResult<List<ExtRole>> result = extAuthService.getRoleByUser(username); | |||||
| return toResponse(result, "获取用户角色失败"); | |||||
| } catch (Exception e) { | |||||
| log.error("[JOINBRIGHT] getRoleByUser 调用异常, username={}", username, e); | |||||
| return Response.fail("外部组织接口异常: " + e.getMessage()); | |||||
| } | |||||
| } | |||||
| private <T> Response<T> toResponse(ApiResult<T> result, String defaultMessage) { | |||||
| if (result != null && result.isSuccess()) { | |||||
| return Response.ok(result.getData()); | |||||
| } | |||||
| String hintMessage = result == null ? null : result.getHintMessage(); | |||||
| log.warn("[JOINBRIGHT] {}: {}", defaultMessage, hintMessage); | |||||
| return Response.fail(StringUtils.isNotEmpty(hintMessage) ? hintMessage : defaultMessage); | |||||
| } | |||||
| private void requireInner(HttpServletRequest request) { | |||||
| if (!SecurityConstants.INNER.equals(request.getHeader(SecurityConstants.FROM_SOURCE))) { | |||||
| throw new InnerAuthException("没有内部访问权限,不允许访问"); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,27 @@ | |||||
| package com.inspect.api.domain; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 外部盐源平台部门信息(GET /ext_auth/getRootDept、/ext_auth/getDeptBySuperId)。 | |||||
| * 仅保留组织同步需要的字段。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtDept { | |||||
| /** 部门id */ | |||||
| private String id; | |||||
| /** 部门名称 */ | |||||
| private String deptName; | |||||
| /** 部门简称 */ | |||||
| private String shortName; | |||||
| /** 部门编码 */ | |||||
| private String deptCode; | |||||
| /** 上级部门id */ | |||||
| private String superDept; | |||||
| /** 完整路径(外部 id 体系,含自身) */ | |||||
| private String fullId; | |||||
| /** 完整名称路径 */ | |||||
| private String fullName; | |||||
| /** 下级部门数量,>0 表示需要继续下钻 */ | |||||
| private Integer leaf; | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package com.inspect.api.domain; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 外部盐源平台 GET /ext_auth/getLoginUser 返回的用户信息。 | |||||
| * 字段按接口文档的真实返回结构映射,单点登录与自动建号共用。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtLoginUser { | |||||
| /** 用户id(对应本地 sys_user.ext_id) */ | |||||
| private String id; | |||||
| /** 账号 */ | |||||
| private String username; | |||||
| /** 用户名 */ | |||||
| private String realName; | |||||
| /** 账号类型编码 */ | |||||
| private String accountType; | |||||
| /** 账号状态编码,0201401 为激活 */ | |||||
| private String accountStatus; | |||||
| /** 账号状态文本,如“激活” */ | |||||
| private String accountStatusText; | |||||
| /** 所属部门id(对应本地 sys_dept.ext_id) */ | |||||
| private String deptId; | |||||
| /** 所属部门名称 */ | |||||
| private String deptName; | |||||
| /** 手机号 */ | |||||
| private String mobilePhone; | |||||
| /** 邮箱 */ | |||||
| private String email; | |||||
| /** 所属场站id */ | |||||
| private String boosterStationId; | |||||
| /** 所属场站名称 */ | |||||
| private String boosterStationName; | |||||
| } | |||||
| @ -0,0 +1,18 @@ | |||||
| package com.inspect.api.domain; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 外部盐源平台用户角色(GET /ext_auth/getRoleByUser)。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtRole { | |||||
| /** 用户角色关联id */ | |||||
| private String id; | |||||
| /** 角色id */ | |||||
| private String roleId; | |||||
| /** 角色名称,外部可能返回 null */ | |||||
| private String roleName; | |||||
| /** 用户id */ | |||||
| private String userId; | |||||
| } | |||||
| @ -0,0 +1,30 @@ | |||||
| package com.inspect.api.domain; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 外部盐源平台部门下用户(GET /ext_auth/getUserByDeptId)。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtUser { | |||||
| /** 用户id */ | |||||
| private String id; | |||||
| /** 账号 */ | |||||
| private String username; | |||||
| /** 姓名 */ | |||||
| private String realName; | |||||
| /** 所属部门id */ | |||||
| private String deptId; | |||||
| /** 所属部门名称 */ | |||||
| private String deptName; | |||||
| /** 账号状态编码 */ | |||||
| private String accountStatus; | |||||
| /** 账号状态文本,如“激活” */ | |||||
| private String accountStatusText; | |||||
| /** 邮箱 */ | |||||
| private String email; | |||||
| /** 手机号 */ | |||||
| private String mobilePhone; | |||||
| /** 排序号 */ | |||||
| private Integer sortNo; | |||||
| } | |||||
| @ -0,0 +1,35 @@ | |||||
| package com.inspect.system.client; | |||||
| import com.inspect.base.core.domain.Response; | |||||
| import com.inspect.system.domain.ext.ExtDept; | |||||
| import com.inspect.system.domain.ext.ExtRole; | |||||
| import com.inspect.system.domain.ext.ExtUser; | |||||
| import org.springframework.cloud.openfeign.FeignClient; | |||||
| import org.springframework.web.bind.annotation.GetMapping; | |||||
| import org.springframework.web.bind.annotation.RequestHeader; | |||||
| import org.springframework.web.bind.annotation.RequestParam; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 调用 inspect-main 的内部接口拉取外部平台组织数据。 | |||||
| * 所有请求必须携带 from-source: inner(定时任务线程没有请求上下文,需显式传入)。 | |||||
| */ | |||||
| @FeignClient(name = "inspect-main", contextId = "ext-org-sync") | |||||
| public interface ExtAuthClient { | |||||
| @GetMapping("/inner/ext_auth/getRootDept") | |||||
| Response<List<ExtDept>> getRootDept(@RequestHeader("from-source") String source); | |||||
| @GetMapping("/inner/ext_auth/getDeptBySuperId") | |||||
| Response<List<ExtDept>> getDeptBySuperId(@RequestParam("superId") String superId, | |||||
| @RequestHeader("from-source") String source); | |||||
| @GetMapping("/inner/ext_auth/getUserByDeptId") | |||||
| Response<List<ExtUser>> getUserByDeptId(@RequestParam("deptId") String deptId, | |||||
| @RequestHeader("from-source") String source); | |||||
| @GetMapping("/inner/ext_auth/getRoleByUser") | |||||
| Response<List<ExtRole>> getRoleByUser(@RequestParam("username") String username, | |||||
| @RequestHeader("from-source") String source); | |||||
| } | |||||
| @ -0,0 +1,60 @@ | |||||
| package com.inspect.system.controller; | |||||
| import com.inspect.base.core.exception.ServiceException; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import com.inspect.common.security.annotation.InnerAuth; | |||||
| import com.inspect.system.domain.ExtSyncResult; | |||||
| import com.inspect.system.service.IExtOrgSyncService; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.springframework.web.bind.annotation.PostMapping; | |||||
| import org.springframework.web.bind.annotation.RequestMapping; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| import java.util.function.Supplier; | |||||
| /** | |||||
| * 内部接口:供 inspect-job 的定时任务触发外部组织同步。 | |||||
| * 仅允许 from-source=inner 的服务间调用。 | |||||
| */ | |||||
| @Slf4j | |||||
| @RestController | |||||
| @RequestMapping("/inner/ext_sync") | |||||
| public class ExtSyncController { | |||||
| private final IExtOrgSyncService extOrgSyncService; | |||||
| public ExtSyncController(IExtOrgSyncService extOrgSyncService) { | |||||
| this.extOrgSyncService = extOrgSyncService; | |||||
| } | |||||
| @InnerAuth | |||||
| @PostMapping("/all") | |||||
| public AjaxResult syncAll() { | |||||
| return run(extOrgSyncService::syncAll); | |||||
| } | |||||
| @InnerAuth | |||||
| @PostMapping("/dept") | |||||
| public AjaxResult syncDept() { | |||||
| return run(extOrgSyncService::syncDept); | |||||
| } | |||||
| @InnerAuth | |||||
| @PostMapping("/user") | |||||
| public AjaxResult syncUser() { | |||||
| return run(extOrgSyncService::syncUser); | |||||
| } | |||||
| private AjaxResult run(Supplier<ExtSyncResult> action) { | |||||
| try { | |||||
| ExtSyncResult result = action.get(); | |||||
| return AjaxResult.success(result.getSummary(), result); | |||||
| } catch (ServiceException e) { | |||||
| log.warn("[EXT-SYNC] 同步失败: {}", e.getMessage()); | |||||
| return AjaxResult.error(e.getMessage()); | |||||
| } catch (Exception e) { | |||||
| log.error("[EXT-SYNC] 同步异常", e); | |||||
| return AjaxResult.error("外部组织同步失败: " + e.getMessage()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,63 @@ | |||||
| package com.inspect.system.controller; | |||||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||||
| import com.inspect.base.core.exception.ServiceException; | |||||
| import com.inspect.base.core.web.domain.AjaxResult; | |||||
| import com.inspect.common.security.annotation.InnerAuth; | |||||
| import com.inspect.system.domain.ext.ExtUser; | |||||
| import com.inspect.system.domain.ext.RoleSyncResult; | |||||
| import com.inspect.system.service.IExtOrgSyncService; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| 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.RequestParam; | |||||
| import org.springframework.web.bind.annotation.RestController; | |||||
| /** | |||||
| * 内部接口:供 dliip-auth 的 joinbrightLogin 按外部用户自动建号。 | |||||
| * 仅允许 from-source=inner 的服务间调用,登录与定时同步共用同一套落库规则。 | |||||
| */ | |||||
| @Slf4j | |||||
| @RestController | |||||
| @RequestMapping("/inner/ext_user") | |||||
| public class ExtUserController { | |||||
| private final IExtOrgSyncService extOrgSyncService; | |||||
| public ExtUserController(IExtOrgSyncService extOrgSyncService) { | |||||
| this.extOrgSyncService = extOrgSyncService; | |||||
| } | |||||
| @InnerAuth | |||||
| @PostMapping("/syncRoles") | |||||
| public AjaxResult syncRoles(@RequestParam("username") String username) { | |||||
| try { | |||||
| RoleSyncResult result = extOrgSyncService.syncUserRoles(username); | |||||
| return AjaxResult.success("角色同步完成", result); | |||||
| } catch (ServiceException e) { | |||||
| log.warn("[EXT-LOGIN] 外部角色同步失败: {}", e.getMessage()); | |||||
| return AjaxResult.error(e.getCode() == null ? ExtAuthConstants.SERVICE_UNAVAILABLE : e.getCode(), | |||||
| e.getMessage()); | |||||
| } catch (Exception e) { | |||||
| log.error("[EXT-LOGIN] 外部角色同步异常", e); | |||||
| return AjaxResult.error(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部角色同步失败,请稍后重试"); | |||||
| } | |||||
| } | |||||
| @InnerAuth | |||||
| @PostMapping("/ensure") | |||||
| public AjaxResult ensure(@RequestBody ExtUser extUser) { | |||||
| try { | |||||
| Long userId = extOrgSyncService.ensureUser(extUser); | |||||
| return AjaxResult.success("用户已就绪", userId); | |||||
| } catch (ServiceException e) { | |||||
| log.warn("[EXT-LOGIN] 外部用户建号失败: {}", e.getMessage()); | |||||
| return AjaxResult.error(e.getCode() == null ? ExtAuthConstants.SERVICE_UNAVAILABLE : e.getCode(), | |||||
| e.getMessage()); | |||||
| } catch (Exception e) { | |||||
| log.error("[EXT-LOGIN] 外部用户建号异常", e); | |||||
| return AjaxResult.error(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部用户同步失败,请稍后重试"); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,37 @@ | |||||
| package com.inspect.system.domain; | |||||
| import lombok.Data; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 外部组织同步结果统计。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtSyncResult { | |||||
| private int deptInsert; | |||||
| private int deptUpdate; | |||||
| private int deptBind; | |||||
| private int deptDeactivate; | |||||
| private int userInsert; | |||||
| private int userUpdate; | |||||
| private int userBind; | |||||
| private int userSkip; | |||||
| private int userDeactivate; | |||||
| private int userRoleBind; | |||||
| private long elapsedMillis; | |||||
| /** 跳过/异常明细,便于在 sys_job_log 中排查 */ | |||||
| private List<String> messages = new ArrayList<>(); | |||||
| public void message(String message) { | |||||
| this.messages.add(message); | |||||
| } | |||||
| public String getSummary() { | |||||
| return String.format( | |||||
| "部门(新增%d/更新%d/绑定%d/停用%d) 用户(新增%d/更新%d/绑定%d/跳过%d/停用%d) 默认角色绑定%d 耗时%dms", | |||||
| deptInsert, deptUpdate, deptBind, deptDeactivate, | |||||
| userInsert, userUpdate, userBind, userSkip, userDeactivate, userRoleBind, elapsedMillis); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,18 @@ | |||||
| package com.inspect.system.domain.ext; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 外部平台部门(对应 inspect-main-api 的 /inner/ext_auth/getRootDept、getDeptBySuperId 返回)。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtDept { | |||||
| private String id; | |||||
| private String deptName; | |||||
| private String shortName; | |||||
| private String deptCode; | |||||
| private String superDept; | |||||
| private String fullId; | |||||
| private String fullName; | |||||
| private Integer leaf; | |||||
| } | |||||
| @ -0,0 +1,15 @@ | |||||
| package com.inspect.system.domain.ext; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 本地已同步部门(用于停用对账)。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtDeptRef { | |||||
| private Long deptId; | |||||
| private String extId; | |||||
| private Long parentId; | |||||
| private String ancestors; | |||||
| private String status; | |||||
| } | |||||
| @ -0,0 +1,18 @@ | |||||
| package com.inspect.system.domain.ext; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 外部盐源平台用户角色(GET /ext_auth/getRoleByUser)。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtRole { | |||||
| /** 用户角色关联 id */ | |||||
| private String id; | |||||
| /** 角色 id */ | |||||
| private String roleId; | |||||
| /** 角色名称,外部可能返回 null */ | |||||
| private String roleName; | |||||
| /** 用户 id */ | |||||
| private String userId; | |||||
| } | |||||
| @ -0,0 +1,20 @@ | |||||
| package com.inspect.system.domain.ext; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 外部平台用户(对应 inspect-main-api 的 /inner/ext_auth/getUserByDeptId 返回)。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtUser { | |||||
| private String id; | |||||
| private String username; | |||||
| private String realName; | |||||
| private String deptId; | |||||
| private String deptName; | |||||
| private String accountStatus; | |||||
| private String accountStatusText; | |||||
| private String email; | |||||
| private String mobilePhone; | |||||
| private Integer sortNo; | |||||
| } | |||||
| @ -0,0 +1,14 @@ | |||||
| package com.inspect.system.domain.ext; | |||||
| import lombok.Data; | |||||
| /** | |||||
| * 本地已同步用户(用于停用对账)。 | |||||
| */ | |||||
| @Data | |||||
| public class ExtUserRef { | |||||
| private Long userId; | |||||
| private String extId; | |||||
| private Long deptId; | |||||
| private String status; | |||||
| } | |||||
| @ -0,0 +1,24 @@ | |||||
| package com.inspect.system.domain.ext; | |||||
| import lombok.Data; | |||||
| import java.util.ArrayList; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 登录时外部角色同步结果。 | |||||
| */ | |||||
| @Data | |||||
| public class RoleSyncResult { | |||||
| private String username; | |||||
| /** 本次新增的本地角色名 */ | |||||
| private List<String> added = new ArrayList<>(); | |||||
| /** 本次回收的本地角色名 */ | |||||
| private List<String> removed = new ArrayList<>(); | |||||
| /** 跳过明细(未映射、本地不存在、已停用等) */ | |||||
| private List<String> skipped = new ArrayList<>(); | |||||
| public String getSummary() { | |||||
| return String.format("角色(新增%d/回收%d/跳过%d)", added.size(), removed.size(), skipped.size()); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,60 @@ | |||||
| package com.inspect.system.mapper; | |||||
| import com.inspect.system.base.domain.SysDept; | |||||
| import com.inspect.system.base.domain.SysRole; | |||||
| import com.inspect.system.base.domain.SysUser; | |||||
| import com.inspect.system.domain.ext.ExtDeptRef; | |||||
| import com.inspect.system.domain.ext.ExtUserRef; | |||||
| import org.apache.ibatis.annotations.Param; | |||||
| import java.util.List; | |||||
| /** | |||||
| * 外部组织同步专用 Mapper。 | |||||
| * 独立于 SysDeptMapper / SysUserMapper,避免 ext_id 字段在未升级的库上影响现有功能。 | |||||
| */ | |||||
| public interface ExtOrgSyncMapper { | |||||
| /* ---------------- 部门 ---------------- */ | |||||
| Long selectDeptIdByExtId(@Param("extId") String extId); | |||||
| Long selectDeptIdByParentAndName(@Param("parentId") Long parentId, @Param("deptName") String deptName); | |||||
| int insertDept(SysDept dept); | |||||
| int updateDept(SysDept dept); | |||||
| int bindDeptExtId(@Param("deptId") Long deptId, @Param("extId") String extId); | |||||
| List<ExtDeptRef> selectSyncedDepts(); | |||||
| int deactivateDept(@Param("deptId") Long deptId); | |||||
| /* ---------------- 用户 ---------------- */ | |||||
| Long selectRoleIdByRoleKey(@Param("roleKey") String roleKey); | |||||
| Long selectUserIdByExtId(@Param("extId") String extId); | |||||
| SysUser selectUserById(@Param("userId") Long userId); | |||||
| SysUser selectUserByUserName(@Param("userName") String userName); | |||||
| int insertUser(SysUser user); | |||||
| int updateUser(SysUser user); | |||||
| int bindUserExtId(@Param("userId") Long userId, @Param("extId") String extId); | |||||
| int insertUserRole(@Param("userId") Long userId, @Param("roleId") Long roleId); | |||||
| /* ---------------- 角色(登录时同步) ---------------- */ | |||||
| SysRole selectRoleByName(@Param("roleName") String roleName); | |||||
| List<SysRole> selectRolesByUserId(@Param("userId") Long userId); | |||||
| int deleteUserRole(@Param("userId") Long userId, @Param("roleId") Long roleId); | |||||
| List<ExtUserRef> selectSyncedUsers(); | |||||
| int deactivateUser(@Param("userId") Long userId); | |||||
| } | |||||
| @ -0,0 +1,33 @@ | |||||
| package com.inspect.system.service; | |||||
| import com.inspect.system.domain.ExtSyncResult; | |||||
| import com.inspect.system.domain.ext.ExtUser; | |||||
| import com.inspect.system.domain.ext.RoleSyncResult; | |||||
| /** | |||||
| * 外部平台(盐源智慧感知平台)部门 / 用户同步。 | |||||
| * <p>角色同步暂缓,后续确认后再补充。</p> | |||||
| */ | |||||
| public interface IExtOrgSyncService { | |||||
| /** 部门 + 用户全量同步,并执行停用对账 */ | |||||
| ExtSyncResult syncAll(); | |||||
| /** 仅部门同步(含停用对账) */ | |||||
| ExtSyncResult syncDept(); | |||||
| /** 用户同步(部门作为前置一并同步,含用户停用对账) */ | |||||
| ExtSyncResult syncUser(); | |||||
| /** | |||||
| * 单点登录自动建号:外部用户不存在则创建,本地已有同名账号则绑定并覆盖为外部数据。 | |||||
| * | |||||
| * @return 本地用户ID | |||||
| */ | |||||
| Long ensureUser(ExtUser extUser); | |||||
| /** | |||||
| * 登录时同步外部角色:白名单映射 + 映射范围内对账回收,common 角色不参与回收。 | |||||
| */ | |||||
| RoleSyncResult syncUserRoles(String username); | |||||
| } | |||||
| @ -0,0 +1,624 @@ | |||||
| package com.inspect.system.service.impl; | |||||
| import com.inspect.base.core.constant.ExtAuthConstants; | |||||
| import com.inspect.base.core.constant.SecurityConstants; | |||||
| import com.inspect.base.core.domain.Response; | |||||
| import com.inspect.base.core.enums.UserType; | |||||
| import com.inspect.base.core.exception.ServiceException; | |||||
| import com.inspect.base.core.utils.StringUtils; | |||||
| import com.inspect.base.redis.service.RedisService; | |||||
| import com.inspect.common.security.utils.SecurityUtils; | |||||
| import com.inspect.system.base.domain.SysDept; | |||||
| import com.inspect.system.base.domain.SysRole; | |||||
| import com.inspect.system.base.domain.SysUser; | |||||
| import com.inspect.system.client.ExtAuthClient; | |||||
| import com.inspect.system.domain.ExtSyncResult; | |||||
| import com.inspect.system.domain.ext.ExtDept; | |||||
| import com.inspect.system.domain.ext.ExtDeptRef; | |||||
| import com.inspect.system.domain.ext.ExtRole; | |||||
| import com.inspect.system.domain.ext.ExtUser; | |||||
| import com.inspect.system.domain.ext.ExtUserRef; | |||||
| import com.inspect.system.domain.ext.RoleSyncResult; | |||||
| import com.inspect.system.mapper.ExtOrgSyncMapper; | |||||
| import com.inspect.system.service.IExtOrgSyncService; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.apache.commons.codec.digest.DigestUtils; | |||||
| import org.springframework.beans.factory.annotation.Value; | |||||
| import org.springframework.stereotype.Service; | |||||
| import org.springframework.transaction.annotation.Transactional; | |||||
| import java.util.ArrayDeque; | |||||
| import java.util.ArrayList; | |||||
| import java.util.Arrays; | |||||
| import java.util.Collections; | |||||
| import java.util.Deque; | |||||
| import java.util.HashMap; | |||||
| import java.util.HashSet; | |||||
| import java.util.LinkedHashMap; | |||||
| import java.util.List; | |||||
| import java.util.Map; | |||||
| import java.util.Set; | |||||
| import java.util.concurrent.TimeUnit; | |||||
| /** | |||||
| * 外部平台组织同步:拉取 -> 转换 -> 落库 -> 停用对账。 | |||||
| * <p>角色同步暂缓,本期只同步部门与用户。</p> | |||||
| * <p>约定:</p> | |||||
| * <ul> | |||||
| * <li>外部数据先全部拉取成功后再落库,任一外部接口失败则整体中止,保留上次数据;</li> | |||||
| * <li>按 ext_id 管理外部数据;同名账号自动绑定并覆盖 ext_id(admin/guest/htjc 除外);</li> | |||||
| * <li>停用对账只处理 ext_id 非空且位于本次同步子树内的记录,手工数据不受影响。</li> | |||||
| * </ul> | |||||
| */ | |||||
| @Slf4j | |||||
| @Service | |||||
| public class ExtOrgSyncServiceImpl implements IExtOrgSyncService { | |||||
| private static final String SOURCE = SecurityConstants.INNER; | |||||
| private static final String SYNC_BY = "ext_sync"; | |||||
| private static final String REMARK = "外部平台同步"; | |||||
| private static final String DEFAULT_PASSWORD = DigestUtils.md5Hex("abcd1234*"); | |||||
| /** 同步用户默认角色:普通角色,可由 yanyuan.sync.default-role-key 覆盖(部分库为 common1) */ | |||||
| @Value("${yanyuan.sync.default-role-key:common}") | |||||
| private String defaultRoleKey; | |||||
| /** 仅同步以该前缀开头的外部角色名 */ | |||||
| @Value("${yanyuan.sync.role-name-prefix:动环}") | |||||
| private String roleNamePrefix; | |||||
| private static final String LOCK_KEY = "ext:org:sync:lock"; | |||||
| private static final String ENSURE_LOCK_PREFIX = "ext:user:ensure:"; | |||||
| private static final long ENSURE_LOCK_SECONDS = 30L; | |||||
| /** 平台内置账号,禁止被外部账号绑定/覆盖 */ | |||||
| private static final Set<String> PROTECTED_USERNAMES = | |||||
| new HashSet<>(Arrays.asList("admin", "htjc")); | |||||
| private static final long LOCK_SECONDS = 1800L; | |||||
| private static final int DEPT_NAME_MAX_LENGTH = 64; | |||||
| private static final int USER_NAME_MAX_LENGTH = 30; | |||||
| private static final int NICK_NAME_MAX_LENGTH = 30; | |||||
| private static final int EMAIL_MAX_LENGTH = 50; | |||||
| private static final int PHONE_MAX_LENGTH = 11; | |||||
| private final ExtAuthClient extAuthClient; | |||||
| private final ExtOrgSyncMapper extOrgSyncMapper; | |||||
| private final RedisService redisService; | |||||
| public ExtOrgSyncServiceImpl(ExtAuthClient extAuthClient, | |||||
| ExtOrgSyncMapper extOrgSyncMapper, | |||||
| RedisService redisService) { | |||||
| this.extAuthClient = extAuthClient; | |||||
| this.extOrgSyncMapper = extOrgSyncMapper; | |||||
| this.redisService = redisService; | |||||
| } | |||||
| @Override | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public ExtSyncResult syncAll() { | |||||
| return doSync(true); | |||||
| } | |||||
| @Override | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public ExtSyncResult syncDept() { | |||||
| return doSync(false); | |||||
| } | |||||
| @Override | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public ExtSyncResult syncUser() { | |||||
| return doSync(true); | |||||
| } | |||||
| /** | |||||
| * @param withUser 是否同步用户;为 false 时仅同步部门并做部门停用对账 | |||||
| */ | |||||
| private ExtSyncResult doSync(boolean withUser) { | |||||
| long start = System.currentTimeMillis(); | |||||
| if (!tryLock()) { | |||||
| throw new ServiceException("外部组织同步正在执行中,请稍后再试"); | |||||
| } | |||||
| ExtSyncResult result = new ExtSyncResult(); | |||||
| try { | |||||
| SyncContext context = fetchAll(withUser); | |||||
| Map<String, Long> deptMap = persistDepts(context.nodes, result); | |||||
| if (withUser) { | |||||
| persistUsers(context, deptMap, result); | |||||
| } | |||||
| reconcileDepts(context, deptMap, result); | |||||
| if (withUser) { | |||||
| reconcileUsers(context, deptMap, result); | |||||
| } | |||||
| } finally { | |||||
| unlock(LOCK_KEY); | |||||
| } | |||||
| result.setElapsedMillis(System.currentTimeMillis() - start); | |||||
| log.info("[EXT-SYNC] 完成: {}", result.getSummary()); | |||||
| for (String message : result.getMessages()) { | |||||
| log.warn("[EXT-SYNC] {}", message); | |||||
| } | |||||
| return result; | |||||
| } | |||||
| // ------------------------------------------------------------------ 拉取 | |||||
| private SyncContext fetchAll(boolean withUser) { | |||||
| SyncContext context = new SyncContext(); | |||||
| List<ExtDept> roots = unwrap(extAuthClient.getRootDept(SOURCE), "getRootDept"); | |||||
| Deque<DeptNode> queue = new ArrayDeque<>(); | |||||
| Set<String> visited = new HashSet<>(); | |||||
| for (ExtDept root : roots) { | |||||
| if (StringUtils.isEmpty(root.getId()) || !visited.add(root.getId())) { | |||||
| continue; | |||||
| } | |||||
| DeptNode node = new DeptNode(root, null, 0); | |||||
| context.nodes.add(node); | |||||
| queue.add(node); | |||||
| } | |||||
| while (!queue.isEmpty()) { | |||||
| DeptNode current = queue.poll(); | |||||
| if (current.dept.getLeaf() == null || current.dept.getLeaf() <= 0) { | |||||
| continue; | |||||
| } | |||||
| List<ExtDept> children = unwrap( | |||||
| extAuthClient.getDeptBySuperId(current.dept.getId(), SOURCE), "getDeptBySuperId"); | |||||
| int order = 0; | |||||
| for (ExtDept child : children) { | |||||
| if (StringUtils.isEmpty(child.getId()) || !visited.add(child.getId())) { | |||||
| continue; | |||||
| } | |||||
| DeptNode node = new DeptNode(child, current.dept.getId(), ++order); | |||||
| context.nodes.add(node); | |||||
| queue.add(node); | |||||
| } | |||||
| } | |||||
| if (withUser) { | |||||
| for (DeptNode node : context.nodes) { | |||||
| List<ExtUser> users = unwrap( | |||||
| extAuthClient.getUserByDeptId(node.dept.getId(), SOURCE), "getUserByDeptId"); | |||||
| for (ExtUser user : users) { | |||||
| if (StringUtils.isEmpty(user.getId())) { | |||||
| continue; | |||||
| } | |||||
| if (StringUtils.isEmpty(user.getDeptId())) { | |||||
| user.setDeptId(node.dept.getId()); | |||||
| } | |||||
| context.users.putIfAbsent(user.getId(), user); | |||||
| } | |||||
| } | |||||
| } | |||||
| log.info("[EXT-SYNC] 拉取完成: 部门 {},用户 {}", context.nodes.size(), context.users.size()); | |||||
| return context; | |||||
| } | |||||
| private <T> T unwrap(Response<T> response, String action) { | |||||
| if (response == null || response.getCode() != Response.SUCCESS) { | |||||
| String message = response == null ? "无响应" : response.getMsg(); | |||||
| throw new ServiceException(action + "失败: " + message); | |||||
| } | |||||
| T data = response.getData(); | |||||
| if (data == null) { | |||||
| @SuppressWarnings("unchecked") | |||||
| T empty = (T) Collections.emptyList(); | |||||
| return empty; | |||||
| } | |||||
| return data; | |||||
| } | |||||
| // ------------------------------------------------------------------ 部门 | |||||
| private Map<String, Long> persistDepts(List<DeptNode> nodes, ExtSyncResult result) { | |||||
| Map<String, Long> extToLocal = new HashMap<>(); | |||||
| Map<Long, String> ancestorsByLocalId = new HashMap<>(); | |||||
| for (DeptNode node : nodes) { | |||||
| String extId = node.dept.getId(); | |||||
| String rawDeptName = node.dept.getDeptName(); | |||||
| String deptName = truncate(rawDeptName, DEPT_NAME_MAX_LENGTH); | |||||
| if (rawDeptName != null && rawDeptName.length() > DEPT_NAME_MAX_LENGTH) { | |||||
| result.message("部门名称超长已截断: extId=" + extId + ",原名=" + rawDeptName); | |||||
| } | |||||
| Long parentLocalId = node.parentExtId == null ? 0L : extToLocal.get(node.parentExtId); | |||||
| if (parentLocalId == null) { | |||||
| result.message("跳过部门[" + deptName + "]:父部门未同步,extId=" + extId); | |||||
| continue; | |||||
| } | |||||
| String ancestors = parentLocalId == 0L | |||||
| ? "0" | |||||
| : ancestorsByLocalId.get(parentLocalId) + "," + parentLocalId; | |||||
| Long localId = extOrgSyncMapper.selectDeptIdByExtId(extId); | |||||
| if (localId != null) { | |||||
| extOrgSyncMapper.updateDept(buildDept(localId, extId, parentLocalId, ancestors, deptName, node.order)); | |||||
| result.setDeptUpdate(result.getDeptUpdate() + 1); | |||||
| } else { | |||||
| Long sameName = extOrgSyncMapper.selectDeptIdByParentAndName(parentLocalId, deptName); | |||||
| if (sameName != null) { | |||||
| extOrgSyncMapper.bindDeptExtId(sameName, extId); | |||||
| extOrgSyncMapper.updateDept(buildDept(sameName, extId, parentLocalId, ancestors, deptName, node.order)); | |||||
| localId = sameName; | |||||
| result.setDeptBind(result.getDeptBind() + 1); | |||||
| result.message("绑定已存在同名部门[" + deptName + "] deptId=" + localId + " -> extId=" + extId); | |||||
| } else { | |||||
| SysDept dept = buildDept(null, extId, parentLocalId, ancestors, deptName, node.order); | |||||
| extOrgSyncMapper.insertDept(dept); | |||||
| localId = dept.getDeptId(); | |||||
| result.setDeptInsert(result.getDeptInsert() + 1); | |||||
| } | |||||
| } | |||||
| extToLocal.put(extId, localId); | |||||
| ancestorsByLocalId.put(localId, ancestors); | |||||
| } | |||||
| return extToLocal; | |||||
| } | |||||
| private SysDept buildDept(Long deptId, String extId, Long parentId, String ancestors, | |||||
| String deptName, Integer order) { | |||||
| SysDept dept = new SysDept(); | |||||
| dept.setDeptId(deptId); | |||||
| dept.setExtId(extId); | |||||
| dept.setParentId(parentId); | |||||
| dept.setAncestors(ancestors); | |||||
| dept.setDeptName(deptName); | |||||
| dept.setOrderNum(String.valueOf(order == null ? 0 : order)); | |||||
| dept.setCreateBy(SYNC_BY); | |||||
| dept.setUpdateBy(SYNC_BY); | |||||
| return dept; | |||||
| } | |||||
| // ------------------------------------------------------------------ 用户 | |||||
| private void persistUsers(SyncContext context, Map<String, Long> deptMap, ExtSyncResult result) { | |||||
| Long commonRoleId = requireCommonRole(); | |||||
| for (ExtUser extUser : context.users.values()) { | |||||
| if (StringUtils.isEmpty(extUser.getUsername())) { | |||||
| result.setUserSkip(result.getUserSkip() + 1); | |||||
| result.message("跳过用户:外部未返回账号,extId=" + extUser.getId()); | |||||
| continue; | |||||
| } | |||||
| Long deptLocalId = deptMap.get(extUser.getDeptId()); | |||||
| if (deptLocalId == null) { | |||||
| result.setUserSkip(result.getUserSkip() + 1); | |||||
| result.message("跳过用户[" + extUser.getUsername() + "]:所属部门未同步,deptId=" + extUser.getDeptId()); | |||||
| continue; | |||||
| } | |||||
| upsertUser(extUser, deptLocalId, commonRoleId, result); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 单点登录自动建号:外部用户不存在则创建,本地已有同名账号则绑定并覆盖为外部数据。 | |||||
| * 部门尚未同步时 dept_id 留空,等待下一次组织同步补齐。 | |||||
| */ | |||||
| @Override | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public Long ensureUser(ExtUser extUser) { | |||||
| if (extUser == null || StringUtils.isEmpty(extUser.getId())) { | |||||
| throw new ServiceException("外部用户信息缺少用户ID", ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| if (StringUtils.isEmpty(extUser.getUsername())) { | |||||
| throw new ServiceException("外部用户信息缺少账号", ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| Long commonRoleId = requireCommonRole(); | |||||
| Long deptLocalId = StringUtils.isEmpty(extUser.getDeptId()) | |||||
| ? null : extOrgSyncMapper.selectDeptIdByExtId(extUser.getDeptId()); | |||||
| String lockKey = ENSURE_LOCK_PREFIX + extUser.getId(); | |||||
| boolean locked = acquireLock(lockKey, ENSURE_LOCK_SECONDS); | |||||
| try { | |||||
| ExtSyncResult result = new ExtSyncResult(); | |||||
| SysUser user = upsertUser(extUser, deptLocalId, commonRoleId, result); | |||||
| for (String message : result.getMessages()) { | |||||
| log.info("[EXT-LOGIN] {}", message); | |||||
| } | |||||
| return user.getUserId(); | |||||
| } finally { | |||||
| if (locked) { | |||||
| unlock(lockKey); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 登录时同步外部角色:只处理以配置前缀(默认动环)开头的外部角色名, | |||||
| * 与本地 role_name 精确匹配后授权;回收范围同样限定为动环前缀角色。 | |||||
| */ | |||||
| @Override | |||||
| @Transactional(rollbackFor = Exception.class) | |||||
| public RoleSyncResult syncUserRoles(String username) { | |||||
| if (StringUtils.isEmpty(username)) { | |||||
| throw new ServiceException("用户名不能为空", ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| RoleSyncResult result = new RoleSyncResult(); | |||||
| result.setUsername(username); | |||||
| SysUser user = extOrgSyncMapper.selectUserByUserName(username); | |||||
| if (user == null || !"0".equals(user.getDelFlag())) { | |||||
| throw new ServiceException("本地用户[" + username + "]不存在或已删除", | |||||
| ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| if (StringUtils.isEmpty(roleNamePrefix)) { | |||||
| log.info("[EXT-ROLE] 未配置角色名前缀,跳过角色同步, username={}", username); | |||||
| return result; | |||||
| } | |||||
| Response<List<ExtRole>> response = extAuthClient.getRoleByUser(username, SOURCE); | |||||
| if (response == null || response.getCode() != Response.SUCCESS || response.getData() == null) { | |||||
| throw new ServiceException("获取外部角色失败", ExtAuthConstants.SERVICE_UNAVAILABLE); | |||||
| } | |||||
| Map<Long, String> desired = new LinkedHashMap<>(); | |||||
| for (ExtRole extRole : response.getData()) { | |||||
| String roleName = StringUtils.trim(extRole.getRoleName()); | |||||
| if (StringUtils.isEmpty(roleName)) { | |||||
| result.getSkipped().add("外部角色名为空"); | |||||
| continue; | |||||
| } | |||||
| if (!roleName.startsWith(roleNamePrefix)) { | |||||
| continue; | |||||
| } | |||||
| SysRole localRole = extOrgSyncMapper.selectRoleByName(roleName); | |||||
| if (localRole == null) { | |||||
| result.getSkipped().add(roleName + "(本地角色不存在)"); | |||||
| continue; | |||||
| } | |||||
| if (!"0".equals(localRole.getStatus())) { | |||||
| result.getSkipped().add(roleName + "(本地角色已停用)"); | |||||
| continue; | |||||
| } | |||||
| desired.put(localRole.getRoleId(), roleName); | |||||
| } | |||||
| Long commonRoleId = extOrgSyncMapper.selectRoleIdByRoleKey(defaultRoleKey); | |||||
| Set<Long> currentIds = new HashSet<>(); | |||||
| for (SysRole current : extOrgSyncMapper.selectRolesByUserId(user.getUserId())) { | |||||
| currentIds.add(current.getRoleId()); | |||||
| if (commonRoleId != null && commonRoleId.equals(current.getRoleId())) { | |||||
| continue; | |||||
| } | |||||
| String currentName = current.getRoleName(); | |||||
| // 只回收动环前缀角色;非动环角色视为管理员手工授权,不参与对账 | |||||
| if (StringUtils.isEmpty(currentName) || !currentName.startsWith(roleNamePrefix)) { | |||||
| continue; | |||||
| } | |||||
| if (!desired.containsKey(current.getRoleId())) { | |||||
| extOrgSyncMapper.deleteUserRole(user.getUserId(), current.getRoleId()); | |||||
| result.getRemoved().add(currentName); | |||||
| } | |||||
| } | |||||
| for (Map.Entry<Long, String> entry : desired.entrySet()) { | |||||
| if (currentIds.contains(entry.getKey())) { | |||||
| continue; | |||||
| } | |||||
| extOrgSyncMapper.insertUserRole(user.getUserId(), entry.getKey()); | |||||
| result.getAdded().add(entry.getValue()); | |||||
| } | |||||
| log.info("[EXT-ROLE] username={}, {}", username, result.getSummary()); | |||||
| for (String skipped : result.getSkipped()) { | |||||
| log.warn("[EXT-ROLE] username={}, 跳过: {}", username, skipped); | |||||
| } | |||||
| return result; | |||||
| } | |||||
| /** | |||||
| * 用户落库:按 ext_id 更新;无 ext_id 记录时优先绑定同名账号(覆盖 ext_id),否则新建。 | |||||
| */ | |||||
| private SysUser upsertUser(ExtUser extUser, Long deptLocalId, Long commonRoleId, ExtSyncResult result) { | |||||
| String username = extUser.getUsername(); | |||||
| if (StringUtils.isEmpty(username)) { | |||||
| throw new ServiceException("外部用户信息缺少账号", ExtAuthConstants.TOKEN_INVALID); | |||||
| } | |||||
| if (username.length() > USER_NAME_MAX_LENGTH) { | |||||
| throw new ServiceException("外部账号[" + username + "]超过本地账号长度上限 " | |||||
| + USER_NAME_MAX_LENGTH + ",请联系管理员处理", ExtAuthConstants.USER_CONFLICT); | |||||
| } | |||||
| String nickName = truncate(StringUtils.isNotEmpty(extUser.getRealName()) ? extUser.getRealName() : username, | |||||
| NICK_NAME_MAX_LENGTH); | |||||
| String status = resolveUserStatus(extUser); | |||||
| Long userId = extOrgSyncMapper.selectUserIdByExtId(extUser.getId()); | |||||
| if (userId != null) { | |||||
| updateUser(extUser, userId, deptLocalId, nickName, status); | |||||
| result.setUserUpdate(result.getUserUpdate() + 1); | |||||
| return extOrgSyncMapper.selectUserById(userId); | |||||
| } | |||||
| SysUser exists = extOrgSyncMapper.selectUserByUserName(username); | |||||
| if (exists != null) { | |||||
| if (PROTECTED_USERNAMES.contains(username.toLowerCase())) { | |||||
| throw new ServiceException("账号[" + username + "]为平台受保护账号,不允许被外部账号绑定", | |||||
| ExtAuthConstants.USER_CONFLICT); | |||||
| } | |||||
| if (!"0".equals(exists.getDelFlag())) { | |||||
| throw new ServiceException("账号[" + username + "]已删除,请联系管理员处理", | |||||
| ExtAuthConstants.USER_CONFLICT); | |||||
| } | |||||
| if (StringUtils.isNotEmpty(exists.getExtId()) && !exists.getExtId().equals(extUser.getId())) { | |||||
| result.message("同名账号[" + username + "]原 extId=" + exists.getExtId() | |||||
| + ",按外部数据改绑为 extId=" + extUser.getId()); | |||||
| } | |||||
| extOrgSyncMapper.bindUserExtId(exists.getUserId(), extUser.getId()); | |||||
| updateUser(extUser, exists.getUserId(), deptLocalId, nickName, status); | |||||
| extOrgSyncMapper.insertUserRole(exists.getUserId(), commonRoleId); | |||||
| result.setUserBind(result.getUserBind() + 1); | |||||
| result.message("绑定已存在同名账号[" + username + "] userId=" + exists.getUserId() | |||||
| + " -> extId=" + extUser.getId()); | |||||
| return extOrgSyncMapper.selectUserById(exists.getUserId()); | |||||
| } | |||||
| SysUser insert = new SysUser(); | |||||
| insert.setExtId(extUser.getId()); | |||||
| insert.setDeptId(deptLocalId); | |||||
| insert.setUserName(username); | |||||
| insert.setNickName(nickName); | |||||
| insert.setEmail(truncate(extUser.getEmail(), EMAIL_MAX_LENGTH)); | |||||
| insert.setPhonenumber(truncate(extUser.getMobilePhone(), PHONE_MAX_LENGTH)); | |||||
| insert.setPassword(SecurityUtils.encryptPassword(DEFAULT_PASSWORD)); | |||||
| insert.setStatus(status); | |||||
| insert.setCreateBy(SYNC_BY); | |||||
| insert.setRemark(REMARK); | |||||
| insert.setUserType(UserType.EXTERNAL.getCode()); | |||||
| insert.setAollowIp("*"); | |||||
| extOrgSyncMapper.insertUser(insert); | |||||
| extOrgSyncMapper.insertUserRole(insert.getUserId(), commonRoleId); | |||||
| result.setUserInsert(result.getUserInsert() + 1); | |||||
| result.setUserRoleBind(result.getUserRoleBind() + 1); | |||||
| return insert; | |||||
| } | |||||
| private void updateUser(ExtUser extUser, Long userId, Long deptLocalId, String nickName, String status) { | |||||
| SysUser update = new SysUser(); | |||||
| update.setUserId(userId); | |||||
| update.setUserType(UserType.EXTERNAL.getCode()); | |||||
| update.setNickName(nickName); | |||||
| update.setDeptId(deptLocalId); | |||||
| update.setStatus(status); | |||||
| update.setEmail(truncate(extUser.getEmail(), EMAIL_MAX_LENGTH)); | |||||
| update.setPhonenumber(truncate(extUser.getMobilePhone(), PHONE_MAX_LENGTH)); | |||||
| update.setUpdateBy(SYNC_BY); | |||||
| extOrgSyncMapper.updateUser(update); | |||||
| } | |||||
| /** | |||||
| * 外部账号状态:优先按文本“激活”判断,其次按编码 0201401,均缺失时视为正常。 | |||||
| */ | |||||
| private String resolveUserStatus(ExtUser extUser) { | |||||
| if (StringUtils.isNotEmpty(extUser.getAccountStatusText())) { | |||||
| return "激活".equals(extUser.getAccountStatusText()) ? "0" : "1"; | |||||
| } | |||||
| if (StringUtils.isNotEmpty(extUser.getAccountStatus())) { | |||||
| return "0201401".equals(extUser.getAccountStatus()) ? "0" : "1"; | |||||
| } | |||||
| return "0"; | |||||
| } | |||||
| private Long requireCommonRole() { | |||||
| Long commonRoleId = extOrgSyncMapper.selectRoleIdByRoleKey(defaultRoleKey); | |||||
| if (commonRoleId == null) { | |||||
| throw new ServiceException("本地不存在 role_key=" + defaultRoleKey + " 的角色,外部用户同步已中止"); | |||||
| } | |||||
| return commonRoleId; | |||||
| } | |||||
| // ------------------------------------------------------------------ 停用对账 | |||||
| private void reconcileDepts(SyncContext context, Map<String, Long> deptMap, ExtSyncResult result) { | |||||
| if (deptMap.isEmpty()) { | |||||
| return; | |||||
| } | |||||
| Set<String> currentExtIds = new HashSet<>(); | |||||
| Set<Long> rootLocalIds = new HashSet<>(); | |||||
| for (DeptNode node : context.nodes) { | |||||
| currentExtIds.add(node.dept.getId()); | |||||
| if (node.parentExtId == null && deptMap.containsKey(node.dept.getId())) { | |||||
| rootLocalIds.add(deptMap.get(node.dept.getId())); | |||||
| } | |||||
| } | |||||
| for (ExtDeptRef ref : extOrgSyncMapper.selectSyncedDepts()) { | |||||
| if (!"0".equals(ref.getStatus()) || currentExtIds.contains(ref.getExtId())) { | |||||
| continue; | |||||
| } | |||||
| if (!inScope(ref, rootLocalIds)) { | |||||
| continue; | |||||
| } | |||||
| extOrgSyncMapper.deactivateDept(ref.getDeptId()); | |||||
| result.setDeptDeactivate(result.getDeptDeactivate() + 1); | |||||
| result.message("停用部门:deptId=" + ref.getDeptId() + ",extId=" + ref.getExtId()); | |||||
| } | |||||
| } | |||||
| private boolean inScope(ExtDeptRef ref, Set<Long> rootLocalIds) { | |||||
| if (rootLocalIds.contains(ref.getDeptId())) { | |||||
| return true; | |||||
| } | |||||
| if (StringUtils.isEmpty(ref.getAncestors())) { | |||||
| return false; | |||||
| } | |||||
| for (String part : ref.getAncestors().split(",")) { | |||||
| if (StringUtils.isEmpty(part)) { | |||||
| continue; | |||||
| } | |||||
| try { | |||||
| if (rootLocalIds.contains(Long.valueOf(part.trim()))) { | |||||
| return true; | |||||
| } | |||||
| } catch (NumberFormatException ignored) { | |||||
| // 非法 ancestors 忽略 | |||||
| } | |||||
| } | |||||
| return false; | |||||
| } | |||||
| private void reconcileUsers(SyncContext context, Map<String, Long> deptMap, ExtSyncResult result) { | |||||
| if (deptMap.isEmpty()) { | |||||
| return; | |||||
| } | |||||
| Set<Long> scopedDeptIds = new HashSet<>(deptMap.values()); | |||||
| Set<String> currentExtIds = new HashSet<>(context.users.keySet()); | |||||
| for (ExtUserRef ref : extOrgSyncMapper.selectSyncedUsers()) { | |||||
| if (!"0".equals(ref.getStatus()) || currentExtIds.contains(ref.getExtId())) { | |||||
| continue; | |||||
| } | |||||
| if (ref.getDeptId() == null || !scopedDeptIds.contains(ref.getDeptId())) { | |||||
| continue; | |||||
| } | |||||
| extOrgSyncMapper.deactivateUser(ref.getUserId()); | |||||
| result.setUserDeactivate(result.getUserDeactivate() + 1); | |||||
| result.message("停用用户:userId=" + ref.getUserId() + ",extId=" + ref.getExtId()); | |||||
| } | |||||
| } | |||||
| // ------------------------------------------------------------------ 工具 | |||||
| private boolean tryLock() { | |||||
| return acquireLock(LOCK_KEY, LOCK_SECONDS); | |||||
| } | |||||
| private boolean acquireLock(String key, long seconds) { | |||||
| for (int i = 0; i < 10; i++) { | |||||
| Boolean success = redisService.redisTemplate.opsForValue() | |||||
| .setIfAbsent(key, String.valueOf(System.currentTimeMillis()), seconds, TimeUnit.SECONDS); | |||||
| if (Boolean.TRUE.equals(success)) { | |||||
| return true; | |||||
| } | |||||
| try { | |||||
| Thread.sleep(200L); | |||||
| } catch (InterruptedException e) { | |||||
| Thread.currentThread().interrupt(); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| log.warn("[EXT-SYNC] 等待锁 {} 超时,本次不加锁执行", key); | |||||
| return false; | |||||
| } | |||||
| private void unlock(String key) { | |||||
| try { | |||||
| redisService.redisTemplate.delete(key); | |||||
| } catch (Exception e) { | |||||
| log.warn("[EXT-SYNC] 释放锁 {} 失败", key, e); | |||||
| } | |||||
| } | |||||
| private String truncate(String value, int maxLength) { | |||||
| if (value == null) { | |||||
| return ""; | |||||
| } | |||||
| return value.length() <= maxLength ? value : value.substring(0, maxLength); | |||||
| } | |||||
| private static class DeptNode { | |||||
| private final ExtDept dept; | |||||
| private final String parentExtId; | |||||
| private final Integer order; | |||||
| private DeptNode(ExtDept dept, String parentExtId, Integer order) { | |||||
| this.dept = dept; | |||||
| this.parentExtId = parentExtId; | |||||
| this.order = order; | |||||
| } | |||||
| } | |||||
| private static class SyncContext { | |||||
| private final List<DeptNode> nodes = new ArrayList<>(); | |||||
| private final Map<String, ExtUser> users = new LinkedHashMap<>(); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,199 @@ | |||||
| <?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"> | |||||
| <mapper namespace="com.inspect.system.mapper.ExtOrgSyncMapper"> | |||||
| <resultMap type="com.inspect.system.base.domain.SysDept" id="DeptResult"> | |||||
| <id property="deptId" column="dept_id"/> | |||||
| <result property="extId" column="ext_id"/> | |||||
| <result property="parentId" column="parent_id"/> | |||||
| <result property="ancestors" column="ancestors"/> | |||||
| <result property="deptName" column="dept_name"/> | |||||
| <result property="orderNum" column="order_num"/> | |||||
| <result property="status" column="status"/> | |||||
| <result property="delFlag" column="del_flag"/> | |||||
| <result property="createBy" column="create_by"/> | |||||
| <result property="createTime" column="create_time"/> | |||||
| <result property="updateBy" column="update_by"/> | |||||
| <result property="updateTime" column="update_time"/> | |||||
| </resultMap> | |||||
| <resultMap type="com.inspect.system.base.domain.SysUser" id="UserResult"> | |||||
| <id property="userId" column="user_id"/> | |||||
| <result property="extId" column="ext_id"/> | |||||
| <result property="deptId" column="dept_id"/> | |||||
| <result property="userName" column="user_name"/> | |||||
| <result property="nickName" column="nick_name"/> | |||||
| <result property="userType" column="user_type"/> | |||||
| <result property="email" column="email"/> | |||||
| <result property="phonenumber" column="phonenumber"/> | |||||
| <result property="sex" column="sex"/> | |||||
| <result property="avatar" column="avatar"/> | |||||
| <result property="password" column="password"/> | |||||
| <result property="status" column="status"/> | |||||
| <result property="delFlag" column="del_flag"/> | |||||
| <result property="remark" column="remark"/> | |||||
| </resultMap> | |||||
| <resultMap type="com.inspect.system.domain.ext.ExtDeptRef" id="DeptRefResult"> | |||||
| <result property="deptId" column="dept_id"/> | |||||
| <result property="extId" column="ext_id"/> | |||||
| <result property="parentId" column="parent_id"/> | |||||
| <result property="ancestors" column="ancestors"/> | |||||
| <result property="status" column="status"/> | |||||
| </resultMap> | |||||
| <resultMap type="com.inspect.system.base.domain.SysRole" id="RoleRefResult"> | |||||
| <id property="roleId" column="role_id"/> | |||||
| <result property="roleName" column="role_name"/> | |||||
| <result property="roleKey" column="role_key"/> | |||||
| <result property="status" column="status"/> | |||||
| <result property="delFlag" column="del_flag"/> | |||||
| </resultMap> | |||||
| <resultMap type="com.inspect.system.domain.ext.ExtUserRef" id="UserRefResult"> | |||||
| <result property="userId" column="user_id"/> | |||||
| <result property="extId" column="ext_id"/> | |||||
| <result property="deptId" column="dept_id"/> | |||||
| <result property="status" column="status"/> | |||||
| </resultMap> | |||||
| <!-- ============================ 部门 ============================ --> | |||||
| <select id="selectDeptIdByExtId" resultType="java.lang.Long"> | |||||
| select dept_id from sys_dept where ext_id = #{extId} limit 1 | |||||
| </select> | |||||
| <select id="selectDeptIdByParentAndName" resultType="java.lang.Long"> | |||||
| select dept_id | |||||
| from sys_dept | |||||
| where parent_id = #{parentId} | |||||
| and dept_name = #{deptName} | |||||
| and ext_id is null | |||||
| and del_flag = '0' | |||||
| limit 1 | |||||
| </select> | |||||
| <insert id="insertDept" parameterType="com.inspect.system.base.domain.SysDept" | |||||
| useGeneratedKeys="true" keyProperty="deptId"> | |||||
| insert into sys_dept (ext_id, parent_id, ancestors, dept_name, order_num, status, del_flag, create_by, create_time) | |||||
| values (#{extId}, #{parentId}, #{ancestors}, #{deptName}, #{orderNum}, '0', '0', #{createBy}, now()) | |||||
| </insert> | |||||
| <update id="updateDept" parameterType="com.inspect.system.base.domain.SysDept"> | |||||
| update sys_dept | |||||
| set parent_id = #{parentId}, | |||||
| ancestors = #{ancestors}, | |||||
| dept_name = #{deptName}, | |||||
| order_num = #{orderNum}, | |||||
| status = '0', | |||||
| update_by = #{updateBy}, | |||||
| update_time = now() | |||||
| where dept_id = #{deptId} | |||||
| </update> | |||||
| <update id="bindDeptExtId"> | |||||
| update sys_dept | |||||
| set ext_id = #{extId}, update_by = 'ext_sync', update_time = now() | |||||
| where dept_id = #{deptId} | |||||
| </update> | |||||
| <select id="selectSyncedDepts" resultMap="DeptRefResult"> | |||||
| select dept_id, ext_id, parent_id, ancestors, status | |||||
| from sys_dept | |||||
| where ext_id is not null | |||||
| </select> | |||||
| <update id="deactivateDept"> | |||||
| update sys_dept | |||||
| set status = '1', update_by = 'ext_sync', update_time = now() | |||||
| where dept_id = #{deptId} and status = '0' | |||||
| </update> | |||||
| <!-- ============================ 用户 ============================ --> | |||||
| <select id="selectRoleIdByRoleKey" resultType="java.lang.Long"> | |||||
| select role_id from sys_role where role_key = #{roleKey} and del_flag = '0' limit 1 | |||||
| </select> | |||||
| <select id="selectUserIdByExtId" resultType="java.lang.Long"> | |||||
| select user_id from sys_user where ext_id = #{extId} limit 1 | |||||
| </select> | |||||
| <select id="selectUserById" resultMap="UserResult"> | |||||
| select user_id, ext_id, dept_id, user_name, nick_name, status, del_flag | |||||
| from sys_user | |||||
| where user_id = #{userId} | |||||
| </select> | |||||
| <select id="selectUserByUserName" resultMap="UserResult"> | |||||
| select user_id, ext_id, dept_id, user_name, nick_name, status, del_flag | |||||
| from sys_user | |||||
| where user_name = #{userName} | |||||
| order by del_flag asc | |||||
| limit 1 | |||||
| </select> | |||||
| <insert id="insertUser" parameterType="com.inspect.system.base.domain.SysUser" | |||||
| useGeneratedKeys="true" keyProperty="userId"> | |||||
| insert into sys_user (ext_id, dept_id, user_name, nick_name, user_type, email, phonenumber, sex, | |||||
| avatar, password, status, del_flag, create_by, create_time, remark, aollow_ip) | |||||
| values (#{extId}, #{deptId}, #{userName}, #{nickName}, #{userType}, #{email}, #{phonenumber}, '0', | |||||
| '', #{password}, #{status}, '0', #{createBy}, now(), #{remark}, #{aollowIp}) | |||||
| </insert> | |||||
| <update id="updateUser" parameterType="com.inspect.system.base.domain.SysUser"> | |||||
| update sys_user | |||||
| set nick_name = #{nickName}, | |||||
| user_type = #{userType}, | |||||
| <if test="deptId != null">dept_id = #{deptId},</if> | |||||
| status = #{status}, | |||||
| <if test="email != null and email != ''">email = #{email},</if> | |||||
| <if test="phonenumber != null and phonenumber != ''">phonenumber = #{phonenumber},</if> | |||||
| update_by = #{updateBy}, | |||||
| update_time = now() | |||||
| where user_id = #{userId} | |||||
| </update> | |||||
| <update id="bindUserExtId"> | |||||
| update sys_user | |||||
| set ext_id = #{extId}, update_by = 'ext_sync', update_time = now() | |||||
| where user_id = #{userId} | |||||
| </update> | |||||
| <insert id="insertUserRole"> | |||||
| insert ignore into sys_user_role (user_id, role_id) values (#{userId}, #{roleId}) | |||||
| </insert> | |||||
| <select id="selectSyncedUsers" resultMap="UserRefResult"> | |||||
| select user_id, ext_id, dept_id, status | |||||
| from sys_user | |||||
| where ext_id is not null | |||||
| </select> | |||||
| <update id="deactivateUser"> | |||||
| update sys_user | |||||
| set status = '1', update_by = 'ext_sync', update_time = now() | |||||
| where user_id = #{userId} and status = '0' | |||||
| </update> | |||||
| <!-- ============================ 角色(登录时同步) ============================ --> | |||||
| <select id="selectRoleByName" resultMap="RoleRefResult"> | |||||
| select role_id, role_name, role_key, status, del_flag | |||||
| from sys_role | |||||
| where role_name = #{roleName} and del_flag = '0' | |||||
| order by role_id | |||||
| limit 1 | |||||
| </select> | |||||
| <select id="selectRolesByUserId" resultMap="RoleRefResult"> | |||||
| select r.role_id, r.role_name, r.role_key, r.status, r.del_flag | |||||
| from sys_role r | |||||
| inner join sys_user_role ur on ur.role_id = r.role_id | |||||
| where ur.user_id = #{userId} and r.del_flag = '0' | |||||
| </select> | |||||
| <delete id="deleteUserRole"> | |||||
| delete from sys_user_role where user_id = #{userId} and role_id = #{roleId} | |||||
| </delete> | |||||
| </mapper> | |||||