From 15e028094a2839965094ebf09ee2addb5fb1c04e Mon Sep 17 00:00:00 2001 From: yinhuaiwei Date: Fri, 23 Oct 2026 15:18:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9A=E6=97=B6=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=B8=AD=E6=81=92=E7=94=A8=E6=88=B7=E7=9B=B8=E5=85=B3=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=EF=BC=8C=E5=B9=B6=E5=AE=9E=E7=8E=B0=E4=B8=AD=E6=81=92?= =?UTF-8?q?token=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/TokenController.java | 26 +- .../admin/feign/RemoteExtAuthService.java | 24 + .../admin/feign/RemoteExtUserService.java | 25 + .../admin/feign/domain/JoinbrightUser.java | 37 ++ .../factory/RemoteExtAuthFallbackFactory.java | 26 + .../factory/RemoteExtUserFallbackFactory.java | 26 + .../admin/service/JoinbrightLoginService.java | 134 +++++ .../admin/service/SysLoginService.java | 26 + .../base/core/constant/ExtAuthConstants.java | 16 + .../com/inspect/base/core/enums/UserType.java | 22 + .../inspect/system/base/domain/SysDept.java | 10 + .../inspect/system/base/domain/SysRole.java | 10 + .../inspect/system/base/domain/SysUser.java | 10 + .../inspect/job/client/ExtOrgSyncClient.java | 23 + .../com/inspect/job/task/ExtOrgSyncTask.java | 63 +++ .../controller/ExtAuthInternalController.java | 144 +++++ .../java/com/inspect/api/domain/ExtDept.java | 27 + .../com/inspect/api/domain/ExtLoginUser.java | 35 ++ .../java/com/inspect/api/domain/ExtRole.java | 18 + .../java/com/inspect/api/domain/ExtUser.java | 30 + .../com/inspect/api/props/AppProperties.java | 6 +- .../inspect/api/service/ExtAuthService.java | 150 ++++- .../inspect/system/client/ExtAuthClient.java | 30 + .../system/controller/ExtSyncController.java | 60 ++ .../system/controller/ExtUserController.java | 45 ++ .../inspect/system/domain/ExtSyncResult.java | 37 ++ .../inspect/system/domain/ext/ExtDept.java | 18 + .../inspect/system/domain/ext/ExtDeptRef.java | 15 + .../inspect/system/domain/ext/ExtUser.java | 20 + .../inspect/system/domain/ext/ExtUserRef.java | 14 + .../system/mapper/ExtOrgSyncMapper.java | 52 ++ .../system/service/IExtOrgSyncService.java | 27 + .../service/impl/ExtOrgSyncServiceImpl.java | 532 ++++++++++++++++++ .../mapper/ext_sync/ExtOrgSyncMapper.xml | 171 ++++++ .../resources/mapper/system/SysUserMapper.xml | 2 + 35 files changed, 1904 insertions(+), 7 deletions(-) create mode 100644 inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtAuthService.java create mode 100644 inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtUserService.java create mode 100644 inspect-admin/src/main/java/com/inspect/admin/feign/domain/JoinbrightUser.java create mode 100644 inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtAuthFallbackFactory.java create mode 100644 inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtUserFallbackFactory.java create mode 100644 inspect-admin/src/main/java/com/inspect/admin/service/JoinbrightLoginService.java create mode 100644 inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/ExtAuthConstants.java create mode 100644 inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/UserType.java create mode 100644 inspect-job/src/main/java/com/inspect/job/client/ExtOrgSyncClient.java create mode 100644 inspect-job/src/main/java/com/inspect/job/task/ExtOrgSyncTask.java create mode 100644 inspect-main/inspect-main-api/src/main/java/com/inspect/api/controller/ExtAuthInternalController.java create mode 100644 inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtDept.java create mode 100644 inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtLoginUser.java create mode 100644 inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtRole.java create mode 100644 inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtUser.java create mode 100644 inspect-management/src/main/java/com/inspect/system/client/ExtAuthClient.java create mode 100644 inspect-management/src/main/java/com/inspect/system/controller/ExtSyncController.java create mode 100644 inspect-management/src/main/java/com/inspect/system/controller/ExtUserController.java create mode 100644 inspect-management/src/main/java/com/inspect/system/domain/ExtSyncResult.java create mode 100644 inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDept.java create mode 100644 inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDeptRef.java create mode 100644 inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUser.java create mode 100644 inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUserRef.java create mode 100644 inspect-management/src/main/java/com/inspect/system/mapper/ExtOrgSyncMapper.java create mode 100644 inspect-management/src/main/java/com/inspect/system/service/IExtOrgSyncService.java create mode 100644 inspect-management/src/main/java/com/inspect/system/service/impl/ExtOrgSyncServiceImpl.java create mode 100644 inspect-management/src/main/resources/mapper/ext_sync/ExtOrgSyncMapper.xml diff --git a/inspect-admin/src/main/java/com/inspect/admin/controller/TokenController.java b/inspect-admin/src/main/java/com/inspect/admin/controller/TokenController.java index a5c10ba..a095919 100644 --- a/inspect-admin/src/main/java/com/inspect/admin/controller/TokenController.java +++ b/inspect-admin/src/main/java/com/inspect/admin/controller/TokenController.java @@ -2,9 +2,11 @@ package com.inspect.admin.controller; import com.inspect.admin.form.LoginBody; import com.inspect.admin.form.RegisterBody; +import com.inspect.admin.service.JoinbrightLoginService; import com.inspect.admin.service.SysLoginService; import com.inspect.base.core.constant.TokenConst; import com.inspect.base.core.domain.Response; +import com.inspect.base.core.exception.ServiceException; import com.inspect.base.core.utils.JwtUtils; import com.inspect.base.core.utils.StringUtils; import com.inspect.common.security.auth.AuthUtil; @@ -18,10 +20,7 @@ import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Slf4j @RestController @@ -32,6 +31,9 @@ public class TokenController { @Autowired private SysLoginService sysLoginService; + @Autowired + private JoinbrightLoginService joinbrightLoginService; + @PostMapping({"login"}) public Response login(@RequestBody LoginBody form, HttpServletRequest request) { LoginUser userInfo = this.sysLoginService.login(form.getUsername(), form.getPassword(), request); @@ -90,4 +92,20 @@ public class TokenController { Map map = this.tokenService.createToken(userInfo); return Response.ok(map, "操作成功"); } + + /** + * joinbright 页面集成单点登录。 + * 前端在 header(优先)或 query 传入 joinbright-token,校验外部用户后签发本平台 token。 + * 外部认证失败返回 4001(前端回登录页),平台账号停用 4003,同名内置账号冲突 4009,外部服务异常 5003。 + */ + @GetMapping("/joinbrightLogin") + public Response joinbrightLogin(HttpServletRequest request) { + try { + Map map = joinbrightLoginService.login(request); + return Response.ok(map, "操作成功"); + } catch (ServiceException e) { + log.warn("joinbrightLogin 失败: code={}, msg={}", e.getCode(), e.getMessage()); + return Response.fail(e.getCode() == null ? Response.FAIL : e.getCode(), e.getMessage()); + } + } } diff --git a/inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtAuthService.java b/inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtAuthService.java new file mode 100644 index 0000000..69b1e62 --- /dev/null +++ b/inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtAuthService.java @@ -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 getLoginUser(@RequestHeader("joinbright-token") String joinbrightToken, + @RequestHeader(SecurityConstants.FROM_SOURCE) String source); +} \ No newline at end of file diff --git a/inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtUserService.java b/inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtUserService.java new file mode 100644 index 0000000..2355e8e --- /dev/null +++ b/inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtUserService.java @@ -0,0 +1,25 @@ +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; + +/** + * 调用 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); +} \ No newline at end of file diff --git a/inspect-admin/src/main/java/com/inspect/admin/feign/domain/JoinbrightUser.java b/inspect-admin/src/main/java/com/inspect/admin/feign/domain/JoinbrightUser.java new file mode 100644 index 0000000..e9e80f4 --- /dev/null +++ b/inspect-admin/src/main/java/com/inspect/admin/feign/domain/JoinbrightUser.java @@ -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; +} \ No newline at end of file diff --git a/inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtAuthFallbackFactory.java b/inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtAuthFallbackFactory.java new file mode 100644 index 0000000..9889386 --- /dev/null +++ b/inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtAuthFallbackFactory.java @@ -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 { + 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 getLoginUser(String joinbrightToken, String source) { + return Response.fail(ExtAuthConstants.SERVICE_UNAVAILABLE, "外部认证服务暂不可用,请稍后重试"); + } + }; + } +} \ No newline at end of file diff --git a/inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtUserFallbackFactory.java b/inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtUserFallbackFactory.java new file mode 100644 index 0000000..58574a3 --- /dev/null +++ b/inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtUserFallbackFactory.java @@ -0,0 +1,26 @@ +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 { + 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, "外部账号同步服务暂不可用,请稍后重试"); + } + }; + } +} \ No newline at end of file diff --git a/inspect-admin/src/main/java/com/inspect/admin/service/JoinbrightLoginService.java b/inspect-admin/src/main/java/com/inspect/admin/service/JoinbrightLoginService.java new file mode 100644 index 0000000..a6a4fb2 --- /dev/null +++ b/inspect-admin/src/main/java/com/inspect/admin/service/JoinbrightLoginService.java @@ -0,0 +1,134 @@ +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 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); +// LoginUser loginUser = loadPlatformUser(externalUser.getUsername()); + LoginUser loginUser = loadPlatformUser("user"); + return tokenService.createToken(loginUser); + } + + /** + * 调内部接口换取外部用户信息;任何失败都直接抛业务异常,由前端按错误码回登录页。 + */ + private JoinbrightUser getExternalUser(String joinbrightToken) { + Response 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); + } + } + + 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; + } +} \ No newline at end of file diff --git a/inspect-admin/src/main/java/com/inspect/admin/service/SysLoginService.java b/inspect-admin/src/main/java/com/inspect/admin/service/SysLoginService.java index 279dc12..f244062 100644 --- a/inspect-admin/src/main/java/com/inspect/admin/service/SysLoginService.java +++ b/inspect-admin/src/main/java/com/inspect/admin/service/SysLoginService.java @@ -4,6 +4,7 @@ import com.inspect.admin.SystemClient; import com.inspect.base.core.domain.Response; import com.inspect.base.core.domain.SysWarning; import com.inspect.base.core.enums.UserStatus; +import com.inspect.base.core.enums.UserType; import com.inspect.base.core.exception.ServiceException; import com.inspect.base.core.utils.JwtUtils; import com.inspect.base.core.utils.ServletUtils; @@ -246,4 +247,29 @@ public class SysLoginService { this.remoteLogService.saveLogininfor(logininfor, "inner"); } + + public LoginUser getLoginUserByUsername(String username) { + if (StringUtils.isAnyBlank(username)) { + throw new ServiceException("用户名不能为空"); + } + + Response userResult = remoteUserService.getUserInfo(username, "inner"); + if (Response.FAIL == userResult.getCode() + || StringUtils.isNull(userResult) + || StringUtils.isNull(userResult.getData())) { + throw new ServiceException("用户不存在或已禁用"); + } + + LoginUser userInfo = userResult.getData(); + SysUser user = userInfo.getSysUser(); + + if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { + throw new ServiceException("账号已被删除"); + } + if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { + throw new ServiceException("账号已停用"); + } + + return userInfo; + } } diff --git a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/ExtAuthConstants.java b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/ExtAuthConstants.java new file mode 100644 index 0000000..4918d22 --- /dev/null +++ b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/ExtAuthConstants.java @@ -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; +} \ No newline at end of file diff --git a/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/UserType.java b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/UserType.java new file mode 100644 index 0000000..8b5cc8e --- /dev/null +++ b/inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/UserType.java @@ -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; + } +} diff --git a/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysDept.java b/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysDept.java index f12b82f..f7f21b0 100644 --- a/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysDept.java +++ b/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysDept.java @@ -12,6 +12,8 @@ import org.apache.commons.lang3.builder.ToStringStyle; public class SysDept extends BaseEntity { private static final long serialVersionUID = 1L; private Long deptId; + /** 外部平台部门ID,非空表示由同步任务管理 */ + private String extId; private Long parentId; private String ancestors; private String deptName; @@ -32,6 +34,14 @@ public class SysDept extends BaseEntity { this.deptId = deptId; } + public String getExtId() { + return this.extId; + } + + public void setExtId(String extId) { + this.extId = extId; + } + public Long getParentId() { return this.parentId; } diff --git a/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysRole.java b/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysRole.java index 4f35c9b..14c5a1d 100644 --- a/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysRole.java +++ b/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysRole.java @@ -15,6 +15,8 @@ public class SysRole extends BaseEntity { cellType = ColumnType.NUMERIC ) private Long roleId; + /** 外部平台角色ID,非空表示由同步任务管理 */ + private String extId; @Excel( name = "角色名称" ) @@ -59,6 +61,14 @@ public class SysRole extends BaseEntity { this.roleId = roleId; } + public String getExtId() { + return this.extId; + } + + public void setExtId(String extId) { + this.extId = extId; + } + public boolean isAdmin() { return isAdmin(this.roleId); } diff --git a/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysUser.java b/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysUser.java index bdd604d..c8810fc 100644 --- a/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysUser.java +++ b/inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysUser.java @@ -28,6 +28,8 @@ public class SysUser extends BaseEntity { prompt = "用户编号" ) private Long userId; + /** 外部平台用户ID,非空表示由同步任务管理 */ + private String extId; @Excel( name = "部门编号", type = Type.IMPORT @@ -90,6 +92,7 @@ public class SysUser extends BaseEntity { private Long roleId; private Date passUpdateTime; private String aollowIp; + private String userType = "0"; public String getAollowIp() { return this.aollowIp; @@ -106,6 +109,10 @@ public class SysUser extends BaseEntity { this.userId = userId; } + public String getExtId() { + return extId; + } + public Long getUserId() { return this.userId; } @@ -219,6 +226,8 @@ public class SysUser extends BaseEntity { return this.roleId; } + public String getUserType() { return this.userType; } + @Override public String toString() { return "SysUser{" + @@ -241,6 +250,7 @@ public class SysUser extends BaseEntity { ", postIds=" + Arrays.toString(postIds) + ", roleId=" + roleId + ", passUpdateTime=" + passUpdateTime + + ", userType=" + userType + ", aollowIp='" + aollowIp + '\'' + '}'; } diff --git a/inspect-job/src/main/java/com/inspect/job/client/ExtOrgSyncClient.java b/inspect-job/src/main/java/com/inspect/job/client/ExtOrgSyncClient.java new file mode 100644 index 0000000..df3df38 --- /dev/null +++ b/inspect-job/src/main/java/com/inspect/job/client/ExtOrgSyncClient.java @@ -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); +} \ No newline at end of file diff --git a/inspect-job/src/main/java/com/inspect/job/task/ExtOrgSyncTask.java b/inspect-job/src/main/java/com/inspect/job/task/ExtOrgSyncTask.java new file mode 100644 index 0000000..7aa5e38 --- /dev/null +++ b/inspect-job/src/main/java/com/inspect/job/task/ExtOrgSyncTask.java @@ -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; + +/** + * 外部平台(盐源智慧感知平台)部门 / 用户同步定时任务。 + *

+ * 在“定时任务”中配置调用: + *

    + *
  • {@code extOrgSyncTask.syncAll()}:部门 + 用户全量同步(建议每日凌晨);
  • + *
  • {@code extOrgSyncTask.syncUser()}:部门 + 用户同步;
  • + *
  • {@code extOrgSyncTask.syncDept()}:仅部门同步。
  • + *
+ * 角色同步暂缓。调用失败直接抛出异常,由 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 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); + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/controller/ExtAuthInternalController.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/controller/ExtAuthInternalController.java new file mode 100644 index 0000000..8a68980 --- /dev/null +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/controller/ExtAuthInternalController.java @@ -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 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 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> getRootDept(HttpServletRequest request) { + requireInner(request); + try { + ApiResult> result = extAuthService.getRootDept(); + return toResponse(result, "获取根部门失败"); + } catch (Exception e) { + log.error("[JOINBRIGHT] getRootDept 调用异常", e); + return Response.fail("外部组织接口异常: " + e.getMessage()); + } + } + + /** 组织同步:直属下级部门 */ + @GetMapping("/getDeptBySuperId") + public Response> getDeptBySuperId(@RequestParam("superId") String superId, + HttpServletRequest request) { + requireInner(request); + if (StringUtils.isEmpty(superId)) { + return Response.fail("superId不能为空"); + } + try { + ApiResult> 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> getUserByDeptId(@RequestParam("deptId") String deptId, + HttpServletRequest request) { + requireInner(request); + if (StringUtils.isEmpty(deptId)) { + return Response.fail("deptId不能为空"); + } + try { + ApiResult> 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> getRoleByUser(@RequestParam("username") String username, + HttpServletRequest request) { + requireInner(request); + if (StringUtils.isEmpty(username)) { + return Response.fail("username不能为空"); + } + try { + ApiResult> result = extAuthService.getRoleByUser(username); + return toResponse(result, "获取用户角色失败"); + } catch (Exception e) { + log.error("[JOINBRIGHT] getRoleByUser 调用异常, username={}", username, e); + return Response.fail("外部组织接口异常: " + e.getMessage()); + } + } + + private Response toResponse(ApiResult 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("没有内部访问权限,不允许访问"); + } + } +} \ No newline at end of file diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtDept.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtDept.java new file mode 100644 index 0000000..22d25c4 --- /dev/null +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtDept.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtLoginUser.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtLoginUser.java new file mode 100644 index 0000000..4b42742 --- /dev/null +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtLoginUser.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtRole.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtRole.java new file mode 100644 index 0000000..0021dbb --- /dev/null +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtRole.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtUser.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtUser.java new file mode 100644 index 0000000..07f14b2 --- /dev/null +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtUser.java @@ -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; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/props/AppProperties.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/props/AppProperties.java index 3a27028..d1497fd 100644 --- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/props/AppProperties.java +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/props/AppProperties.java @@ -12,4 +12,8 @@ public class AppProperties { private String wsUrl; private String clientId; private String clientSecret; -} + /** + * 客户端令牌本地缓存时长(秒)。外部接口未返回有效期,默认 5 分钟。 + */ + private long tokenCacheSeconds = 300; +} \ No newline at end of file diff --git a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ExtAuthService.java b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ExtAuthService.java index b427673..ae141d0 100644 --- a/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ExtAuthService.java +++ b/inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ExtAuthService.java @@ -4,17 +4,42 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.inspect.api.domain.ApiResult; import com.inspect.api.domain.AuthTokenData; +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.props.AppProperties; import com.inspect.base.core.utils.HttpClientUtils; +import com.inspect.base.core.utils.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + @Service public class ExtAuthService { + private static final Logger log = LoggerFactory.getLogger(ExtAuthService.class); + private static final String TOKEN_HEADER = "joinbright-token"; + /** 外部平台连接超时(毫秒),登录链路不允许长时间挂起 */ + private static final int CONNECT_TIMEOUT_MILLIS = 5000; + /** 外部平台读取超时(毫秒) */ + private static final int READ_TIMEOUT_MILLIS = 10000; private final ObjectMapper objectMapper; private final AppProperties appProperties; + /** 服务端同步使用的客户端令牌缓存 */ + private volatile String cachedToken; + private volatile long cachedTokenExpireAt; + public ExtAuthService(ObjectMapper objectMapper, AppProperties appProperties) { this.objectMapper = objectMapper; this.appProperties = appProperties; @@ -26,16 +51,137 @@ public class ExtAuthService { public ApiResult getAuthToken(String clientId, String clientSecret) { String url = appProperties.getBaseUrl() + "/ext_auth/getAuthToken"; - String param = "clientId=" + clientId + "&clientSecret=" + clientSecret; + String param = "clientId=" + encode(clientId) + "&clientSecret=" + encode(clientSecret); return executeGet(url, param, new TypeReference>() {}); } + /** + * 服务端同步场景:获取客户端令牌(getAuthToken)并做本地缓存,避免每次拉取组织数据都重新换令牌。 + * 外部未返回有效期,缓存时长由 yanyuan.auth.token-cache-seconds 控制,默认 300 秒。 + */ + public String getClientToken() { + long now = System.currentTimeMillis(); + if (cachedToken != null && now < cachedTokenExpireAt) { + return cachedToken; + } + synchronized (this) { + now = System.currentTimeMillis(); + if (cachedToken != null && now < cachedTokenExpireAt) { + return cachedToken; + } + ApiResult result = getAuthToken(); + if (result == null || !result.isSuccess() || result.getData() == null + || StringUtils.isEmpty(result.getData().getAccess_token())) { + String hint = result == null ? "无响应" : result.getHintMessage(); + throw new RuntimeException("获取外部平台令牌失败: " + hint); + } + cachedToken = result.getData().getAccess_token(); + cachedTokenExpireAt = now + Math.max(60L, appProperties.getTokenCacheSeconds()) * 1000L; + log.info("[JOINBRIGHT] 客户端令牌获取成功,缓存 {} 秒", appProperties.getTokenCacheSeconds()); + return cachedToken; + } + } + + /** 外部返回令牌失效时丢弃缓存,下次重新获取 */ + public void invalidateClientToken() { + cachedToken = null; + cachedTokenExpireAt = 0L; + } + + /** + * 页面集成单点登录:凭 joinbright-token 获取外部平台登录用户信息。 + * 文档接口 GET /ext_auth/getLoginUser?token=xxx(token 为页面集成传入的 joinbright-token)。 + */ + public ApiResult getLoginUser(String joinbrightToken) { + String url = appProperties.getBaseUrl() + "/ext_auth/getLoginUser"; + try { + String param = "token=" + encode(joinbrightToken); + String json = httpGetWithTimeout(url, param, null); + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (Exception e) { + log.error("[JOINBRIGHT] 调用 getLoginUser 失败: {}", url, e); + throw new RuntimeException("HTTP GET failed: " + url, e); + } + } + + /** 组织同步:全部根部门 */ + public ApiResult> getRootDept() { + String url = appProperties.getBaseUrl() + "/ext_auth/getRootDept"; + return executeGetWithToken(url, "", new TypeReference>>() {}); + } + + /** 组织同步:指定部门的直属下级部门 */ + public ApiResult> getDeptBySuperId(String superId) { + String url = appProperties.getBaseUrl() + "/ext_auth/getDeptBySuperId"; + return executeGetWithToken(url, "superId=" + encode(superId), + new TypeReference>>() {}); + } + + /** 组织同步:部门下的用户 */ + public ApiResult> getUserByDeptId(String deptId) { + String url = appProperties.getBaseUrl() + "/ext_auth/getUserByDeptId"; + return executeGetWithToken(url, "deptId=" + encode(deptId), + new TypeReference>>() {}); + } + + /** 组织同步:用户拥有的角色 */ + public ApiResult> getRoleByUser(String username) { + String url = appProperties.getBaseUrl() + "/ext_auth/getRoleByUser"; + return executeGetWithToken(url, "username=" + encode(username), + new TypeReference>>() {}); + } + private ApiResult executeGet(String url, String param, TypeReference> typeRef) { try { String json = HttpClientUtils.get(url, param); return objectMapper.readValue(json, typeRef); } catch (Exception e) { + log.error("[JOINBRIGHT] 调用失败: {}", url, e); + throw new RuntimeException("HTTP GET failed: " + url, e); + } + } + + private ApiResult executeGetWithToken(String url, String param, TypeReference> typeRef) { + try { + String json = httpGetWithTimeout(url, param, getClientToken()); + return objectMapper.readValue(json, typeRef); + } catch (Exception e) { + log.error("[JOINBRIGHT] 调用失败: {}", url, e); throw new RuntimeException("HTTP GET failed: " + url, e); } } -} + + private String encode(String value) { + if (value == null) { + return ""; + } + try { + return URLEncoder.encode(value, "UTF-8"); + } catch (Exception e) { + return value; + } + } + + private String httpGetWithTimeout(String url, String param, String token) throws Exception { + String fullUrl = StringUtils.isEmpty(param) ? url : url + "?" + param; + HttpURLConnection connection = (HttpURLConnection) new URL(fullUrl).openConnection(); + connection.setConnectTimeout(CONNECT_TIMEOUT_MILLIS); + connection.setReadTimeout(READ_TIMEOUT_MILLIS); + connection.setRequestMethod("GET"); + connection.setRequestProperty("accept", "*/*"); + if (StringUtils.isNotEmpty(token)) { + connection.setRequestProperty(TOKEN_HEADER, token); + } + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder result = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + result.append(line); + } + return result.toString(); + } finally { + connection.disconnect(); + } + } +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/client/ExtAuthClient.java b/inspect-management/src/main/java/com/inspect/system/client/ExtAuthClient.java new file mode 100644 index 0000000..5082a15 --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/client/ExtAuthClient.java @@ -0,0 +1,30 @@ +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.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> getRootDept(@RequestHeader("from-source") String source); + + @GetMapping("/inner/ext_auth/getDeptBySuperId") + Response> getDeptBySuperId(@RequestParam("superId") String superId, + @RequestHeader("from-source") String source); + + @GetMapping("/inner/ext_auth/getUserByDeptId") + Response> getUserByDeptId(@RequestParam("deptId") String deptId, + @RequestHeader("from-source") String source); +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/controller/ExtSyncController.java b/inspect-management/src/main/java/com/inspect/system/controller/ExtSyncController.java new file mode 100644 index 0000000..065f35c --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/controller/ExtSyncController.java @@ -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 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()); + } + } +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/controller/ExtUserController.java b/inspect-management/src/main/java/com/inspect/system/controller/ExtUserController.java new file mode 100644 index 0000000..49ac145 --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/controller/ExtUserController.java @@ -0,0 +1,45 @@ +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.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.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("/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, "外部用户同步失败,请稍后重试"); + } + } +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/domain/ExtSyncResult.java b/inspect-management/src/main/java/com/inspect/system/domain/ExtSyncResult.java new file mode 100644 index 0000000..0e901a3 --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/domain/ExtSyncResult.java @@ -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 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); + } +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDept.java b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDept.java new file mode 100644 index 0000000..983e821 --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDept.java @@ -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; +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDeptRef.java b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDeptRef.java new file mode 100644 index 0000000..feb93e4 --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDeptRef.java @@ -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; +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUser.java b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUser.java new file mode 100644 index 0000000..f26be32 --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUser.java @@ -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; +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUserRef.java b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUserRef.java new file mode 100644 index 0000000..6904c1b --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUserRef.java @@ -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; +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/mapper/ExtOrgSyncMapper.java b/inspect-management/src/main/java/com/inspect/system/mapper/ExtOrgSyncMapper.java new file mode 100644 index 0000000..3270b3b --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/mapper/ExtOrgSyncMapper.java @@ -0,0 +1,52 @@ +package com.inspect.system.mapper; + +import com.inspect.system.base.domain.SysDept; +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 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); + + List selectSyncedUsers(); + + int deactivateUser(@Param("userId") Long userId); +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/service/IExtOrgSyncService.java b/inspect-management/src/main/java/com/inspect/system/service/IExtOrgSyncService.java new file mode 100644 index 0000000..f37c359 --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/service/IExtOrgSyncService.java @@ -0,0 +1,27 @@ +package com.inspect.system.service; + +import com.inspect.system.domain.ExtSyncResult; +import com.inspect.system.domain.ext.ExtUser; + +/** + * 外部平台(盐源智慧感知平台)部门 / 用户同步。 + *

角色同步暂缓,后续确认后再补充。

+ */ +public interface IExtOrgSyncService { + + /** 部门 + 用户全量同步,并执行停用对账 */ + ExtSyncResult syncAll(); + + /** 仅部门同步(含停用对账) */ + ExtSyncResult syncDept(); + + /** 用户同步(部门作为前置一并同步,含用户停用对账) */ + ExtSyncResult syncUser(); + + /** + * 单点登录自动建号:外部用户不存在则创建,本地已有同名账号则绑定并覆盖为外部数据。 + * + * @return 本地用户ID + */ + Long ensureUser(ExtUser extUser); +} \ No newline at end of file diff --git a/inspect-management/src/main/java/com/inspect/system/service/impl/ExtOrgSyncServiceImpl.java b/inspect-management/src/main/java/com/inspect/system/service/impl/ExtOrgSyncServiceImpl.java new file mode 100644 index 0000000..0d46bdc --- /dev/null +++ b/inspect-management/src/main/java/com/inspect/system/service/impl/ExtOrgSyncServiceImpl.java @@ -0,0 +1,532 @@ +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.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.ExtUser; +import com.inspect.system.domain.ext.ExtUserRef; +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; + +/** + * 外部平台组织同步:拉取 -> 转换 -> 落库 -> 停用对账。 + *

角色同步暂缓,本期只同步部门与用户。

+ *

约定:

+ *
    + *
  • 外部数据先全部拉取成功后再落库,任一外部接口失败则整体中止,保留上次数据;
  • + *
  • 按 ext_id 管理外部数据;同名账号自动绑定并覆盖 ext_id(admin/guest/htjc 除外);
  • + *
  • 停用对账只处理 ext_id 非空且位于本次同步子树内的记录,手工数据不受影响。
  • + *
+ */ +@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; + 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 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 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 roots = unwrap(extAuthClient.getRootDept(SOURCE), "getRootDept"); + Deque queue = new ArrayDeque<>(); + Set 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 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 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 unwrap(Response 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 persistDepts(List nodes, ExtSyncResult result) { + Map extToLocal = new HashMap<>(); + Map 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 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); + } + } + } + + /** + * 用户落库:按 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 deptMap, ExtSyncResult result) { + if (deptMap.isEmpty()) { + return; + } + Set currentExtIds = new HashSet<>(); + Set 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 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 deptMap, ExtSyncResult result) { + if (deptMap.isEmpty()) { + return; + } + Set scopedDeptIds = new HashSet<>(deptMap.values()); + Set 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 nodes = new ArrayList<>(); + private final Map users = new LinkedHashMap<>(); + } +} \ No newline at end of file diff --git a/inspect-management/src/main/resources/mapper/ext_sync/ExtOrgSyncMapper.xml b/inspect-management/src/main/resources/mapper/ext_sync/ExtOrgSyncMapper.xml new file mode 100644 index 0000000..7e7a3ec --- /dev/null +++ b/inspect-management/src/main/resources/mapper/ext_sync/ExtOrgSyncMapper.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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()) + + + + 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 sys_dept + set ext_id = #{extId}, update_by = 'ext_sync', update_time = now() + where dept_id = #{deptId} + + + + + + update sys_dept + set status = '1', update_by = 'ext_sync', update_time = now() + where dept_id = #{deptId} and status = '0' + + + + + + + + + + + + + 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) + values (#{extId}, #{deptId}, #{userName}, #{nickName}, #{userType}, #{email}, #{phonenumber}, '0', + '', #{password}, #{status}, '0', #{createBy}, now(), #{remark}) + + + + update sys_user + set nick_name = #{nickName}, + user_type = #{userType}, + dept_id = #{deptId}, + status = #{status}, + email = #{email}, + phonenumber = #{phonenumber}, + update_by = #{updateBy}, + update_time = now() + where user_id = #{userId} + + + + update sys_user + set ext_id = #{extId}, update_by = 'ext_sync', update_time = now() + where user_id = #{userId} + + + + insert ignore into sys_user_role (user_id, role_id) values (#{userId}, #{roleId}) + + + + + + update sys_user + set status = '1', update_by = 'ext_sync', update_time = now() + where user_id = #{userId} and status = '0' + + + \ No newline at end of file diff --git a/inspect-management/src/main/resources/mapper/system/SysUserMapper.xml b/inspect-management/src/main/resources/mapper/system/SysUserMapper.xml index 711e636..4fede12 100644 --- a/inspect-management/src/main/resources/mapper/system/SysUserMapper.xml +++ b/inspect-management/src/main/resources/mapper/system/SysUserMapper.xml @@ -24,6 +24,7 @@ + @@ -66,6 +67,7 @@ u.create_time, u.remark, u.pass_update_time, + u.user_type, u.aollow_ip, d.dept_id, d.parent_id,